diff --git a/.github/workflows/run-end-to-end.yml b/.github/workflows/run-end-to-end.yml index a9b7c4a8bf4..0d8ba7b0333 100644 --- a/.github/workflows/run-end-to-end.yml +++ b/.github/workflows/run-end-to-end.yml @@ -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 diff --git a/docs/understand/weblogs/end-to-end_weblog.md b/docs/understand/weblogs/end-to-end_weblog.md index 23fa0d6127b..55c4244e6ee 100644 --- a/docs/understand/weblogs/end-to-end_weblog.md +++ b/docs/understand/weblogs/end-to-end_weblog.md @@ -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. diff --git a/manifests/java_lambda.yml b/manifests/java_lambda.yml index adfe282c35d..4dc485300fa 100644 --- a/manifests/java_lambda.yml +++ b/manifests/java_lambda.yml @@ -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 diff --git a/manifests/nodejs_lambda.yml b/manifests/nodejs_lambda.yml index 3b0b4007d34..7920240c596 100644 --- a/manifests/nodejs_lambda.yml +++ b/manifests/nodejs_lambda.yml @@ -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 diff --git a/manifests/python_lambda.yml b/manifests/python_lambda.yml index f9e05136cd5..ef76c50362c 100644 --- a/manifests/python_lambda.yml +++ b/manifests/python_lambda.yml @@ -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) diff --git a/manifests/ruby_lambda.yml b/manifests/ruby_lambda.yml index cd183c66b17..8e805da030b 100644 --- a/manifests/ruby_lambda.yml +++ b/manifests/ruby_lambda.yml @@ -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 diff --git a/tests/appsec/test_sca_reachability.py b/tests/appsec/test_sca_reachability.py index 04e1fdcf30b..aef5a54ef42 100644 --- a/tests/appsec/test_sca_reachability.py +++ b/tests/appsec/test_sca_reachability.py @@ -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", + }, } @@ -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.""" @@ -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.""" @@ -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.""" @@ -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" @@ -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).""" @@ -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" @@ -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" diff --git a/utils/_context/_scenarios/__init__.py b/utils/_context/_scenarios/__init__.py index c79e6396e7a..d6c9620f57d 100644 --- a/utils/_context/_scenarios/__init__.py +++ b/utils/_context/_scenarios/__init__.py @@ -56,6 +56,7 @@ AppsecLambdaDefault, AppsecLambdaInferredSpans, AppsecLambdaRasp, + AppsecLambdaSca, ) from .appsec_rasp import ( AppsecRasp, @@ -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() diff --git a/utils/_context/_scenarios/aws_lambda.py b/utils/_context/_scenarios/aws_lambda.py index ce3e125ac56..30919185d00 100644 --- a/utils/_context/_scenarios/aws_lambda.py +++ b/utils/_context/_scenarios/aws_lambda.py @@ -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__( diff --git a/utils/build/docker/java_lambda/java-alb-multi.Dockerfile b/utils/build/docker/java_lambda/java-alb-multi.Dockerfile index 4d19c09b0b5..74685897f23 100644 --- a/utils/build/docker/java_lambda/java-alb-multi.Dockerfile +++ b/utils/build/docker/java_lambda/java-alb-multi.Dockerfile @@ -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 diff --git a/utils/build/docker/java_lambda/java-alb.Dockerfile b/utils/build/docker/java_lambda/java-alb.Dockerfile index 8b7fadb89ad..14728d91b06 100644 --- a/utils/build/docker/java_lambda/java-alb.Dockerfile +++ b/utils/build/docker/java_lambda/java-alb.Dockerfile @@ -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 diff --git a/utils/build/docker/java_lambda/java-apigw-http.Dockerfile b/utils/build/docker/java_lambda/java-apigw-http.Dockerfile index 8a3913f234e..0b92cb657cc 100644 --- a/utils/build/docker/java_lambda/java-apigw-http.Dockerfile +++ b/utils/build/docker/java_lambda/java-apigw-http.Dockerfile @@ -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 diff --git a/utils/build/docker/java_lambda/java-apigw-rest.Dockerfile b/utils/build/docker/java_lambda/java-apigw-rest.Dockerfile index d91b15ad8e0..2719e780e3f 100644 --- a/utils/build/docker/java_lambda/java-apigw-rest.Dockerfile +++ b/utils/build/docker/java_lambda/java-apigw-rest.Dockerfile @@ -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 diff --git a/utils/build/docker/java_lambda/java-function-url.Dockerfile b/utils/build/docker/java_lambda/java-function-url.Dockerfile index 6ac4014575f..e9b31e0ef99 100644 --- a/utils/build/docker/java_lambda/java-function-url.Dockerfile +++ b/utils/build/docker/java_lambda/java-function-url.Dockerfile @@ -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 diff --git a/utils/build/docker/java_lambda/pom.xml b/utils/build/docker/java_lambda/pom.xml index 4d9ee8f3916..a15bab66620 100644 --- a/utils/build/docker/java_lambda/pom.xml +++ b/utils/build/docker/java_lambda/pom.xml @@ -93,16 +93,44 @@ sqlite-jdbc 3.47.1.0 + + + com.github.junrar + junrar + 7.5.5 + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + copy-sca-dependency + package + copy-dependencies + + com.github.junrar + junrar + ${project.build.directory}/sca-libs + + + + org.apache.maven.plugins maven-shade-plugin 3.2.4 false + + + com.github.junrar:junrar + + diff --git a/utils/build/docker/java_lambda/src/main/java/com/datadoghq/lambda/Handler.java b/utils/build/docker/java_lambda/src/main/java/com/datadoghq/lambda/Handler.java index f93bef3e5e6..da78f1a1380 100644 --- a/utils/build/docker/java_lambda/src/main/java/com/datadoghq/lambda/Handler.java +++ b/utils/build/docker/java_lambda/src/main/java/com/datadoghq/lambda/Handler.java @@ -53,6 +53,10 @@ public class Handler implements RequestHandler { 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 } /** @@ -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)) { diff --git a/utils/build/docker/java_lambda/src/main/java/com/datadoghq/lambda/ScaReachability.java b/utils/build/docker/java_lambda/src/main/java/com/datadoghq/lambda/ScaReachability.java new file mode 100644 index 00000000000..dbc63c55f6b --- /dev/null +++ b/utils/build/docker/java_lambda/src/main/java/com/datadoghq/lambda/ScaReachability.java @@ -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"; + } +} diff --git a/utils/build/docker/lambda_proxy/main.py b/utils/build/docker/lambda_proxy/main.py index 17e5dee2dbf..7588166391d 100644 --- a/utils/build/docker/lambda_proxy/main.py +++ b/utils/build/docker/lambda_proxy/main.py @@ -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"]), diff --git a/utils/scripts/libraries_and_scenarios_rules.yml b/utils/scripts/libraries_and_scenarios_rules.yml index e8f17ae6fe6..806149e4c5d 100644 --- a/utils/scripts/libraries_and_scenarios_rules.yml +++ b/utils/scripts/libraries_and_scenarios_rules.yml @@ -589,7 +589,7 @@ patterns: tests/appsec/test_sca_reachability.py: scenario_groups: null - libraries: [java, python] + libraries: [java, java_lambda, python] ################################# Default rules ################################ # The rules in this section are only applied if no other rule is applicable.