diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 06f9407696..2c705f28ee 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -9,7 +9,7 @@ runs: using: composite steps: - id: yarn-cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: key: yarn-cache-${{ github.workflow }}-${{ github.job }}-${{ hashFiles('yarn.lock') }}-v2 path: node_modules.tar diff --git a/.github/actions/install/branch-diff/action.yml b/.github/actions/install/branch-diff/action.yml index 21c253a87b..1827f26d95 100644 --- a/.github/actions/install/branch-diff/action.yml +++ b/.github/actions/install/branch-diff/action.yml @@ -7,7 +7,7 @@ inputs: runs: using: composite steps: - - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.npm key: ${{ github.workflow }}-branch-diff-3.1.1 diff --git a/.github/actions/node/action.yml b/.github/actions/node/action.yml index 4740b96d53..a110ce6884 100644 --- a/.github/actions/node/action.yml +++ b/.github/actions/node/action.yml @@ -29,7 +29,7 @@ runs: id: cache-key shell: bash run: echo "block=$(( $(date -u +%s) / 1200 ))" >> "$GITHUB_OUTPUT" - - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: node-version-cache with: path: /tmp/.node-resolved-version-${{ steps.node-version.outputs.version }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 56bbe01767..450885671d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: languages: ${{ matrix.language }} config-file: .github/codeql_config.yml @@ -48,7 +48,7 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Autobuild - uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 diff --git a/.github/workflows/test-optimization.yml b/.github/workflows/test-optimization.yml index 30f35a4254..77ea6d6c74 100644 --- a/.github/workflows/test-optimization.yml +++ b/.github/workflows/test-optimization.yml @@ -54,7 +54,7 @@ jobs: echo "version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT echo "Playwright version: $PLAYWRIGHT_VERSION" - name: Cache Playwright browsers - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.cache/ms-playwright key: playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} @@ -99,7 +99,7 @@ jobs: echo "dd-trace major version: $MAJOR" - name: Cache Playwright browsers if: matrix.playwright-version == 'oldest' - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: /github/home/.cache/ms-playwright key: playwright-browsers-oldest-dd${{ steps.dd-version.outputs.major }} @@ -252,7 +252,7 @@ jobs: # as that changes frequently and would have a low cache hit rate - name: Cache Cypress binary if: matrix.cypress-version != 'latest' - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.cache/Cypress key: cypress-binary-${{ matrix.cypress-version }} diff --git a/.gitlab/benchmarks.yml b/.gitlab/benchmarks.yml index 3ffde4eea6..3c01362a45 100644 --- a/.gitlab/benchmarks.yml +++ b/.gitlab/benchmarks.yml @@ -143,9 +143,12 @@ benchmark-serverless-trigger: - if: $CI_COMMIT_BRANCH == 'master' interruptible: false - # dont run on merges to release branches (vN.x where N is any integer) + # don't run on merges to release branches ("vN.x" where N is any integer) - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^v\d+\.x$/' when: never + # don't run on pushes to release branches + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^v\d+\.x$/' + when: never - interruptible: true trigger: project: DataDog/serverless-tools diff --git a/ext/tags.js b/ext/tags.js index dc02b07469..f4a8545f89 100644 --- a/ext/tags.js +++ b/ext/tags.js @@ -25,6 +25,7 @@ const tags = { HTTP_RESPONSE_HEADERS: 'http.response.headers', HTTP_USERAGENT: 'http.useragent', HTTP_CLIENT_IP: 'http.client_ip', + NETWORK_CLIENT_IP: 'network.client.ip', // Messaging diff --git a/integration-tests/aiguard/index.spec.js b/integration-tests/aiguard/index.spec.js index 61e5d613db..9f4a74fcf3 100644 --- a/integration-tests/aiguard/index.spec.js +++ b/integration-tests/aiguard/index.spec.js @@ -40,6 +40,7 @@ describe('AIGuard SDK integration tests', () => { DD_SERVICE: 'ai_guard_integration_test', DD_ENV: 'test', DD_TRACE_ENABLED: 'true', + DD_TRACE_CLIENT_IP_ENABLED: 'false', DD_TRACE_AGENT_PORT: String(agent.port), DD_AI_GUARD_ENABLED: 'true', DD_AI_GUARD_BLOCK: 'true', @@ -69,6 +70,43 @@ describe('AIGuard SDK integration tests', () => { }) }) + it('adds client ip tags to the request root span when AI Guard runs', async () => { + const response = await executeRequest(`${url}/allow`, 'GET', { + 'x-forwarded-for': '203.0.113.10, 10.0.0.1', + }) + + assert.strictEqual(response.status, 200) + + await agent.assertMessageReceived(({ payload }) => { + const requestSpan = payload[0].find(span => span.name === 'express.request') + const guardSpan = payload[0].find(span => span.name === 'ai_guard') + + assert.notStrictEqual(requestSpan, undefined) + assert.notStrictEqual(guardSpan, undefined) + assert.strictEqual(requestSpan.meta['http.client_ip'], '203.0.113.10') + assert.ok(requestSpan.meta['network.client.ip']) + }) + }) + + it('does not add client ip tags when no AI Guard span is created', async () => { + const response = await executeRequest(`${url}/no-aiguard`, 'GET', { + 'x-forwarded-for': '203.0.113.10, 10.0.0.1', + }) + + assert.strictEqual(response.status, 200) + assert.deepStrictEqual(response.body, { ok: true }) + + await agent.assertMessageReceived(({ payload }) => { + const requestSpan = payload[0].find(span => span.name === 'express.request') + const guardSpan = payload[0].find(span => span.name === 'ai_guard') + + assert.notStrictEqual(requestSpan, undefined) + assert.strictEqual(guardSpan, undefined) + assert.strictEqual(requestSpan.meta['http.client_ip'], undefined) + assert.strictEqual(requestSpan.meta['network.client.ip'], undefined) + }) + }) + const directApiSuite = [ { endpoint: '/allow', action: 'ALLOW', reason: 'The prompt looks harmless' }, { endpoint: '/deny', action: 'DENY', reason: 'I am feeling suspicious today' }, diff --git a/integration-tests/aiguard/server.js b/integration-tests/aiguard/server.js index 254f964381..d2cc0e7a0a 100644 --- a/integration-tests/aiguard/server.js +++ b/integration-tests/aiguard/server.js @@ -6,6 +6,10 @@ const express = require('express') const app = express() +app.get('/no-aiguard', (req, res) => { + res.status(200).json({ ok: true }) +}) + app.get('/allow', async (req, res) => { const evaluation = await tracer.aiguard.evaluate([ { role: 'system', content: 'You are a beautiful AI' }, diff --git a/package.json b/package.json index 48bf3f3ea6..17edf1e2c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "5.98.0", + "version": "5.98.1", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", diff --git a/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js b/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js index 88c7504a75..5749c2a3ef 100644 --- a/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +++ b/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js @@ -1,5 +1,13 @@ 'use strict' +/** + * This file is meant to be only thin wrappers over core + * parsing/traversing/generating functionality with the goal to eventually move + * them out of the project. No other code should be added to this file such as + * helpers etc, and the API should be kept exactly as an external API would be + * expected to be. + */ + const log = require('../../../../dd-trace/src/log') // eslint-disable-next-line camelcase, no-undef @@ -24,7 +32,6 @@ const compiler = { } catch (e) { log.error(e) - // Fallback for when OXC is not available. const meriyah = require('../../../../../vendor/dist/meriyah') compiler.parse = (sourceText, { range, sourceType } = {}) => { diff --git a/packages/datadog-plugin-http/src/server.js b/packages/datadog-plugin-http/src/server.js index 71a499e18a..a4c99c0b87 100644 --- a/packages/datadog-plugin-http/src/server.js +++ b/packages/datadog-plugin-http/src/server.js @@ -45,9 +45,8 @@ class HttpServerPlugin extends ServerPlugin { context.parentStore = store } - // Only AppSec needs the request scope to be active for any async work that - // may be scheduled after the synchronous `request` event returns (e.g. - // Fastify). + // AppSec, IAST, and AI Guard need req/res on the store so downstream + // subscribers can access them from the async context. if (incomingHttpRequestStart.hasSubscribers) { store = { ...store, req, res } } diff --git a/packages/dd-trace/src/aiguard/channels.js b/packages/dd-trace/src/aiguard/channels.js new file mode 100644 index 0000000000..6229919898 --- /dev/null +++ b/packages/dd-trace/src/aiguard/channels.js @@ -0,0 +1,8 @@ +'use strict' + +const dc = require('dc-polyfill') + +module.exports = { + aiguardChannel: dc.channel('dd-trace:ai:aiguard'), + incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'), +} diff --git a/packages/dd-trace/src/aiguard/index.js b/packages/dd-trace/src/aiguard/index.js index d3a523ec98..dd26baa6bd 100644 --- a/packages/dd-trace/src/aiguard/index.js +++ b/packages/dd-trace/src/aiguard/index.js @@ -1,15 +1,17 @@ 'use strict' -const { channel } = require('dc-polyfill') const log = require('../log') +const { incomingHttpRequestStart, aiguardChannel } = require('./channels') const AIGuard = require('./sdk') -const aiguardChannel = channel('dd-trace:ai:aiguard') - let isEnabled = false let aiguard let block +function onIncomingHttpRequestStart () { + // No-op: subscribing ensures the HTTP plugin spreads req onto the store +} + function enable (tracer, config) { if (isEnabled) return @@ -17,6 +19,7 @@ function enable (tracer, config) { aiguard = new AIGuard(tracer, config) block = config.experimental?.aiguard?.block !== false + incomingHttpRequestStart.subscribe(onIncomingHttpRequestStart) aiguardChannel.subscribe(onEvaluate) isEnabled = true @@ -29,6 +32,7 @@ function enable (tracer, config) { function disable () { if (!isEnabled) return + incomingHttpRequestStart.unsubscribe(onIncomingHttpRequestStart) aiguardChannel.unsubscribe(onEvaluate) aiguard = undefined diff --git a/packages/dd-trace/src/aiguard/sdk.js b/packages/dd-trace/src/aiguard/sdk.js index f817856bfc..0ab7561f66 100644 --- a/packages/dd-trace/src/aiguard/sdk.js +++ b/packages/dd-trace/src/aiguard/sdk.js @@ -1,7 +1,10 @@ 'use strict' const rfdc = require('../../../../vendor/dist/rfdc')({ proto: false, circles: false }) +const { HTTP_CLIENT_IP, NETWORK_CLIENT_IP } = require('../../../../ext/tags') +const { storage } = require('../../../datadog-core') const log = require('../log') +const { extractIp } = require('../plugins/util/ip_extractor') const telemetryMetrics = require('../telemetry/metrics') const tracerVersion = require('../../../../package.json').version const { keepTrace } = require('../priority_sampler') @@ -57,6 +60,7 @@ class AIGuard extends NoopAIGuard { #maxMessagesLength #maxContentSize #meta + #config /** * @param {import('../tracer')} tracer - Tracer instance @@ -84,6 +88,7 @@ class AIGuard extends NoopAIGuard { this.#maxMessagesLength = config.experimental.aiguard.maxMessagesLength this.#maxContentSize = config.experimental.aiguard.maxContentSize this.#meta = { service: config.service, env: config.env } + this.#config = config this.#initialized = true } @@ -139,6 +144,42 @@ class AIGuard extends NoopAIGuard { return null } + #setRootSpanClientIpTags (rootSpan) { + if (!rootSpan) return + + const currentTags = rootSpan.context()._tags + const needsHttpClientIp = !Object.hasOwn(currentTags, HTTP_CLIENT_IP) + const needsNetworkClientIp = !Object.hasOwn(currentTags, NETWORK_CLIENT_IP) + + if (!needsHttpClientIp && !needsNetworkClientIp) return + + const req = storage('legacy').getStore()?.req + + if (!req) return + + const newTags = {} + + if (needsHttpClientIp) { + const clientIp = extractIp(this.#config, req) + + if (clientIp) { + newTags[HTTP_CLIENT_IP] = clientIp + } + } + + if (needsNetworkClientIp) { + const networkClientIp = req.socket?.remoteAddress + + if (networkClientIp) { + newTags[NETWORK_CLIENT_IP] = networkClientIp + } + } + + if (Object.keys(newTags).length > 0) { + rootSpan.addTags(newTags) + } + } + evaluate (messages, opts) { if (!this.#initialized) { return super.evaluate(messages, opts) @@ -162,6 +203,7 @@ class AIGuard extends NoopAIGuard { } const rootSpan = span.context()?._trace?.started?.[0] if (rootSpan) { + this.#setRootSpanClientIpTags(rootSpan) // keepTrace must be called before executeRequest so the sampling decision // is propagated correctly to outgoing HTTP client calls. keepTrace(rootSpan, AI_GUARD) diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index 9fc1b798fe..4f4a7b3f0d 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -3,6 +3,7 @@ const zlib = require('zlib') const dc = require('dc-polyfill') +const { NETWORK_CLIENT_IP } = require('../../../../ext/tags') const { storage } = require('../../../datadog-core') const web = require('../plugins/util/web') const { ipHeaderList } = require('../plugins/util/ip_extractor') @@ -363,7 +364,7 @@ function reportAttack ({ events: attackData, actions }, req) { : '{"triggers":' + attackDataStr + '}' if (req.socket) { - newTags['network.client.ip'] = req.socket.remoteAddress + newTags[NETWORK_CLIENT_IP] = req.socket.remoteAddress } rootSpan.addTags(newTags) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js index 058b75a76d..db9d1aa22b 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js @@ -12,7 +12,9 @@ const agent = require('../../../plugins/agent') const { withVersions } = require('../../../setup/mocha') const { prepareTestServerForIastInExpress } = require('../utils') -describe('nosql injection detection with mquery', () => { +// TODO(APPSEC-62431): re-enable once duplicate NOSQL_MONGODB_INJECTION +// detection (N+1 !== N) is fixed +describe.skip('nosql injection detection with mquery', () => { // https://github.com/fiznool/express-mongo-sanitize/issues/200 withVersions('mquery', 'express', '>4.18.0 <5.0.0', expressVersion => { withVersions('mquery', 'mongodb', mongodbVersion => {