Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fingerprints for JDK classes #63

Merged
merged 61 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
16775bc
feat: add fingerprints for JDK classes
MartinWitt Aug 25, 2023
aabd918
use faster gen with JDK methods
MartinWitt Aug 25, 2023
76bfaaa
up
MartinWitt Aug 25, 2023
aab672e
up
MartinWitt Aug 25, 2023
6553033
up
MartinWitt Aug 25, 2023
891d80a
Merge branch 'main' into molecular-puma
MartinWitt Aug 25, 2023
441743b
spotless
MartinWitt Aug 25, 2023
61e87cd
add more prints and 2 class files
MartinWitt Aug 25, 2023
a175ef1
Merge branch 'main' into molecular-puma
algomaster99 Aug 25, 2023
cac3eac
Please spotless
algomaster99 Aug 25, 2023
96237d0
fix: skip classes that are generated by ASM
algomaster99 Aug 25, 2023
fab4705
up
MartinWitt Sep 1, 2023
c787df7
up
MartinWitt Sep 1, 2023
c6245a2
up
MartinWitt Sep 1, 2023
2c88cb7
up
MartinWitt Sep 1, 2023
8101634
Merge branch 'main' into molecular-puma
MartinWitt Sep 1, 2023
3dc7f3c
up
MartinWitt Sep 1, 2023
7eedf84
up
MartinWitt Sep 4, 2023
259469e
up
MartinWitt Sep 4, 2023
9051114
up
MartinWitt Sep 4, 2023
9a3b0e9
add output capture
MartinWitt Sep 4, 2023
f3f57c1
up
MartinWitt Sep 4, 2023
3949ac5
up
MartinWitt Sep 4, 2023
3f22515
reset to master
MartinWitt Sep 4, 2023
b4ca4fe
remove old logging framework
MartinWitt Sep 4, 2023
10e9fe5
convert to old style
MartinWitt Sep 4, 2023
ca3af8b
remove debug print
MartinWitt Sep 4, 2023
94fc781
up
MartinWitt Sep 4, 2023
8d5a87e
up
MartinWitt Sep 4, 2023
aae441a
up
MartinWitt Sep 4, 2023
8fe1d0c
up
MartinWitt Sep 4, 2023
ea8e838
up
MartinWitt Sep 4, 2023
d305806
spotless
MartinWitt Sep 4, 2023
b24c759
up
MartinWitt Sep 4, 2023
0cca5ce
up
MartinWitt Sep 4, 2023
f3a0c46
use syserr
MartinWitt Sep 4, 2023
eba79a7
up
MartinWitt Sep 4, 2023
92d6e4d
up
MartinWitt Sep 4, 2023
8c3859b
up
MartinWitt Sep 4, 2023
e5f4bfe
/ ->.
MartinWitt Sep 4, 2023
8db8322
also allow JDK package
MartinWitt Sep 4, 2023
251e9c4
enbale scanning of private classes
MartinWitt Sep 4, 2023
e89a183
remove logging
MartinWitt Sep 4, 2023
93a5620
up
MartinWitt Sep 4, 2023
fea9374
reduce log
MartinWitt Sep 4, 2023
bcadc30
up
MartinWitt Sep 4, 2023
29bf50d
up
MartinWitt Sep 4, 2023
c5770f8
spotless
MartinWitt Sep 4, 2023
dc7d9df
cleanup
MartinWitt Sep 4, 2023
54591f3
up
MartinWitt Sep 4, 2023
f79c388
remove old code
MartinWitt Sep 4, 2023
c6eddf3
try removing flag
MartinWitt Sep 4, 2023
3c2b52b
Revert "try removing flag"
MartinWitt Sep 4, 2023
a4cc082
add contract
algomaster99 Sep 4, 2023
b80d078
Refactor test out of nested class
algomaster99 Sep 4, 2023
ac2ffaa
Update watchdog-agent/src/test/java/AgentTest.java
MartinWitt Sep 4, 2023
6b59668
add org.xml and netscape
MartinWitt Sep 5, 2023
6ff64f3
remove old class
MartinWitt Sep 5, 2023
ab259df
remove sysout from testcase
MartinWitt Sep 5, 2023
cc9fd5a
Revert "remove old class"
MartinWitt Sep 5, 2023
e37fbf9
fix testcase
MartinWitt Sep 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions terminator-commons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
</dependency>
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>4.8.162</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.github.algomaster99.terminator.commons.fingerprint;

import java.nio.ByteBuffer;

