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.
Files changed (85) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/README.md +17 -14
  3. package/index.d.ts +11 -1
  4. package/package.json +6 -5
  5. package/packages/datadog-instrumentations/src/aws-sdk.js +4 -1
  6. package/packages/datadog-instrumentations/src/cucumber.js +31 -14
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  8. package/packages/datadog-instrumentations/src/jest.js +105 -56
  9. package/packages/datadog-instrumentations/src/mocha/main.js +9 -4
  10. package/packages/datadog-instrumentations/src/mocha/utils.js +27 -9
  11. package/packages/datadog-instrumentations/src/mocha/worker.js +4 -2
  12. package/packages/datadog-instrumentations/src/node-serialize.js +22 -0
  13. package/packages/datadog-instrumentations/src/openai.js +2 -0
  14. package/packages/datadog-instrumentations/src/playwright.js +8 -3
  15. package/packages/datadog-instrumentations/src/vitest.js +134 -62
  16. package/packages/datadog-instrumentations/src/vm.js +49 -0
  17. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +16 -0
  18. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +63 -0
  19. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +287 -0
  20. package/packages/datadog-plugin-aws-sdk/src/services/index.js +1 -0
  21. package/packages/datadog-plugin-cucumber/src/index.js +31 -31
  22. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -8
  23. package/packages/datadog-plugin-cypress/src/support.js +6 -2
  24. package/packages/datadog-plugin-fetch/src/index.js +3 -3
  25. package/packages/datadog-plugin-http/src/client.js +5 -33
  26. package/packages/datadog-plugin-jest/src/index.js +37 -37
  27. package/packages/datadog-plugin-langchain/src/index.js +12 -80
  28. package/packages/datadog-plugin-langchain/src/tracing.js +89 -0
  29. package/packages/datadog-plugin-mocha/src/index.js +19 -35
  30. package/packages/datadog-plugin-playwright/src/index.js +3 -1
  31. package/packages/datadog-plugin-vitest/src/index.js +33 -35
  32. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  33. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +2 -0
  34. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +3 -3
  35. package/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js +16 -0
  36. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +41 -24
  37. package/packages/dd-trace/src/appsec/iast/iast-context.js +12 -0
  38. package/packages/dd-trace/src/appsec/iast/path-line.js +19 -23
  39. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +9 -8
  40. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -0
  41. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  42. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +75 -24
  43. package/packages/dd-trace/src/appsec/rasp/utils.js +10 -5
  44. package/packages/dd-trace/src/appsec/stack_trace.js +38 -28
  45. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +37 -0
  46. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +65 -28
  47. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +57 -17
  48. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -4
  49. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +18 -3
  50. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +5 -3
  51. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +20 -3
  52. package/packages/dd-trace/src/config.js +43 -3
  53. package/packages/dd-trace/src/crashtracking/crashtracker.js +9 -0
  54. package/packages/dd-trace/src/crashtracking/noop.js +3 -0
  55. package/packages/dd-trace/src/datastreams/fnv.js +1 -1
  56. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -2
  57. package/packages/dd-trace/src/debugger/devtools_client/config.js +1 -0
  58. package/packages/dd-trace/src/debugger/devtools_client/defaults.js +1 -0
  59. package/packages/dd-trace/src/debugger/devtools_client/index.js +30 -13
  60. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -8
  61. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +35 -1
  62. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +112 -0
  63. package/packages/dd-trace/src/debugger/devtools_client/status.js +12 -10
  64. package/packages/dd-trace/src/debugger/index.js +2 -13
  65. package/packages/dd-trace/src/llmobs/plugins/base.js +40 -11
  66. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +59 -0
  67. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +24 -0
  68. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +111 -0
  69. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +42 -0
  70. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +102 -0
  71. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js +32 -0
  72. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +131 -0
  73. package/packages/dd-trace/src/llmobs/plugins/openai.js +1 -1
  74. package/packages/dd-trace/src/llmobs/tagger.js +11 -3
  75. package/packages/dd-trace/src/llmobs/util.js +7 -1
  76. package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +3 -3
  77. package/packages/dd-trace/src/opentelemetry/context_manager.js +43 -3
  78. package/packages/dd-trace/src/plugins/ci_plugin.js +58 -27
  79. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +0 -2
  80. package/packages/dd-trace/src/plugins/util/test.js +44 -12
  81. package/packages/dd-trace/src/priority_sampler.js +4 -1
  82. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +21 -0
  83. package/packages/dd-trace/src/profiling/profiler.js +11 -8
  84. package/packages/dd-trace/src/profiling/profilers/events.js +17 -1
  85. 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 { getFirstNonDDPathAndLine } = require('../path-line')
5
- const { addVulnerability } = require('../vulnerability-reporter')
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 { getOriginalPathAndLineFromSourceMap } = require('../taint-tracking/rewriter')
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 location = this._getLocation(value)
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 locationSourceMap = this._replaceLocationFromSourceMap(location)
40
+ const originalLocation = this._getOriginalLocation(location)
34
41
  const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
35
- const vulnerability = this._createVulnerability(this._type, evidence, spanId, locationSourceMap)
36
- addVulnerability(context, vulnerability)
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 getFirstNonDDPathAndLine(this._getExcludedPaths())
67
+ _getLocation (value, callSiteFrames) {
68
+ return callSiteFrames[0]
54
69
  }
55
70
 
