dd-trace 5.110.0 → 5.111.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -8
- package/ci/init.js +21 -2
- package/ci/vitest-no-worker-init-setup.mjs +430 -0
- package/ext/tags.js +2 -0
- package/index.d.ts +13 -0
- package/initialize.mjs +5 -6
- package/loader-hook.mjs +76 -55
- package/package.json +9 -9
- package/packages/datadog-instrumentations/src/fs.js +8 -6
- package/packages/datadog-instrumentations/src/graphql.js +26 -495
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/mongoose.js +14 -2
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1293 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +1567 -0
- package/packages/datadog-instrumentations/src/vitest-util.js +249 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +753 -0
- package/packages/datadog-instrumentations/src/vitest.js +11 -1855
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +18 -10
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +10 -4
- package/packages/datadog-plugin-cypress/src/support.js +45 -1
- package/packages/datadog-plugin-express/src/code_origin.js +1 -1
- package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +627 -11
- package/packages/datadog-plugin-graphql/src/index.js +20 -8
- package/packages/datadog-plugin-graphql/src/parse.js +24 -4
- package/packages/datadog-plugin-graphql/src/validate.js +16 -5
- package/packages/datadog-plugin-http/src/index.js +6 -8
- package/packages/datadog-plugin-jest/src/index.js +1 -1
- package/packages/datadog-plugin-mocha/src/index.js +1 -1
- package/packages/datadog-plugin-openai/src/services.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +1 -1
- package/packages/datadog-plugin-vitest/src/index.js +69 -63
- package/packages/datadog-shimmer/src/shimmer.js +37 -16
- package/packages/dd-trace/src/aiguard/sdk.js +2 -2
- package/packages/dd-trace/src/appsec/api_security/sampler.js +3 -3
- package/packages/dd-trace/src/appsec/channels.js +3 -1
- package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
- package/packages/dd-trace/src/appsec/graphql.js +5 -5
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +26 -19
- package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
- package/packages/dd-trace/src/appsec/index.js +10 -1
- package/packages/dd-trace/src/appsec/lambda.js +135 -0
- package/packages/dd-trace/src/appsec/reporter.js +49 -10
- package/packages/dd-trace/src/appsec/telemetry/index.js +1 -1
- package/packages/dd-trace/src/appsec/waf/index.js +16 -4
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -2
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -2
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -13
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
- package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
- package/packages/dd-trace/src/config/defaults.js +3 -2
- package/packages/dd-trace/src/config/generated-config-types.d.ts +61 -57
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +35 -22
- package/packages/dd-trace/src/config/major-overrides.js +4 -2
- package/packages/dd-trace/src/config/remote_config.js +1 -1
- package/packages/dd-trace/src/config/supported-configurations.json +90 -56
- package/packages/dd-trace/src/constants.js +7 -0
- package/packages/dd-trace/src/datastreams/processor.js +11 -3
- package/packages/dd-trace/src/exporters/agent/index.js +1 -1
- package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
- package/packages/dd-trace/src/llmobs/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +3 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
- package/packages/dd-trace/src/llmobs/tagger.js +3 -3
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +38 -39
- package/packages/dd-trace/src/opentelemetry/trace/index.js +4 -0
- package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
- package/packages/dd-trace/src/opentracing/tracer.js +6 -1
- package/packages/dd-trace/src/plugin_manager.js +7 -7
- package/packages/dd-trace/src/plugins/ci_plugin.js +12 -3
- package/packages/dd-trace/src/plugins/util/llm.js +6 -1
- package/packages/dd-trace/src/plugins/util/test.js +11 -9
- package/packages/dd-trace/src/profiling/exporter_cli.js +2 -2
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
- package/packages/dd-trace/src/proxy.js +8 -8
- package/packages/dd-trace/src/span_processor.js +5 -6
- package/packages/dd-trace/src/span_stats.js +96 -78
- package/packages/dd-trace/src/startup-log.js +2 -1
- package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
- package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
- package/packages/dd-trace/src/telemetry/index.js +2 -2
- package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
- package/packages/dd-trace/src/telemetry/send-data.js +8 -8
- package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
- package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
|
@@ -41,7 +41,7 @@ function enable (_config) {
|
|
|
41
41
|
downstreamAnalysisCount = new WeakMap()
|
|
42
42
|
responseBodyIgnoredCount = new WeakMap()
|
|
43
43
|
|
|
44
|
-
const bodyAnalysisSampleRate = config.appsec.
|
|
44
|
+
const bodyAnalysisSampleRate = config.appsec.DD_API_SECURITY_DOWNSTREAM_BODY_ANALYSIS_SAMPLE_RATE
|
|
45
45
|
samplingRate = Math.min(Math.max(bodyAnalysisSampleRate, 0), 1)
|
|
46
46
|
|
|
47
47
|
if (samplingRate !== bodyAnalysisSampleRate) {
|
|
@@ -105,7 +105,7 @@ function recordResponseBodyIgnored (req, tag) {
|
|
|
105
105
|
* @returns {boolean} whether downstream response body should be collected for AppSec.
|
|
106
106
|
*/
|
|
107
107
|
function evaluateResponseBodyCollection (originatingReq, res) {
|
|
108
|
-
const maxBytes = config.appsec.
|
|
108
|
+
const maxBytes = config.appsec.DD_API_SECURITY_MAX_DOWNSTREAM_BODY_BYTES
|
|
109
109
|
|
|
110
110
|
const mime = extractMimeType(res.headers?.['content-type'])
|
|
111
111
|
if (!mime || !SUPPORTED_RESPONSE_BODY_MIME_TYPES.has(mime)) {
|
|
@@ -137,7 +137,7 @@ function shouldSampleBody (req) {
|
|
|
137
137
|
globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX
|
|
138
138
|
|
|
139
139
|
const currentCount = bodyAnalysisCount.get(req) || 0
|
|
140
|
-
if (currentCount >= config.appsec.
|
|
140
|
+
if (currentCount >= config.appsec.DD_API_SECURITY_MAX_DOWNSTREAM_REQUEST_BODY_ANALYSIS) {
|
|
141
141
|
return false
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -166,7 +166,7 @@ function isRedirectResponse (res) {
|
|
|
166
166
|
* @param {object} ctx http client instrumentation context (mutated).
|
|
167
167
|
*/
|
|
168
168
|
function planResponseBodyCollection (originatingReq, res, ctx) {
|
|
169
|
-
if (!config
|
|
169
|
+
if (!config) {
|
|
170
170
|
return
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
const waf = require('./waf')
|
|
13
13
|
const addresses = require('./addresses')
|
|
14
14
|
const {
|
|
15
|
-
|
|
15
|
+
startGraphqlResolver,
|
|
16
16
|
graphqlMiddlewareChannel,
|
|
17
17
|
apolloHttpServerChannel,
|
|
18
18
|
apolloChannel,
|
|
@@ -32,7 +32,7 @@ function disable () {
|
|
|
32
32
|
disableGraphql()
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function
|
|
35
|
+
function onGraphqlStartResolver ({ abortController, resolverInfo }) {
|
|
36
36
|
const req = getActiveRequest()
|
|
37
37
|
|
|
38
38
|
if (!req) return
|
|
@@ -46,7 +46,7 @@ function onGraphqlStartResolve ({ context, resolverInfo }) {
|
|
|
46
46
|
if (requestData?.isInGraphqlRequest) {
|
|
47
47
|
requestData.blocked = true
|
|
48
48
|
requestData.wafAction = blockingAction
|
|
49
|
-
|
|
49
|
+
abortController?.abort()
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -153,11 +153,11 @@ function disableApollo () {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
function enableGraphql () {
|
|
156
|
-
|
|
156
|
+
startGraphqlResolver.subscribe(onGraphqlStartResolver)
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function disableGraphql () {
|
|
160
|
-
if (
|
|
160
|
+
if (startGraphqlResolver.hasSubscribers) startGraphqlResolver.unsubscribe(onGraphqlStartResolver)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
module.exports = {
|
|
@@ -15,6 +15,20 @@ const EXCLUDED_PATHS_FROM_STACK = getNodeModulesPaths('mongodb', 'mongoose', 'mq
|
|
|
15
15
|
const SAFE_OPERATORS = new Set(['$eq', '$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin',
|
|
16
16
|
'$exists', '$type', '$mod', '$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet'])
|
|
17
17
|
|
|
18
|
+
// Scope a marked child store for a query's deferred execution so the nested mongodb
|
|
19
|
+
// driver query skips re-analysis. `bindStore` enters the child only for that async
|
|
20
|
+
// scope and restores the parent on its own, so no marker can be stranded for a later
|
|
21
|
+
// request to inherit, and two concurrent queries of the same request each branch from
|
|
22
|
+
// the shared parent rather than from each other.
|
|
23
|
+
function markExecStore () {
|
|
24
|
+
const store = storage('legacy').getStore()
|
|
25
|
+
if (store && !store.nosqlAnalyzed && getIastContext(store)) {
|
|
26
|
+
return { ...store, nosqlAnalyzed: true }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return store
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
|
|
19
33
|
constructor () {
|
|
20
34
|
super(NOSQL_MONGODB_INJECTION)
|
|
@@ -27,7 +41,7 @@ class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
|
|
|
27
41
|
// Anything that accesses the storage is context dependent
|
|
28
42
|
const onStart = ({ filters }) => {
|
|
29
43
|
const store = storage('legacy').getStore()
|
|
30
|
-
if (store && !store.nosqlAnalyzed && filters?.length) {
|
|
44
|
+
if (store && !store.nosqlAnalyzed && filters?.length && getIastContext(store)) {
|
|
31
45
|
for (const filter of filters) {
|
|
32
46
|
this.analyze({ filter }, store)
|
|
33
47
|
}
|
|
@@ -36,30 +50,23 @@ class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
|
|
|
36
50
|
return store
|
|
37
51
|
}
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Anything that accesses the storage is context dependent
|
|
47
|
-
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
48
|
-
const onFinish = () => {
|
|
49
|
-
const store = storage('legacy').getStore()
|
|
50
|
-
if (store?.nosqlParentStore) {
|
|
51
|
-
storage('legacy').enterWith(store.nosqlParentStore)
|
|
52
|
-
}
|
|
53
|
+
// mquery analyzes and marks on the same channel: its tracing start both reaches
|
|
54
|
+
// the driver scope and is the first place the filters are available.
|
|
55
|
+
const onStartAndBind = (message) => {
|
|
56
|
+
onStart(message)
|
|
57
|
+
return markExecStore()
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
this.addSub('datadog:mongodb:collection:filter:start', onStart)
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
// A mongoose query builds, executes, and reaches the driver in separate async
|
|
63
|
+
// steps. The filters are analyzed at the build, but the marker is bound around
|
|
64
|
+
// the execution channel, where the driver call still runs under the parent store.
|
|
65
|
+
this.addSub('datadog:mongoose:model:filter:start', onStart)
|
|
66
|
+
this.addBind('datadog:mongoose:model:filter:exec', markExecStore)
|
|
59
67
|
|
|
60
68
|
this.addSub('datadog:mquery:filter:prepare', onStart)
|
|
61
|
-
this.
|
|
62
|
-
this.addSub('tracing:datadog:mquery:filter:asyncEnd', onFinish)
|
|
69
|
+
this.addBind('tracing:datadog:mquery:filter:start', onStartAndBind)
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
configureSanitizers () {
|
|
@@ -21,10 +21,10 @@ let controlsKeys
|
|
|
21
21
|
let hooks
|
|
22
22
|
|
|
23
23
|
function configure (iastConfig) {
|
|
24
|
-
if (!iastConfig
|
|
24
|
+
if (!iastConfig.DD_IAST_SECURITY_CONTROLS_CONFIGURATION) return
|
|
25
25
|
|
|
26
26
|
try {
|
|
27
|
-
controls = parse(iastConfig.
|
|
27
|
+
controls = parse(iastConfig.DD_IAST_SECURITY_CONTROLS_CONFIGURATION)
|
|
28
28
|
if (controls?.size > 0) {
|
|
29
29
|
hooks = new WeakSet()
|
|
30
30
|
controlsKeys = [...controls.keys()]
|
|
@@ -6,7 +6,8 @@ const { initRequestNamespace, finalizeRequestNamespace, globalNamespace } = requ
|
|
|
6
6
|
|
|
7
7
|
class Telemetry {
|
|
8
8
|
configure (config, verbosity) {
|
|
9
|
-
const telemetryAndMetricsEnabled = config
|
|
9
|
+
const telemetryAndMetricsEnabled = config.telemetry.DD_INSTRUMENTATION_TELEMETRY_ENABLED &&
|
|
10
|
+
config.telemetry.DD_TELEMETRY_METRICS_ENABLED
|
|
10
11
|
|
|
11
12
|
this.verbosity = telemetryAndMetricsEnabled ? getVerbosity(verbosity) : Verbosity.OFF
|
|
12
13
|
this.enabled = this.verbosity !== Verbosity.OFF
|
|
@@ -16,6 +16,8 @@ const {
|
|
|
16
16
|
fastifyCookieParser,
|
|
17
17
|
incomingHttpRequestStart,
|
|
18
18
|
incomingHttpRequestEnd,
|
|
19
|
+
lambdaStartInvocation,
|
|
20
|
+
lambdaEndInvocation,
|
|
19
21
|
passportVerify,
|
|
20
22
|
passportUser,
|
|
21
23
|
expressSession,
|
|
@@ -43,6 +45,7 @@ const { isBlocked, block, callBlockDelegation, setTemplates, getBlockingAction }
|
|
|
43
45
|
const { getActiveRequest } = require('./store')
|
|
44
46
|
const UserTracking = require('./user_tracking')
|
|
45
47
|
const graphql = require('./graphql')
|
|
48
|
+
const lambda = require('./lambda')
|
|
46
49
|
const rasp = require('./rasp')
|
|
47
50
|
|
|
48
51
|
const responseAnalyzedSet = new WeakSet()
|
|
@@ -99,11 +102,15 @@ function enable (_config) {
|
|
|
99
102
|
stripeCheckoutSessionCreate.subscribe(onStripeCheckoutSessionCreate)
|
|
100
103
|
stripePaymentIntentCreate.subscribe(onStripePaymentIntentCreate)
|
|
101
104
|
stripeConstructEvent.subscribe(onStripeConstructEvent)
|
|
105
|
+
lambdaStartInvocation.subscribe(lambda.onLambdaStartInvocation)
|
|
106
|
+
lambdaEndInvocation.subscribe(lambda.onLambdaEndInvocation)
|
|
102
107
|
|
|
103
108
|
isEnabled = true
|
|
104
109
|
config = _config
|
|
105
110
|
} catch (err) {
|
|
106
|
-
if (
|
|
111
|
+
if (IS_SERVERLESS) {
|
|
112
|
+
log.debug('[ASM] Serverless mode: suppressing error log, calling disable()')
|
|
113
|
+
} else {
|
|
107
114
|
log.error('[ASM] Unable to start AppSec', err)
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -544,6 +551,8 @@ function disable () {
|
|
|
544
551
|
if (stripeCheckoutSessionCreate.hasSubscribers) stripeCheckoutSessionCreate.unsubscribe(onStripeCheckoutSessionCreate)
|
|
545
552
|
if (stripePaymentIntentCreate.hasSubscribers) stripePaymentIntentCreate.unsubscribe(onStripePaymentIntentCreate)
|
|
546
553
|
if (stripeConstructEvent.hasSubscribers) stripeConstructEvent.unsubscribe(onStripeConstructEvent)
|
|
554
|
+
if (lambdaStartInvocation.hasSubscribers) lambdaStartInvocation.unsubscribe(lambda.onLambdaStartInvocation)
|
|
555
|
+
if (lambdaEndInvocation.hasSubscribers) lambdaEndInvocation.unsubscribe(lambda.onLambdaEndInvocation)
|
|
547
556
|
}
|
|
548
557
|
|
|
549
558
|
/**
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
|
|
4
|
+
|
|
5
|
+
const log = require('../log')
|
|
6
|
+
const addresses = require('./addresses')
|
|
7
|
+
const Reporter = require('./reporter')
|
|
8
|
+
const waf = require('./waf')
|
|
9
|
+
|
|
10
|
+
// Tracks spans for which start-invocation has been processed, so that
|
|
11
|
+
// end-invocation can gate correctly
|
|
12
|
+
const activeInvocations = new WeakSet()
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Maps pre-extracted HTTP data from the Lambda event to WAF addresses,
|
|
16
|
+
* runs the WAF, and reports results on the span.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ span: object, headers: Record<string, string>, method: string, path: string,
|
|
19
|
+
* query: Record<string, string | string[]> | undefined, body: string | object | undefined,
|
|
20
|
+
* isBase64Encoded: boolean, clientIp: string | undefined,
|
|
21
|
+
* pathParams: Record<string, string> | undefined,
|
|
22
|
+
* cookies: Record<string, string> | undefined,
|
|
23
|
+
* route: string | undefined }} data
|
|
24
|
+
*/
|
|
25
|
+
function onLambdaStartInvocation (data) {
|
|
26
|
+
try {
|
|
27
|
+
const { span, headers, method, path, query, body, clientIp, pathParams, cookies } = data
|
|
28
|
+
|
|
29
|
+
if (!span) {
|
|
30
|
+
log.warn('[ASM] No span provided in Lambda start invocation')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
activeInvocations.add(span)
|
|
35
|
+
|
|
36
|
+
span.addTags({
|
|
37
|
+
'_dd.appsec.enabled': 1,
|
|
38
|
+
'_dd.runtime_family': 'nodejs',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const persistent = {}
|
|
42
|
+
|
|
43
|
+
if (path) {
|
|
44
|
+
persistent[addresses.HTTP_INCOMING_URL] = path
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (method) {
|
|
48
|
+
persistent[addresses.HTTP_INCOMING_METHOD] = method
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (headers) {
|
|
52
|
+
// Cookie header is already stripped by the Lambda layer's event-data-extractor
|
|
53
|
+
persistent[addresses.HTTP_INCOMING_HEADERS] = headers
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (clientIp) {
|
|
57
|
+
span.setTag(HTTP_CLIENT_IP, clientIp)
|
|
58
|
+
persistent[addresses.HTTP_CLIENT_IP] = clientIp
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (query) {
|
|
62
|
+
persistent[addresses.HTTP_INCOMING_QUERY] = query
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (body !== undefined && body !== null) {
|
|
66
|
+
persistent[addresses.HTTP_INCOMING_BODY] = body
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (pathParams) {
|
|
70
|
+
persistent[addresses.HTTP_INCOMING_PARAMS] = pathParams
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (cookies) {
|
|
74
|
+
persistent[addresses.HTTP_INCOMING_COOKIES] = cookies
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
waf.run({ persistent }, span, undefined, span)
|
|
78
|
+
} catch (err) {
|
|
79
|
+
log.error('[ASM] Error in Lambda start-invocation handler', err)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Maps response data to WAF addresses, runs a final WAF pass,
|
|
85
|
+
* disposes the WAF context, and finishes the request report.
|
|
86
|
+
*
|
|
87
|
+
* @param {{ span: object, statusCode: string | undefined,
|
|
88
|
+
* responseHeaders: Record<string, string> | undefined }} data
|
|
89
|
+
*/
|
|
90
|
+
function onLambdaEndInvocation (data) {
|
|
91
|
+
try {
|
|
92
|
+
const { span, statusCode, responseHeaders } = data
|
|
93
|
+
|
|
94
|
+
if (!span) {
|
|
95
|
+
log.warn('[ASM] No span provided in Lambda end invocation')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!activeInvocations.has(span)) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
activeInvocations.delete(span)
|
|
104
|
+
|
|
105
|
+
let hasPersistentData = false
|
|
106
|
+
const persistent = {}
|
|
107
|
+
|
|
108
|
+
if (statusCode) {
|
|
109
|
+
persistent[addresses.HTTP_INCOMING_RESPONSE_CODE] = String(statusCode)
|
|
110
|
+
hasPersistentData = true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (responseHeaders) {
|
|
114
|
+
const filteredHeaders = { ...responseHeaders }
|
|
115
|
+
delete filteredHeaders['set-cookie']
|
|
116
|
+
persistent[addresses.HTTP_INCOMING_RESPONSE_HEADERS] = filteredHeaders
|
|
117
|
+
hasPersistentData = true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (hasPersistentData) {
|
|
121
|
+
waf.run({ persistent }, span, undefined, span)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
waf.disposeContext(span)
|
|
125
|
+
|
|
126
|
+
Reporter.finishRequest(span, null, {}, undefined, span)
|
|
127
|
+
} catch (err) {
|
|
128
|
+
log.error('[ASM] Error in Lambda end-invocation handler', err)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
onLambdaStartInvocation,
|
|
134
|
+
onLambdaEndInvocation,
|
|
135
|
+
}
|
|
@@ -318,13 +318,21 @@ function reportWafConfigUpdate (product, rcConfigId, diagnostics, wafVersion) {
|
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
|
|
321
|
+
/**
|
|
322
|
+
* @param {object} metrics - WAF run metrics
|
|
323
|
+
* @param {string} [raspRule] - RASP rule identifier
|
|
324
|
+
* @param {object} [req] - Request key (plain object for lambda)
|
|
325
|
+
* @param {object} [rootSpan] - Root span (required for lambda)
|
|
326
|
+
*/
|
|
327
|
+
function reportMetrics (metrics, raspRule, req, rootSpan) {
|
|
322
328
|
if (!req) {
|
|
323
329
|
req = getActiveRequest()
|
|
324
330
|
}
|
|
325
|
-
|
|
331
|
+
if (!rootSpan) {
|
|
332
|
+
rootSpan = req && web.root(req)
|
|
333
|
+
}
|
|
326
334
|
|
|
327
|
-
if (!rootSpan) return
|
|
335
|
+
if (!req || !rootSpan) return
|
|
328
336
|
|
|
329
337
|
if (metrics.rulesVersion) {
|
|
330
338
|
rootSpan.setTag('_dd.appsec.event_rules.version', metrics.rulesVersion)
|
|
@@ -353,13 +361,26 @@ function reportTruncationMetrics (rootSpan, metrics) {
|
|
|
353
361
|
}
|
|
354
362
|
}
|
|
355
363
|
|
|
356
|
-
|
|
364
|
+
// NOTE: `req` in the WAF execution path may be any object, not necessarily
|
|
365
|
+
// an HTTP IncomingMessage. In Lambda, it is a plain context key ({}) with no
|
|
366
|
+
// HTTP properties. Always guard HTTP-specific property access
|
|
367
|
+
// See tests in lambda.spec.js for enforcement.
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* @param {{ events: Array, actions: object }} result - WAF result with attack data
|
|
371
|
+
* @param {object} [req] - Request key. May be an HTTP IncomingMessage or a plain object (Lambda)
|
|
372
|
+
* @param {object} [rootSpan] - Root span (required for lambda)
|
|
373
|
+
*/
|
|
374
|
+
function reportAttack ({ events: attackData, actions }, req, rootSpan) {
|
|
357
375
|
if (!req) {
|
|
358
376
|
req = getActiveRequest()
|
|
359
377
|
}
|
|
360
378
|
|
|
361
|
-
|
|
362
|
-
|
|
379
|
+
if (!rootSpan) {
|
|
380
|
+
rootSpan = web.root(req)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!req || !rootSpan) return
|
|
363
384
|
|
|
364
385
|
const spanContext = rootSpan.context()
|
|
365
386
|
|
|
@@ -494,14 +515,21 @@ function isSchemaAttribute (attribute) {
|
|
|
494
515
|
return attribute.startsWith('_dd.appsec.s.')
|
|
495
516
|
}
|
|
496
517
|
|
|
497
|
-
|
|
518
|
+
/**
|
|
519
|
+
* @param {object} [attributes] - WAF result attributes
|
|
520
|
+
* @param {object} [req] - Request key (plain object for lambda)
|
|
521
|
+
* @param {object} [rootSpan] - Root span (required for lambda)
|
|
522
|
+
*/
|
|
523
|
+
function reportAttributes (attributes, req, rootSpan) {
|
|
498
524
|
if (!attributes) return
|
|
499
525
|
|
|
500
526
|
if (!req) {
|
|
501
527
|
req = getActiveRequest()
|
|
502
528
|
}
|
|
503
529
|
|
|
504
|
-
|
|
530
|
+
if (!rootSpan) {
|
|
531
|
+
rootSpan = web.root(req)
|
|
532
|
+
}
|
|
505
533
|
|
|
506
534
|
if (!rootSpan) return
|
|
507
535
|
|
|
@@ -517,8 +545,17 @@ function reportAttributes (attributes, req) {
|
|
|
517
545
|
rootSpan.addTags(tags)
|
|
518
546
|
}
|
|
519
547
|
|
|
520
|
-
|
|
521
|
-
|
|
548
|
+
/**
|
|
549
|
+
* @param {object} [req] - Request key (null for Lambda)
|
|
550
|
+
* @param {object} [res] - Response object (null for lambda)
|
|
551
|
+
* @param {object} storedResponseHeaders
|
|
552
|
+
* @param {object} [requestBody]
|
|
553
|
+
* @param {object} [rootSpan] - Root span (required for lambda)
|
|
554
|
+
*/
|
|
555
|
+
function finishRequest (req, res, storedResponseHeaders, requestBody, rootSpan) {
|
|
556
|
+
if (!rootSpan) {
|
|
557
|
+
rootSpan = web.root(req)
|
|
558
|
+
}
|
|
522
559
|
if (!rootSpan) return
|
|
523
560
|
|
|
524
561
|
if (metricsQueue.size) {
|
|
@@ -569,6 +606,8 @@ function finishRequest (req, res, storedResponseHeaders, requestBody) {
|
|
|
569
606
|
rootSpan.setTag('_dd.appsec.rasp.rule.eval', metrics.raspEvalCount)
|
|
570
607
|
}
|
|
571
608
|
|
|
609
|
+
if (!req) return
|
|
610
|
+
|
|
572
611
|
incrementWafRequestsMetric(req)
|
|
573
612
|
|
|
574
613
|
const tags = rootSpan.context().getTags()
|
|
@@ -27,7 +27,7 @@ const metricsStoreMap = new WeakMap()
|
|
|
27
27
|
let enabled = false
|
|
28
28
|
|
|
29
29
|
function enable (config) {
|
|
30
|
-
enabled = config.telemetry
|
|
30
|
+
enabled = config.telemetry.DD_INSTRUMENTATION_TELEMETRY_ENABLED && config.telemetry.DD_TELEMETRY_METRICS_ENABLED
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function disable () {
|
|
@@ -110,7 +110,19 @@ function removeConfig (configPath) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
// `req` in the WAF execution path may be any object, not necessarily
|
|
114
|
+
// an HTTP IncomingMessage (it is a plain object for lambda).
|
|
115
|
+
// Always guard HTTP-specific property access.
|
|
116
|
+
// @see packages/dd-trace/test/appsec/lambda.spec.js
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {object} data - WAF address data ({ persistent, ephemeral })
|
|
120
|
+
* @param {object} req - Request key for WAF context lookup. May be an HTTP
|
|
121
|
+
* IncomingMessage or a plain object (Lambda invocation key).
|
|
122
|
+
* @param {string} [raspRule] - RASP rule identifier
|
|
123
|
+
* @param {object} [rootSpan] - Root span to tag (required for Lambda)
|
|
124
|
+
*/
|
|
125
|
+
function run (data, req, raspRule, rootSpan) {
|
|
114
126
|
if (!req) {
|
|
115
127
|
req = getActiveRequest()
|
|
116
128
|
if (!req) {
|
|
@@ -120,12 +132,12 @@ function run (data, req, raspRule) {
|
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
const wafContext = waf.wafManager.getWAFContext(req)
|
|
123
|
-
const result = wafContext.run(data, raspRule, req)
|
|
135
|
+
const result = wafContext.run(data, raspRule, req, rootSpan)
|
|
124
136
|
|
|
125
137
|
if (result?.keep) {
|
|
126
138
|
if (limiter.isAllowed()) {
|
|
127
|
-
const
|
|
128
|
-
keepTrace(
|
|
139
|
+
const span = rootSpan || web.root(req)
|
|
140
|
+
keepTrace(span, ASM)
|
|
129
141
|
} else {
|
|
130
142
|
updateRateLimitedMetric(req)
|
|
131
143
|
}
|
|
@@ -36,7 +36,7 @@ class WAFContextWrapper {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
run ({ persistent, ephemeral }, raspRule, req) {
|
|
39
|
+
run ({ persistent, ephemeral }, raspRule, req, rootSpan) {
|
|
40
40
|
if (this.ddwafContext.disposed) {
|
|
41
41
|
log.warn('[ASM] Calling run on a disposed context')
|
|
42
42
|
if (raspRule) {
|
|
@@ -156,10 +156,10 @@ class WAFContextWrapper {
|
|
|
156
156
|
metrics.wafTimeout = result.timeout
|
|
157
157
|
|
|
158
158
|
if (ruleTriggered) {
|
|
159
|
-
Reporter.reportAttack(result, req)
|
|
159
|
+
Reporter.reportAttack(result, req, rootSpan)
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
Reporter.reportAttributes(result.attributes, req)
|
|
162
|
+
Reporter.reportAttributes(result.attributes, req, rootSpan)
|
|
163
163
|
|
|
164
164
|
return result
|
|
165
165
|
} catch (err) {
|
|
@@ -171,7 +171,7 @@ class WAFContextWrapper {
|
|
|
171
171
|
wafRunFinished.publish({ payload })
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
Reporter.reportMetrics(metrics, raspRule, req)
|
|
174
|
+
Reporter.reportMetrics(metrics, raspRule, req, rootSpan)
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
|
|
18
18
|
const { getNumFromKnownTests } = require('../../plugins/util/test')
|
|
19
19
|
const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
|
|
20
|
+
const { validateKnownTestsResponse } = require('../test-optimization-http-cache-schema')
|
|
20
21
|
|
|
21
22
|
const MAX_KNOWN_TESTS_PAGES = 10_000
|
|
22
23
|
|
|
@@ -52,6 +53,19 @@ function mergeKnownTests (aggregate, page) {
|
|
|
52
53
|
return aggregate
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
function parseJsonResponse (rawJson) {
|
|
57
|
+
return typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseKnownTestsResponse (rawJson, options = {}) {
|
|
61
|
+
const parsedResponse = parseJsonResponse(rawJson)
|
|
62
|
+
if (options.validateRequiredFields) {
|
|
63
|
+
validateKnownTestsResponse(parsedResponse)
|
|
64
|
+
}
|
|
65
|
+
const { data: { attributes: { tests } } } = parsedResponse
|
|
66
|
+
return tests
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
function getKnownTests ({
|
|
56
70
|
url,
|
|
57
71
|
isEvpProxy,
|
|
@@ -151,12 +165,12 @@ function fetchFromApi ({
|
|
|
151
165
|
options.path = `${evpProxyPrefix}/api/v2/ci/libraries/tests`
|
|
152
166
|
options.headers['X-Datadog-EVP-Subdomain'] = 'api'
|
|
153
167
|
} else {
|
|
154
|
-
const {
|
|
155
|
-
if (!
|
|
168
|
+
const { DD_API_KEY } = getConfig()
|
|
169
|
+
if (!DD_API_KEY) {
|
|
156
170
|
return done(new Error('Known tests were not fetched because Datadog API key is not defined.'))
|
|
157
171
|
}
|
|
158
172
|
|
|
159
|
-
options.headers['dd-api-key'] =
|
|
173
|
+
options.headers['dd-api-key'] = DD_API_KEY
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
const configurations = {
|
|
@@ -211,8 +225,9 @@ function fetchFromApi ({
|
|
|
211
225
|
try {
|
|
212
226
|
totalResponseBytes += res.length
|
|
213
227
|
|
|
214
|
-
const
|
|
215
|
-
const
|
|
228
|
+
const parsedResponse = parseJsonResponse(res)
|
|
229
|
+
const pageTests = parseKnownTestsResponse(parsedResponse)
|
|
230
|
+
const { page_info: responsePageInfo } = parsedResponse.data.attributes
|
|
216
231
|
|
|
217
232
|
aggregateTests = mergeKnownTests(aggregateTests, pageTests)
|
|
218
233
|
|
|
@@ -251,4 +266,4 @@ function fetchFromApi ({
|
|
|
251
266
|
fetchPage(null)
|
|
252
267
|
}
|
|
253
268
|
|
|
254
|
-
module.exports = { getKnownTests }
|
|
269
|
+
module.exports = { getKnownTests, parseKnownTestsResponse }
|
|
@@ -40,7 +40,7 @@ class AgentProxyCiVisibilityExporter extends CiVisibilityExporter {
|
|
|
40
40
|
lookup,
|
|
41
41
|
protocolVersion,
|
|
42
42
|
headers,
|
|
43
|
-
|
|
43
|
+
testOptimization,
|
|
44
44
|
} = config
|
|
45
45
|
|
|
46
46
|
fetchAgentInfo(this._url, (err, agentInfo) => {
|
|
@@ -68,7 +68,7 @@ class AgentProxyCiVisibilityExporter extends CiVisibilityExporter {
|
|
|
68
68
|
evpProxyPrefix,
|
|
69
69
|
})
|
|
70
70
|
this._codeCoverageReportUrl = this._url
|
|
71
|
-
if (
|
|
71
|
+
if (testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED) {
|
|
72
72
|
const canFowardLogs = getCanForwardDebuggerLogs(err, agentInfo)
|
|
73
73
|
if (canFowardLogs) {
|
|
74
74
|
const DynamicInstrumentationLogsWriter = require('../agentless/di-logs-writer')
|
|
@@ -26,7 +26,7 @@ class DynamicInstrumentationLogsWriter extends BaseWriter {
|
|
|
26
26
|
path: '/api/v2/logs',
|
|
27
27
|
method: 'POST',
|
|
28
28
|
headers: {
|
|
29
|
-
'dd-api-key': getConfig().
|
|
29
|
+
'dd-api-key': getConfig().DD_API_KEY,
|
|
30
30
|
'Content-Type': 'application/json',
|
|
31
31
|
},
|
|
32
32
|
timeout: this.timeout,
|
|
@@ -9,7 +9,8 @@ const CoverageWriter = require('./coverage-writer')
|
|
|
9
9
|
class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
|
|
10
10
|
constructor (config) {
|
|
11
11
|
super(config)
|
|
12
|
-
const { tags, site,
|
|
12
|
+
const { tags, site, testOptimization } = config
|
|
13
|
+
const { DD_CIVISIBILITY_AGENTLESS_URL: url } = testOptimization
|
|
13
14
|
// we don't need to request /info because we are using agentless by configuration
|
|
14
15
|
this._isInitialized = true
|
|
15
16
|
this._resolveCanUseCiVisProtocol(true)
|
|
@@ -23,7 +24,7 @@ class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
|
|
|
23
24
|
|
|
24
25
|
this._codeCoverageReportUrl = url || new URL(`https://ci-intake.${site}`)
|
|
25
26
|
|
|
26
|
-
if (
|
|
27
|
+
if (testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED) {
|
|
27
28
|
const DynamicInstrumentationLogsWriter = require('./di-logs-writer')
|
|
28
29
|
this._logsUrl = url || new URL(`https://http-intake.logs.${site}`)
|
|
29
30
|
this._logsWriter = new DynamicInstrumentationLogsWriter({ url: this._logsUrl, tags })
|