/**
* A class that represents a JDK class. It contains the name of the class and the bytes of the class as a {@link ByteBuffer}.
*/
public record JdkClass(String name, ByteBuffer bytes) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.github.algomaster99.terminator.commons.fingerprint;

import io.github.classgraph.ClassGraph;
import io.github.classgraph.Resource;
import io.github.classgraph.ScanResult;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import nonapi.io.github.classgraph.classpath.SystemJarFinder;

/**
* The JdkIndexer class provides a utility to list all JDK classes by scanning the JDK used for the execution of the application.
*/
public class JdkIndexer {

/**
* Returns a list of all JDK classes. The list is populated by scanning the JDK used for the execution of this application.
*
* @return a list of all JDK classes, never null.
*/
public static List<JdkClass> listJdkClasses() {
List<JdkClass> jdkClasses = new ArrayList<>();
try (ScanResult scanResult = new ClassGraph()
.enableSystemJarsAndModules()
.disableDirScanning()
.disableJarScanning()
.acceptLibOrExtJars()
.acceptModules("jdk.*", "java.*")
.ignoreClassVisibility()
.enableMemoryMapping()
.scan(); ) {
scanResult.getAllClasses().forEach(classInfo -> {
Resource resource = classInfo.getResource();
if (resource != null) {
byte[] byteBuffer;
try {
byteBuffer = resource.load();
jdkClasses.add(
new JdkClass(classInfo.getName().replaceAll("\\.", "/"), ByteBuffer.wrap(byteBuffer)));
} catch (IOException e) {
System.err.println("Error loading resource " + resource + ": " + e);
}
}
});
}
jdkClasses.addAll(indexJrt());
return jdkClasses;
}

/**
* Creates an index of the external Jrt jar. This jar provides an API for older jvms to access the modules in the JDK.
* The jvm itself does not need this jar.
* @return the list of external jrt jdk classes
*/
private static List<JdkClass> indexJrt() {
List<JdkClass> jdkClasses = new ArrayList<>();
Set<String> jreLibOrExtJars = SystemJarFinder.getJreLibOrExtJars();
for (String path : jreLibOrExtJars) {
try {
jdkClasses.addAll(readJarFile(path));
} catch (Exception e) {
System.err.println("Error reading jar file " + path + ": " + e);
}
}
return jdkClasses;
}

private static List<JdkClass> readJarFile(String jarFilePath) throws IOException {
List<JdkClass> jdkClasses = new ArrayList<>();
try (JarFile jarFile = new JarFile(jarFilePath)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().endsWith(".class")) {
byte[] byteBuffer = jarFile.getInputStream(entry).readAllBytes();
jdkClasses.add(new JdkClass(entry.getName().replace(".class", ""), ByteBuffer.wrap(byteBuffer)));
}
}
}
return jdkClasses;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,21 @@ public static boolean isSynthetic(byte[] classfileBytes) {
ClassReader reader = new ClassReader(classfileBytes);
return (reader.getAccess() & Opcodes.ACC_SYNTHETIC) != 0;
}

