dd-trace 5.31.0 → 5.33.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/LICENSE-3rdparty.csv +1 -0
- package/README.md +17 -14
- package/index.d.ts +11 -1
- package/package.json +6 -5
- package/packages/datadog-instrumentations/src/aws-sdk.js +4 -1
- package/packages/datadog-instrumentations/src/cucumber.js +31 -14
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
- package/packages/datadog-instrumentations/src/jest.js +105 -56
- package/packages/datadog-instrumentations/src/mocha/main.js +9 -4
- package/packages/datadog-instrumentations/src/mocha/utils.js +27 -9
- package/packages/datadog-instrumentations/src/mocha/worker.js +4 -2
- package/packages/datadog-instrumentations/src/node-serialize.js +22 -0
- package/packages/datadog-instrumentations/src/openai.js +2 -0
- package/packages/datadog-instrumentations/src/playwright.js +8 -3
- package/packages/datadog-instrumentations/src/vitest.js +134 -62
- package/packages/datadog-instrumentations/src/vm.js +49 -0
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +16 -0
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +63 -0
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +287 -0
- package/packages/datadog-plugin-aws-sdk/src/services/index.js +1 -0
- package/packages/datadog-plugin-cucumber/src/index.js +31 -31
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -8
- package/packages/datadog-plugin-cypress/src/support.js +6 -2
- package/packages/datadog-plugin-fetch/src/index.js +3 -3
- package/packages/datadog-plugin-http/src/client.js +5 -33
- package/packages/datadog-plugin-jest/src/index.js +37 -37
- package/packages/datadog-plugin-langchain/src/index.js +12 -80
- package/packages/datadog-plugin-langchain/src/tracing.js +89 -0
- package/packages/datadog-plugin-mocha/src/index.js +19 -35
- package/packages/datadog-plugin-playwright/src/index.js +3 -1
- package/packages/datadog-plugin-vitest/src/index.js +33 -35
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +2 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +3 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js +16 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +41 -24
- package/packages/dd-trace/src/appsec/iast/iast-context.js +12 -0
- package/packages/dd-trace/src/appsec/iast/path-line.js +19 -23
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +9 -8
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +75 -24
- package/packages/dd-trace/src/appsec/rasp/utils.js +10 -5
- package/packages/dd-trace/src/appsec/stack_trace.js +38 -28
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +37 -0
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +65 -28
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +57 -17
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -4
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +18 -3
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +5 -3
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +20 -3
- package/packages/dd-trace/src/config.js +43 -3
- package/packages/dd-trace/src/crashtracking/crashtracker.js +9 -0
- package/packages/dd-trace/src/crashtracking/noop.js +3 -0
- package/packages/dd-trace/src/datastreams/fnv.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -2
- package/packages/dd-trace/src/debugger/devtools_client/config.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/defaults.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +30 -13
- package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -8
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +35 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +112 -0
- package/packages/dd-trace/src/debugger/devtools_client/status.js +12 -10
- package/packages/dd-trace/src/debugger/index.js +2 -13
- package/packages/dd-trace/src/llmobs/plugins/base.js +40 -11
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +59 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +24 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +111 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +42 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +102 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js +32 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +131 -0
- package/packages/dd-trace/src/llmobs/plugins/openai.js +1 -1
- package/packages/dd-trace/src/llmobs/tagger.js +11 -3
- package/packages/dd-trace/src/llmobs/util.js +7 -1
- package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +3 -3
- package/packages/dd-trace/src/opentelemetry/context_manager.js +43 -3
- package/packages/dd-trace/src/plugins/ci_plugin.js +58 -27
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +0 -2
- package/packages/dd-trace/src/plugins/util/test.js +44 -12
- package/packages/dd-trace/src/priority_sampler.js +4 -1
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +21 -0
- package/packages/dd-trace/src/profiling/profiler.js +11 -8
- package/packages/dd-trace/src/profiling/profilers/events.js +17 -1
- package/packages/dd-trace/src/proxy.js +6 -3
|
@@ -54,15 +54,15 @@ class CookieAnalyzer extends Analyzer {
|
|
|
54
54
|
return super._checkOCE(context, value)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
_getLocation (value) {
|
|
57
|
+
_getLocation (value, callSiteFrames) {
|
|
58
58
|
if (!value) {
|
|
59
|
-
return super._getLocation()
|
|
59
|
+
return super._getLocation(value, callSiteFrames)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (value.location) {
|
|
63
63
|
return value.location
|
|
64
64
|
}
|
|
65
|
-
const location = super._getLocation(value)
|
|
65
|
+
const location = super._getLocation(value, callSiteFrames)
|
|
66
66
|
value.location = location
|
|
67
67
|
return location
|
|
68
68
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const InjectionAnalyzer = require('./injection-analyzer')
|
|
4
|
+
const { UNTRUSTED_DESERIALIZATION } = require('../vulnerabilities')
|
|
5
|
+
|
|
6
|
+
class UntrustedDeserializationAnalyzer extends InjectionAnalyzer {
|
|
7
|
+
constructor () {
|
|
8
|
+
super(UNTRUSTED_DESERIALIZATION)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
onConfigure () {
|
|
12
|
+
this.addSub('datadog:node-serialize:unserialize:start', ({ obj }) => this.analyze(obj))
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = new UntrustedDeserializationAnalyzer()
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { storage } = require('../../../../../datadog-core')
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
6
|
-
const { getIastContext } = require('../iast-context')
|
|
4
|
+
const { getNonDDCallSiteFrames } = require('../path-line')
|
|
5
|
+
const { getIastContext, getIastStackTraceId } = require('../iast-context')
|
|
7
6
|
const overheadController = require('../overhead-controller')
|
|
8
7
|
const { SinkIastPlugin } = require('../iast-plugin')
|
|
9
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
addVulnerability,
|
|
10
|
+
getVulnerabilityCallSiteFrames,
|
|
11
|
+
replaceCallSiteFromSourceMap
|
|
12
|
+
} = require('../vulnerability-reporter')
|
|
10
13
|
|
|
11
14
|
class Analyzer extends SinkIastPlugin {
|
|
12
15
|
constructor (type) {
|
|
@@ -28,12 +31,24 @@ class Analyzer extends SinkIastPlugin {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
_reportEvidence (value, context, evidence) {
|
|
31
|
-
const
|
|
34
|
+
const callSiteFrames = getVulnerabilityCallSiteFrames()
|
|
35
|
+
const nonDDCallSiteFrames = getNonDDCallSiteFrames(callSiteFrames, this._getExcludedPaths())
|
|
36
|
+
|
|
37
|
+
const location = this._getLocation(value, nonDDCallSiteFrames)
|
|
38
|
+
|
|
32
39
|
if (!this._isExcluded(location)) {
|
|
33
|
-
const
|
|
40
|
+
const originalLocation = this._getOriginalLocation(location)
|
|
34
41
|
const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
|
|
35
|
-
const
|
|
36
|
-
|
|
42
|
+
const stackId = getIastStackTraceId(context)
|
|
43
|
+
const vulnerability = this._createVulnerability(
|
|
44
|
+
this._type,
|
|
45
|
+
evidence,
|
|
46
|
+
spanId,
|
|
47
|
+
originalLocation,
|
|
48
|
+
stackId
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
addVulnerability(context, vulnerability, nonDDCallSiteFrames)
|
|
37
52
|
}
|
|
38
53
|
}
|
|
39
54
|
|
|
@@ -49,24 +64,25 @@ class Analyzer extends SinkIastPlugin {
|
|
|
49
64
|
return { value }
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
_getLocation () {
|
|
53
|
-
return
|
|
67
|
+
_getLocation (value, callSiteFrames) {
|
|
68
|
+
return callSiteFrames[0]
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (column) {
|
|
66
|
-
location.column = column
|
|
67
|
-
}
|
|
71
|
+
_getOriginalLocation (location) {
|
|
72
|
+
const locationFromSourceMap = replaceCallSiteFromSourceMap(location)
|
|
73
|
+
const originalLocation = {}
|
|
74
|
+
|
|
75
|
+
if (locationFromSourceMap?.path) {
|
|
76
|
+
originalLocation.path = locationFromSourceMap.path
|
|
77
|
+
}
|
|
78
|
+
if (locationFromSourceMap?.line) {
|
|
79
|
+
originalLocation.line = locationFromSourceMap.line
|
|
68
80
|
}
|
|
69
|
-
|
|
81
|
+
if (locationFromSourceMap?.column) {
|
|
82
|
+
originalLocation.column = locationFromSourceMap.column
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return originalLocation
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
_getExcludedPaths () {}
|
|
@@ -102,12 +118,13 @@ class Analyzer extends SinkIastPlugin {
|
|
|
102
118
|
return overheadController.hasQuota(overheadController.OPERATIONS.REPORT_VULNERABILITY, context)
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
_createVulnerability (type, evidence, spanId, location) {
|
|
121
|
+
_createVulnerability (type, evidence, spanId, location, stackId) {
|
|
106
122
|
if (type && evidence) {
|
|
107
123
|
const _spanId = spanId || 0
|
|
108
124
|
return {
|
|
109
125
|
type,
|
|
110
126
|
evidence,
|
|
127
|
+
stackId,
|
|
111
128
|
location: {
|
|
112
129
|
spanId: _spanId,
|
|
113
130
|
...location
|
|
@@ -9,6 +9,17 @@ function getIastContext (store, topContext) {
|
|
|
9
9
|
return iastContext
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function getIastStackTraceId (iastContext) {
|
|
13
|
+
if (!iastContext) return 0
|
|
14
|
+
|
|
15
|
+
if (!iastContext.stackTraceId) {
|
|
16
|
+
iastContext.stackTraceId = 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
iastContext.stackTraceId += 1
|
|
20
|
+
return iastContext.stackTraceId
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
/* TODO Fix storage problem when the close event is called without
|
|
13
24
|
finish event to remove `topContext` references
|
|
14
25
|
We have to save the context in two places, because
|
|
@@ -51,6 +62,7 @@ module.exports = {
|
|
|
51
62
|
getIastContext,
|
|
52
63
|
saveIastContext,
|
|
53
64
|
cleanIastContext,
|
|
65
|
+
getIastStackTraceId,
|
|
54
66
|
IAST_CONTEXT_KEY,
|
|
55
67
|
IAST_TRANSACTION_ID
|
|
56
68
|
}
|
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const process = require('process')
|
|
5
5
|
const { calculateDDBasePath } = require('../../util')
|
|
6
|
-
const { getCallSiteList } = require('../stack_trace')
|
|
7
6
|
const pathLine = {
|
|
8
|
-
getFirstNonDDPathAndLine,
|
|
9
7
|
getNodeModulesPaths,
|
|
10
8
|
getRelativePath,
|
|
11
|
-
|
|
9
|
+
getNonDDCallSiteFrames,
|
|
12
10
|
calculateDDBasePath, // Exported only for test purposes
|
|
13
11
|
ddBasePath: calculateDDBasePath(__dirname) // Only for test purposes
|
|
14
12
|
}
|
|
@@ -25,22 +23,24 @@ const EXCLUDED_PATH_PREFIXES = [
|
|
|
25
23
|
'async_hooks'
|
|
26
24
|
]
|
|
27
25
|
|
|
28
|
-
function
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
function getNonDDCallSiteFrames (callSiteFrames, externallyExcludedPaths) {
|
|
27
|
+
if (!callSiteFrames) {
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = []
|
|
32
|
+
|
|
33
|
+
for (const callsite of callSiteFrames) {
|
|
34
|
+
const filepath = callsite.file
|
|
35
|
+
if (!isExcluded(callsite, externallyExcludedPaths) && filepath.indexOf(pathLine.ddBasePath) === -1) {
|
|
36
|
+
callsite.path = getRelativePath(filepath)
|
|
37
|
+
callsite.isInternal = !path.isAbsolute(filepath)
|
|
38
|
+
|
|
39
|
+
result.push(callsite)
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
|
-
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function getRelativePath (filepath) {
|
|
@@ -48,8 +48,8 @@ function getRelativePath (filepath) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function isExcluded (callsite, externallyExcludedPaths) {
|
|
51
|
-
if (callsite.isNative
|
|
52
|
-
const filename = callsite.
|
|
51
|
+
if (callsite.isNative) return true
|
|
52
|
+
const filename = callsite.file
|
|
53
53
|
if (!filename) {
|
|
54
54
|
return true
|
|
55
55
|
}
|
|
@@ -73,10 +73,6 @@ function isExcluded (callsite, externallyExcludedPaths) {
|
|
|
73
73
|
return false
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function getFirstNonDDPathAndLine (externallyExcludedPaths) {
|
|
77
|
-
return getFirstNonDDPathAndLineFromCallsites(getCallSiteList(), externallyExcludedPaths)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
76
|
function getNodeModulesPaths (...paths) {
|
|
81
77
|
const nodeModulesPaths = []
|
|
82
78
|
|
|
@@ -25,19 +25,20 @@ class SensitiveHandler {
|
|
|
25
25
|
|
|
26
26
|
this._sensitiveAnalyzers = new Map()
|
|
27
27
|
this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, taintedRangeBasedSensitiveAnalyzer)
|
|
28
|
-
this._sensitiveAnalyzers.set(vulnerabilities.TEMPLATE_INJECTION, taintedRangeBasedSensitiveAnalyzer)
|
|
29
28
|
this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, commandSensitiveAnalyzer)
|
|
30
|
-
this._sensitiveAnalyzers.set(vulnerabilities.
|
|
29
|
+
this._sensitiveAnalyzers.set(vulnerabilities.HARDCODED_PASSWORD, (evidence) => {
|
|
30
|
+
return hardcodedPasswordAnalyzer(evidence, this._valuePattern)
|
|
31
|
+
})
|
|
32
|
+
this._sensitiveAnalyzers.set(vulnerabilities.HEADER_INJECTION, (evidence) => {
|
|
33
|
+
return headerSensitiveAnalyzer(evidence, this._namePattern, this._valuePattern)
|
|
34
|
+
})
|
|
31
35
|
this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, ldapSensitiveAnalyzer)
|
|
36
|
+
this._sensitiveAnalyzers.set(vulnerabilities.NOSQL_MONGODB_INJECTION, jsonSensitiveAnalyzer)
|
|
32
37
|
this._sensitiveAnalyzers.set(vulnerabilities.SQL_INJECTION, sqlSensitiveAnalyzer)
|
|
33
38
|
this._sensitiveAnalyzers.set(vulnerabilities.SSRF, urlSensitiveAnalyzer)
|
|
39
|
+
this._sensitiveAnalyzers.set(vulnerabilities.TEMPLATE_INJECTION, taintedRangeBasedSensitiveAnalyzer)
|
|
40
|
+
this._sensitiveAnalyzers.set(vulnerabilities.UNTRUSTED_DESERIALIZATION, taintedRangeBasedSensitiveAnalyzer)
|
|
34
41
|
this._sensitiveAnalyzers.set(vulnerabilities.UNVALIDATED_REDIRECT, urlSensitiveAnalyzer)
|
|
35
|
-
this._sensitiveAnalyzers.set(vulnerabilities.HEADER_INJECTION, (evidence) => {
|
|
36
|
-
return headerSensitiveAnalyzer(evidence, this._namePattern, this._valuePattern)
|
|
37
|
-
})
|
|
38
|
-
this._sensitiveAnalyzers.set(vulnerabilities.HARDCODED_PASSWORD, (evidence) => {
|
|
39
|
-
return hardcodedPasswordAnalyzer(evidence, this._valuePattern)
|
|
40
|
-
})
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
isSensibleName (name) {
|
|
@@ -84,6 +84,7 @@ class VulnerabilityFormatter {
|
|
|
84
84
|
const formattedVulnerability = {
|
|
85
85
|
type: vulnerability.type,
|
|
86
86
|
hash: vulnerability.hash,
|
|
87
|
+
stackId: vulnerability.stackId,
|
|
87
88
|
evidence: this.formatEvidence(vulnerability.type, vulnerability.evidence, sourcesIndexes, sources),
|
|
88
89
|
location: {
|
|
89
90
|
spanId: vulnerability.location.spanId
|
|
@@ -15,6 +15,7 @@ module.exports = {
|
|
|
15
15
|
SSRF: 'SSRF',
|
|
16
16
|
TEMPLATE_INJECTION: 'TEMPLATE_INJECTION',
|
|
17
17
|
UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT',
|
|
18
|
+
UNTRUSTED_DESERIALIZATION: 'UNTRUSTED_DESERIALIZATION',
|
|
18
19
|
WEAK_CIPHER: 'WEAK_CIPHER',
|
|
19
20
|
WEAK_HASH: 'WEAK_HASH',
|
|
20
21
|
WEAK_RANDOMNESS: 'WEAK_RANDOMNESS',
|
|
@@ -6,6 +6,8 @@ const { IAST_ENABLED_TAG_KEY, IAST_JSON_TAG_KEY } = require('./tags')
|
|
|
6
6
|
const standalone = require('../standalone')
|
|
7
7
|
const { SAMPLING_MECHANISM_APPSEC } = require('../../constants')
|
|
8
8
|
const { keepTrace } = require('../../priority_sampler')
|
|
9
|
+
const { reportStackTrace, getCallsiteFrames, canReportStackTrace, STACK_TRACE_NAMESPACES } = require('../stack_trace')
|
|
10
|
+
const { getOriginalPathAndLineFromSourceMap } = require('./taint-tracking/rewriter')
|
|
9
11
|
|
|
10
12
|
const VULNERABILITIES_KEY = 'vulnerabilities'
|
|
11
13
|
const VULNERABILITY_HASHES_MAX_SIZE = 1000
|
|
@@ -15,39 +17,60 @@ const RESET_VULNERABILITY_CACHE_INTERVAL = 60 * 60 * 1000 // 1 hour
|
|
|
15
17
|
let tracer
|
|
16
18
|
let resetVulnerabilityCacheTimer
|
|
17
19
|
let deduplicationEnabled = true
|
|
20
|
+
let stackTraceEnabled = true
|
|
21
|
+
let stackTraceMaxDepth
|
|
22
|
+
let maxStackTraces
|
|
18
23
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
function canAddVulnerability (vulnerability) {
|
|
25
|
+
const hasRequiredFields = vulnerability?.evidence && vulnerability?.type && vulnerability?.location
|
|
26
|
+
if (!hasRequiredFields) return false
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
const isDuplicated = deduplicationEnabled && isDuplicatedVulnerability(vulnerability)
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
return !isDuplicated
|
|
31
|
+
}
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
type: 'vulnerability'
|
|
30
|
-
})
|
|
33
|
+
function addVulnerability (iastContext, vulnerability, callSiteFrames) {
|
|
34
|
+
if (!canAddVulnerability(vulnerability)) return
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
VULNERABILITY_HASHES.set(`${vulnerability.type}${vulnerability.hash}`, true)
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
[IAST_ENABLED_TAG_KEY]: 1
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
+
let span = iastContext?.rootSpan
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
if (!span && tracer) {
|
|
41
|
+
span = tracer.startSpan('vulnerability', {
|
|
42
|
+
type: 'vulnerability'
|
|
43
|
+
})
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
standalone.sample(span)
|
|
45
|
+
vulnerability.location.spanId = span.context().toSpanId()
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
span.addTags({
|
|
48
|
+
[IAST_ENABLED_TAG_KEY]: 1
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!span) return
|
|
53
|
+
|
|
54
|
+
keepTrace(span, SAMPLING_MECHANISM_APPSEC)
|
|
55
|
+
standalone.sample(span)
|
|
56
|
+
|
|
57
|
+
if (stackTraceEnabled && canReportStackTrace(span, maxStackTraces, STACK_TRACE_NAMESPACES.IAST)) {
|
|
58
|
+
const originalCallSiteList = callSiteFrames.map(callsite => replaceCallSiteFromSourceMap(callsite))
|
|
59
|
+
|
|
60
|
+
reportStackTrace(
|
|
61
|
+
span,
|
|
62
|
+
vulnerability.stackId,
|
|
63
|
+
originalCallSiteList,
|
|
64
|
+
STACK_TRACE_NAMESPACES.IAST
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (iastContext?.rootSpan) {
|
|
69
|
+
iastContext[VULNERABILITIES_KEY] = iastContext[VULNERABILITIES_KEY] || []
|
|
70
|
+
iastContext[VULNERABILITIES_KEY].push(vulnerability)
|
|
71
|
+
} else {
|
|
72
|
+
sendVulnerabilities([vulnerability], span)
|
|
73
|
+
span.finish()
|
|
51
74
|
}
|
|
52
75
|
}
|
|
53
76
|
|
|
@@ -94,8 +117,34 @@ function isDuplicatedVulnerability (vulnerability) {
|
|
|
94
117
|
return VULNERABILITY_HASHES.get(`${vulnerability.type}${vulnerability.hash}`)
|
|
95
118
|
}
|
|
96
119
|
|
|
120
|
+
function getVulnerabilityCallSiteFrames () {
|
|
121
|
+
return getCallsiteFrames(stackTraceMaxDepth)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function replaceCallSiteFromSourceMap (callsite) {
|
|
125
|
+
if (callsite) {
|
|
126
|
+
const { path, line, column } = getOriginalPathAndLineFromSourceMap(callsite)
|
|
127
|
+
if (path) {
|
|
128
|
+
callsite.file = path
|
|
129
|
+
callsite.path = path
|
|
130
|
+
}
|
|
131
|
+
if (line) {
|
|
132
|
+
callsite.line = line
|
|
133
|
+
}
|
|
134
|
+
if (column) {
|
|
135
|
+
callsite.column = column
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return callsite
|
|
140
|
+
}
|
|
141
|
+
|
|
97
142
|
function start (config, _tracer) {
|
|
98
143
|
deduplicationEnabled = config.iast.deduplicationEnabled
|
|
144
|
+
stackTraceEnabled = config.iast.stackTrace.enabled
|
|
145
|
+
stackTraceMaxDepth = config.appsec.stackTrace.maxDepth
|
|
146
|
+
maxStackTraces = config.appsec.stackTrace.maxStackTraces
|
|
147
|
+
|
|
99
148
|
vulnerabilitiesFormatter.setRedactVulnerabilities(
|
|
100
149
|
config.iast.redactionEnabled,
|
|
101
150
|
config.iast.redactionNamePattern,
|
|
@@ -114,6 +163,8 @@ function stop () {
|
|
|
114
163
|
module.exports = {
|
|
115
164
|
addVulnerability,
|
|
116
165
|
sendVulnerabilities,
|
|
166
|
+
getVulnerabilityCallSiteFrames,
|
|
167
|
+
replaceCallSiteFromSourceMap,
|
|
117
168
|
clearCache,
|
|
118
169
|
start,
|
|
119
170
|
stop
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const web = require('../../plugins/util/web')
|
|
4
|
-
const { reportStackTrace } = require('../stack_trace')
|
|
4
|
+
const { getCallsiteFrames, reportStackTrace, canReportStackTrace } = require('../stack_trace')
|
|
5
5
|
const { getBlockingAction } = require('../blocking')
|
|
6
6
|
const log = require('../../log')
|
|
7
7
|
|
|
@@ -30,13 +30,18 @@ class DatadogRaspAbortError extends Error {
|
|
|
30
30
|
|
|
31
31
|
function handleResult (actions, req, res, abortController, config) {
|
|
32
32
|
const generateStackTraceAction = actions?.generate_stack
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
|
|
34
|
+
const { enabled, maxDepth, maxStackTraces } = config.appsec.stackTrace
|
|
35
|
+
|
|
36
|
+
const rootSpan = web.root(req)
|
|
37
|
+
|
|
38
|
+
if (generateStackTraceAction && enabled && canReportStackTrace(rootSpan, maxStackTraces)) {
|
|
39
|
+
const frames = getCallsiteFrames(maxDepth)
|
|
40
|
+
|
|
35
41
|
reportStackTrace(
|
|
36
42
|
rootSpan,
|
|
37
43
|
generateStackTraceAction.stack_id,
|
|
38
|
-
|
|
39
|
-
config.appsec.stackTrace.maxStackTraces
|
|
44
|
+
frames
|
|
40
45
|
)
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -6,11 +6,18 @@ const ddBasePath = calculateDDBasePath(__dirname)
|
|
|
6
6
|
|
|
7
7
|
const LIBRARY_FRAMES_BUFFER = 20
|
|
8
8
|
|
|
9
|
+
const STACK_TRACE_NAMESPACES = {
|
|
10
|
+
RASP: 'exploit',
|
|
11
|
+
IAST: 'vulnerability'
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
function getCallSiteList (maxDepth = 100) {
|
|
10
15
|
const previousPrepareStackTrace = Error.prepareStackTrace
|
|
11
16
|
const previousStackTraceLimit = Error.stackTraceLimit
|
|
12
17
|
let callsiteList
|
|
13
|
-
|
|
18
|
+
// Since some frames will be discarded because they come from tracer codebase, a buffer is added
|
|
19
|
+
// to the limit in order to get as close as `maxDepth` number of frames.
|
|
20
|
+
Error.stackTraceLimit = maxDepth + LIBRARY_FRAMES_BUFFER
|
|
14
21
|
|
|
15
22
|
try {
|
|
16
23
|
Error.prepareStackTrace = function (_, callsites) {
|
|
@@ -30,7 +37,10 @@ function filterOutFramesFromLibrary (callSiteList) {
|
|
|
30
37
|
return callSiteList.filter(callSite => !callSite.getFileName()?.startsWith(ddBasePath))
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
function
|
|
40
|
+
function getCallsiteFrames (maxDepth = 32, callSiteListGetter = getCallSiteList) {
|
|
41
|
+
if (maxDepth < 1) maxDepth = Infinity
|
|
42
|
+
|
|
43
|
+
const callSiteList = callSiteListGetter(maxDepth)
|
|
34
44
|
const filteredFrames = filterOutFramesFromLibrary(callSiteList)
|
|
35
45
|
|
|
36
46
|
const half = filteredFrames.length > maxDepth ? Math.round(maxDepth / 2) : Infinity
|
|
@@ -45,46 +55,46 @@ function getFramesForMetaStruct (callSiteList, maxDepth = 32) {
|
|
|
45
55
|
line: callSite.getLineNumber(),
|
|
46
56
|
column: callSite.getColumnNumber(),
|
|
47
57
|
function: callSite.getFunctionName(),
|
|
48
|
-
class_name: callSite.getTypeName()
|
|
58
|
+
class_name: callSite.getTypeName(),
|
|
59
|
+
isNative: callSite.isNative()
|
|
49
60
|
})
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
return indexedFrames
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
function reportStackTrace (rootSpan, stackId,
|
|
66
|
+
function reportStackTrace (rootSpan, stackId, frames, namespace = STACK_TRACE_NAMESPACES.RASP) {
|
|
56
67
|
if (!rootSpan) return
|
|
68
|
+
if (!Array.isArray(frames)) return
|
|
57
69
|
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (maxDepth < 1) maxDepth = Infinity
|
|
62
|
-
const callSiteList = callSiteListGetter(maxDepth + LIBRARY_FRAMES_BUFFER)
|
|
63
|
-
if (!Array.isArray(callSiteList)) return
|
|
70
|
+
if (!rootSpan.meta_struct) {
|
|
71
|
+
rootSpan.meta_struct = {}
|
|
72
|
+
}
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
if (!rootSpan.meta_struct['_dd.stack']) {
|
|
75
|
+
rootSpan.meta_struct['_dd.stack'] = {}
|
|
76
|
+
}
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
if (!rootSpan.meta_struct['_dd.stack'][namespace]) {
|
|
79
|
+
rootSpan.meta_struct['_dd.stack'][namespace] = []
|
|
80
|
+
}
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
rootSpan.meta_struct['_dd.stack'][namespace].push({
|
|
83
|
+
id: stackId,
|
|
84
|
+
language: 'nodejs',
|
|
85
|
+
frames
|
|
86
|
+
})
|
|
87
|
+
}
|
|
76
88
|
|
|
77
|
-
|
|
89
|
+
function canReportStackTrace (rootSpan, maxStackTraces, namespace = STACK_TRACE_NAMESPACES.RASP) {
|
|
90
|
+
if (!rootSpan) return false
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
id: stackId,
|
|
81
|
-
language: 'nodejs',
|
|
82
|
-
frames
|
|
83
|
-
})
|
|
84
|
-
}
|
|
92
|
+
return maxStackTraces < 1 || (rootSpan.meta_struct?.['_dd.stack']?.[namespace]?.length ?? 0) < maxStackTraces
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
module.exports = {
|
|
88
|
-
|
|
89
|
-
reportStackTrace
|
|
96
|
+
getCallsiteFrames,
|
|
97
|
+
reportStackTrace,
|
|
98
|
+
canReportStackTrace,
|
|
99
|
+
STACK_TRACE_NAMESPACES
|
|
90
100
|
}
|
|
@@ -19,6 +19,7 @@ class WAFContextWrapper {
|
|
|
19
19
|
this.rulesVersion = rulesVersion
|
|
20
20
|
this.addressesToSkip = new Set()
|
|
21
21
|
this.knownAddresses = knownAddresses
|
|
22
|
+
this.cachedUserIdActions = new Map()
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
run ({ persistent, ephemeral }, raspRule) {
|
|
@@ -27,6 +28,16 @@ class WAFContextWrapper {
|
|
|
27
28
|
return
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// SPECIAL CASE FOR USER_ID
|
|
32
|
+
// TODO: make this universal
|
|
33
|
+
const userId = persistent?.[addresses.USER_ID] || ephemeral?.[addresses.USER_ID]
|
|
34
|
+
if (userId) {
|
|
35
|
+
const cachedAction = this.cachedUserIdActions.get(userId)
|
|
36
|
+
if (cachedAction) {
|
|
37
|
+
return cachedAction
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
const payload = {}
|
|
31
42
|
let payloadHasData = false
|
|
32
43
|
const newAddressesToSkip = new Set(this.addressesToSkip)
|
|
@@ -79,6 +90,12 @@ class WAFContextWrapper {
|
|
|
79
90
|
|
|
80
91
|
const blockTriggered = !!getBlockingAction(result.actions)
|
|
81
92
|
|
|
93
|
+
// SPECIAL CASE FOR USER_ID
|
|
94
|
+
// TODO: make this universal
|
|
95
|
+
if (userId && ruleTriggered && blockTriggered) {
|
|
96
|
+
this.setUserIdCache(userId, result)
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
Reporter.reportMetrics({
|
|
83
100
|
duration: result.totalRuntime / 1e3,
|
|
84
101
|
durationExt: parseInt(end - start) / 1e3,
|
|
@@ -105,6 +122,26 @@ class WAFContextWrapper {
|
|
|
105
122
|
}
|
|
106
123
|
}
|
|
107
124
|
|
|
125
|
+
setUserIdCache (userId, result) {
|
|
126
|
+
// using old loops for speed
|
|
127
|
+
for (let i = 0; i < result.events.length; i++) {
|
|
128
|
+
const event = result.events[i]
|
|
129
|
+
|
|
130
|
+
for (let j = 0; j < event?.rule_matches?.length; j++) {
|
|
131
|
+
const match = event.rule_matches[j]
|
|
132
|
+
|
|
133
|
+
for (let k = 0; k < match?.parameters?.length; k++) {
|
|
134
|
+
const parameter = match.parameters[k]
|
|
135
|
+
|
|
136
|
+
if (parameter?.address === addresses.USER_ID) {
|
|
137
|
+
this.cachedUserIdActions.set(userId, result.actions)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
108
145
|
dispose () {
|
|
109
146
|
this.ddwafContext.dispose()
|
|
110
147
|
}
|