diff --git a/.gitignore b/.gitignore index f10a528..5dcf37b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ target/ -*.class .DS_Store # jdtls/eclipse stuff diff --git a/classport-agent/pom.xml b/classport-agent/pom.xml index ba4c6f1..493d1f9 100644 --- a/classport-agent/pom.xml +++ b/classport-agent/pom.xml @@ -29,7 +29,24 @@ classport-commons ${project.parent.version} + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + diff --git a/classport-agent/src/main/java/io/github/chains_project/classport/agent/ClassportAgent.java b/classport-agent/src/main/java/io/github/chains_project/classport/agent/ClassportAgent.java index 235a75d..9ad01e8 100644 --- a/classport-agent/src/main/java/io/github/chains_project/classport/agent/ClassportAgent.java +++ b/classport-agent/src/main/java/io/github/chains_project/classport/agent/ClassportAgent.java @@ -4,17 +4,16 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.lang.instrument.*; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import io.github.chains_project.classport.commons.AnnotationReader; import io.github.chains_project.classport.commons.ClassportInfo; import io.github.chains_project.classport.commons.ClassportProject; -import io.github.chains_project.classport.commons.AnnotationReader; /** * A classport agent, ready to check the classports of any and all incoming @@ -25,7 +24,7 @@ public class ClassportAgent { private static final ArrayList noAnnotations = new ArrayList<>(); // TODO: Output in a useful format (JSON?) - private static void writeSBOM(Map sbom) throws IOException { + public static void writeSBOM(Map sbom) throws IOException { File treeOutputFile = new File("classport-deps-tree"); File listOutputFile = new File("classport-deps-list"); diff --git a/classport-agent/src/test/java/ClassportAgentTest.java b/classport-agent/src/test/java/ClassportAgentTest.java new file mode 100644 index 0000000..7812290 --- /dev/null +++ b/classport-agent/src/test/java/ClassportAgentTest.java @@ -0,0 +1,147 @@ +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import io.github.chains_project.classport.agent.ClassportAgent; +import io.github.chains_project.classport.commons.AnnotationReader; +import io.github.chains_project.classport.commons.ClassportInfo; + +public class ClassportAgentTest { + + private static final Path CLASSPORT_TREE_PATH = Path.of("src/test/resources/classport-deps/classport-deps-tree"); + private static final Path CLASSPORT_LIST_PATH = Path.of("src/test/resources/classport-deps/classport-deps-list"); + private static final Path ANNOTATED_CLASS_PATH = Path.of("src/test/resources/annotated-classes/StringUtils.class"); + private static final Path NOT_ANNOTATED_CLASS_PATH = Path.of("src/test/resources/not-annotated-classes/StringUtils.class"); + + @Test + void shouldGenerateDependencyListAndTreeFiles() throws Exception { + HashMap sbom = mockSBOM(); + + ClassportAgent.writeSBOM(sbom); + + File actualTreeFile = new File(System.getProperty("user.dir"), "classport-deps-tree"); + File actualListFile = new File(System.getProperty("user.dir"), "classport-deps-list"); + + assertAll( + () -> assertTrue(actualTreeFile.exists(), "Tree output file should be created"), + () -> assertTrue(actualListFile.exists(), "List output file should be created"), + () -> assertEquals( + Files.readString(CLASSPORT_TREE_PATH), + Files.readString(actualTreeFile.toPath()), + "Tree files should be identical"), + () -> assertEquals( + Files.readString(CLASSPORT_LIST_PATH), + Files.readString(actualListFile.toPath()), + "List files should be identical") + ); + + actualTreeFile.delete(); + actualListFile.delete(); + } + + + @Test + void shouldHandleEmptySBOM() throws Exception { + HashMap emptySBOM = new HashMap<>(); + + ClassportAgent.writeSBOM(emptySBOM); + + File treeFile = new File(System.getProperty("user.dir"), "classport-deps-tree"); + File listFile = new File(System.getProperty("user.dir"), "classport-deps-list"); + + assertTrue(treeFile.exists(), "Tree output file should be created even if SBOM is empty"); + assertTrue(listFile.exists(), "List output file should be created even if SBOM is empty"); + + String treeContent = new String(Files.readAllBytes(treeFile.toPath())); + String expectedPlaceholder = " (parent-only artefact)"; + + assertTrue(treeContent.contains(expectedPlaceholder), "Tree file should contain placeholder text when SBOM is empty"); + assertTrue(listFile.length() == 0, "List file should be empty when SBOM is empty"); + + // Clean up after the test + treeFile.delete(); + listFile.delete(); + } + + @Test + void shouldAnnotationBeCorrectlyRead() throws IOException { + File classFile = ANNOTATED_CLASS_PATH.toFile(); + byte[] buffer = loadClassFromFile(classFile); + ClassportInfo actualAnnotation = AnnotationReader.getAnnotationValues(buffer); + + assertAll( + () -> assertEquals("commons-lang3", actualAnnotation.artefact()), + () -> assertEquals("org.apache.commons", actualAnnotation.group()), + () -> assertEquals("org.apache.commons:commons-lang3:jar:3.17.0", actualAnnotation.id()), + () -> assertTrue(actualAnnotation.isDirectDependency()), + () -> assertEquals("com.example:test-agent-app:jar:1.0-SNAPSHOT", actualAnnotation.sourceProjectId()), + () -> assertEquals("3.17.0", actualAnnotation.version()), + () -> assertEquals("org.apache.commons:commons-text:jar:1.12.0", actualAnnotation.childIds()[0]) + ); + } + + @Test + void shouldReturnNonAnnotatedClassesNull() throws Exception { + byte[] nonAnnotatedClassBytes = loadClassFromFile(NOT_ANNOTATED_CLASS_PATH.toFile()); + ClassportInfo actualAnnotation = AnnotationReader.getAnnotationValues(nonAnnotatedClassBytes); + assertNull(actualAnnotation); + } + + + private byte[] loadClassFromFile(File classFile) throws IOException { + try (InputStream inputStream = new FileInputStream(classFile)) { + return inputStream.readAllBytes(); + } + } + + + private HashMap mockSBOM() { + HashMap sbom = new HashMap<>(); + sbom.put("com.example:test-agent-app:jar:1.0-SNAPSHOT", + createClassportInfo("com.example", "1.0-SNAPSHOT", + "com.example:test-agent-app:jar:1.0-SNAPSHOT", + "test-agent-app", false, + "com.example:test-agent-app:jar:1.0-SNAPSHOT", + new String[]{"com.google.code.gson:gson:jar:2.11.0", + "org.apache.commons:commons-lang3:jar:3.17.0"})); + sbom.put("com.google.code.gson:gson:jar:2.11.0", + createClassportInfo("com.google.code.gson", "2.11.0", + "com.google.code.gson:gson:jar:2.11.0", + "gson", true, + "com.example:test-agent-app:jar:1.0-SNAPSHOT", + new String[]{"com.google.errorprone:error_prone_annotations:jar:2.27.0"})); + sbom.put("org.apache.commons:commons-lang3:jar:3.17.0", + createClassportInfo("org.apache.commons", "3.17.0", + "org.apache.commons:commons-lang3:jar:3.17.0", + "commons-lang3", true, + "com.example:test-agent-app:jar:1.0-SNAPSHOT", + new String[]{"org.apache.commons:commons-text:jar:1.12.0"})); + return sbom; + } + + private ClassportInfo createClassportInfo(String group, String version, String id, + String artefact, boolean isDirectDependency, + String sourceProjectId, String[] childIds) { + return new ClassportInfo() { + @Override public String group() { return group; } + @Override public String version() { return version; } + @Override public String id() { return id; } + @Override public String artefact() { return artefact; } + @Override public boolean isDirectDependency() { return isDirectDependency; } + @Override public String sourceProjectId() { return sourceProjectId; } + @Override public String[] childIds() { return childIds; } + @Override public Class annotationType() { return ClassportInfo.class; } + }; + } +} diff --git a/classport-agent/src/test/resources/annotated-classes/StringUtils.class b/classport-agent/src/test/resources/annotated-classes/StringUtils.class new file mode 100644 index 0000000..0b27790 Binary files /dev/null and b/classport-agent/src/test/resources/annotated-classes/StringUtils.class differ diff --git a/classport-agent/src/test/resources/classport-deps/classport-deps-list b/classport-agent/src/test/resources/classport-deps/classport-deps-list new file mode 100644 index 0000000..41751cf --- /dev/null +++ b/classport-agent/src/test/resources/classport-deps/classport-deps-list @@ -0,0 +1,2 @@ +com.google.code.gson:gson:jar:2.11.0 +org.apache.commons:commons-lang3:jar:3.17.0 diff --git a/classport-agent/src/test/resources/classport-deps/classport-deps-tree b/classport-agent/src/test/resources/classport-deps/classport-deps-tree new file mode 100644 index 0000000..3ef7a72 --- /dev/null +++ b/classport-agent/src/test/resources/classport-deps/classport-deps-tree @@ -0,0 +1,3 @@ +com.example:test-agent-app:jar:1.0-SNAPSHOT ++- com.google.code.gson:gson:jar:2.11.0 +\- org.apache.commons:commons-lang3:jar:3.17.0 diff --git a/classport-agent/src/test/resources/not-annotated-classes/StringUtils.class b/classport-agent/src/test/resources/not-annotated-classes/StringUtils.class new file mode 100644 index 0000000..136013c Binary files /dev/null and b/classport-agent/src/test/resources/not-annotated-classes/StringUtils.class differ diff --git a/classport-analyser/pom.xml b/classport-analyser/pom.xml index 93acc88..2b80344 100644 --- a/classport-analyser/pom.xml +++ b/classport-analyser/pom.xml @@ -50,6 +50,21 @@ asm-util 9.6 + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + diff --git a/classport-analyser/src/main/java/io/github/chains_project/classport/analyser/Analyser.java b/classport-analyser/src/main/java/io/github/chains_project/classport/analyser/Analyser.java index 41bde43..8d13e7a 100644 --- a/classport-analyser/src/main/java/io/github/chains_project/classport/analyser/Analyser.java +++ b/classport-analyser/src/main/java/io/github/chains_project/classport/analyser/Analyser.java @@ -30,7 +30,7 @@ public class Analyser { // and use this from there. private static final byte[] magicBytes = new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE }; - private static HashMap getSBOM(JarFile jar) { + public static HashMap getSBOM(JarFile jar) { HashMap sbom = new HashMap<>(); HashMap noAnnotations = new HashMap<>(); try { diff --git a/classport-analyser/src/test/java/AnalyserTest.java b/classport-analyser/src/test/java/AnalyserTest.java new file mode 100644 index 0000000..18fcb09 --- /dev/null +++ b/classport-analyser/src/test/java/AnalyserTest.java @@ -0,0 +1,112 @@ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.jar.JarFile; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import io.github.chains_project.classport.analyser.Analyser; +import io.github.chains_project.classport.analyser.ClassLoadingAdder; +import io.github.chains_project.classport.commons.ClassportInfo; + +class AnalyserTest { + + private final Path annotatedJarPath = Path.of("src/test/resources/annotated-classes/test-agent-app-1.0-SNAPSHOT.jar"); + private final Path notAnnotatedJarPath = Path.of("src/test/resources/not-annotated-classes/test-agent-app-1.0-SNAPSHOT.jar"); + private final String addedClassName = "__classportForceClassLoading"; + private final String addedClassDescriptor = "()V"; + private final Path annotatedClassPath = Path.of("src/test/resources/annotated-classes/Main.class"); + private boolean methodPresent; + private boolean methodInvoked; + + @Test + void testGetSBOM_withAnnotatedClasses() throws IOException { + JarFile jar = new JarFile(annotatedJarPath.toFile()); + HashMap actualSbom = Analyser.getSBOM(jar); + + assertTrue(!actualSbom.isEmpty()); + assertEquals(4, actualSbom.size(), "SBOM should contain 4 annotated classes"); + assertTrue(actualSbom.containsKey("com.example:test-agent-app:jar:1.0-SNAPSHOT"), "SBOM should contain class com.example:test-agent-app:jar:1.0-SNAPSHOT"); + assertTrue(actualSbom.containsKey("com.google.errorprone:error_prone_annotations:jar:2.27.0"), "SBOM should contain class com.google.errorprone:error_prone_annotations:jar:2.27.0"); + assertTrue(actualSbom.containsKey("com.google.code.gson:gson:jar:2.11.0"), "SBOM should contain class com.google.code.gson:gson:jar:2.11.0"); + assertTrue(actualSbom.containsKey("org.apache.commons:commons-lang3:jar:3.17.0"), "SBOM should contain class org.apache.commons:commons-lang3:jar:3.17.0"); + } + + @Test + void testGetSBOM_withNonAnnotatedClasses() throws IOException { + ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + System.setErr(new PrintStream(errContent)); + + JarFile jar = new JarFile(notAnnotatedJarPath.toFile()); + HashMap actualSbom = Analyser.getSBOM(jar); + + assertTrue(actualSbom.isEmpty(),"The sbom should be empty if the jar file does not contain annotated classes"); + String actualMessage = errContent.toString(); + assertTrue(actualMessage.contains("[Warning]"), "The warning message should match the expected pattern"); + } + + @Test + void testForceClassLoading_withAnnotation() throws IOException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { + Path path = annotatedClassPath; + byte[] originalMainClass = Files.readAllBytes(path); + + String[] classesToLoad = {"com/google/errorprone/annotations/MustBeClosed", "com/google/gson/ExclusionStrategy", "org/apache/commons/lang3/BooleanUtils"}; + byte[] modifiedClass = ClassLoadingAdder.forceClassLoading(originalMainClass, classesToLoad); + + assertNotNull(modifiedClass, "Modified class byte array should not be null"); + assertTrue(modifiedClass.length > 0, "Modified class should have content"); + + ClassReader classReader = new ClassReader(modifiedClass); + assertTrue(isAdditionalMethodPresent(classReader), "The additional method __classportForceClassLoading should be present"); + assertTrue(isAdditionalMethodInvoked(classReader), "The additional method __classportForceClassLoading should be invoked"); + + } + + private boolean isAdditionalMethodPresent(ClassReader classReader) { + methodPresent = false; + classReader.accept(new ClassVisitor(Opcodes.ASM9) { + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (name.equals(addedClassName) && descriptor.equals(addedClassDescriptor)) { + methodPresent = true; + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + }, 0); + return methodPresent; + } + + private boolean isAdditionalMethodInvoked(ClassReader classReader) { + methodInvoked = false; + classReader.accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return new MethodVisitor(Opcodes.ASM9) { + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (name.equals(addedClassName) && descriptor.equals(addedClassDescriptor)) { + methodInvoked = true; + } + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + }; + } + + }, 0); + return methodInvoked; + } +} + diff --git a/classport-analyser/src/test/resources/annotated-classes/Main.class b/classport-analyser/src/test/resources/annotated-classes/Main.class new file mode 100644 index 0000000..be9efc0 Binary files /dev/null and b/classport-analyser/src/test/resources/annotated-classes/Main.class differ diff --git a/classport-analyser/src/test/resources/annotated-classes/test-agent-app-1.0-SNAPSHOT.jar b/classport-analyser/src/test/resources/annotated-classes/test-agent-app-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..7bb707a Binary files /dev/null and b/classport-analyser/src/test/resources/annotated-classes/test-agent-app-1.0-SNAPSHOT.jar differ diff --git a/classport-analyser/src/test/resources/not-annotated-classes/test-agent-app-1.0-SNAPSHOT.jar b/classport-analyser/src/test/resources/not-annotated-classes/test-agent-app-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..4ea9617 Binary files /dev/null and b/classport-analyser/src/test/resources/not-annotated-classes/test-agent-app-1.0-SNAPSHOT.jar differ diff --git a/pom.xml b/pom.xml index f4119c5..682c95c 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ https://github.com/chains-project/classport 2024 + @@ -48,6 +49,26 @@ UTF-8 + + + + org.junit.jupiter + junit-jupiter-api + 5.11.3 + + + org.junit.jupiter + junit-jupiter-engine + 5.11.3 + + + org.mockito + mockito-core + 5.14.2 + + + +