/**
* Skip classes inherited from {@link jdk.internal.reflect.MagicAccessorImpl} because they are generated at runtime using ASM.
*/
public static boolean isGeneratedClassExtendingMagicAccessor(byte[] classfileBytes) {
ClassReader reader = new ClassReader(classfileBytes);
try {
return RuntimeClass.class
.getClassLoader()
.loadClass(reader.getSuperName().replace("/", "."))
.getSuperclass()
.getName()
.equals("jdk.internal.reflect.MagicAccessorImpl");
} catch (ClassNotFoundException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.github.algomaster99.terminator.commons.fingerprint.provenance;

import io.github.algomaster99.terminator.commons.fingerprint.classfile.ClassFileAttributes;

public record Jdk(ClassFileAttributes classFileAttributes) implements Provenance {
MartinWitt marked this conversation as resolved.
Show resolved Hide resolved
@Override
public ClassFileAttributes classFileAttributes() {
return classFileAttributes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ void isSyntheticClass_true() throws IOException {
assertThat(RuntimeClass.isSynthetic(modifiedBytes)).isTrue();
}

@Test
void isGeneratedClassExtendingMagicAccessor_true() throws IOException {
Path generatedConstructorAccessor = CLASSFILE.resolve("GeneratedConstructorAccessor15.class");
assertThat(RuntimeClass.isGeneratedClassExtendingMagicAccessor(
Files.readAllBytes(generatedConstructorAccessor)))
.isTrue();

Path generatedMethodAccessor = CLASSFILE.resolve("GeneratedMethodAccessor1.class");
assertThat(RuntimeClass.isGeneratedClassExtendingMagicAccessor(Files.readAllBytes(generatedMethodAccessor)))
.isTrue();
}

private static byte[] makeClassfileSynthetic(byte[] classfileBytes) {
ClassNode classNode = new ClassNode();
ClassReader classReader = new ClassReader(classfileBytes);
Expand Down
Binary file not shown.
Binary file not shown.
61 changes: 60 additions & 1 deletion watchdog-agent/src/main/java/io/github/algomaster99/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@
import io.github.algomaster99.terminator.commons.cyclonedx.Bom14Schema;
import io.github.algomaster99.terminator.commons.cyclonedx.Component;
import io.github.algomaster99.terminator.commons.cyclonedx.CycloneDX;
import io.github.algomaster99.terminator.commons.fingerprint.JdkIndexer;
import io.github.algomaster99.terminator.commons.fingerprint.classfile.ClassFileAttributes;
import io.github.algomaster99.terminator.commons.fingerprint.classfile.ClassfileVersion;
import io.github.algomaster99.terminator.commons.fingerprint.classfile.HashComputer;
import io.github.algomaster99.terminator.commons.fingerprint.provenance.Jdk;
import io.github.algomaster99.terminator.commons.fingerprint.provenance.Provenance;
import io.github.algomaster99.terminator.commons.jar.JarDownloader;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -23,7 +31,7 @@ public class Options {

private static final Logger LOGGER = LoggerFactory.getLogger(Options.class);
private Map<String, List<Provenance>> fingerprints = new HashMap<>();

private Map<String, List<Provenance>> jdkFingerprints = new HashMap<>();
private boolean skipShutdown = false;

private boolean isSbomPassed = false;
Expand Down Expand Up @@ -91,12 +99,17 @@ public Options(String agentArgs) {
} else {
LOGGER.info("Taking fingerprint from file: " + fingerprints);
}
processJdk();
}

public Map<String, List<Provenance>> getFingerprints() {
return fingerprints;
}

public Map<String, List<Provenance>> getJdkFingerprints() {
return jdkFingerprints;
}

public boolean shouldSkipShutdown() {
return skipShutdown;
}
Expand Down Expand Up @@ -151,4 +164,50 @@ private void processAllComponents(Bom14Schema sbom) {
}
}
}

private void processJdk() {
JdkIndexer.listJdkClasses().forEach(resource -> {
try {
byte[] classfileBytes = toArray(resource.bytes());
String classfileVersion = ClassfileVersion.getVersion(classfileBytes);
String hash = HashComputer.computeHash(classfileBytes, algorithm);
jdkFingerprints.computeIfAbsent(
resource.name(),
k -> new ArrayList<>(
List.of((new Jdk(new ClassFileAttributes(classfileVersion, hash, algorithm))))));
jdkFingerprints.computeIfPresent(resource.name(), (k, v) -> {
v.add(new Jdk(new ClassFileAttributes(classfileVersion, hash, algorithm)));
return v;
});
fingerprints.computeIfAbsent(
resource.name(),
k -> new ArrayList<>(
List.of((new Jdk(new ClassFileAttributes(classfileVersion, hash, algorithm))))));
fingerprints.computeIfPresent(resource.name(), (k, v) -> {
v.add(new Jdk(new ClassFileAttributes(classfileVersion, hash, algorithm)));
return v;
});
} catch (NoSuchAlgorithmException e) {
LOGGER.error("Failed to compute hash with algorithm: " + algorithm, e);
throw new RuntimeException(e);
} catch (Exception e) {
LOGGER.error("Failed to compute hash for: " + resource, e);
}
});
}

/**
* Converts a bytebuffer to a byte array. If the buffer has an array, it returns it, otherwise it copies the bytes. This is needed because the buffer is not guaranteed to have an array.
* See {@link java.nio.ByteBuffer#hasArray()} and {@link java.nio.DirectByteBuffer}.
* @param buffer the buffer to convert
* @return the byte array
*/
private byte[] toArray(ByteBuffer buffer) {
if (buffer.hasArray()) {
return buffer.array();
}
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return bytes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
public class Terminator {
private static Options options;

private static final List<String> INTERNAL_PACKAGES =
List.of("java/", "javax/", "jdk/", "sun/", "com/sun/", "org/xml/sax", "org/w3c/dom/");

MartinWitt marked this conversation as resolved.
Show resolved Hide resolved
public static void premain(String agentArgs, Instrumentation inst) {
options = new Options(agentArgs);
inst.addTransformer(new ClassFileTransformer() {
Expand All @@ -27,17 +24,21 @@ public byte[] transform(
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {

return isLoadedClassWhitelisted(className, classfileBuffer);
}
});
}

private static byte[] isLoadedClassWhitelisted(String className, byte[] classfileBuffer) {
Map<String, List<Provenance>> fingerprints = options.getFingerprints();
if (RuntimeClass.isProxyClass(classfileBuffer)) {
if (RuntimeClass.isProxyClass(classfileBuffer)
|| RuntimeClass.isGeneratedClassExtendingMagicAccessor(classfileBuffer)) {
return classfileBuffer;
}
if (INTERNAL_PACKAGES.stream().anyMatch(className::startsWith)) {
if (className.contains("$")) {
// FIXME: we need to check inner classes without loading them. Maybe add the hashes for inner classes in the
// fingerprints?
Comment on lines +40 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is added by default.

public class A {
static class B { }
}

will be compiled to A.class and A$B.class. Or do you mean something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be compiled to a single class file. The files are currently missing in the index for jars.

return classfileBuffer;
}
for (String expectedClassName : fingerprints.keySet()) {
Expand Down
7 changes: 2 additions & 5 deletions watchdog-agent/src/test/java/AgentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ void shouldDisallowLoadingCustomJDKClass() throws MavenInvocationException, IOEx
}

@Test
void sorald_0_8_5_shouldExitWith_1() throws IOException, InterruptedException {
void sorald_0_8_5_shouldExitWith_0() throws IOException, InterruptedException {
// contract: sorald 0.8.5 should execute as the SBOM + external jars has every dependency.
Path project = Paths.get("src/test/resources/sorald-0.8.5");

Path sbom = project.resolve("bom.json");
Expand All @@ -93,7 +94,6 @@ void sorald_0_8_5_shouldExitWith_1() throws IOException, InterruptedException {

Process p = pb.start();
int exitCode = p.waitFor();

assertThat(exitCode).isEqualTo(0);
}

Expand Down Expand Up @@ -124,7 +124,6 @@ void spoon_10_4_0_depscan_4_2_2() throws IOException, InterruptedException {
private int runSpoonWithSbom(Path sbom) throws IOException, InterruptedException {
Path spoonExecutable = project.resolve("spoon-core-10.4.0-jar-with-dependencies.jar");
Path workload = project.resolve("Main.java").toAbsolutePath();

String agentArgs = "sbom=" + sbom;
String[] cmd = {
"java",
Expand Down Expand Up @@ -206,11 +205,9 @@ private static void deleteContentsOfFile(String file) throws InterruptedExceptio
private static String getAgentPath(String agentArgs) throws IOException {
String tempDir = System.getProperty("java.io.tmpdir");
Path traceCollector = Path.of(tempDir, "watchdog-agent.jar");

try (InputStream traceCollectorStream = Terminator.class.getResourceAsStream("/watchdog-agent.jar")) {
Files.copy(traceCollectorStream, traceCollector, StandardCopyOption.REPLACE_EXISTING);
}

return traceCollector.toAbsolutePath() + "=" + agentArgs;
}

Expand Down
20 changes: 20 additions & 0 deletions watchdog-agent/src/test/java/OptionsTest.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import static io.github.algomaster99.terminator.commons.fingerprint.ParsingHelper.deserializeFingerprints;
import static org.assertj.core.api.Assertions.assertThat;

import io.github.algomaster99.Options;
import io.github.algomaster99.terminator.commons.fingerprint.provenance.Jar;
import io.github.algomaster99.terminator.commons.fingerprint.provenance.Maven;
import io.github.algomaster99.terminator.commons.fingerprint.provenance.Provenance;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -46,4 +48,22 @@ void maven_jar() throws NoSuchMethodException, InvocationTargetException, Illega
.hasAtLeastOneElementOfType(Jar.class);
}
}

@Test
void verifyIfJdkIndexerFindsJdkClassesDeterministically() throws Exception {
// generating 2 times the jdk fingerprint should result in the same fingerprint
Options options = new Options("skipShutdown=true");
Options options2 = new Options("skipShutdown=true");
assertThat(options.getJdkFingerprints()).isNotEmpty();
assertThat(options2.getJdkFingerprints()).isNotEmpty();
assertThat(options.getJdkFingerprints()).isEqualTo(options2.getJdkFingerprints());
System.out.println(options.getJdkFingerprints().size());
}

@Test
void verifyJdkIndexerFindsOrgXmlSax() throws Exception {
Options options = new Options("skipShutdown=true");
var var = options.getJdkFingerprints().keySet().stream().collect(Collectors.toSet());
assertThat(var).contains("org/xml/sax/helpers/NamespaceSupport");
}
}