Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions dd-java-agent/appsec/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ ext {
'com.datadog.appsec.config.AppSecFeatures.AutoUserInstrum',
'com.datadog.appsec.AppSecModule.AppSecModuleActivationException',
'com.datadog.appsec.event.ReplaceableEventProducerService',
'com.datadog.appsec.event.data.IntrospectionExcludedTypesTrie',
'com.datadog.appsec.api.security.ApiSecuritySampler.NoOp',
'com.datadog.appsec.sca.ScaStackExclusionTrie',
]
excludedClassesBranchCoverage = [
'com.datadog.appsec.gateway.GatewayBridge',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.datadog.appsec.sca;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import net.bytebuddy.jar.asm.ClassReader;
import net.bytebuddy.jar.asm.ClassWriter;

final class ScaBytecodeTestUtils {

private ScaBytecodeTestUtils() {}

static byte[] bytecodeOf(Class<?> clazz) throws Exception {
String path = clazz.getName().replace('.', '/') + ".class";
try (InputStream is = clazz.getClassLoader().getResourceAsStream(path)) {
assertNotNull(is, "Cannot load bytecode for " + clazz.getName());
ByteArrayOutputStream buf = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int n;
while ((n = is.read(chunk)) != -1) {
buf.write(chunk, 0, n);
}
return buf.toByteArray();
}
}

static Class<?> loadModified(byte[] bytecode) {
return new ClassLoader(ScaBytecodeTestUtils.class.getClassLoader()) {
Class<?> define() {
return defineClass(null, bytecode, 0, bytecode.length);
}
}.define();
}

static byte[] bytecodeWithoutDebugInfo(Class<?> clazz) throws Exception {
ClassReader cr = new ClassReader(bytecodeOf(clazz));
ClassWriter cw = new ClassWriter(0);
cr.accept(cw, ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.datadog.appsec.sca;

import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.StringReader;
Expand Down Expand Up @@ -125,6 +127,29 @@ void multipleEntriesForSameClass() throws Exception {
assertEquals(2, entries.size());
}

@Test
void scaEntryMatchesVersions() {
List<String> expectedRanges = singletonList("< 2.0.0");
Comment thread
bric3 marked this conversation as resolved.
List<ScaSymbol> symbols = singletonList(new ScaSymbol("com/example/Foo", "op"));
ScaEntry entry = new ScaEntry("GHSA-entry", "com.example:lib", expectedRanges, symbols);

assertEquals(expectedRanges, entry.versionRanges());
assertTrue(entry.isVersionVulnerable("1.9.9"));
assertFalse(entry.isVersionVulnerable("2.0.0"));
}

@Test
void scaEntryExposesImmutableLists() {
List<String> ranges = singletonList("< 2.0.0");
List<ScaSymbol> symbols = singletonList(new ScaSymbol("com/example/Foo", "op"));
ScaEntry entry = new ScaEntry("GHSA-entry", "com.example:lib", ranges, symbols);

assertThrows(UnsupportedOperationException.class, () -> entry.versionRanges().add("< 3.0.0"));
assertThrows(
UnsupportedOperationException.class,
() -> entry.symbols().add(new ScaSymbol("com/example/Bar", "op")));
}

@Test
void entryWithMultipleSymbolsInSameClassIndexedOnce() throws Exception {
// An entry with two symbols for the same class (e.g. Yaml.load + Yaml.loadAll) must appear
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.datadog.appsec.sca;

import static com.datadog.appsec.sca.ScaBytecodeTestUtils.bytecodeOf;
import static com.datadog.appsec.sca.ScaBytecodeTestUtils.bytecodeWithoutDebugInfo;
import static com.datadog.appsec.sca.ScaBytecodeTestUtils.loadModified;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand All @@ -10,8 +13,6 @@
import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry.DependencySnapshot;
import datadog.trace.api.telemetry.ScaReachabilityHit;
import datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.util.ArrayList;
Expand Down Expand Up @@ -43,6 +44,24 @@ public String safeMethod() {
}
}

/** Fixture compiled normally, then stripped of line numbers before callback injection. */
public static class ClassToBeStrippedOfLineNumber {
// Intentionally non-final so javac emits a field read instead of inlining a constant.
Comment thread
bric3 marked this conversation as resolved.
private static int runtimeFieldValue = 7;

public static int readField() {
return runtimeFieldValue;
}

public static Object returnArgument(Object value) {
return value;
}

public static String callToString(Object value) {
return value.toString();
}
}

private ScaCveDatabase db;
private ScaReachabilityTransformer transformer;

Expand Down Expand Up @@ -171,6 +190,38 @@ void inject_injectsMultipleMethodsIndependently() throws Exception {
assertTrue(hits.stream().anyMatch(h -> h.symbolName().equals("safeMethod")));
}

@Test
void inject_withoutLineNumbersInjectsBeforeFirstInstruction() throws Exception {
byte[] original = bytecodeWithoutDebugInfo(ClassToBeStrippedOfLineNumber.class);
String className = ClassToBeStrippedOfLineNumber.class.getName();
Map<String, List<ScaMethodCallbackInjector.MethodCallbackSpec>> callbacks = new HashMap<>();
callbacks.put(
"readField",
Collections.singletonList(
spec("GHSA-field", "com.example:lib", "1.0.0", className, "readField")));
callbacks.put(
"returnArgument",
Collections.singletonList(
spec("GHSA-var", "com.example:lib", "1.0.0", className, "returnArgument")));
callbacks.put(
"callToString",
Collections.singletonList(
spec("GHSA-method", "com.example:lib", "1.0.0", className, "callToString")));

Class<?> cls = loadModified(ScaMethodCallbackInjector.inject(original, callbacks));

assertEquals(7, cls.getMethod("readField").invoke(null));
assertEquals("value", cls.getMethod("returnArgument", Object.class).invoke(null, "value"));
assertEquals("value", cls.getMethod("callToString", Object.class).invoke(null, "value"));

List<ScaReachabilityHit> hits = drainHits();
assertEquals(3, hits.size());
assertTrue(hits.stream().allMatch(hit -> hit.line() == 1));
assertTrue(hits.stream().anyMatch(hit -> hit.symbolName().equals("readField")));
assertTrue(hits.stream().anyMatch(hit -> hit.symbolName().equals("returnArgument")));
assertTrue(hits.stream().anyMatch(hit -> hit.symbolName().equals("callToString")));
}

@Test
void inject_sameMethodNameInDifferentClassesProduceIndependentHits() throws Exception {
// Regression test for dedup key bug: if two classes in the same artifact share a method
Expand Down Expand Up @@ -325,24 +376,4 @@ private static ScaMethodCallbackInjector.MethodCallbackSpec spec(
return new ScaMethodCallbackInjector.MethodCallbackSpec(
vulnId, artifact, version, dotClass, method);
}

private static byte[] bytecodeOf(Class<?> clazz) throws Exception {
String path = clazz.getName().replace('.', '/') + ".class";
try (InputStream is = clazz.getClassLoader().getResourceAsStream(path)) {
assertNotNull(is, "Cannot load bytecode for " + clazz.getName());
ByteArrayOutputStream buf = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int n;
while ((n = is.read(chunk)) != -1) buf.write(chunk, 0, n);
return buf.toByteArray();
}
}

private static Class<?> loadModified(byte[] bytecode) {
return new ClassLoader(ScaReachabilityMethodLevelTest.class.getClassLoader()) {
Class<?> define() {
return defineClass(null, bytecode, 0, bytecode.length);
}
}.define();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ void findCallsite_returnsDirectCallerWhenNoIntermediateLibrary() {
assertEquals("yamlHitDirect", result.getMethodName());
}

@Test
void findCallsite_skipsRepeatedVulnerableFrames() {
StackTraceElement[] stack = {
frame(VULNERABLE_CLASS, "load"),
frame(VULNERABLE_CLASS, "loadAll"),
frame("sca.test.TestController", "yamlHitRecursive"),
};

StackTraceElement result = ScaReachabilitySystem.findCallsite(VULNERABLE_CLASS, stack);

assertEquals("sca.test.TestController", result.getClassName());
assertEquals("yamlHitRecursive", result.getMethodName());
}

@Test
void findCallsite_skipsIntermediateLibraryFrameAndReturnsClientCode() {
// com.google.* is excluded by the SCA trie (value >= 1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.datadog.appsec.sca;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry;
import datadog.trace.api.telemetry.ScaReachabilityHit;
import datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback;
import java.lang.instrument.Instrumentation;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

class ScaReachabilitySystemTest {

@AfterEach
void tearDown() {
ScaReachabilityDependencyRegistry.INSTANCE.resetForTesting();
ScaReachabilityCallback.register(null);
}

@Test
void startRegistersTransformerCallbackAndPeriodicWork() {
Instrumentation instrumentation = mock(Instrumentation.class);
when(instrumentation.getAllLoadedClasses()).thenReturn(new Class<?>[0]);

ScaReachabilitySystem.start(instrumentation);

verify(instrumentation).addTransformer(any(ScaReachabilityTransformer.class), eq(true));
assertNotNull(ScaReachabilityDependencyRegistry.INSTANCE.getPeriodicWorkCallback());

ScaReachabilityCallback.onMethodHit(
"GHSA-start", "com.example:lib", "1.0.0", "missing.Vulnerable", "danger", 42);

List<ScaReachabilityDependencyRegistry.DependencySnapshot> snapshots =
ScaReachabilityDependencyRegistry.INSTANCE.drainPendingDependencies();
assertEquals(1, snapshots.size());
assertEquals("com.example:lib", snapshots.get(0).artifact);
assertEquals("1.0.0", snapshots.get(0).version);
assertEquals(1, snapshots.get(0).cves.size());
ScaReachabilityHit hit = snapshots.get(0).cves.get(0).hit;
assertNotNull(hit);
assertEquals("GHSA-start", hit.vulnId());
assertEquals("missing.Vulnerable", hit.className());
assertEquals("danger", hit.symbolName());
assertEquals(42, hit.line());
}
}
Loading