dd-trace 5.107.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 +6 -5
- 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 +114 -60
- package/packages/dd-trace/src/appsec/iast/index.js +3 -2
- 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 +21 -12
- package/packages/dd-trace/src/appsec/reporter.js +1 -1
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
- package/packages/dd-trace/src/config/supported-configurations.json +31 -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/profiling/config.js +2 -0
- package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
- package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
- 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/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/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const web = require('../plugins/util/web')
|
|
4
4
|
const log = require('../log')
|
|
5
|
+
const { isEmpty } = require('../util')
|
|
5
6
|
const {
|
|
6
7
|
HTTP_OUTGOING_METHOD,
|
|
7
8
|
HTTP_OUTGOING_HEADERS,
|
|
@@ -13,19 +14,32 @@ const {
|
|
|
13
14
|
const KNUTH_FACTOR = 11400714819323199488n // eslint-disable-line unicorn/numeric-separators-style
|
|
14
15
|
const UINT64_MAX = (1n << 64n) - 1n
|
|
15
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
|
+
|
|
16
30
|
let config
|
|
17
31
|
let samplingRate
|
|
18
32
|
let globalRequestCounter
|
|
19
33
|
let bodyAnalysisCount
|
|
20
34
|
let downstreamAnalysisCount
|
|
21
|
-
let
|
|
35
|
+
let responseBodyIgnoredCount
|
|
22
36
|
|
|
23
37
|
function enable (_config) {
|
|
24
38
|
config = _config
|
|
25
39
|
globalRequestCounter = 0n
|
|
26
40
|
bodyAnalysisCount = new WeakMap()
|
|
27
41
|
downstreamAnalysisCount = new WeakMap()
|
|
28
|
-
|
|
42
|
+
responseBodyIgnoredCount = new WeakMap()
|
|
29
43
|
|
|
30
44
|
const bodyAnalysisSampleRate = config.appsec.apiSecurity?.downstreamBodyAnalysisSampleRate
|
|
31
45
|
samplingRate = Math.min(Math.max(bodyAnalysisSampleRate, 0), 1)
|
|
@@ -42,49 +56,84 @@ function disable () {
|
|
|
42
56
|
globalRequestCounter = null
|
|
43
57
|
bodyAnalysisCount = null
|
|
44
58
|
downstreamAnalysisCount = null
|
|
45
|
-
|
|
59
|
+
responseBodyIgnoredCount = null
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
/**
|
|
49
|
-
*
|
|
50
|
-
* @
|
|
51
|
-
* @param {string} outgoingUrl the URL being requested.
|
|
52
|
-
* @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.
|
|
53
65
|
*/
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
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)
|
|
57
73
|
|
|
58
|
-
|
|
74
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parsed
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
/**
|
|
62
|
-
*
|
|
63
|
-
* @param {import('http').IncomingMessage} req
|
|
64
|
-
* @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.
|
|
65
85
|
*/
|
|
66
|
-
function
|
|
67
|
-
|
|
86
|
+
function recordResponseBodyIgnored (req, tag) {
|
|
87
|
+
const span = web.root(req)
|
|
88
|
+
if (!span) return
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
let counts = responseBodyIgnoredCount.get(req)
|
|
91
|
+
if (!counts) {
|
|
92
|
+
counts = {}
|
|
93
|
+
responseBodyIgnoredCount.set(req, counts)
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
|
|
96
|
+
const current = counts[tag] || 0
|
|
97
|
+
const next = current + 1
|
|
98
|
+
counts[tag] = next
|
|
99
|
+
span.setTag(tag, next)
|
|
75
100
|
}
|
|
76
101
|
|
|
77
102
|
/**
|
|
78
|
-
*
|
|
79
|
-
* @param {import('http').IncomingMessage}
|
|
80
|
-
* @
|
|
81
|
-
* @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.
|
|
82
106
|
*/
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|
|
87
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) {
|
|
88
137
|
globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX
|
|
89
138
|
|
|
90
139
|
const currentCount = bodyAnalysisCount.get(req) || 0
|
|
@@ -96,14 +145,43 @@ function shouldSampleBody (req, outgoingUrl) {
|
|
|
96
145
|
// Replace 1000n with the accuraccy that we want to maintain
|
|
97
146
|
const threshold = (UINT64_MAX * BigInt(Math.round(samplingRate * 1000))) / 1000n
|
|
98
147
|
|
|
99
|
-
|
|
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
|
+
}
|
|
172
|
+
|
|
173
|
+
if (isRedirectResponse(res)) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
100
176
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
incrementBodyAnalysisCount(req)
|
|
177
|
+
if (!shouldSampleBody(originatingReq)) {
|
|
178
|
+
return
|
|
104
179
|
}
|
|
105
180
|
|
|
106
|
-
|
|
181
|
+
if (evaluateResponseBodyCollection(originatingReq, res)) {
|
|
182
|
+
ctx.shouldCollectBody = true
|
|
183
|
+
incrementBodyAnalysisCount(originatingReq)
|
|
184
|
+
}
|
|
107
185
|
}
|
|
108
186
|
|
|
109
187
|
/**
|
|
@@ -137,32 +215,13 @@ function extractRequestData (ctx) {
|
|
|
137
215
|
addresses[HTTP_OUTGOING_METHOD] = getMethod(options.method)
|
|
138
216
|
|
|
139
217
|
const headers = options?.headers
|
|
140
|
-
if (headers &&
|
|
218
|
+
if (headers && !isEmpty(headers)) {
|
|
141
219
|
addresses[HTTP_OUTGOING_HEADERS] = lowercaseHeaderKeys(headers)
|
|
142
220
|
}
|
|
143
221
|
|
|
144
222
|
return addresses
|
|
145
223
|
}
|
|
146
224
|
|
|
147
|
-
/**
|
|
148
|
-
* Checks if a response is a redirect
|
|
149
|
-
* @param {import('http').IncomingMessage} req incoming server request.
|
|
150
|
-
* @param {import('http').IncomingMessage} res downstream response object.
|
|
151
|
-
* @returns {boolean} is redirect.
|
|
152
|
-
*/
|
|
153
|
-
function handleRedirectResponse (req, res) {
|
|
154
|
-
const isRedirect = res.statusCode >= 300 && res.statusCode < 400
|
|
155
|
-
const redirectLocation = res.headers?.location || ''
|
|
156
|
-
|
|
157
|
-
if (isRedirect && redirectLocation) {
|
|
158
|
-
// Store the body collection decision for the redirect target
|
|
159
|
-
storeRedirectBodyCollectionDecision(req, redirectLocation)
|
|
160
|
-
return true
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return false
|
|
164
|
-
}
|
|
165
|
-
|
|
166
225
|
/**
|
|
167
226
|
* Extracts response data for WAF analysis.
|
|
168
227
|
* @param {import('http').IncomingMessage} res downstream response object.
|
|
@@ -177,7 +236,7 @@ function extractResponseData (res, responseBody) {
|
|
|
177
236
|
}
|
|
178
237
|
|
|
179
238
|
const headers = res.headers
|
|
180
|
-
if (headers &&
|
|
239
|
+
if (headers && !isEmpty(headers)) {
|
|
181
240
|
addresses[HTTP_OUTGOING_RESPONSE_HEADERS] = headers
|
|
182
241
|
}
|
|
183
242
|
|
|
@@ -290,13 +349,8 @@ function extractMimeType (contentType) {
|
|
|
290
349
|
module.exports = {
|
|
291
350
|
enable,
|
|
292
351
|
disable,
|
|
293
|
-
|
|
294
|
-
handleRedirectResponse,
|
|
352
|
+
planResponseBodyCollection,
|
|
295
353
|
incrementDownstreamAnalysisCount,
|
|
296
354
|
extractRequestData,
|
|
297
355
|
extractResponseData,
|
|
298
|
-
// exports for tests
|
|
299
|
-
parseBody,
|
|
300
|
-
getMethod,
|
|
301
|
-
storeRedirectBodyCollectionDecision,
|
|
302
356
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const dc = require('dc-polyfill')
|
|
4
4
|
const web = require('../../plugins/util/web')
|
|
5
5
|
const { storage } = require('../../../../datadog-core')
|
|
6
|
+
const { isEmpty } = require('../../util')
|
|
6
7
|
const { enable: enableFsPlugin, disable: disableFsPlugin, IAST_MODULE } = require('../rasp/fs-plugin')
|
|
7
8
|
const { incomingHttpRequestStart, incomingHttpRequestEnd, responseWriteHead } = require('../channels')
|
|
8
9
|
const vulnerabilityReporter = require('./vulnerability-reporter')
|
|
@@ -96,7 +97,7 @@ function onIncomingHttpRequestEnd (data) {
|
|
|
96
97
|
|
|
97
98
|
iastResponseEnd.publish({ ...data, storedHeaders })
|
|
98
99
|
|
|
99
|
-
if (
|
|
100
|
+
if (!isEmpty(storedHeaders)) {
|
|
100
101
|
collectedResponseHeaders.delete(data.res)
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -118,7 +119,7 @@ function onIncomingHttpRequestEnd (data) {
|
|
|
118
119
|
function onResponseWriteHeadCollect ({ res, responseHeaders = {} }) {
|
|
119
120
|
if (!res) return
|
|
120
121
|
|
|
121
|
-
if (
|
|
122
|
+
if (!isEmpty(responseHeaders)) {
|
|
122
123
|
collectedResponseHeaders.set(res, responseHeaders)
|
|
123
124
|
}
|
|
124
125
|
}
|
|
@@ -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,12 +3,14 @@
|
|
|
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')
|
|
9
10
|
const web = require('../../plugins/util/web')
|
|
10
11
|
const { getActiveRequest } = require('../store')
|
|
11
12
|
const waf = require('../waf')
|
|
13
|
+
const { isEmpty } = require('../../util')
|
|
12
14
|
const downstream = require('../downstream_requests')
|
|
13
15
|
const { updateRaspRuleMatchMetricTags } = require('../telemetry')
|
|
14
16
|
const { RULE_TYPES, handleResult } = require('./utils')
|
|
@@ -20,6 +22,7 @@ function enable (_config) {
|
|
|
20
22
|
downstream.enable(_config)
|
|
21
23
|
|
|
22
24
|
httpClientRequestStart.subscribe(analyzeSsrf)
|
|
25
|
+
httpClientResponseStart.subscribe(planResponseBodyCollection)
|
|
23
26
|
httpClientResponseFinish.subscribe(handleResponseFinish)
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -27,6 +30,7 @@ function disable () {
|
|
|
27
30
|
downstream.disable()
|
|
28
31
|
|
|
29
32
|
if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
|
|
33
|
+
if (httpClientResponseStart.hasSubscribers) httpClientResponseStart.unsubscribe(planResponseBodyCollection)
|
|
30
34
|
if (httpClientResponseFinish.hasSubscribers) httpClientResponseFinish.unsubscribe(handleResponseFinish)
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -36,9 +40,6 @@ function analyzeSsrf (ctx) {
|
|
|
36
40
|
|
|
37
41
|
if (!req || !outgoingUrl) return
|
|
38
42
|
|
|
39
|
-
// Determine if we should collect the response body based on sampling rate and redirect URL
|
|
40
|
-
ctx.shouldCollectBody = downstream.shouldSampleBody(req, outgoingUrl)
|
|
41
|
-
|
|
42
43
|
const requestAddresses = downstream.extractRequestData(ctx)
|
|
43
44
|
|
|
44
45
|
const ephemeral = {
|
|
@@ -55,13 +56,23 @@ function analyzeSsrf (ctx) {
|
|
|
55
56
|
downstream.incrementDownstreamAnalysisCount(req)
|
|
56
57
|
}
|
|
57
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
|
+
|
|
58
70
|
/**
|
|
59
71
|
* Finalizes body collection for the response and triggers RASP analysis.
|
|
60
|
-
* @param {
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* }} 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.
|
|
65
76
|
*/
|
|
66
77
|
function handleResponseFinish ({ ctx, res, body }) {
|
|
67
78
|
// downstream response object
|
|
@@ -70,9 +81,7 @@ function handleResponseFinish ({ ctx, res, body }) {
|
|
|
70
81
|
const originatingRequest = getActiveRequest()
|
|
71
82
|
if (!originatingRequest) return
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
const evaluateBody = ctx.shouldCollectBody && !downstream.handleRedirectResponse(originatingRequest, res)
|
|
75
|
-
const responseBody = evaluateBody ? body : null
|
|
84
|
+
const responseBody = ctx.shouldCollectBody ? body : null
|
|
76
85
|
runResponseEvaluation(res, originatingRequest, responseBody)
|
|
77
86
|
}
|
|
78
87
|
|
|
@@ -85,7 +94,7 @@ function handleResponseFinish ({ ctx, res, body }) {
|
|
|
85
94
|
function runResponseEvaluation (res, req, responseBody) {
|
|
86
95
|
const responseAddresses = downstream.extractResponseData(res, responseBody)
|
|
87
96
|
|
|
88
|
-
if (
|
|
97
|
+
if (isEmpty(responseAddresses)) return
|
|
89
98
|
|
|
90
99
|
const raspRule = { type: RULE_TYPES.SSRF, variant: 'response' }
|
|
91
100
|
const result = waf.run({ ephemeral: responseAddresses }, req, raspRule)
|
|
@@ -461,7 +461,7 @@ function truncateRequestBody (target, depth = 0) {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
function reportRequestBody (rootSpan, requestBody, comesFromRaspAction = false) {
|
|
464
|
-
if (!requestBody ||
|
|
464
|
+
if (!requestBody || isEmpty(requestBody)) return
|
|
465
465
|
|
|
466
466
|
if (!rootSpan.meta_struct) {
|
|
467
467
|
rootSpan.meta_struct = {}
|
|
@@ -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;
|
|
@@ -130,6 +132,7 @@ export interface GeneratedConfig {
|
|
|
130
132
|
DD_MINI_AGENT_PATH: string | undefined;
|
|
131
133
|
DD_PIPELINE_EXECUTION_ID: string | undefined;
|
|
132
134
|
DD_PLAYWRIGHT_WORKER: string | undefined;
|
|
135
|
+
DD_PROFILING_ALLOCATION_ENABLED: boolean;
|
|
133
136
|
DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED: boolean;
|
|
134
137
|
DD_PROFILING_CODEHOTSPOTS_ENABLED: boolean;
|
|
135
138
|
DD_PROFILING_CPU_ENABLED: boolean;
|
|
@@ -176,6 +179,7 @@ export interface GeneratedConfig {
|
|
|
176
179
|
DD_TRACE_APOLLO_SUBGRAPH_ENABLED: boolean;
|
|
177
180
|
DD_TRACE_AVSC_ENABLED: boolean;
|
|
178
181
|
DD_TRACE_AWS_ADD_SPAN_POINTERS: boolean;
|
|
182
|
+
DD_TRACE_AWS_DURABLE_EXECUTION_SDK_JS_ENABLED: boolean;
|
|
179
183
|
DD_TRACE_AWS_SDK_AWS_BATCH_PROPAGATION_ENABLED: boolean;
|
|
180
184
|
DD_TRACE_AWS_SDK_AWS_ENABLED: boolean;
|
|
181
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",
|
|
@@ -1308,6 +1330,13 @@
|
|
|
1308
1330
|
"default": null
|
|
1309
1331
|
}
|
|
1310
1332
|
],
|
|
1333
|
+
"DD_PROFILING_ALLOCATION_ENABLED": [
|
|
1334
|
+
{
|
|
1335
|
+
"implementation": "A",
|
|
1336
|
+
"type": "boolean",
|
|
1337
|
+
"default": "false"
|
|
1338
|
+
}
|
|
1339
|
+
],
|
|
1311
1340
|
"DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED": [
|
|
1312
1341
|
{
|
|
1313
1342
|
"implementation": "A",
|
|
@@ -2626,9 +2655,9 @@
|
|
|
2626
2655
|
],
|
|
2627
2656
|
"DD_TRACE_FS_ENABLED": [
|
|
2628
2657
|
{
|
|
2629
|
-
"implementation": "
|
|
2658
|
+
"implementation": "B",
|
|
2630
2659
|
"type": "boolean",
|
|
2631
|
-
"default": "
|
|
2660
|
+
"default": "false"
|
|
2632
2661
|
}
|
|
2633
2662
|
],
|
|
2634
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) {
|