Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
44c29a8
Activate Java Lambda AppSec and API Security system tests
claponcet Oct 21, 2026
b2cfbbe
Merge branch 'main' into clara.poncet/java-lambda-response-schema-weblog
claponcet Oct 21, 2026
7702f56
Fix Lambda proxy events and Java weblog payload handling
claponcet Oct 21, 2026
8f13eba
Preserve Lambda request headers in the test proxy
claponcet Oct 22, 2026
7f5e232
Merge branch 'main' into clara.poncet/java-lambda-response-schema-weblog
claponcet Oct 23, 2026
5f2b289
Merge branch 'main' into clara.poncet/java-lambda-response-schema-weblog
claponcet Oct 23, 2026
91fad85
Merge branch 'main' into clara.poncet/java-lambda-response-schema-weblog
claponcet Oct 26, 2026
4335108
Gate Lambda tests requiring fixes on Java 1.68.0-SNAPSHOT
claponcet Oct 26, 2026
a0cd2bb
Add Java Lambda SCA system-test coverage
claponcet Oct 26, 2026
b11338a
Fix Lambda SCA assertions and test with AppSec disabled
claponcet Oct 27, 2026
693425a
Merge branch 'main' into clara.poncet/java-lambda-sca
claponcet Oct 27, 2026
35b779a
Restore Lambda SCA scenario and test activation after merge
claponcet Oct 27, 2026
e348b6f
Enable Java Lambda SCA tests from released 1.68.0
claponcet Oct 27, 2026
9b8ba14
Declare SCA reachability gaps in non-Java Lambda manifests
claponcet Oct 28, 2026
c05a4db
Merge branch 'main' into clara.poncet/java-lambda-sca
claponcet Oct 28, 2026
2356c4d
Merge branch 'main' into clara.poncet/java-lambda-sca
claponcet Oct 28, 2026
81127bb
Address Java Lambda SCA reachability review feedback
claponcet Oct 29, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/run-end-to-end.yml
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,9 @@ jobs:
- name: Run APPSEC_LAMBDA_API_SECURITY scenario
if: steps.build.outcome == 'success' && !cancelled() && contains(inputs.scenarios, '"APPSEC_LAMBDA_API_SECURITY"')
run: ./run.sh APPSEC_LAMBDA_API_SECURITY
- name: Run APPSEC_LAMBDA_SCA scenario
if: steps.build.outcome == 'success' && !cancelled() && contains(inputs.scenarios, '"APPSEC_LAMBDA_SCA"')
run: ./run.sh APPSEC_LAMBDA_SCA
- name: Run APPSEC_LAMBDA_RASP scenario
if: steps.build.outcome == 'success' && !cancelled() && contains(inputs.scenarios, '"APPSEC_LAMBDA_RASP"')
run: ./run.sh APPSEC_LAMBDA_RASP
Expand Down
2 changes: 2 additions & 0 deletions docs/understand/weblogs/end-to-end_weblog.md
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,8 @@ Alternate call site for the same vulnerable function targeted by `/sca/vulnerabl
test first-hit-wins deduplication: when the same CVE is triggered from two different call sites, only the
first occurrence is reported in the `reached` array.

The Java Lambda weblogs also implement both SCA endpoints, using Junrar 7.5.5 kept as a separate JAR. The vulnerable class is preloaded during handler initialization.

### GET /log/library

This endpoint facilitates logging a message using a logging library. It is primarily designed for testing log injection functionality. Weblog apps must log using JSON format.
Expand Down
5 changes: 5 additions & 0 deletions manifests/java_lambda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ manifest:
tests/appsec/test_reports.py::Test_TagsFromRule: v1.67.0
tests/appsec/test_reports.py::Test_TagsFromRule::test_category: v1.68.0
tests/appsec/test_reports.py::Test_TagsFromRule::test_type: v1.68.0
tests/appsec/test_sca_reachability.py::Test_SCA_Reachability_CVE_After_Vulnerable_Call: v1.68.0
tests/appsec/test_sca_reachability.py::Test_SCA_Reachability_CVE_Registered_At_Load_Time: v1.68.0
tests/appsec/test_sca_reachability.py::Test_SCA_Reachability_Deduplication: v1.68.0
tests/appsec/test_sca_reachability.py::Test_SCA_Reachability_Dependencies_Have_Metadata: v1.68.0
tests/appsec/test_sca_reachability.py::Test_SCA_Reachability_First_Hit_Wins: v1.68.0
tests/appsec/test_trace_tagging.py::Test_TraceTaggingRules: v1.67.0
tests/appsec/test_trace_tagging.py::Test_TraceTaggingRules::test_rule_with_attributes_keep_event: v1.68.0
tests/appsec/test_trace_tagging.py::Test_TraceTaggingRules::test_rule_with_attributes_keep_no_event: v1.68.0
Expand Down
1 change: 1 addition & 0 deletions manifests/nodejs_lambda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ manifest:
tests/appsec/test_reports.py::Test_RequestHeaders: '>=12.142.0' # TODO: a lower version might be supported
tests/appsec/test_reports.py::Test_StatusCode: missing_feature
tests/appsec/test_reports.py::Test_TagsFromRule: '>=12.142.0' # TODO: a lower version might be supported
tests/appsec/test_sca_reachability.py: missing_feature (SCA runtime reachability is not implemented)
tests/appsec/test_trace_tagging.py::Test_TraceTaggingRules: # TODO: a lower version might be supported
- weblog_declaration:
'*': missing_feature
Expand Down
1 change: 1 addition & 0 deletions manifests/python_lambda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ manifest:
tests/appsec/test_reports.py::Test_Info: v7.112.0
tests/appsec/test_reports.py::Test_RequestHeaders: v7.112.0
tests/appsec/test_reports.py::Test_StatusCode: v7.112.0
tests/appsec/test_sca_reachability.py: incomplete_test_app (Lambda SCA dependency fixtures and vulnerable-call endpoints are not implemented)
tests/appsec/test_trace_tagging.py::Test_TraceTaggingRules: v8.113.0
tests/appsec/test_traces.py::Test_AppSecEventSpanTags: v7.112.0
tests/appsec/test_traces.py::Test_AppSecEventSpanTags::test_header_collection: bug (APPSEC-58202)
Expand Down
1 change: 1 addition & 0 deletions manifests/ruby_lambda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ manifest:
tests/appsec/test_reports.py::Test_Info: v3.29.0
tests/appsec/test_reports.py::Test_RequestHeaders: v3.29.0
tests/appsec/test_reports.py::Test_StatusCode: v3.29.0
tests/appsec/test_sca_reachability.py: missing_feature (SCA runtime reachability is not implemented)
tests/appsec/test_trace_tagging.py::Test_TraceTaggingRules: v3.29.0
tests/appsec/test_trace_tagging.py::Test_TraceTaggingRules::test_rule_with_attributes_no_keep_event: v3.30.0
tests/appsec/test_trace_tagging.py::Test_TraceTaggingRules::test_rule_with_attributes_no_keep_no_event: v3.30.0
Expand Down
19 changes: 17 additions & 2 deletions tests/appsec/test_sca_reachability.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
"path": "com.datadoghq.system_tests.springboot.ScaReachability",
"symbol": "junrarHit",
},
"java_lambda": {
"cve_id": ["GHSA-hf5p-q87m-crj7"],
"vulnerable_dep": "junrar",
"path": "com.datadoghq.lambda.ScaReachability",
"symbol": "junrarHit",
},
}


Expand Down Expand Up @@ -112,6 +118,7 @@ def _get_deps_with_reachability_metadata() -> set[str]:
@rfc(SCA_REACHABILITY_RFC)
@features.runtime_sca_reachability
@scenarios.runtime_sca_reachability
@scenarios.appsec_lambda_sca
class Test_SCA_Reachability_Dependencies_Have_Metadata:
"""When DD_APPSEC_SCA_ENABLED=true, vulnerable dependencies have reachability metadata."""

Expand All @@ -129,6 +136,7 @@ def test_dependencies_have_metadata_key(self) -> None:
@rfc(SCA_REACHABILITY_RFC)
@features.runtime_sca_reachability
@scenarios.runtime_sca_reachability
@scenarios.appsec_lambda_sca
class Test_SCA_Reachability_CVE_Registered_At_Load_Time:
"""CVEs are registered at startup with reached=[] before any vulnerable call."""

Expand All @@ -148,6 +156,7 @@ def test_cve_registered_at_load_time(self) -> None:
@rfc(SCA_REACHABILITY_RFC)
@features.runtime_sca_reachability
@scenarios.runtime_sca_reachability
@scenarios.appsec_lambda_sca
class Test_SCA_Reachability_CVE_After_Vulnerable_Call:
"""Calling a vulnerable function reports CVE with caller info in reached array."""

Expand All @@ -156,6 +165,7 @@ def setup_cve_metadata_after_vulnerable_call(self) -> None:
self.r1 = weblog.get("/sca/vulnerable-call")

def test_cve_metadata_after_vulnerable_call(self) -> None:
assert self.r1.status_code == 200
cve_entries = _get_dependency_cve_metadata(_vulnerable_dep(), _cve_id())
assert len(cve_entries) >= 1, f"{_cve_id()} not found in {_vulnerable_dep()} metadata"

Expand Down Expand Up @@ -186,6 +196,7 @@ def test_cve_metadata_after_vulnerable_call(self) -> None:
@rfc(SCA_REACHABILITY_RFC)
@features.runtime_sca_reachability
@scenarios.runtime_sca_reachability
@scenarios.appsec_lambda_sca
class Test_SCA_Reachability_First_Hit_Wins:
"""Two call sites for same CVE: only first hit is reported (max 1 reached per entry)."""

Expand All @@ -195,6 +206,8 @@ def setup_same_cve_first_hit_wins(self) -> None:
self.r2 = weblog.get("/sca/vulnerable-call-alt")

def test_same_cve_first_hit_wins(self) -> None:
assert self.r1.status_code == 200
assert self.r2.status_code == 200
cve_entries = _get_dependency_cve_metadata(_vulnerable_dep(), _cve_id())
assert len(cve_entries) >= 1, f"{_cve_id()} not found in {_vulnerable_dep()} metadata"

Expand Down Expand Up @@ -234,15 +247,17 @@ def test_same_cve_first_hit_wins(self) -> None:
@rfc(SCA_REACHABILITY_RFC)
@features.runtime_sca_reachability
@scenarios.runtime_sca_reachability
@scenarios.appsec_lambda_sca
class Test_SCA_Reachability_Deduplication:
"""Repeated calls from same site for same CVE: still max 1 reached entry."""

def setup_deduplication_repeated_calls(self) -> None:
self.r0 = weblog.get("/")
for _ in range(5):
weblog.get("/sca/vulnerable-call")
self.responses = [weblog.get("/sca/vulnerable-call") for _ in range(5)]

def test_deduplication_repeated_calls(self) -> None:
for response in self.responses:
assert response.status_code == 200
cve_entries = _get_dependency_cve_metadata(_vulnerable_dep(), _cve_id())
assert len(cve_entries) >= 1, f"{_cve_id()} not found in {_vulnerable_dep()} metadata"

Expand Down
2 changes: 2 additions & 0 deletions utils/_context/_scenarios/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
AppsecLambdaDefault,
AppsecLambdaInferredSpans,
AppsecLambdaRasp,
AppsecLambdaSca,
)
from .appsec_rasp import (
AppsecRasp,
Expand Down Expand Up @@ -399,6 +400,7 @@ class _Scenarios:
appsec_lambda_default = AppsecLambdaDefault()
appsec_lambda_blocking = AppsecLambdaBlocking()
appsec_lambda_api_security = AppsecLambdaApiSecurity()
appsec_lambda_sca = AppsecLambdaSca()
appsec_lambda_rasp = AppsecLambdaRasp()
appsec_lambda_inferred_spans = AppsecLambdaInferredSpans()
otel_collector = OtelCollectorScenario()
Expand Down
10 changes: 10 additions & 0 deletions utils/_context/_scenarios/aws_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,16 @@ def __init__(self) -> None:
)


class AppsecLambdaSca(_LambdaScenario):
def __init__(self) -> None:
super().__init__(
"APPSEC_LAMBDA_SCA",
weblog_env={"DD_APPSEC_ENABLED": "false", "DD_APPSEC_SCA_ENABLED": "true"},
doc="SCA dependency metadata and runtime reachability in Lambda with AppSec disabled",
scenario_groups=[scenario_groups.appsec, scenario_groups.appsec_lambda],
)


class AppsecLambdaRasp(_LambdaScenario):
def __init__(self) -> None:
super().__init__(
Expand Down
1 change: 1 addition & 0 deletions utils/build/docker/java_lambda/java-alb-multi.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN /binaries/install_ddtrace.sh
# Copy built JAR and extract to Lambda task root
COPY --from=builder /build/target/java-lambda-weblog-*.jar /tmp/app.jar
RUN cd ${LAMBDA_TASK_ROOT} && jar xf /tmp/app.jar && rm /tmp/app.jar
COPY --from=builder /build/target/sca-libs/*.jar ${LAMBDA_TASK_ROOT}/lib/

# Copy Datadog agent and entrypoint
RUN cp /dd-tracer/dd-java-agent.jar ${LAMBDA_TASK_ROOT}/dd-java-agent.jar
Expand Down
1 change: 1 addition & 0 deletions utils/build/docker/java_lambda/java-alb.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN /binaries/install_ddtrace.sh
# Copy built JAR and extract to Lambda task root
COPY --from=builder /build/target/java-lambda-weblog-*.jar /tmp/app.jar
RUN cd ${LAMBDA_TASK_ROOT} && jar xf /tmp/app.jar && rm /tmp/app.jar
COPY --from=builder /build/target/sca-libs/*.jar ${LAMBDA_TASK_ROOT}/lib/

# Copy Datadog agent and entrypoint
RUN cp /dd-tracer/dd-java-agent.jar ${LAMBDA_TASK_ROOT}/dd-java-agent.jar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN /binaries/install_ddtrace.sh
# Copy built JAR and extract to Lambda task root
COPY --from=builder /build/target/java-lambda-weblog-*.jar /tmp/app.jar
RUN cd ${LAMBDA_TASK_ROOT} && jar xf /tmp/app.jar && rm /tmp/app.jar
COPY --from=builder /build/target/sca-libs/*.jar ${LAMBDA_TASK_ROOT}/lib/

# Copy Datadog agent and entrypoint
RUN cp /dd-tracer/dd-java-agent.jar ${LAMBDA_TASK_ROOT}/dd-java-agent.jar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN /binaries/install_ddtrace.sh
# Copy built JAR and extract to Lambda task root
COPY --from=builder /build/target/java-lambda-weblog-*.jar /tmp/app.jar
RUN cd ${LAMBDA_TASK_ROOT} && jar xf /tmp/app.jar && rm /tmp/app.jar
COPY --from=builder /build/target/sca-libs/*.jar ${LAMBDA_TASK_ROOT}/lib/

# Copy Datadog agent and entrypoint
RUN cp /dd-tracer/dd-java-agent.jar ${LAMBDA_TASK_ROOT}/dd-java-agent.jar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN /binaries/install_ddtrace.sh
# Copy built JAR and extract to Lambda task root
COPY --from=builder /build/target/java-lambda-weblog-*.jar /tmp/app.jar
RUN cd ${LAMBDA_TASK_ROOT} && jar xf /tmp/app.jar && rm /tmp/app.jar
COPY --from=builder /build/target/sca-libs/*.jar ${LAMBDA_TASK_ROOT}/lib/

# Copy Datadog agent and entrypoint
RUN cp /dd-tracer/dd-java-agent.jar ${LAMBDA_TASK_ROOT}/dd-java-agent.jar
Expand Down
28 changes: 28 additions & 0 deletions utils/build/docker/java_lambda/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,44 @@
<artifactId>sqlite-jdbc</artifactId>
<version>3.47.1.0</version>
</dependency>
<!-- Kept as a separate JAR so runtime SCA can resolve its Maven identity. -->
<dependency>
<groupId>com.github.junrar</groupId>
<artifactId>junrar</artifactId>
<version>7.5.5</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<id>copy-sca-dependency</id>
<phase>package</phase>
<goals><goal>copy-dependencies</goal></goals>
<configuration>
<includeGroupIds>com.github.junrar</includeGroupIds>
<includeArtifactIds>junrar</includeArtifactIds>
<outputDirectory>${project.build.directory}/sca-libs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<artifactSet>
<excludes>
<exclude>com.github.junrar:junrar</exclude>
</excludes>
</artifactSet>
</configuration>
<executions>
<execution>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public class Handler implements RequestHandler<Object, Object> {
private static final String TRACK_CUSTOM_APPSEC_EVENT_NAME = "system_tests_appsec_event";
private static final String TRACK_USER = "system_tests_user";

static {
ScaReachability.preloadVulnerableClass();
}

private enum ResponseType { API_GATEWAY_REST, API_GATEWAY_HTTP, ALB, ALB_MULTI }

/**
Expand Down Expand Up @@ -241,6 +245,10 @@ private Object routeRequest(RequestContext ctx) {
// Route to handlers
if (ctx.path.equals("/") || ctx.path.isEmpty()) {
return handleRoot(ctx);
} else if (ctx.path.equals("/sca/vulnerable-call") && "GET".equals(ctx.method)) {
return createResponse(ctx, ScaReachability.junrarHit(), 200);
} else if (ctx.path.equals("/sca/vulnerable-call-alt") && "GET".equals(ctx.method)) {
return createResponse(ctx, ScaReachability.junrarHitAlt(), 200);
} else if (ctx.path.equals("/headers") && "GET".equals(ctx.method)) {
return handleHeaders(ctx);
} else if (ctx.path.equals("/healthcheck") && "GET".equals(ctx.method)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.datadoghq.lambda;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ScaReachability {

public static void preloadVulnerableClass() {
// Force LocalFolderExtractor class load during Lambda initialization so the SCA transformer can
// register GHSA-hf5p-q87m-crj7 with reached=[] before any HTTP request triggers junrarHit().
// Without this, the class loads lazily on the first request, so registerCve() and recordHit()
// fire in the same request with no heartbeat window between them.
try {
Class.forName("com.github.junrar.LocalFolderExtractor");
} catch (Exception ignored) {}
}

public static String junrarHit() {
try {
Class<?> cls = Class.forName("com.github.junrar.LocalFolderExtractor");
Constructor<?> ctor = cls.getDeclaredConstructors()[0];
ctor.setAccessible(true);
Object instance = ctor.newInstance(new File("/tmp"));
Class<?> fileHeaderClass = Class.forName("com.github.junrar.rarfile.FileHeader");
Method method = cls.getDeclaredMethod("createDirectory", fileHeaderClass);
method.setAccessible(true);
method.invoke(instance, (Object) null);
} catch (InvocationTargetException e) {
if (!(e.getCause() instanceof NullPointerException)) {
throw new IllegalStateException("Unexpected exception from Junrar", e.getCause());
}
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Failed to invoke Junrar", e);
}
return "OK";
}

// Keep the reflective invocation in each method: a shared helper would report the same caller
// symbol for both endpoints, preventing First_Hit_Wins from distinguishing the call sites.
public static String junrarHitAlt() {
try {
Class<?> cls = Class.forName("com.github.junrar.LocalFolderExtractor");
Constructor<?> ctor = cls.getDeclaredConstructors()[0];
ctor.setAccessible(true);
Object instance = ctor.newInstance(new File("/tmp"));
Class<?> fileHeaderClass = Class.forName("com.github.junrar.rarfile.FileHeader");
Method method = cls.getDeclaredMethod("createDirectory", fileHeaderClass);
method.setAccessible(true);
method.invoke(instance, (Object) null);
} catch (InvocationTargetException e) {
if (!(e.getCause() instanceof NullPointerException)) {
throw new IllegalStateException("Unexpected exception from Junrar", e.getCause());
}
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Failed to invoke Junrar", e);
}
return "OK";
}
}
2 changes: 2 additions & 0 deletions utils/build/docker/lambda_proxy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ def invoke_lambda_function_application_load_balancer_multi_value_headers():
exit(1)

ROUTES = [
("/sca/vulnerable-call", ["GET"]),
("/sca/vulnerable-call-alt", ["GET"]),
("/", ["GET", "POST", "OPTIONS"]),
("/finger_print", ["GET"]),
("/headers", ["GET"]),
Expand Down
2 changes: 1 addition & 1 deletion utils/scripts/libraries_and_scenarios_rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ patterns:

tests/appsec/test_sca_reachability.py:
scenario_groups: null
libraries: [java, python]
libraries: [java, java_lambda, python]
Comment thread
cbeauchesne marked this conversation as resolved.
Resolved

################################# Default rules ################################
# The rules in this section are only applied if no other rule is applicable.
Expand Down
Loading