Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Expand Up @@ -4,9 +4,12 @@
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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -125,6 +128,22 @@ void multipleEntriesForSameClass() throws Exception {
assertEquals(2, entries.size());
}

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

assertEquals(Collections.singletonList("< 2.0.0"), entry.versionRanges());
assertTrue(entry.isVersionVulnerable("1.9.9"));
assertFalse(entry.isVersionVulnerable("2.0.0"));
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,22 @@ public String safeMethod() {
}
}

public static class ClassToBeStrippedOfLineNumber {
public static int value = 7;
Comment thread
bric3 marked this conversation as resolved.
Outdated

public static int readField() {
return value;
}

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 +188,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 +374,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
Loading