dd-trace 5.108.0 → 5.109.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/index.d.ts +22 -1
- package/package.json +2 -1
- package/packages/datadog-instrumentations/src/ai.js +43 -48
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
- package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
- package/packages/datadog-instrumentations/src/electron.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/http/client.js +12 -2
- package/packages/datadog-instrumentations/src/ioredis.js +0 -1
- package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
- package/packages/datadog-instrumentations/src/next.js +34 -0
- package/packages/datadog-instrumentations/src/openai.js +77 -18
- package/packages/datadog-instrumentations/src/redis.js +0 -1
- package/packages/datadog-instrumentations/src/vitest.js +60 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +1 -7
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +44 -27
- package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
- package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
- package/packages/datadog-plugin-fs/src/index.js +1 -0
- package/packages/datadog-plugin-redis/src/index.js +1 -2
- package/packages/datadog-plugin-vitest/src/index.js +4 -1
- package/packages/dd-trace/src/aiguard/channels.js +0 -1
- package/packages/dd-trace/src/aiguard/index.js +11 -49
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
- package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
- package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
- package/packages/dd-trace/src/config/supported-configurations.json +24 -2
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
- package/packages/dd-trace/src/dogstatsd.js +15 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
- package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
- package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
|
@@ -14,19 +14,32 @@ const {
|
|
|
14
14
|
const KNUTH_FACTOR = 11400714819323199488n // eslint-disable-line unicorn/numeric-separators-style
|
|
15
15
|
const UINT64_MAX = (1n << 64n) - 1n
|
|
16
16
|
|
|
17
|
+
const SUPPORTED_RESPONSE_BODY_MIME_TYPES = new Set([
|
|
18
|
+
'application/json',
|
|
19
|
+
'text/json',
|
|
20
|
+
'application/x-www-form-urlencoded',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const RESPONSE_BODY_IGNORED_TAG_CONTENT_TYPE =
|
|
24
|
+
'_dd.appsec.downstream_request.response_body_ignored.content_type_invalid'
|
|
25
|
+
const RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_MISSING =
|
|
26
|
+
'_dd.appsec.downstream_request.response_body_ignored.content_length_missing'
|
|
27
|
+
const RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_TOO_BIG =
|
|
28
|
+
'_dd.appsec.downstream_request.response_body_ignored.content_length_too_big'
|
|
29
|
+
|
|
17
30
|
let config
|
|
18
31
|
let samplingRate
|
|
19
32
|
let globalRequestCounter
|
|
20
33
|
let bodyAnalysisCount
|
|
21
34
|
let downstreamAnalysisCount
|
|
22
|
-
let
|
|
35
|
+
let responseBodyIgnoredCount
|
|
23
36
|
|
|
24
37
|
function enable (_config) {
|
|
25
38
|
config = _config
|
|
26
39
|
globalRequestCounter = 0n
|
|
27
40
|
bodyAnalysisCount = new WeakMap()
|
|
28
41
|
downstreamAnalysisCount = new WeakMap()
|
|
29
|
-
|
|
42
|
+
responseBodyIgnoredCount = new WeakMap()
|
|
30
43
|
|
|
31
44
|
const bodyAnalysisSampleRate = config.appsec.apiSecurity?.downstreamBodyAnalysisSampleRate
|
|
32
45
|
samplingRate = Math.min(Math.max(bodyAnalysisSampleRate, 0), 1)
|
|
@@ -43,49 +56,84 @@ function disable () {
|
|
|
43
56
|
globalRequestCounter = null
|
|
44
57
|
bodyAnalysisCount = null
|
|
45
58
|
downstreamAnalysisCount = null
|
|
46
|
-
|
|
59
|
+
responseBodyIgnoredCount = null
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
/**
|
|
50
|
-
*
|
|
51
|
-
* @
|
|
52
|
-
* @param {string} outgoingUrl the URL being requested.
|
|
53
|
-
* @returns {boolean} the stored decision
|
|
63
|
+
* @param {string|string[]|undefined} contentLength raw content-length header value.
|
|
64
|
+
* @returns {number|null} parsed content length or null when invalid.
|
|
54
65
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
function parseContentLengthHeader (contentLength) {
|
|
67
|
+
if (contentLength == null) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const value = Array.isArray(contentLength) ? contentLength[0] : contentLength
|
|
72
|
+
const parsed = Number.parseInt(String(value), 10)
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parsed
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
/**
|
|
63
|
-
*
|
|
64
|
-
* @param {import('http').IncomingMessage} req
|
|
65
|
-
* @param {string}
|
|
82
|
+
* Increments a response-body-ignored counter on the service-entry span.
|
|
83
|
+
* @param {import('http').IncomingMessage} req originating request.
|
|
84
|
+
* @param {string} tag full `_dd.appsec.downstream_request.response_body_ignored.*` span tag.
|
|
66
85
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
86
|
+
function recordResponseBodyIgnored (req, tag) {
|
|
87
|
+
const span = web.root(req)
|
|
88
|
+
if (!span) return
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
let counts = responseBodyIgnoredCount.get(req)
|
|
91
|
+
if (!counts) {
|
|
92
|
+
counts = {}
|
|
93
|
+
responseBodyIgnoredCount.set(req, counts)
|
|
73
94
|
}
|
|
74
95
|
|
|
75
|
-
|
|
96
|
+
const current = counts[tag] || 0
|
|
97
|
+
const next = current + 1
|
|
98
|
+
counts[tag] = next
|
|
99
|
+
span.setTag(tag, next)
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
/**
|
|
79
|
-
*
|
|
80
|
-
* @param {import('http').IncomingMessage}
|
|
81
|
-
* @
|
|
82
|
-
* @returns {boolean} true when the downstream response body should be captured.
|
|
103
|
+
* @param {import('http').IncomingMessage} originatingReq inbound request (for metrics).
|
|
104
|
+
* @param {import('http').IncomingMessage} res downstream response.
|
|
105
|
+
* @returns {boolean} whether downstream response body should be collected for AppSec.
|
|
83
106
|
*/
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
function evaluateResponseBodyCollection (originatingReq, res) {
|
|
108
|
+
const maxBytes = config.appsec.apiSecurity.maxDownstreamBodyBytes
|
|
109
|
+
|
|
110
|
+
const mime = extractMimeType(res.headers?.['content-type'])
|
|
111
|
+
if (!mime || !SUPPORTED_RESPONSE_BODY_MIME_TYPES.has(mime)) {
|
|
112
|
+
recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_TYPE)
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const declaredContentLength = parseContentLengthHeader(res.headers?.['content-length'])
|
|
117
|
+
if (declaredContentLength == null || declaredContentLength === 0) {
|
|
118
|
+
recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_MISSING)
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (declaredContentLength > maxBytes) {
|
|
123
|
+
recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_TOO_BIG)
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
88
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Probabilistic gate for downstream response body capture (rate + per-request cap).
|
|
132
|
+
* Only used from {@link planResponseBodyCollection}; does not increment {@link bodyAnalysisCount}.
|
|
133
|
+
* @param {import('http').IncomingMessage} req originating server request.
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
function shouldSampleBody (req) {
|
|
89
137
|
globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX
|
|
90
138
|
|
|
91
139
|
const currentCount = bodyAnalysisCount.get(req) || 0
|
|
@@ -97,14 +145,43 @@ function shouldSampleBody (req, outgoingUrl) {
|
|
|
97
145
|
// Replace 1000n with the accuraccy that we want to maintain
|
|
98
146
|
const threshold = (UINT64_MAX * BigInt(Math.round(samplingRate * 1000))) / 1000n
|
|
99
147
|
|
|
100
|
-
|
|
148
|
+
return hashed <= threshold
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {import('http').IncomingMessage} res downstream HTTP response.
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
function isRedirectResponse (res) {
|
|
156
|
+
const location = res.headers?.location || ''
|
|
157
|
+
return res.statusCode >= 300 && res.statusCode < 400 && !!location
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Plans downstream response body capture on the instrumentation ctx when response headers arrive.
|
|
162
|
+
* Redirect responses (3xx + Location) are ignored; each outbound hop is evaluated independently
|
|
163
|
+
* when its own non-redirect response arrives.
|
|
164
|
+
* @param {import('http').IncomingMessage} originatingReq incoming server request.
|
|
165
|
+
* @param {import('http').IncomingMessage} res downstream response.
|
|
166
|
+
* @param {object} ctx http client instrumentation context (mutated).
|
|
167
|
+
*/
|
|
168
|
+
function planResponseBodyCollection (originatingReq, res, ctx) {
|
|
169
|
+
if (!config?.appsec.apiSecurity) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
101
172
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
incrementBodyAnalysisCount(req)
|
|
173
|
+
if (isRedirectResponse(res)) {
|
|
174
|
+
return
|
|
105
175
|
}
|
|
106
176
|
|
|
107
|
-
|
|
177
|
+
if (!shouldSampleBody(originatingReq)) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (evaluateResponseBodyCollection(originatingReq, res)) {
|
|
182
|
+
ctx.shouldCollectBody = true
|
|
183
|
+
incrementBodyAnalysisCount(originatingReq)
|
|
184
|
+
}
|
|
108
185
|
}
|
|
109
186
|
|
|
110
187
|
/**
|
|
@@ -145,25 +222,6 @@ function extractRequestData (ctx) {
|
|
|
145
222
|
return addresses
|
|
146
223
|
}
|
|
147
224
|
|
|
148
|
-
/**
|
|
149
|
-
* Checks if a response is a redirect
|
|
150
|
-
* @param {import('http').IncomingMessage} req incoming server request.
|
|
151
|
-
* @param {import('http').IncomingMessage} res downstream response object.
|
|
152
|
-
* @returns {boolean} is redirect.
|
|
153
|
-
*/
|
|
154
|
-
function handleRedirectResponse (req, res) {
|
|
155
|
-
const isRedirect = res.statusCode >= 300 && res.statusCode < 400
|
|
156
|
-
const redirectLocation = res.headers?.location || ''
|
|
157
|
-
|
|
158
|
-
if (isRedirect && redirectLocation) {
|
|
159
|
-
// Store the body collection decision for the redirect target
|
|
160
|
-
storeRedirectBodyCollectionDecision(req, redirectLocation)
|
|
161
|
-
return true
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return false
|
|
165
|
-
}
|
|
166
|
-
|
|
167
225
|
/**
|
|
168
226
|
* Extracts response data for WAF analysis.
|
|
169
227
|
* @param {import('http').IncomingMessage} res downstream response object.
|
|
@@ -291,13 +349,8 @@ function extractMimeType (contentType) {
|
|
|
291
349
|
module.exports = {
|
|
292
350
|
enable,
|
|
293
351
|
disable,
|
|
294
|
-
|
|
295
|
-
handleRedirectResponse,
|
|
352
|
+
planResponseBodyCollection,
|
|
296
353
|
incrementDownstreamAnalysisCount,
|
|
297
354
|
extractRequestData,
|
|
298
355
|
extractResponseData,
|
|
299
|
-
// exports for tests
|
|
300
|
-
parseBody,
|
|
301
|
-
getMethod,
|
|
302
|
-
storeRedirectBodyCollectionDecision,
|
|
303
356
|
}
|
|
@@ -2,24 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../../../../../log')
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const OPEN_PAREN = 0x28
|
|
6
|
+
const CLOSE_PAREN = 0x29
|
|
7
|
+
const EQUALS = 0x3D
|
|
8
|
+
const TILDE = 0x7E
|
|
9
|
+
const LESS = 0x3C
|
|
10
|
+
const GREATER = 0x3E
|
|
7
11
|
|
|
12
|
+
// Linear scanner for LDAP assertion-filter values. For each parenthesised group
|
|
13
|
+
// "(attr <op> value)" where <op> is "=", "~=", "<=", or ">=" and no nested
|
|
14
|
+
// parenthesis appears before the operator, report the value range [opEnd, ')').
|
|
15
|
+
// The cursor only ever moves forward, so total work is O(input length).
|
|
8
16
|
module.exports = function extractSensitiveRanges (evidence) {
|
|
9
17
|
try {
|
|
10
|
-
|
|
18
|
+
const value = evidence?.value
|
|
11
19
|
const tokens = []
|
|
20
|
+
if (typeof value !== 'string') return tokens
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
const length = value.length
|
|
23
|
+
let cursor = 0
|
|
24
|
+
|
|
25
|
+
while (cursor < length) {
|
|
26
|
+
const open = value.indexOf('(', cursor)
|
|
27
|
+
if (open === -1) break
|
|
28
|
+
|
|
29
|
+
let scan = open + 1
|
|
30
|
+
let opStart = -1
|
|
31
|
+
let opLen = 0
|
|
32
|
+
|
|
33
|
+
while (scan < length) {
|
|
34
|
+
const code = value.charCodeAt(scan)
|
|
35
|
+
if (code === OPEN_PAREN || code === CLOSE_PAREN) break
|
|
36
|
+
if (code === EQUALS) {
|
|
37
|
+
opStart = scan
|
|
38
|
+
opLen = 1
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
if ((code === TILDE || code === LESS || code === GREATER) &&
|
|
42
|
+
scan + 1 < length && value.charCodeAt(scan + 1) === EQUALS) {
|
|
43
|
+
opStart = scan
|
|
44
|
+
opLen = 2
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
scan++
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (opStart === -1) {
|
|
51
|
+
cursor = open + 1
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const close = value.indexOf(')', opStart + opLen)
|
|
56
|
+
if (close === -1) break
|
|
57
|
+
|
|
58
|
+
const start = opStart + opLen
|
|
59
|
+
if (start < close) {
|
|
60
|
+
tokens.push({ start, end: close })
|
|
61
|
+
}
|
|
62
|
+
cursor = close + 1
|
|
22
63
|
}
|
|
64
|
+
|
|
23
65
|
return tokens
|
|
24
66
|
} catch (e) {
|
|
25
67
|
log.debug('[ASM] Error extracting sensitive ranges', e)
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
const log = require('../../../../../log')
|
|
4
4
|
|
|
5
5
|
const AUTHORITY = '^(?:[^:]+:)?//([^@]+)@'
|
|
6
|
-
|
|
6
|
+
// The key class excludes `?` and `#` so the greedy quantifier is bounded per fragment.
|
|
7
|
+
// Query keys cannot legitimately contain those characters (they delimit query/fragment
|
|
8
|
+
// boundaries), so excluding them preserves match semantics for valid URLs while keeping
|
|
9
|
+
// the regex linear on arbitrary input.
|
|
10
|
+
const QUERY_FRAGMENT = '[?#&]([^=&;?#]+)=([^?#&]+)'
|
|
7
11
|
const pattern = new RegExp([AUTHORITY, QUERY_FRAGMENT].join('|'), 'gmi')
|
|
8
12
|
|
|
9
13
|
module.exports = function extractSensitiveRanges (evidence) {
|
|
@@ -17,6 +17,11 @@ const urlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyz
|
|
|
17
17
|
|
|
18
18
|
const REDACTED_SOURCE_BUFFER = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
19
19
|
|
|
20
|
+
// Upper bound on the evidence-string length the redaction analyzers will scan. Oversized
|
|
21
|
+
// values bypass the analyzer entirely and are emitted as a fully-redacted placeholder.
|
|
22
|
+
// Counted in JS string characters (UTF-16 code units), not bytes.
|
|
23
|
+
const MAX_EVIDENCE_LENGTH = 32_768
|
|
24
|
+
|
|
20
25
|
class SensitiveHandler {
|
|
21
26
|
constructor () {
|
|
22
27
|
this._namePattern = new RegExp(/** @type {string} */ (defaults['iast.redactionNamePattern']), 'gmi')
|
|
@@ -53,11 +58,31 @@ class SensitiveHandler {
|
|
|
53
58
|
|
|
54
59
|
scrubEvidence (vulnerabilityType, evidence, sourcesIndexes, sources) {
|
|
55
60
|
const sensitiveAnalyzer = this._sensitiveAnalyzers.get(vulnerabilityType)
|
|
56
|
-
if (sensitiveAnalyzer) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
if (!sensitiveAnalyzer) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Oversized evidence: skip the analyzer and emit a fully-redacted placeholder. Mark
|
|
66
|
+
// every source backing this vulnerability as redacted so the formatter strips their
|
|
67
|
+
// raw `value` before they leave the process.
|
|
68
|
+
if (typeof evidence.value === 'string' && evidence.value.length > MAX_EVIDENCE_LENGTH) {
|
|
69
|
+
const redactedSources = []
|
|
70
|
+
for (const sourceIndex of sourcesIndexes) {
|
|
71
|
+
const source = sources[sourceIndex]
|
|
72
|
+
if (source && !source.redacted) {
|
|
73
|
+
source.pattern = ''.padEnd(source.value.length, REDACTED_SOURCE_BUFFER)
|
|
74
|
+
source.redacted = true
|
|
75
|
+
}
|
|
76
|
+
if (!redactedSources.includes(sourceIndex)) {
|
|
77
|
+
redactedSources.push(sourceIndex)
|
|
78
|
+
}
|
|
60
79
|
}
|
|
80
|
+
return { redactedValueParts: [{ redacted: true }], redactedSources }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sensitiveRanges = sensitiveAnalyzer(evidence)
|
|
84
|
+
if (evidence.ranges || sensitiveRanges?.length) {
|
|
85
|
+
return this.toRedactedJson(evidence, sensitiveRanges, sourcesIndexes, sources)
|
|
61
86
|
}
|
|
62
87
|
return null
|
|
63
88
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { format } = require('url')
|
|
4
4
|
const {
|
|
5
5
|
httpClientRequestStart,
|
|
6
|
+
httpClientResponseStart,
|
|
6
7
|
httpClientResponseFinish,
|
|
7
8
|
} = require('../channels')
|
|
8
9
|
const addresses = require('../addresses')
|
|
@@ -21,6 +22,7 @@ function enable (_config) {
|
|
|
21
22
|
downstream.enable(_config)
|
|
22
23
|
|
|
23
24
|
httpClientRequestStart.subscribe(analyzeSsrf)
|
|
25
|
+
httpClientResponseStart.subscribe(planResponseBodyCollection)
|
|
24
26
|
httpClientResponseFinish.subscribe(handleResponseFinish)
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -28,6 +30,7 @@ function disable () {
|
|
|
28
30
|
downstream.disable()
|
|
29
31
|
|
|
30
32
|
if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
|
|
33
|
+
if (httpClientResponseStart.hasSubscribers) httpClientResponseStart.unsubscribe(planResponseBodyCollection)
|
|
31
34
|
if (httpClientResponseFinish.hasSubscribers) httpClientResponseFinish.unsubscribe(handleResponseFinish)
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -37,9 +40,6 @@ function analyzeSsrf (ctx) {
|
|
|
37
40
|
|
|
38
41
|
if (!req || !outgoingUrl) return
|
|
39
42
|
|
|
40
|
-
// Determine if we should collect the response body based on sampling rate and redirect URL
|
|
41
|
-
ctx.shouldCollectBody = downstream.shouldSampleBody(req, outgoingUrl)
|
|
42
|
-
|
|
43
43
|
const requestAddresses = downstream.extractRequestData(ctx)
|
|
44
44
|
|
|
45
45
|
const ephemeral = {
|
|
@@ -56,13 +56,23 @@ function analyzeSsrf (ctx) {
|
|
|
56
56
|
downstream.incrementDownstreamAnalysisCount(req)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Channel handler: plans downstream response body capture once response headers are available.
|
|
61
|
+
* @param {{ ctx: object, res: import('http').IncomingMessage }} payload channel payload.
|
|
62
|
+
*/
|
|
63
|
+
function planResponseBodyCollection ({ ctx, res }) {
|
|
64
|
+
const originatingRequest = getActiveRequest()
|
|
65
|
+
if (!originatingRequest || !res) return
|
|
66
|
+
|
|
67
|
+
downstream.planResponseBodyCollection(originatingRequest, res, ctx)
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
/**
|
|
60
71
|
* Finalizes body collection for the response and triggers RASP analysis.
|
|
61
|
-
* @param {
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* }} payload event payload from the channel.
|
|
72
|
+
* @param {object} params event payload from the channel.
|
|
73
|
+
* @param {object} params.ctx instrumentation context.
|
|
74
|
+
* @param {import('http').IncomingMessage} params.res downstream response.
|
|
75
|
+
* @param {string|Buffer|null} params.body collected body.
|
|
66
76
|
*/
|
|
67
77
|
function handleResponseFinish ({ ctx, res, body }) {
|
|
68
78
|
// downstream response object
|
|
@@ -71,9 +81,7 @@ function handleResponseFinish ({ ctx, res, body }) {
|
|
|
71
81
|
const originatingRequest = getActiveRequest()
|
|
72
82
|
if (!originatingRequest) return
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
const evaluateBody = ctx.shouldCollectBody && !downstream.handleRedirectResponse(originatingRequest, res)
|
|
76
|
-
const responseBody = evaluateBody ? body : null
|
|
84
|
+
const responseBody = ctx.shouldCollectBody ? body : null
|
|
77
85
|
runResponseEvaluation(res, originatingRequest, responseBody)
|
|
78
86
|
}
|
|
79
87
|
|
|
@@ -11,6 +11,7 @@ export interface GeneratedConfig {
|
|
|
11
11
|
enabled: boolean;
|
|
12
12
|
endpointCollectionEnabled: boolean;
|
|
13
13
|
endpointCollectionMessageLimit: number;
|
|
14
|
+
maxDownstreamBodyBytes: number;
|
|
14
15
|
maxDownstreamRequestBodyAnalysis: number;
|
|
15
16
|
sampleDelay: number;
|
|
16
17
|
};
|
|
@@ -87,6 +88,7 @@ export interface GeneratedConfig {
|
|
|
87
88
|
DD_CRASHTRACKING_ENABLED: boolean;
|
|
88
89
|
DD_CUSTOM_PARENT_ID: string | undefined;
|
|
89
90
|
DD_CUSTOM_TRACE_ID: string | undefined;
|
|
91
|
+
DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED: boolean;
|
|
90
92
|
DD_ENABLE_LAGE_PACKAGE_NAME: boolean;
|
|
91
93
|
DD_ENABLE_NX_SERVICE_NAME: boolean;
|
|
92
94
|
DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED: boolean;
|
|
@@ -177,6 +179,7 @@ export interface GeneratedConfig {
|
|
|
177
179
|
DD_TRACE_APOLLO_SUBGRAPH_ENABLED: boolean;
|
|
178
180
|
DD_TRACE_AVSC_ENABLED: boolean;
|
|
179
181
|
DD_TRACE_AWS_ADD_SPAN_POINTERS: boolean;
|
|
182
|
+
DD_TRACE_AWS_DURABLE_EXECUTION_SDK_JS_ENABLED: boolean;
|
|
180
183
|
DD_TRACE_AWS_SDK_AWS_BATCH_PROPAGATION_ENABLED: boolean;
|
|
181
184
|
DD_TRACE_AWS_SDK_AWS_ENABLED: boolean;
|
|
182
185
|
DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED: boolean;
|
|
@@ -167,6 +167,14 @@
|
|
|
167
167
|
"default": "1"
|
|
168
168
|
}
|
|
169
169
|
],
|
|
170
|
+
"DD_API_SECURITY_MAX_DOWNSTREAM_BODY_BYTES": [
|
|
171
|
+
{
|
|
172
|
+
"implementation": "A",
|
|
173
|
+
"type": "int",
|
|
174
|
+
"internalPropertyName": "appsec.apiSecurity.maxDownstreamBodyBytes",
|
|
175
|
+
"default": "10485760"
|
|
176
|
+
}
|
|
177
|
+
],
|
|
170
178
|
"DD_API_SECURITY_SAMPLE_DELAY": [
|
|
171
179
|
{
|
|
172
180
|
"implementation": "A",
|
|
@@ -1247,6 +1255,20 @@
|
|
|
1247
1255
|
"default": "false"
|
|
1248
1256
|
}
|
|
1249
1257
|
],
|
|
1258
|
+
"DD_TRACE_AWS_DURABLE_EXECUTION_SDK_JS_ENABLED": [
|
|
1259
|
+
{
|
|
1260
|
+
"implementation": "A",
|
|
1261
|
+
"type": "boolean",
|
|
1262
|
+
"default": "true"
|
|
1263
|
+
}
|
|
1264
|
+
],
|
|
1265
|
+
"DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED": [
|
|
1266
|
+
{
|
|
1267
|
+
"implementation": "A",
|
|
1268
|
+
"type": "boolean",
|
|
1269
|
+
"default": "true"
|
|
1270
|
+
}
|
|
1271
|
+
],
|
|
1250
1272
|
"DD_TRACE_LOG_LEVEL": [
|
|
1251
1273
|
{
|
|
1252
1274
|
"implementation": "C",
|
|
@@ -2633,9 +2655,9 @@
|
|
|
2633
2655
|
],
|
|
2634
2656
|
"DD_TRACE_FS_ENABLED": [
|
|
2635
2657
|
{
|
|
2636
|
-
"implementation": "
|
|
2658
|
+
"implementation": "B",
|
|
2637
2659
|
"type": "boolean",
|
|
2638
|
-
"default": "
|
|
2660
|
+
"default": "false"
|
|
2639
2661
|
}
|
|
2640
2662
|
],
|
|
2641
2663
|
"DD_TRACE_GCP_PUBSUB_PUSH_ENABLED": [
|
|
@@ -269,6 +269,8 @@ async function reEvaluateProbe (probe) {
|
|
|
269
269
|
if (probeToLocation.has(probe.id)) {
|
|
270
270
|
await removeBreakpoint(probe)
|
|
271
271
|
}
|
|
272
|
+
// TODO: Revisit diagnostic status handling for probes that recover during re-evaluation. A probe can initially
|
|
273
|
+
// report ERROR because no script matched, then attach successfully here without reporting INSTALLED.
|
|
272
274
|
await addBreakpoint(probe)
|
|
273
275
|
}
|
|
274
276
|
}
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
const dgram = require('dgram')
|
|
4
4
|
const isIP = require('net').isIP
|
|
5
5
|
|
|
6
|
+
const { storage } = require('../../datadog-core')
|
|
6
7
|
const request = require('./exporters/common/request')
|
|
7
8
|
const log = require('./log')
|
|
8
9
|
const Histogram = require('./histogram')
|
|
9
10
|
const { entityId } = require('./exporters/common/docker')
|
|
10
11
|
|
|
12
|
+
const legacyStorage = storage('legacy')
|
|
13
|
+
|
|
11
14
|
const MAX_BUFFER_SIZE = 1024 // limit from the agent
|
|
12
15
|
|
|
13
16
|
const TYPE_COUNTER = 'c'
|
|
@@ -98,14 +101,18 @@ class DogStatsDClient {
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
_sendUdp (queue) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
// dgram resolves the local address via the instrumented dns.lookup when it
|
|
105
|
+
// binds on first send; the noop store keeps that self-traffic off the trace.
|
|
106
|
+
legacyStorage.run({ noop: true }, () => {
|
|
107
|
+
if (this._family === 0) {
|
|
108
|
+
this.#lookup(this._host, (error, address, family) => {
|
|
109
|
+
if (error) return log.error('DogStatsDClient: Host not found', error)
|
|
110
|
+
this._sendUdpFromQueue(queue, address, family)
|
|
111
|
+
})
|
|
112
|
+
} else {
|
|
113
|
+
this._sendUdpFromQueue(queue, this._host, this._family)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
_sendUdpFromQueue (queue, address, family) {
|
|
@@ -7,6 +7,7 @@ const log = require('../../log')
|
|
|
7
7
|
const { entityId } = require('../common/docker')
|
|
8
8
|
const tracerVersion = require('../../../../../package.json').version
|
|
9
9
|
const Writer = require('./writer')
|
|
10
|
+
const { computeIntakeUrl } = require('./intake')
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Agentless exporter for APM trace intake.
|
|
@@ -15,6 +16,7 @@ const Writer = require('./writer')
|
|
|
15
16
|
*/
|
|
16
17
|
class AgentlessExporter {
|
|
17
18
|
#timer
|
|
19
|
+
#config
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* @param {object} config - Configuration object
|
|
@@ -24,13 +26,13 @@ class AgentlessExporter {
|
|
|
24
26
|
* @param {object} [config.tags] - Tags including runtime-id
|
|
25
27
|
*/
|
|
26
28
|
constructor (config) {
|
|
27
|
-
this
|
|
29
|
+
this.#config = config
|
|
28
30
|
const site = config.site ?? 'datadoghq.com'
|
|
29
31
|
|
|
30
32
|
try {
|
|
31
|
-
// Agentless traffic carries the Datadog API key, so the intake is always
|
|
32
|
-
//
|
|
33
|
-
this._url = new URL(
|
|
33
|
+
// Agentless traffic carries the Datadog API key, so the intake is always an https endpoint
|
|
34
|
+
// derived from the site; never config.url (the agent's cleartext http) or the key leaks.
|
|
35
|
+
this._url = new URL(computeIntakeUrl(site))
|
|
34
36
|
} catch (err) {
|
|
35
37
|
log.error('Invalid site for agentless exporter. site=%s. Error: %s', site, err.message)
|
|
36
38
|
this._url = null
|
|
@@ -89,7 +91,7 @@ class AgentlessExporter {
|
|
|
89
91
|
export (spans) {
|
|
90
92
|
this._writer.append(spans)
|
|
91
93
|
|
|
92
|
-
const { flushInterval } = this
|
|
94
|
+
const { flushInterval } = this.#config
|
|
93
95
|
|
|
94
96
|
if (flushInterval === 0) {
|
|
95
97
|
try {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Per-site hosts for the agentless JSON span intake. Regional data centers serve it from
|
|
4
|
+
// browser-intake-* hosts rather than public-trace-http-intake.logs.<site>, so a single template
|
|
5
|
+
// silently drops spans on us3/us5/ap1/ap2. Mirrors dd-trace-py's AgentlessTraceWriter.INTAKE_URLS
|
|
6
|
+
// (DataDog/dd-trace-py#18514).
|
|
7
|
+
const INTAKE_URLS = {
|
|
8
|
+
'datadoghq.com': 'https://public-trace-http-intake.logs.datadoghq.com',
|
|
9
|
+
'datadoghq.eu': 'https://public-trace-http-intake.logs.datadoghq.eu',
|
|
10
|
+
'us3.datadoghq.com': 'https://trace.browser-intake-us3-datadoghq.com',
|
|
11
|
+
'us5.datadoghq.com': 'https://trace.browser-intake-us5-datadoghq.com',
|
|
12
|
+
'ap1.datadoghq.com': 'https://browser-intake-ap1-datadoghq.com',
|
|
13
|
+
'ap2.datadoghq.com': 'https://browser-intake-ap2-datadoghq.com',
|
|
14
|
+
'uk1.datadoghq.com': 'https://browser-intake-uk1-datadoghq.com',
|
|
15
|
+
'datad0g.com': 'https://public-trace-http-intake.logs.datad0g.com',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Path of the JSON span intake on every intake host.
|
|
19
|
+
const INTAKE_PATH = '/api/v2/spans'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the agentless intake origin for a Datadog site.
|
|
23
|
+
*
|
|
24
|
+
* Unknown sites fall back to the browser-intake naming: strip the TLD, dash-join the rest, then
|
|
25
|
+
* reattach the TLD, e.g. 'us2.ddog-gov.com' -> 'https://browser-intake-us2-ddog-gov.com'.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} [site] - The Datadog site, e.g. 'us3.datadoghq.com'. Defaults to 'datadoghq.com'.
|
|
28
|
+
* @returns {string} The intake origin, without a path.
|
|
29
|
+
*/
|
|
30
|
+
function computeIntakeUrl (site = 'datadoghq.com') {
|
|
31
|
+
const normalized = site.toLowerCase()
|
|
32
|
+
const known = INTAKE_URLS[normalized]
|
|
33
|
+
if (known !== undefined) {
|
|
34
|
+
return known
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lastDot = normalized.lastIndexOf('.')
|
|
38
|
+
const prefix = lastDot === -1 ? '' : normalized.slice(0, lastDot)
|
|
39
|
+
const tld = lastDot === -1 ? normalized : normalized.slice(lastDot + 1)
|
|
40
|
+
return `https://browser-intake-${prefix.replaceAll('.', '-')}.${tld}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { INTAKE_URLS, INTAKE_PATH, computeIntakeUrl }
|