56
- _replaceLocationFromSourceMap (location) {
57
- if (location) {
58
- const { path, line, column } = getOriginalPathAndLineFromSourceMap(location)
59
- if (path) {
60
- location.path = path
61
- }
62
- if (line) {
63
- location.line = line
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
- return location
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
- getFirstNonDDPathAndLineFromCallsites, // Exported only for test purposes
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 getFirstNonDDPathAndLineFromCallsites (callsites, externallyExcludedPaths) {
29
- if (callsites) {
30
- for (let i = 0; i < callsites.length; i++) {
31
- const callsite = callsites[i]
32
- const filepath = callsite.getFileName()
33
- if (!isExcluded(callsite, externallyExcludedPaths) && filepath.indexOf(pathLine.ddBasePath) === -1) {
34
- return {
35
- path: getRelativePath(filepath),
36
- line: callsite.getLineNumber(),
37
- column: callsite.getColumnNumber(),
38
- isInternal: !path.isAbsolute(filepath)
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
- return null
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()) return true
52
- const filename = callsite.getFileName()
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.NOSQL_MONGODB_INJECTION, jsonSensitiveAnalyzer)
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 addVulnerability (iastContext, vulnerability) {
20
- if (vulnerability?.evidence && vulnerability?.type && vulnerability?.location) {
21
- if (deduplicationEnabled && isDuplicatedVulnerability(vulnerability)) return
24
+ function canAddVulnerability (vulnerability) {
25
+ const hasRequiredFields = vulnerability?.evidence && vulnerability?.type && vulnerability?.location
26
+ if (!hasRequiredFields) return false
22
27
 
23
- VULNERABILITY_HASHES.set(`${vulnerability.type}${vulnerability.hash}`, true)
28
+ const isDuplicated = deduplicationEnabled && isDuplicatedVulnerability(vulnerability)
24
29
 
25
- let span = iastContext?.rootSpan
30
+ return !isDuplicated
31
+ }
26
32
 
27
- if (!span && tracer) {
28
- span = tracer.startSpan('vulnerability', {
29
- type: 'vulnerability'
30
- })
33
+ function addVulnerability (iastContext, vulnerability, callSiteFrames) {
34
+ if (!canAddVulnerability(vulnerability)) return
31
35
 
32
- vulnerability.location.spanId = span.context().toSpanId()
36
+ VULNERABILITY_HASHES.set(`${vulnerability.type}${vulnerability.hash}`, true)
33
37
 
34
- span.addTags({
35
- [IAST_ENABLED_TAG_KEY]: 1
36
- })
37
- }
38
+ let span = iastContext?.rootSpan
38
39
 
39
- if (!span) return
40
+ if (!span && tracer) {
41
+ span = tracer.startSpan('vulnerability', {
42
+ type: 'vulnerability'
43
+ })
40
44
 
41
- keepTrace(span, SAMPLING_MECHANISM_APPSEC)
42
- standalone.sample(span)
45
+ vulnerability.location.spanId = span.context().toSpanId()
43
46
 
44
- if (iastContext?.rootSpan) {
45
- iastContext[VULNERABILITIES_KEY] = iastContext[VULNERABILITIES_KEY] || []
46
- iastContext[VULNERABILITIES_KEY].push(vulnerability)
47
- } else {
48
- sendVulnerabilities([vulnerability], span)
49
- span.finish()
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
- if (generateStackTraceAction && config.appsec.stackTrace.enabled) {
34
- const rootSpan = web.root(req)
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
- config.appsec.stackTrace.maxDepth,
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
- Error.stackTraceLimit = maxDepth
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 getFramesForMetaStruct (callSiteList, maxDepth = 32) {
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, maxDepth, maxStackTraces, callSiteListGetter = getCallSiteList) {
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 (maxStackTraces < 1 || (rootSpan.meta_struct?.['_dd.stack']?.exploit?.length ?? 0) < maxStackTraces) {
59
- // Since some frames will be discarded because they come from tracer codebase, a buffer is added
60
- // to the limit in order to get as close as `maxDepth` number of frames.
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
- if (!rootSpan.meta_struct) {
66
- rootSpan.meta_struct = {}
67
- }
74
+ if (!rootSpan.meta_struct['_dd.stack']) {
75
+ rootSpan.meta_struct['_dd.stack'] = {}
76
+ }
68
77
 
69
- if (!rootSpan.meta_struct['_dd.stack']) {
70
- rootSpan.meta_struct['_dd.stack'] = {}
71
- }
78
+ if (!rootSpan.meta_struct['_dd.stack'][namespace]) {
79
+ rootSpan.meta_struct['_dd.stack'][namespace] = []
80
+ }
72
81
 
73
- if (!rootSpan.meta_struct['_dd.stack'].exploit) {
74
- rootSpan.meta_struct['_dd.stack'].exploit = []
75
- }
82
+ rootSpan.meta_struct['_dd.stack'][namespace].push({
83
+ id: stackId,
84
+ language: 'nodejs',
85
+ frames
86
+ })
87
+ }
76
88
 
77
- const frames = getFramesForMetaStruct(callSiteList, maxDepth)
89
+ function canReportStackTrace (rootSpan, maxStackTraces, namespace = STACK_TRACE_NAMESPACES.RASP) {
90
+ if (!rootSpan) return false
78
91
 
79
- rootSpan.meta_struct['_dd.stack'].exploit.push({
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
- getCallSiteList,
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
  }