diff --git a/manifests/agent.yml b/manifests/agent.yml index 2ce3cad9ebb..5ad8e8e39f2 100644 --- a/manifests/agent.yml +++ b/manifests/agent.yml @@ -7,7 +7,9 @@ manifest: tests/appsec/smoke_tests/test_apm_standalone.py: - component_version: "<7.77.0-0" declaration: irrelevant (APM Standalone option was added in 7.77.0) - tests/cws/test_thread_context_sharing.py::Test_ThreadContextSharing: bug (APPSEC-70532) + tests/cws/test_thread_context_sharing.py::Test_ThreadContextSharing: + - declaration: missing_feature (Trace collection not available in agents pre 7.84) + component_version: '<7.84.0-devel' tests/debugger/test_debugger_condition_errors.py::Test_Debugger_Invalid_Condition_DSL: - component_version: "<7.77.1" weblog_declaration: diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 101d6548197..bc132b8f86d 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -1675,7 +1675,10 @@ manifest: tests/auto_inject/test_auto_inject_install.py::TestContainerAutoInjectInstallScriptAppsec: *ref_5_43_0 tests/auto_inject/test_auto_inject_install.py::TestHostAutoInjectInstallScriptAppsec: *ref_5_43_0 tests/auto_inject/test_auto_inject_install.py::TestSimpleInstallerAutoInjectManualAppsec: *ref_5_43_0 - tests/cws/test_thread_context_sharing.py::Test_ThreadContextSharing: missing_feature (missing /security/thread_context_sharing endpoint on weblog) + tests/cws/test_thread_context_sharing.py::Test_ThreadContextSharing: + - weblog_declaration: + "*": irrelevant (requires AsyncContextFrame, Node.js 22.9+; covered on fastify) + fastify: *ref_4_1_0 tests/debugger/test_debugger_capture_expressions.py::Test_Debugger_Line_Capture_Expressions: - weblog_declaration: "*": irrelevant diff --git a/mirror_images.lock.yaml b/mirror_images.lock.yaml index 795739ade8c..5a1c27dd81a 100644 --- a/mirror_images.lock.yaml +++ b/mirror_images.lock.yaml @@ -166,10 +166,18 @@ images: digest: sha256:b71678692a476b6acc3eb235edbb98bbb9c6b943e17c246ecb24937276807bc3 target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:django-py3.13.base-d8262a2c816c tag: django-py3.13.base-d8262a2c816c + datadog/system-tests:express4-typescript.base-5a66898a3a49: + digest: sha256:9e5fd36cdd73416a07c690fc3d11ca6feef6988cb1b882de4ef1de4d594eee24 + target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:express4-typescript.base-5a66898a3a49 + tag: express4-typescript.base-5a66898a3a49 datadog/system-tests:express4-typescript.base-eb800c09d602: digest: sha256:4c1359af1571b63dfcb95471fb9fa6a368329368ab5b51d755f35a347161e388 target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:express4-typescript.base-eb800c09d602 tag: express4-typescript.base-eb800c09d602 + datadog/system-tests:express4.base-93dbfc9a5699: + digest: sha256:96a80f92a127af451ac5a1f2324ba2d22a3def21f8c70bd3e250ae6729169115 + target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:express4.base-93dbfc9a5699 + tag: express4.base-93dbfc9a5699 datadog/system-tests:express4.base-cb2b80abf6d1: digest: sha256:06f818aebeadbcaf9614e5f9b75f388b98afe0903b458be96ee1aa7c47aae9b3 target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:express4.base-cb2b80abf6d1 @@ -178,6 +186,10 @@ images: digest: sha256:89561e1d8fa4a4effbb68f840847454c11249f3d0b4e4f4fb1e7f954dc7a095f target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:express5.base-3ef9e2c68c47 tag: express5.base-3ef9e2c68c47 + datadog/system-tests:express5.base-6df40f6a8621: + digest: sha256:8221ab40116f5421ff278df706c3d20a8c1146a4687222f1ef76dc401b96bd83 + target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:express5.base-6df40f6a8621 + tag: express5.base-6df40f6a8621 datadog/system-tests:fastapi.base-1795d4d88178: digest: sha256:bf65195d25f6e0d663c0f414590471fba7f18b1a0e3d284dda0cbd1896bea9a0 target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:fastapi.base-1795d4d88178 @@ -186,10 +198,22 @@ images: digest: sha256:f72a31702ffaabc1ba5e43042963e9cea965d1be6256d9d8a58e7e2381090d40 target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:fastapi.base-2917769b3665 tag: fastapi.base-2917769b3665 + datadog/system-tests:fastify.base-9613346d64d0: + digest: sha256:a4fa8c8c475c3d854a935bc753c1b13f774459f5d79b2712486d637e8659356f + target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:fastify.base-9613346d64d0 + tag: fastify.base-9613346d64d0 + datadog/system-tests:fastify.base-9dddb89b99c4: + digest: sha256:e09ccb29386b7ae3cfd07e99cf11a532d0ccd14bf7b6b5415199fc96d79799c5 + target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:fastify.base-9dddb89b99c4 + tag: fastify.base-9dddb89b99c4 datadog/system-tests:fastify.base-bce27c61b3f5: digest: sha256:984e566d8eb604d53c1532a55559549f4c443ac73caf41664bb73241e529357c target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:fastify.base-bce27c61b3f5 tag: fastify.base-bce27c61b3f5 + datadog/system-tests:fastify.base-edcf078de5d9: + digest: sha256:07f1aa403fa5be8ea02687fda1c9cd58f25dc02939be00762240a74fad3e4db5 + target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:fastify.base-edcf078de5d9 + tag: fastify.base-edcf078de5d9 datadog/system-tests:flask-poc.base-fa81f4f89ea4: digest: sha256:f300cff471470574ce8263292f54ab745ffacfa8af741dd41be15a8c947e2e0e target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:flask-poc.base-fa81f4f89ea4 @@ -210,6 +234,10 @@ images: digest: sha256:2b09f33c61b2b19f4a239e4d8f10164ece66d925b72337720998ce43d7090d2f target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:lambda-proxy-v1 tag: lambda-proxy-v1 + datadog/system-tests:nextjs.base-312a008bf0e8: + digest: sha256:e7c6ccd084c27819ea41b0ca2c2a03b6b372c18f2c8ac4cac2891fe9d1e54661 + target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:nextjs.base-312a008bf0e8 + tag: nextjs.base-312a008bf0e8 datadog/system-tests:nextjs.base-95582a903467: digest: sha256:128b7524cde00a739246166feef66b0d512388005dc125b0f11034555a8b6a70 target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:nextjs.base-95582a903467 @@ -454,6 +482,10 @@ images: digest: sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 target: registry.ddbuild.io/system-tests/mirror/node:22-alpine tag: 22-alpine + node:24-alpine: + digest: sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 + target: registry.ddbuild.io/system-tests/mirror/node:24-alpine + tag: 24-alpine otel/opentelemetry-collector-contrib:0.137.0: digest: sha256:886722fe0f37af9d1fe24d29529253ec59fbf263b3b1df4facaf221373e19d23 target: registry.ddbuild.io/system-tests/mirror/otel/opentelemetry-collector-contrib:0.137.0 diff --git a/mirror_images.yaml b/mirror_images.yaml index 2993dd8ac7b..16fa4e9e9e4 100644 --- a/mirror_images.yaml +++ b/mirror_images.yaml @@ -49,17 +49,25 @@ - "datadog/system-tests:apache-mod-8.2.base-d1aeacb6306a" - "datadog/system-tests:django-poc.base-78e2159815b1" - "datadog/system-tests:django-py3.13.base-d8262a2c816c" +- "datadog/system-tests:express4-typescript.base-5a66898a3a49" - "datadog/system-tests:express4-typescript.base-eb800c09d602" +- "datadog/system-tests:express4.base-93dbfc9a5699" - "datadog/system-tests:express4.base-cb2b80abf6d1" - "datadog/system-tests:express5.base-3ef9e2c68c47" +- "datadog/system-tests:express5.base-6df40f6a8621" - "datadog/system-tests:fastapi.base-1795d4d88178" - "datadog/system-tests:fastapi.base-2917769b3665" +- "datadog/system-tests:fastify.base-9613346d64d0" +- "datadog/system-tests:fastify.base-9dddb89b99c4" - "datadog/system-tests:fastify.base-bce27c61b3f5" +- "datadog/system-tests:fastify.base-edcf078de5d9" +- "datadog/system-tests:fastify.base-f42683fff2b3" - "datadog/system-tests:flask-poc.base-fa81f4f89ea4" - "datadog/system-tests:golang_buddy-v2" - "datadog/system-tests:java-lambda-runtime.base-e849230b09df" - "datadog/system-tests:java_buddy-v1" - "datadog/system-tests:lambda-proxy-v1" +- "datadog/system-tests:nextjs.base-312a008bf0e8" - "datadog/system-tests:nextjs.base-95582a903467" - "datadog/system-tests:nodejs_buddy-v1" - "datadog/system-tests:openai-py.base-b4fd56aee3f2" @@ -121,6 +129,7 @@ - "node:18-alpine" - "node:20-alpine" - "node:22-alpine" +- "node:24-alpine" - "otel/opentelemetry-collector-contrib:0.137.0" - "postgres:alpine" - "public.ecr.aws/lambda/java:17" diff --git a/utils/build/docker/base-images.lock.json b/utils/build/docker/base-images.lock.json index b52719fd05f..1ffdd8fbc08 100644 --- a/utils/build/docker/base-images.lock.json +++ b/utils/build/docker/base-images.lock.json @@ -2,11 +2,11 @@ "version": 1, "images": { "system_tests_base_java_lambda_java_lambda_runtime": "datadog/system-tests:java-lambda-runtime.base-e849230b09df", - "system_tests_base_nodejs_express4": "datadog/system-tests:express4.base-cb2b80abf6d1", - "system_tests_base_nodejs_express4_typescript": "datadog/system-tests:express4-typescript.base-eb800c09d602", - "system_tests_base_nodejs_express5": "datadog/system-tests:express5.base-3ef9e2c68c47", - "system_tests_base_nodejs_fastify": "datadog/system-tests:fastify.base-bce27c61b3f5", - "system_tests_base_nodejs_nextjs": "datadog/system-tests:nextjs.base-95582a903467", + "system_tests_base_nodejs_express4": "datadog/system-tests:express4.base-93dbfc9a5699", + "system_tests_base_nodejs_express4_typescript": "datadog/system-tests:express4-typescript.base-5a66898a3a49", + "system_tests_base_nodejs_express5": "datadog/system-tests:express5.base-6df40f6a8621", + "system_tests_base_nodejs_fastify": "datadog/system-tests:fastify.base-f42683fff2b3", + "system_tests_base_nodejs_nextjs": "datadog/system-tests:nextjs.base-312a008bf0e8", "system_tests_base_php_apache_mod_7_0": "datadog/system-tests:apache-mod-7.0.base-8c8102938177", "system_tests_base_php_apache_mod_7_0_zts": "datadog/system-tests:apache-mod-7.0-zts.base-01cc5715b403", "system_tests_base_php_apache_mod_7_1": "datadog/system-tests:apache-mod-7.1.base-96cd329b60b0", diff --git a/utils/build/docker/nodejs/express/app.js b/utils/build/docker/nodejs/express/app.js index b5c4e25f98a..4485de611db 100644 --- a/utils/build/docker/nodejs/express/app.js +++ b/utils/build/docker/nodejs/express/app.js @@ -300,6 +300,23 @@ app.get('/make_distant_call', (req, res) => { request.end() }) +app.get('/security/thread_context_sharing', (req, res) => { + const path = req.query.path + if (typeof path !== 'string' || path === '') { + return res.status(400).send('missing path query parameter') + } + + // Synchronous on purpose: async fs opens the file on a libuv worker thread, which does not carry + // the request's thread context that the security agent reads. + fs.writeFileSync(path, 'thread context sharing') + + const context = tracer.scope().active().context() + res.json({ + trace_id: BigInt(`0x${context.toTraceId(true)}`).toString(), + span_id: context.toSpanId() + }) +}) + app.get('/user_login_success_event', (req, res) => { const userId = req.query.event_user_id || 'system_tests_user' diff --git a/utils/build/docker/nodejs/express4-typescript/app.ts b/utils/build/docker/nodejs/express4-typescript/app.ts index 903f32c1ffa..9b7d2f4fd58 100644 --- a/utils/build/docker/nodejs/express4-typescript/app.ts +++ b/utils/build/docker/nodejs/express4-typescript/app.ts @@ -221,6 +221,24 @@ app.get("/make_distant_call", (req: Request, res: Response) => { request.end() }) +app.get("/security/thread_context_sharing", (req: Request, res: Response) => { + const path = req.query.path; + if (typeof path !== "string" || path === "") { + res.status(400).send("missing path query parameter"); + return; + } + + // Synchronous on purpose: async fs opens the file on a libuv worker thread, which does not carry + // the request's thread context that the security agent reads. + require("fs").writeFileSync(path, "thread context sharing"); + + const context = tracer.scope().active().context(); + res.json({ + trace_id: BigInt(`0x${context.toTraceId(true)}`).toString(), + span_id: context.toSpanId() + }); +}); + app.get("/user_login_success_event", (req: Request, res: Response) => { const userId = req.query.event_user_id || "system_tests_user"; diff --git a/utils/build/docker/nodejs/fastify.Dockerfile b/utils/build/docker/nodejs/fastify.Dockerfile index 2fd5c72c07b..d8deee6b05e 100644 --- a/utils/build/docker/nodejs/fastify.Dockerfile +++ b/utils/build/docker/nodejs/fastify.Dockerfile @@ -26,3 +26,7 @@ CMD ./app.sh COPY utils/build/docker/nodejs/install_ddtrace.sh binaries* /binaries/ RUN /binaries/install_ddtrace.sh && rm -rf /root/.bun ENV DD_TRACE_HEADER_TAGS=user-agent +# TEMPORARY (do not merge): run the thread context writer end to end on Node.js 24. +ENV DD_TRACE_OTEL_CTX_ENABLED=true +# TEMPORARY (do not merge): Node.js 22 control run, AsyncContextFrame needs the flag. +ENV NODE_OPTIONS=--experimental-async-context-frame diff --git a/utils/build/docker/nodejs/fastify/app.js b/utils/build/docker/nodejs/fastify/app.js index 80b10445db4..34b64d55894 100644 --- a/utils/build/docker/nodejs/fastify/app.js +++ b/utils/build/docker/nodejs/fastify/app.js @@ -252,7 +252,7 @@ fastify.get('/make_distant_call', async (request, reply) => { resolve({ url, status_code: response.statusCode, - request_headers: response.req._headers, + request_headers: response.req.getHeaders(), response_headers: response.headers, response_body: responseBody }) @@ -273,6 +273,24 @@ fastify.get('/make_distant_call', async (request, reply) => { }) }) +fastify.get('/security/thread_context_sharing', async (request, reply) => { + const path = request.query.path + if (typeof path !== 'string' || path === '') { + reply.status(400) + return 'missing path query parameter' + } + + // Synchronous on purpose: async fs opens the file on a libuv worker thread, which does not carry + // the request's thread context that the security agent reads. + require('fs').writeFileSync(path, 'thread context sharing') + + const context = tracer.scope().active().context() + return { + trace_id: BigInt(`0x${context.toTraceId(true)}`).toString(), + span_id: context.toSpanId() + } +}) + fastify.get('/user_login_success_event', async (request, reply) => { const userId = request.query.event_user_id || 'system_tests_user' @@ -681,7 +699,7 @@ fastify.get('/otel_drop_in_baggage_api_otel', async (request, reply) => { resolve({ url, status_code: response.statusCode, - request_headers: response.req._headers, + request_headers: response.req.getHeaders(), response_headers: response.headers, response_body: responseBody }) @@ -743,7 +761,7 @@ fastify.get('/otel_drop_in_baggage_api_datadog', async (request, reply) => { resolve({ url, status_code: response.statusCode, - request_headers: response.req._headers, + request_headers: response.req.getHeaders(), response_headers: response.headers, response_body: responseBody }) diff --git a/utils/build/docker/nodejs/nextjs.Dockerfile b/utils/build/docker/nodejs/nextjs.Dockerfile index 7034ba89196..fed62ec4477 100644 --- a/utils/build/docker/nodejs/nextjs.Dockerfile +++ b/utils/build/docker/nodejs/nextjs.Dockerfile @@ -5,6 +5,7 @@ EXPOSE 7777 # Refresh the application route and dependencies baked into the base image. COPY utils/build/docker/nodejs/nextjs/package.json utils/build/docker/nodejs/nextjs/bun.lock ./ COPY utils/build/docker/nodejs/nextjs/src/app/ffe ./src/app/ffe +COPY utils/build/docker/nodejs/nextjs/src/app/security ./src/app/security RUN rm -rf node_modules \ && bun install --frozen-lockfile --network-concurrency 8 --linker=hoisted diff --git a/utils/build/docker/nodejs/nextjs/src/app/security/thread_context_sharing/route.js b/utils/build/docker/nodejs/nextjs/src/app/security/thread_context_sharing/route.js new file mode 100644 index 00000000000..727be6865c4 --- /dev/null +++ b/utils/build/docker/nodejs/nextjs/src/app/security/thread_context_sharing/route.js @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server' +import { writeFileSync } from 'fs' + +export const dynamic = 'force-dynamic' + +export async function GET (request) { + const path = request.nextUrl.searchParams.get('path') + if (!path) { + return new NextResponse('missing path query parameter', { status: 400 }) + } + + // Synchronous on purpose: async fs opens the file on a libuv worker thread, which does not carry + // the request's thread context that the security agent reads. + writeFileSync(path, 'thread context sharing') + + const context = global._ddtrace.scope().active().context() + return NextResponse.json({ + trace_id: BigInt(`0x${context.toTraceId(true)}`).toString(), + span_id: context.toSpanId() + }) +} diff --git a/utils/build/docker/nodejs/weblog_metadata.yml b/utils/build/docker/nodejs/weblog_metadata.yml index faf1a7f5746..9d6600af557 100644 --- a/utils/build/docker/nodejs/weblog_metadata.yml +++ b/utils/build/docker/nodejs/weblog_metadata.yml @@ -26,7 +26,7 @@ express5: categories: [dd_trace] fastify: build_mode: local - supported_scenarios: &framework_scenarios + supported_scenarios: - DEFAULT - APPSEC_API_SECURITY - APPSEC_API_SECURITY_NO_RESPONSE_BODY @@ -51,9 +51,34 @@ fastify: - APPSEC_STANDALONE_RASP - IAST_DEDUPLICATION - IAST_STANDALONE + - THREAD_CONTEXT_SHARING nextjs: build_mode: local - supported_scenarios: *framework_scenarios + supported_scenarios: + - DEFAULT + - APPSEC_API_SECURITY + - APPSEC_API_SECURITY_NO_RESPONSE_BODY + - APPSEC_API_SECURITY_RC + - APPSEC_API_SECURITY_WITH_SAMPLING + - APPSEC_APM_STANDALONE + - APPSEC_ATO_SDK + - APPSEC_AUTO_EVENTS_EXTENDED + - APPSEC_AUTO_EVENTS_RC + - APPSEC_BLOCKING + - APPSEC_BLOCKING_FULL_DENYLIST + - APPSEC_LOW_WAF_TIMEOUT + - APPSEC_RASP + - APPSEC_RASP_NON_BLOCKING + - APPSEC_RASP_WITHOUT_DOWNSTREAM_BODY_ANALYSIS_USING_MAX + - APPSEC_RASP_WITHOUT_DOWNSTREAM_BODY_ANALYSIS_USING_SAMPLE_RATE + - APPSEC_RATE_LIMITER + - APPSEC_RUNTIME_ACTIVATION + - APPSEC_STANDALONE + - APPSEC_STANDALONE_API_SECURITY + - APPSEC_STANDALONE_APM_STANDALONE + - APPSEC_STANDALONE_RASP + - IAST_DEDUPLICATION + - IAST_STANDALONE uds-express4: build_mode: local supported_scenarios: