dd-trace 5.19.0 → 5.21.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 (67) hide show
  1. package/ext/formats.d.ts +1 -0
  2. package/ext/formats.js +2 -1
  3. package/index.d.ts +2 -1
  4. package/init.js +3 -15
  5. package/package.json +5 -4
  6. package/packages/datadog-instrumentations/src/body-parser.js +14 -2
  7. package/packages/datadog-instrumentations/src/cucumber.js +10 -0
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -2
  9. package/packages/datadog-instrumentations/src/helpers/register.js +21 -12
  10. package/packages/datadog-instrumentations/src/http/client.js +7 -1
  11. package/packages/datadog-instrumentations/src/http/server.js +50 -13
  12. package/packages/datadog-instrumentations/src/mocha/main.js +111 -78
  13. package/packages/datadog-instrumentations/src/nyc.js +23 -0
  14. package/packages/datadog-instrumentations/src/process.js +29 -0
  15. package/packages/datadog-instrumentations/src/vitest.js +65 -25
  16. package/packages/datadog-plugin-aws-sdk/src/base.js +15 -1
  17. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
  18. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +1 -1
  19. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +3 -3
  20. package/packages/datadog-plugin-cucumber/src/index.js +12 -2
  21. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +53 -12
  22. package/packages/datadog-plugin-jest/src/index.js +17 -4
  23. package/packages/datadog-plugin-mocha/src/index.js +25 -6
  24. package/packages/datadog-plugin-nyc/src/index.js +35 -0
  25. package/packages/datadog-plugin-playwright/src/index.js +9 -4
  26. package/packages/datadog-plugin-vitest/src/index.js +32 -5
  27. package/packages/dd-trace/src/appsec/blocking.js +10 -1
  28. package/packages/dd-trace/src/appsec/channels.js +4 -1
  29. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  30. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +16 -0
  31. package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +2 -0
  32. package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +2 -1
  33. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +11 -0
  34. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/code-injection-sensitive-analyzer.js +25 -0
  35. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -0
  36. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +2 -2
  37. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  38. package/packages/dd-trace/src/appsec/index.js +12 -7
  39. package/packages/dd-trace/src/appsec/rasp.js +121 -7
  40. package/packages/dd-trace/src/appsec/recommended.json +220 -2
  41. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +40 -1
  42. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +2 -4
  43. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +2 -4
  44. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +8 -7
  45. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -4
  46. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +2 -4
  47. package/packages/dd-trace/src/ci-visibility/telemetry.js +29 -2
  48. package/packages/dd-trace/src/config.js +158 -153
  49. package/packages/dd-trace/src/data_streams.js +44 -0
  50. package/packages/dd-trace/src/datastreams/pathway.js +4 -2
  51. package/packages/dd-trace/src/log/index.js +32 -0
  52. package/packages/dd-trace/src/opentelemetry/context_manager.js +22 -39
  53. package/packages/dd-trace/src/opentelemetry/span_context.js +2 -2
  54. package/packages/dd-trace/src/opentelemetry/tracer.js +23 -14
  55. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +9 -1
  56. package/packages/dd-trace/src/opentracing/propagation/log.js +1 -1
  57. package/packages/dd-trace/src/opentracing/propagation/text_map.js +60 -0
  58. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +43 -0
  59. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  60. package/packages/dd-trace/src/opentracing/tracer.js +10 -6
  61. package/packages/dd-trace/src/plugins/ci_plugin.js +11 -4
  62. package/packages/dd-trace/src/plugins/index.js +1 -0
  63. package/packages/dd-trace/src/plugins/plugin.js +12 -1
  64. package/packages/dd-trace/src/plugins/util/git.js +14 -1
  65. package/packages/dd-trace/src/proxy.js +1 -0
  66. package/packages/dd-trace/src/telemetry/index.js +1 -1
  67. package/packages/dd-trace/src/tracer.js +2 -0
@@ -7,9 +7,16 @@ const {
7
7
  getTestSuitePath,
8
8
  getTestSuiteCommonTags,
9
9
  TEST_SOURCE_FILE,
10
- TEST_IS_RETRY
10
+ TEST_IS_RETRY,
11
+ TEST_CODE_COVERAGE_LINES_PCT,
12
+ TEST_CODE_OWNERS
11
13
  } = require('../../dd-trace/src/plugins/util/test')
12
14
  const { COMPONENT } = require('../../dd-trace/src/constants')
15
+ const {
16
+ TELEMETRY_EVENT_CREATED,
17
+ TELEMETRY_EVENT_FINISHED,
18
+ TELEMETRY_TEST_SESSION
19
+ } = require('../../dd-trace/src/ci-visibility/telemetry')
13
20
 
14
21
  // Milliseconds that we subtract from the error test duration
15
22
  // so that they do not overlap with the following test
@@ -64,6 +71,9 @@ class VitestPlugin extends CiPlugin {
64
71
  const span = store?.span
65
72
 
66
73
  if (span) {
74
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
75
+ hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS]
76
+ })
67
77
  span.setTag(TEST_STATUS, 'pass')
68
78
  span.finish(this.taskToFinishTime.get(task))
69
79
  finishAllTraceSpans(span)
@@ -75,6 +85,9 @@ class VitestPlugin extends CiPlugin {
75
85
  const span = store?.span
76
86
 
77
87
  if (span) {
88
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
89
+ hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS]
90
+ })
78
91
  span.setTag(TEST_STATUS, 'fail')
79
92
 
80
93
  if (error) {
@@ -91,7 +104,7 @@ class VitestPlugin extends CiPlugin {
91
104
 
92
105
  this.addSub('ci:vitest:test:skip', ({ testName, testSuiteAbsolutePath }) => {
93
106
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
94
- this.startTestSpan(
107
+ const testSpan = this.startTestSpan(
95
108
  testName,
96
109
  testSuite,
97
110
  this.testSuiteSpan,
@@ -99,10 +112,15 @@ class VitestPlugin extends CiPlugin {
99
112
  [TEST_SOURCE_FILE]: testSuite,
100
113
  [TEST_STATUS]: 'skip'
101
114
  }
102
- ).finish()
115
+ )
116
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
117
+ hasCodeowners: !!testSpan.context()._tags[TEST_CODE_OWNERS]
118
+ })
119
+ testSpan.finish()
103
120
  })
104
121
 
105
- this.addSub('ci:vitest:test-suite:start', (testSuiteAbsolutePath) => {
122
+ this.addSub('ci:vitest:test-suite:start', ({ testSuiteAbsolutePath, frameworkVersion }) => {
123
+ this.frameworkVersion = frameworkVersion
106
124
  const testSessionSpanContext = this.tracer.extract('text_map', {
107
125
  'x-datadog-trace-id': process.env.DD_CIVISIBILITY_TEST_SESSION_ID,
108
126
  'x-datadog-parent-id': process.env.DD_CIVISIBILITY_TEST_MODULE_ID
@@ -123,6 +141,7 @@ class VitestPlugin extends CiPlugin {
123
141
  ...testSuiteMetadata
124
142
  }
125
143
  })
144
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite')
126
145
  const store = storage.getStore()
127
146
  this.enter(testSuiteSpan, store)
128
147
  this.testSuiteSpan = testSuiteSpan
@@ -136,6 +155,7 @@ class VitestPlugin extends CiPlugin {
136
155
  span.finish()
137
156
  finishAllTraceSpans(span)
138
157
  }
158
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
139
159
  // TODO: too frequent flush - find for method in worker to decrease frequency
140
160
  this.tracer._exporter.flush(onFinish)
141
161
  })
@@ -149,16 +169,23 @@ class VitestPlugin extends CiPlugin {
149
169
  }
150
170
  })
151
171
 
152
- this.addSub('ci:vitest:session:finish', ({ status, onFinish, error }) => {
172
+ this.addSub('ci:vitest:session:finish', ({ status, onFinish, error, testCodeCoverageLinesTotal }) => {
153
173
  this.testSessionSpan.setTag(TEST_STATUS, status)
154
174
  this.testModuleSpan.setTag(TEST_STATUS, status)
155
175
  if (error) {
156
176
  this.testModuleSpan.setTag('error', error)
157
177
  this.testSessionSpan.setTag('error', error)
158
178
  }
179
+ if (testCodeCoverageLinesTotal) {
180
+ this.testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
181
+ this.testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
182
+ }
159
183
  this.testModuleSpan.finish()
184
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
160
185
  this.testSessionSpan.finish()
186
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
161
187
  finishAllTraceSpans(this.testSessionSpan)
188
+ this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName })
162
189
  this.tracer._exporter.flush(onFinish)
163
190
  })
164
191
  }
@@ -9,6 +9,8 @@ let templateHtml = blockedTemplates.html
9
9
  let templateJson = blockedTemplates.json
10
10
  let templateGraphqlJson = blockedTemplates.graphqlJson
11
11
 
12
+ const responseBlockedSet = new WeakSet()
13
+
12
14
  const specificBlockingTypes = {
13
15
  GRAPHQL: 'graphql'
14
16
  }
@@ -117,6 +119,8 @@ function block (req, res, rootSpan, abortController, actionParameters) {
117
119
 
118
120
  res.writeHead(statusCode, headers).end(body)
119
121
 
122
+ responseBlockedSet.add(res)
123
+
120
124
  abortController?.abort()
121
125
  }
122
126
 
@@ -144,11 +148,16 @@ function setTemplates (config) {
144
148
  }
145
149
  }
146
150
 
151
+ function isBlocked (res) {
152
+ return responseBlockedSet.has(res)
153
+ }
154
+
147
155
  module.exports = {
148
156
  addSpecificEndpoint,
149
157
  block,
150
158
  specificBlockingTypes,
151
159
  getBlockingData,
152
160
  getBlockingAction,
153
- setTemplates
161
+ setTemplates,
162
+ isBlocked
154
163
  }
@@ -19,5 +19,8 @@ module.exports = {
19
19
  nextQueryParsed: dc.channel('apm:next:query-parsed'),
20
20
  responseBody: dc.channel('datadog:express:response:json:start'),
21
21
  responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
22
- httpClientRequestStart: dc.channel('apm:http:client:request:start')
22
+ httpClientRequestStart: dc.channel('apm:http:client:request:start'),
23
+ responseSetHeader: dc.channel('datadog:http:server:response:set-header:start'),
24
+ setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start')
25
+
23
26
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
+ CODE_INJECTION_ANALYZER: require('./code-injection-analyzer'),
4
5
  COMMAND_INJECTION_ANALYZER: require('./command-injection-analyzer'),
5
6
  HARCODED_PASSWORD_ANALYZER: require('./hardcoded-password-analyzer'),
6
7
  HARCODED_SECRET_ANALYZER: require('./hardcoded-secret-analyzer'),
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ const InjectionAnalyzer = require('./injection-analyzer')
4
+ const { CODE_INJECTION } = require('../vulnerabilities')
5
+
6
+ class CodeInjectionAnalyzer extends InjectionAnalyzer {
7
+ constructor () {
8
+ super(CODE_INJECTION)
9
+ }
10
+
11
+ onConfigure () {
12
+ this.addSub('datadog:eval:call', ({ script }) => this.analyze(script))
13
+ }
14
+ }
15
+
16
+ module.exports = new CodeInjectionAnalyzer()
@@ -47,6 +47,8 @@ class WeakHashAnalyzer extends Analyzer {
47
47
  }
48
48
 
49
49
  _isExcluded (location) {
50
+ if (!location) return false
51
+
50
52
  return EXCLUDED_LOCATIONS.some(excludedLocation => {
51
53
  return location.path.includes(excludedLocation)
52
54
  })
@@ -14,7 +14,8 @@ const csiMethods = [
14
14
  { src: 'toUpperCase', dst: 'stringCase' },
15
15
  { src: 'trim' },
16
16
  { src: 'trimEnd' },
17
- { src: 'trimStart', dst: 'trim' }
17
+ { src: 'trimStart', dst: 'trim' },
18
+ { src: 'eval', allowedWithoutCallee: true }
18
19
  ]
19
20
 
20
21
  module.exports = {
@@ -10,6 +10,7 @@ const { isDebugAllowed } = require('../telemetry/verbosity')
10
10
  const { taintObject } = require('./operations-taint-object')
11
11
 
12
12
  const mathRandomCallCh = dc.channel('datadog:random:call')
13
+ const evalCallCh = dc.channel('datadog:eval:call')
13
14
 
14
15
  const JSON_VALUE = 'json.value'
15
16
 
@@ -18,6 +19,7 @@ function noop (res) { return res }
18
19
  // Otherwise you may end up rewriting a method and not providing its rewritten implementation
19
20
  const TaintTrackingNoop = {
20
21
  concat: noop,
22
+ eval: noop,
21
23
  join: noop,
22
24
  parse: noop,
23
25
  plusOperator: noop,
@@ -136,6 +138,15 @@ function csiMethodsOverrides (getContext) {
136
138
  return res
137
139
  },
138
140
 
141
+ eval: function (res, fn, target, script) {
142
+ // eslint-disable-next-line no-eval
143
+ if (evalCallCh.hasSubscribers && fn === globalThis.eval) {
144
+ evalCallCh.publish({ script })
145
+ }
146
+
147
+ return res
148
+ },
149
+
139
150
  parse: function (res, fn, target, json) {
140
151
  if (fn === JSON.parse) {
141
152
  try {
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ module.exports = function extractSensitiveRanges (evidence) {
4
+ const newRanges = []
5
+ if (evidence.ranges[0].start > 0) {
6
+ newRanges.push({
7
+ start: 0,
8
+ end: evidence.ranges[0].start
9
+ })
10
+ }
11
+
12
+ for (let i = 0; i < evidence.ranges.length; i++) {
13
+ const currentRange = evidence.ranges[i]
14
+ const nextRange = evidence.ranges[i + 1]
15
+
16
+ const start = currentRange.end
17
+ const end = nextRange?.start || evidence.value.length
18
+
19
+ if (start < end) {
20
+ newRanges.push({ start, end })
21
+ }
22
+ }
23
+
24
+ return newRanges
25
+ }
@@ -5,6 +5,7 @@ const vulnerabilities = require('../../vulnerabilities')
5
5
 
6
6
  const { contains, intersects, remove } = require('./range-utils')
7
7
 
8
+ const codeInjectionSensitiveAnalyzer = require('./sensitive-analyzers/code-injection-sensitive-analyzer')
8
9
  const commandSensitiveAnalyzer = require('./sensitive-analyzers/command-sensitive-analyzer')
9
10
  const hardcodedPasswordAnalyzer = require('./sensitive-analyzers/hardcoded-password-analyzer')
10
11
  const headerSensitiveAnalyzer = require('./sensitive-analyzers/header-sensitive-analyzer')
@@ -23,6 +24,7 @@ class SensitiveHandler {
23
24
  this._valuePattern = new RegExp(DEFAULT_IAST_REDACTION_VALUE_PATTERN, 'gmi')
24
25
 
25
26
  this._sensitiveAnalyzers = new Map()
27
+ this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, codeInjectionSensitiveAnalyzer)
26
28
  this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, commandSensitiveAnalyzer)
27
29
  this._sensitiveAnalyzers.set(vulnerabilities.NOSQL_MONGODB_INJECTION, jsonSensitiveAnalyzer)
28
30
  this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, ldapSensitiveAnalyzer)
@@ -1,7 +1,7 @@
1
1
  // eslint-disable-next-line max-len
2
- const DEFAULT_IAST_REDACTION_NAME_PATTERN = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)'
2
+ const DEFAULT_IAST_REDACTION_NAME_PATTERN = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|(?:sur|last)name|user(?:name)?|address|e?mail)'
3
3
  // eslint-disable-next-line max-len
4
- const DEFAULT_IAST_REDACTION_VALUE_PATTERN = '(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,}))'
4
+ const DEFAULT_IAST_REDACTION_VALUE_PATTERN = '(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,})|[\\w\\.-]+@[a-zA-Z\\d\\.-]+\\.[a-zA-Z]{2,})'
5
5
 
6
6
  module.exports = {
7
7
  DEFAULT_IAST_REDACTION_NAME_PATTERN,
@@ -1,5 +1,6 @@
1
1
  module.exports = {
2
2
  COMMAND_INJECTION: 'COMMAND_INJECTION',
3
+ CODE_INJECTION: 'CODE_INJECTION',
3
4
  HARDCODED_PASSWORD: 'HARDCODED_PASSWORD',
4
5
  HARDCODED_SECRET: 'HARDCODED_SECRET',
5
6
  HEADER_INJECTION: 'HEADER_INJECTION',
@@ -13,7 +13,8 @@ const {
13
13
  nextBodyParsed,
14
14
  nextQueryParsed,
15
15
  responseBody,
16
- responseWriteHead
16
+ responseWriteHead,
17
+ responseSetHeader
17
18
  } = require('./channels')
18
19
  const waf = require('./waf')
19
20
  const addresses = require('./addresses')
@@ -23,7 +24,7 @@ const apiSecuritySampler = require('./api_security_sampler')
23
24
  const web = require('../plugins/util/web')
24
25
  const { extractIp } = require('../plugins/util/ip_extractor')
25
26
  const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
26
- const { block, setTemplates, getBlockingAction } = require('./blocking')
27
+ const { isBlocked, block, setTemplates, getBlockingAction } = require('./blocking')
27
28
  const { passportTrackEvent } = require('./passport')
28
29
  const { storage } = require('../../../datadog-core')
29
30
  const graphql = require('./graphql')
@@ -62,6 +63,7 @@ function enable (_config) {
62
63
  cookieParser.subscribe(onRequestCookieParser)
63
64
  responseBody.subscribe(onResponseBody)
64
65
  responseWriteHead.subscribe(onResponseWriteHead)
66
+ responseSetHeader.subscribe(onResponseSetHeader)
65
67
 
66
68
  if (_config.appsec.eventTracking.enabled) {
67
69
  passportVerify.subscribe(onPassportVerify)
@@ -223,11 +225,10 @@ function onPassportVerify ({ credentials, user }) {
223
225
  }
224
226
 
225
227
  const responseAnalyzedSet = new WeakSet()
226
- const responseBlockedSet = new WeakSet()
227
228
 
228
229
  function onResponseWriteHead ({ req, res, abortController, statusCode, responseHeaders }) {
229
230
  // avoid "write after end" error
230
- if (responseBlockedSet.has(res)) {
231
+ if (isBlocked(res)) {
231
232
  abortController?.abort()
232
233
  return
233
234
  }
@@ -255,15 +256,18 @@ function onResponseWriteHead ({ req, res, abortController, statusCode, responseH
255
256
  handleResults(results, req, res, rootSpan, abortController)
256
257
  }
257
258
 
259
+ function onResponseSetHeader ({ res, abortController }) {
260
+ if (isBlocked(res)) {
261
+ abortController?.abort()
262
+ }
263
+ }
264
+
258
265
  function handleResults (actions, req, res, rootSpan, abortController) {
259
266
  if (!actions || !req || !res || !rootSpan || !abortController) return
260
267
 
261
268
  const blockingAction = getBlockingAction(actions)
262
269
  if (blockingAction) {
263
270
  block(req, res, rootSpan, abortController, blockingAction)
264
- if (!abortController.signal || abortController.signal.aborted) {
265
- responseBlockedSet.add(res)
266
- }
267
271
  }
268
272
  }
269
273
 
@@ -290,6 +294,7 @@ function disable () {
290
294
  if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
291
295
  if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
292
296
  if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
297
+ if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader)
293
298
  }
294
299
 
295
300
  module.exports = {
@@ -3,23 +3,118 @@
3
3
  const { storage } = require('../../../datadog-core')
4
4
  const web = require('./../plugins/util/web')
5
5
  const addresses = require('./addresses')
6
- const { httpClientRequestStart } = require('./channels')
6
+ const { httpClientRequestStart, setUncaughtExceptionCaptureCallbackStart } = require('./channels')
7
7
  const { reportStackTrace } = require('./stack_trace')
8
8
  const waf = require('./waf')
9
+ const { getBlockingAction, block } = require('./blocking')
10
+ const log = require('../log')
9
11
 
10
12
  const RULE_TYPES = {
11
13
  SSRF: 'ssrf'
12
14
  }
13
15
 
14
- let config
16
+ class DatadogRaspAbortError extends Error {
17
+ constructor (req, res, blockingAction) {
18
+ super('DatadogRaspAbortError')
19
+ this.name = 'DatadogRaspAbortError'
20
+ this.req = req
21
+ this.res = res
22
+ this.blockingAction = blockingAction
23
+ }
24
+ }
25
+
26
+ let config, abortOnUncaughtException
27
+
28
+ function removeAllListeners (emitter, event) {
29
+ const listeners = emitter.listeners(event)
30
+ emitter.removeAllListeners(event)
31
+
32
+ let cleaned = false
33
+ return function () {
34
+ if (cleaned === true) {
35
+ return
36
+ }
37
+ cleaned = true
38
+
39
+ for (let i = 0; i < listeners.length; ++i) {
40
+ emitter.on(event, listeners[i])
41
+ }
42
+ }
43
+ }
44
+
45
+ function findDatadogRaspAbortError (err, deep = 10) {
46
+ if (err instanceof DatadogRaspAbortError) {
47
+ return err
48
+ }
49
+
50
+ if (err.cause && deep > 0) {
51
+ return findDatadogRaspAbortError(err.cause, deep - 1)
52
+ }
53
+ }
54
+
55
+ function handleUncaughtExceptionMonitor (err) {
56
+ const abortError = findDatadogRaspAbortError(err)
57
+ if (!abortError) return
58
+
59
+ const { req, res, blockingAction } = abortError
60
+ block(req, res, web.root(req), null, blockingAction)
61
+
62
+ if (!process.hasUncaughtExceptionCaptureCallback()) {
63
+ const cleanUp = removeAllListeners(process, 'uncaughtException')
64
+ const handler = () => {
65
+ process.removeListener('uncaughtException', handler)
66
+ }
67
+
68
+ setTimeout(() => {
69
+ process.removeListener('uncaughtException', handler)
70
+ cleanUp()
71
+ })
72
+
73
+ process.on('uncaughtException', handler)
74
+ } else {
75
+ // uncaughtException event is not executed when hasUncaughtExceptionCaptureCallback is true
76
+ let previousCb
77
+ const cb = ({ currentCallback, abortController }) => {
78
+ setUncaughtExceptionCaptureCallbackStart.unsubscribe(cb)
79
+ if (!currentCallback) {
80
+ abortController.abort()
81
+ return
82
+ }
83
+
84
+ previousCb = currentCallback
85
+ }
86
+
87
+ setUncaughtExceptionCaptureCallbackStart.subscribe(cb)
88
+
89
+ process.setUncaughtExceptionCaptureCallback(null)
90
+
91
+ // For some reason, previous callback was defined before the instrumentation
92
+ // We can not restore it, so we let the app decide
93
+ if (previousCb) {
94
+ process.setUncaughtExceptionCaptureCallback(() => {
95
+ process.setUncaughtExceptionCaptureCallback(null)
96
+ process.setUncaughtExceptionCaptureCallback(previousCb)
97
+ })
98
+ }
99
+ }
100
+ }
15
101
 
16
102
  function enable (_config) {
17
103
  config = _config
18
104
  httpClientRequestStart.subscribe(analyzeSsrf)
105
+
106
+ process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
107
+ abortOnUncaughtException = process.execArgv?.includes('--abort-on-uncaught-exception')
108
+
109
+ if (abortOnUncaughtException) {
110
+ log.warn('The --abort-on-uncaught-exception flag is enabled. The RASP module will not block operations.')
111
+ }
19
112
  }
20
113
 
21
114
  function disable () {
22
115
  if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
116
+
117
+ process.off('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
23
118
  }
24
119
 
25
120
  function analyzeSsrf (ctx) {
@@ -32,17 +127,18 @@ function analyzeSsrf (ctx) {
32
127
  const persistent = {
33
128
  [addresses.HTTP_OUTGOING_URL]: url
34
129
  }
35
- // TODO: Currently this is only monitoring, we should
36
- // block the request if SSRF attempt
130
+
37
131
  const result = waf.run({ persistent }, req, RULE_TYPES.SSRF)
38
- handleResult(result, req)
132
+
133
+ const res = store?.res
134
+ handleResult(result, req, res, ctx.abortController)
39
135
  }
40
136
 
41
137
  function getGenerateStackTraceAction (actions) {
42
138
  return actions?.generate_stack
43
139
  }
44
140
 
45
- function handleResult (actions, req) {
141
+ function handleResult (actions, req, res, abortController) {
46
142
  const generateStackTraceAction = getGenerateStackTraceAction(actions)
47
143
  if (generateStackTraceAction && config.appsec.stackTrace.enabled) {
48
144
  const rootSpan = web.root(req)
@@ -53,10 +149,28 @@ function handleResult (actions, req) {
53
149
  config.appsec.stackTrace.maxStackTraces
54
150
  )
55
151
  }
152
+
153
+ if (!abortController || abortOnUncaughtException) return
154
+
155
+ const blockingAction = getBlockingAction(actions)
156
+ if (blockingAction) {
157
+ const rootSpan = web.root(req)
158
+ // Should block only in express
159
+ if (rootSpan?.context()._name === 'express.request') {
160
+ const abortError = new DatadogRaspAbortError(req, res, blockingAction)
161
+ abortController.abort(abortError)
162
+
163
+ // TODO Delete this when support for node 16 is removed
164
+ if (!abortController.signal.reason) {
165
+ abortController.signal.reason = abortError
166
+ }
167
+ }
168
+ }
56
169
  }
57
170
 
58
171
  module.exports = {
59
172
  enable,
60
173
  disable,
61
- handleResult
174
+ handleResult,
175
+ handleUncaughtExceptionMonitor // exported only for testing purpose
62
176
  }