dd-trace 5.21.0 → 5.23.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 (153) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/index.d.ts +20 -8
  3. package/package.json +11 -5
  4. package/packages/datadog-instrumentations/src/aerospike.js +1 -1
  5. package/packages/datadog-instrumentations/src/apollo-server.js +1 -1
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +4 -4
  7. package/packages/datadog-instrumentations/src/body-parser.js +4 -4
  8. package/packages/datadog-instrumentations/src/cassandra-driver.js +2 -2
  9. package/packages/datadog-instrumentations/src/child_process.js +2 -2
  10. package/packages/datadog-instrumentations/src/connect.js +4 -4
  11. package/packages/datadog-instrumentations/src/cookie-parser.js +4 -4
  12. package/packages/datadog-instrumentations/src/couchbase.js +12 -12
  13. package/packages/datadog-instrumentations/src/cucumber.js +294 -56
  14. package/packages/datadog-instrumentations/src/dns.js +10 -10
  15. package/packages/datadog-instrumentations/src/elasticsearch.js +4 -4
  16. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +3 -3
  17. package/packages/datadog-instrumentations/src/express.js +4 -4
  18. package/packages/datadog-instrumentations/src/fastify.js +6 -6
  19. package/packages/datadog-instrumentations/src/fetch.js +1 -1
  20. package/packages/datadog-instrumentations/src/find-my-way.js +2 -2
  21. package/packages/datadog-instrumentations/src/fs.js +2 -2
  22. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +2 -2
  23. package/packages/datadog-instrumentations/src/grpc/client.js +4 -6
  24. package/packages/datadog-instrumentations/src/grpc/server.js +2 -2
  25. package/packages/datadog-instrumentations/src/hapi.js +10 -13
  26. package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
  27. package/packages/datadog-instrumentations/src/http/client.js +3 -3
  28. package/packages/datadog-instrumentations/src/jest.js +8 -5
  29. package/packages/datadog-instrumentations/src/kafkajs.js +67 -31
  30. package/packages/datadog-instrumentations/src/knex.js +2 -2
  31. package/packages/datadog-instrumentations/src/koa.js +5 -5
  32. package/packages/datadog-instrumentations/src/ldapjs.js +1 -1
  33. package/packages/datadog-instrumentations/src/mariadb.js +8 -8
  34. package/packages/datadog-instrumentations/src/memcached.js +2 -2
  35. package/packages/datadog-instrumentations/src/microgateway-core.js +7 -5
  36. package/packages/datadog-instrumentations/src/mocha/common.js +1 -1
  37. package/packages/datadog-instrumentations/src/mocha/main.js +139 -53
  38. package/packages/datadog-instrumentations/src/mocha/utils.js +37 -18
  39. package/packages/datadog-instrumentations/src/mocha/worker.js +29 -1
  40. package/packages/datadog-instrumentations/src/mocha.js +4 -0
  41. package/packages/datadog-instrumentations/src/moleculer/server.js +2 -2
  42. package/packages/datadog-instrumentations/src/mongodb-core.js +7 -7
  43. package/packages/datadog-instrumentations/src/mongoose.js +5 -6
  44. package/packages/datadog-instrumentations/src/mysql.js +3 -3
  45. package/packages/datadog-instrumentations/src/mysql2.js +6 -6
  46. package/packages/datadog-instrumentations/src/net.js +2 -2
  47. package/packages/datadog-instrumentations/src/next.js +5 -5
  48. package/packages/datadog-instrumentations/src/openai.js +62 -71
  49. package/packages/datadog-instrumentations/src/oracledb.js +8 -8
  50. package/packages/datadog-instrumentations/src/passport-http.js +1 -1
  51. package/packages/datadog-instrumentations/src/passport-local.js +1 -1
  52. package/packages/datadog-instrumentations/src/passport-utils.js +1 -1
  53. package/packages/datadog-instrumentations/src/pg.js +60 -5
  54. package/packages/datadog-instrumentations/src/pino.js +4 -4
  55. package/packages/datadog-instrumentations/src/playwright.js +6 -4
  56. package/packages/datadog-instrumentations/src/redis.js +2 -2
  57. package/packages/datadog-instrumentations/src/restify.js +4 -4
  58. package/packages/datadog-instrumentations/src/rhea.js +4 -4
  59. package/packages/datadog-instrumentations/src/router.js +5 -5
  60. package/packages/datadog-instrumentations/src/sharedb.js +2 -2
  61. package/packages/datadog-instrumentations/src/vitest.js +188 -12
  62. package/packages/datadog-instrumentations/src/winston.js +2 -3
  63. package/packages/datadog-plugin-amqplib/src/consumer.js +1 -3
  64. package/packages/datadog-plugin-aws-sdk/src/base.js +33 -0
  65. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
  66. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +2 -0
  67. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -1
  68. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  69. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +39 -10
  70. package/packages/datadog-plugin-cypress/src/support.js +4 -1
  71. package/packages/datadog-plugin-hapi/src/index.js +2 -2
  72. package/packages/datadog-plugin-http/src/client.js +1 -42
  73. package/packages/datadog-plugin-http2/src/client.js +1 -26
  74. package/packages/datadog-plugin-jest/src/index.js +18 -1
  75. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +20 -0
  76. package/packages/datadog-plugin-kafkajs/src/consumer.js +1 -2
  77. package/packages/datadog-plugin-kafkajs/src/index.js +3 -1
  78. package/packages/datadog-plugin-mocha/src/index.js +18 -0
  79. package/packages/datadog-plugin-openai/src/index.js +85 -65
  80. package/packages/datadog-plugin-playwright/src/index.js +9 -0
  81. package/packages/datadog-plugin-rhea/src/consumer.js +1 -3
  82. package/packages/datadog-plugin-vitest/src/index.js +68 -3
  83. package/packages/datadog-shimmer/src/shimmer.js +144 -10
  84. package/packages/dd-trace/src/appsec/addresses.js +3 -1
  85. package/packages/dd-trace/src/appsec/blocking.js +23 -17
  86. package/packages/dd-trace/src/appsec/channels.js +4 -2
  87. package/packages/dd-trace/src/appsec/graphql.js +3 -1
  88. package/packages/dd-trace/src/appsec/iast/iast-log.js +2 -1
  89. package/packages/dd-trace/src/appsec/rasp/index.js +103 -0
  90. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +86 -0
  91. package/packages/dd-trace/src/appsec/rasp/ssrf.js +37 -0
  92. package/packages/dd-trace/src/appsec/rasp/utils.js +63 -0
  93. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -0
  94. package/packages/dd-trace/src/appsec/remote_config/index.js +16 -7
  95. package/packages/dd-trace/src/appsec/remote_config/manager.js +93 -52
  96. package/packages/dd-trace/src/appsec/rule_manager.js +8 -0
  97. package/packages/dd-trace/src/appsec/telemetry.js +3 -3
  98. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +33 -14
  99. package/packages/dd-trace/src/appsec/waf/waf_manager.js +2 -1
  100. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -0
  101. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +15 -1
  102. package/packages/dd-trace/src/config.js +100 -40
  103. package/packages/dd-trace/src/constants.js +11 -1
  104. package/packages/dd-trace/src/data_streams_context.js +3 -0
  105. package/packages/dd-trace/src/datastreams/fnv.js +23 -0
  106. package/packages/dd-trace/src/datastreams/pathway.js +12 -5
  107. package/packages/dd-trace/src/datastreams/processor.js +35 -0
  108. package/packages/dd-trace/src/datastreams/schemas/schema.js +8 -0
  109. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +125 -0
  110. package/packages/dd-trace/src/datastreams/schemas/schema_sampler.js +29 -0
  111. package/packages/dd-trace/src/debugger/devtools_client/config.js +24 -0
  112. package/packages/dd-trace/src/debugger/devtools_client/index.js +57 -0
  113. package/packages/dd-trace/src/debugger/devtools_client/inspector_promises_polyfill.js +23 -0
  114. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +164 -0
  115. package/packages/dd-trace/src/debugger/devtools_client/send.js +28 -0
  116. package/packages/dd-trace/src/debugger/devtools_client/session.js +7 -0
  117. package/packages/dd-trace/src/debugger/devtools_client/state.js +47 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/status.js +109 -0
  119. package/packages/dd-trace/src/debugger/index.js +92 -0
  120. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +29 -2
  121. package/packages/dd-trace/src/exporters/common/request.js +1 -1
  122. package/packages/dd-trace/src/lambda/handler.js +1 -0
  123. package/packages/dd-trace/src/lambda/index.js +12 -1
  124. package/packages/dd-trace/src/opentracing/propagation/text_map.js +1 -6
  125. package/packages/dd-trace/src/payload-tagging/config/aws.json +30 -0
  126. package/packages/dd-trace/src/payload-tagging/config/index.js +30 -0
  127. package/packages/dd-trace/src/payload-tagging/index.js +93 -0
  128. package/packages/dd-trace/src/payload-tagging/tagging.js +83 -0
  129. package/packages/dd-trace/src/plugin_manager.js +11 -10
  130. package/packages/dd-trace/src/plugins/ci_plugin.js +33 -8
  131. package/packages/dd-trace/src/plugins/util/env.js +5 -2
  132. package/packages/dd-trace/src/plugins/util/test.js +24 -4
  133. package/packages/dd-trace/src/profiler.js +15 -5
  134. package/packages/dd-trace/src/profiling/config.js +7 -4
  135. package/packages/dd-trace/src/profiling/exporter_cli.js +13 -1
  136. package/packages/dd-trace/src/profiling/exporters/agent.js +8 -2
  137. package/packages/dd-trace/src/profiling/profiler.js +0 -9
  138. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns.js +13 -0
  139. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookup.js +16 -0
  140. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookupservice.js +16 -0
  141. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_resolve.js +24 -0
  142. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_reverse.js +16 -0
  143. package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +48 -0
  144. package/packages/dd-trace/src/profiling/profilers/event_plugins/net.js +24 -0
  145. package/packages/dd-trace/src/profiling/profilers/events.js +108 -32
  146. package/packages/dd-trace/src/profiling/profilers/shared.js +5 -0
  147. package/packages/dd-trace/src/profiling/profilers/wall.js +9 -3
  148. package/packages/dd-trace/src/profiling/ssi-heuristics.js +59 -60
  149. package/packages/dd-trace/src/proxy.js +31 -24
  150. package/packages/dd-trace/src/span_stats.js +4 -2
  151. package/packages/dd-trace/src/telemetry/index.js +23 -6
  152. package/packages/dd-trace/src/telemetry/logs/index.js +20 -0
  153. package/packages/dd-trace/src/appsec/rasp.js +0 -176
@@ -6,10 +6,18 @@ const {
6
6
  finishAllTraceSpans,
7
7
  getTestSuitePath,
8
8
  getTestSuiteCommonTags,
9
+ getTestSessionName,
10
+ getIsFaultyEarlyFlakeDetection,
9
11
  TEST_SOURCE_FILE,
10
12
  TEST_IS_RETRY,
11
13
  TEST_CODE_COVERAGE_LINES_PCT,
12
- TEST_CODE_OWNERS
14
+ TEST_CODE_OWNERS,
15
+ TEST_LEVEL_EVENT_TYPES,
16
+ TEST_SESSION_NAME,
17
+ TEST_SOURCE_START,
18
+ TEST_IS_NEW,
19
+ TEST_EARLY_FLAKE_ENABLED,
20
+ TEST_EARLY_FLAKE_ABORT_REASON
13
21
  } = require('../../dd-trace/src/plugins/util/test')
14
22
  const { COMPONENT } = require('../../dd-trace/src/constants')
15
23
  const {
@@ -33,7 +41,26 @@ class VitestPlugin extends CiPlugin {
33
41
 
34
42
  this.taskToFinishTime = new WeakMap()
35
43
 
36
- this.addSub('ci:vitest:test:start', ({ testName, testSuiteAbsolutePath, isRetry }) => {
44
+ this.addSub('ci:vitest:test:is-new', ({ knownTests, testSuiteAbsolutePath, testName, onDone }) => {
45
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
46
+ const testsForThisTestSuite = knownTests[testSuite] || []
47
+ onDone(!testsForThisTestSuite.includes(testName))
48
+ })
49
+
50
+ this.addSub('ci:vitest:is-early-flake-detection-faulty', ({
51
+ knownTests,
52
+ testFilepaths,
53
+ onDone
54
+ }) => {
55
+ const isFaulty = getIsFaultyEarlyFlakeDetection(
56
+ testFilepaths.map(testFilepath => getTestSuitePath(testFilepath, this.repositoryRoot)),
57
+ knownTests,
58
+ this.libraryConfig.earlyFlakeDetectionFaultyThreshold
59
+ )
60
+ onDone(isFaulty)
61
+ })
62
+
63
+ this.addSub('ci:vitest:test:start', ({ testName, testSuiteAbsolutePath, isRetry, isNew }) => {
37
64
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
38
65
  const store = storage.getStore()
39
66
 
@@ -43,6 +70,9 @@ class VitestPlugin extends CiPlugin {
43
70
  if (isRetry) {
44
71
  extraTags[TEST_IS_RETRY] = 'true'
45
72
  }
73
+ if (isNew) {
74
+ extraTags[TEST_IS_NEW] = 'true'
75
+ }
46
76
 
47
77
  const span = this.startTestSpan(
48
78
  testName,
@@ -110,6 +140,7 @@ class VitestPlugin extends CiPlugin {
110
140
  this.testSuiteSpan,
111
141
  {
112
142
  [TEST_SOURCE_FILE]: testSuite,
143
+ [TEST_SOURCE_START]: 1, // we can't get the proper start line in vitest
113
144
  [TEST_STATUS]: 'skip'
114
145
  }
115
146
  )
@@ -120,12 +151,25 @@ class VitestPlugin extends CiPlugin {
120
151
  })
121
152
 
122
153
  this.addSub('ci:vitest:test-suite:start', ({ testSuiteAbsolutePath, frameworkVersion }) => {
154
+ this.command = process.env.DD_CIVISIBILITY_TEST_COMMAND
123
155
  this.frameworkVersion = frameworkVersion
124
156
  const testSessionSpanContext = this.tracer.extract('text_map', {
125
157
  'x-datadog-trace-id': process.env.DD_CIVISIBILITY_TEST_SESSION_ID,
126
158
  'x-datadog-parent-id': process.env.DD_CIVISIBILITY_TEST_MODULE_ID
127
159
  })
128
160
 
161
+ // test suites run in a different process, so they also need to init the metadata dictionary
162
+ const testSessionName = getTestSessionName(this.config, this.command, this.testEnvironmentMetadata)
163
+ const metadataTags = {}
164
+ for (const testLevel of TEST_LEVEL_EVENT_TYPES) {
165
+ metadataTags[testLevel] = {
166
+ [TEST_SESSION_NAME]: testSessionName
167
+ }
168
+ }
169
+ if (this.tracer._exporter.setMetadataTags) {
170
+ this.tracer._exporter.setMetadataTags(metadataTags)
171
+ }
172
+
129
173
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
130
174
  const testSuiteMetadata = getTestSuiteCommonTags(
131
175
  this.command,
@@ -133,6 +177,14 @@ class VitestPlugin extends CiPlugin {
133
177
  testSuite,
134
178
  'vitest'
135
179
  )
180
+ testSuiteMetadata[TEST_SOURCE_FILE] = testSuite
181
+ testSuiteMetadata[TEST_SOURCE_START] = 1
182
+
183
+ const codeOwners = this.getCodeOwners(testSuiteMetadata)
184
+ if (codeOwners) {
185
+ testSuiteMetadata[TEST_CODE_OWNERS] = codeOwners
186
+ }
187
+
136
188
  const testSuiteSpan = this.tracer.startSpan('vitest.test_suite', {
137
189
  childOf: testSessionSpanContext,
138
190
  tags: {
@@ -169,7 +221,14 @@ class VitestPlugin extends CiPlugin {
169
221
  }
170
222
  })
171
223
 
172
- this.addSub('ci:vitest:session:finish', ({ status, onFinish, error, testCodeCoverageLinesTotal }) => {
224
+ this.addSub('ci:vitest:session:finish', ({
225
+ status,
226
+ error,
227
+ testCodeCoverageLinesTotal,
228
+ isEarlyFlakeDetectionEnabled,
229
+ isEarlyFlakeDetectionFaulty,
230
+ onFinish
231
+ }) => {
173
232
  this.testSessionSpan.setTag(TEST_STATUS, status)
174
233
  this.testModuleSpan.setTag(TEST_STATUS, status)
175
234
  if (error) {
@@ -180,6 +239,12 @@ class VitestPlugin extends CiPlugin {
180
239
  this.testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
181
240
  this.testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
182
241
  }
242
+ if (isEarlyFlakeDetectionEnabled) {
243
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
244
+ }
245
+ if (isEarlyFlakeDetectionFaulty) {
246
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
247
+ }
183
248
  this.testModuleSpan.finish()
184
249
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
185
250
  this.testSessionSpan.finish()
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('../../dd-trace/src/log')
4
+
3
5
  // Use a weak map to avoid polluting the wrapped function/method.
4
6
  const unwrappers = new WeakMap()
5
7
 
@@ -18,9 +20,12 @@ function copyProperties (original, wrapped) {
18
20
  }
19
21
  }
20
22
 
21
- function wrapFn (original, delegate) {
22
- assertFunction(delegate)
23
- assertNotClass(original) // TODO: support constructors of native classes
23
+ function wrapFunction (original, wrapper) {
24
+ if (typeof original === 'function') assertNotClass(original)
25
+ // TODO This needs to be re-done so that this and wrapMethod are distinct.
26
+ const target = { func: original }
27
+ wrapMethod(target, 'func', wrapper, typeof original !== 'function')
28
+ let delegate = target.func
24
29
 
25
30
  const shim = function shim () {
26
31
  return delegate.apply(this, arguments)
@@ -30,17 +35,144 @@ function wrapFn (original, delegate) {
30
35
  delegate = original
31
36
  })
32
37
 
33
- copyProperties(original, shim)
38
+ if (typeof original === 'function') copyProperties(original, shim)
34
39
 
35
40
  return shim
36
41
  }
37
42
 
38
- function wrapMethod (target, name, wrapper) {
39
- assertMethod(target, name)
40
- assertFunction(wrapper)
43
+ const wrapFn = function (original, delegate) {
44
+ throw new Error('calling `wrap()` with 2 args is deprecated. Use wrapFunction instead.')
45
+ }
46
+
47
+ // This is only used in safe mode. It's a simple state machine to track if the
48
+ // original method was called and if it returned. We need this to determine if
49
+ // an error was thrown by the original method, or by us. We'll use one of these
50
+ // per call to a wrapped method.
51
+ class CallState {
52
+ constructor () {
53
+ this.called = false
54
+ this.completed = false
55
+ this.retVal = undefined
56
+ }
57
+
58
+ startCall () {
59
+ this.called = true
60
+ }
61
+
62
+ endCall (retVal) {
63
+ this.completed = true
64
+ this.retVal = retVal
65
+ }
66
+ }
67
+
68
+ function isPromise (obj) {
69
+ return obj && typeof obj === 'object' && typeof obj.then === 'function'
70
+ }
71
+
72
+ let safeMode = !!process.env.DD_INEJCTION_ENABLED
73
+ function setSafe (value) {
74
+ safeMode = value
75
+ }
76
+
77
+ function wrapMethod (target, name, wrapper, noAssert) {
78
+ if (!noAssert) {
79
+ assertMethod(target, name)
80
+ assertFunction(wrapper)
81
+ }
41
82
 
42
83
  const original = target[name]
43
- const wrapped = wrapper(original)
84
+ let wrapped
85
+
86
+ if (safeMode && original) {
87
+ // In this mode, we make a best-effort attempt to handle errors that are thrown
88
+ // by us, rather than wrapped code. With such errors, we log them, and then attempt
89
+ // to return the result as if no wrapping was done at all.
90
+ //
91
+ // Caveats:
92
+ // * If the original function is called in a later iteration of the event loop,
93
+ // and we throw _then_, then it won't be caught by this. In practice, we always call
94
+ // the original function synchronously, so this is not a problem.
95
+ // * While async errors are dealt with here, errors in callbacks are not. This
96
+ // is because we don't necessarily know _for sure_ that any function arguments
97
+ // are wrapped by us. We could wrap them all anyway and just make that assumption,
98
+ // or just assume that the last argument is always a callback set by us if it's a
99
+ // function, but those don't seem like things we can rely on. We could add a
100
+ // `shimmer.markCallbackAsWrapped()` function that's a no-op outside safe-mode,
101
+ // but that means modifying every instrumentation. Even then, the complexity of
102
+ // this code increases because then we'd need to effectively do the reverse of
103
+ // what we're doing for synchronous functions. This is a TODO.
104
+
105
+ // We're going to hold on to current callState in this variable in this scope,
106
+ // which is fine because any time we reference it, we're referencing it synchronously.
107
+ // We'll use it in the our wrapper (which, again, is called syncrhonously), and in the
108
+ // errorHandler, which will already have been bound to this callState.
109
+ let currentCallState
110
+
111
+ // Rather than calling the original function directly from the shim wrapper, we wrap
112
+ // it again so that we can track if it was called and if it returned. This is because
113
+ // we need to know if an error was thrown by the original function, or by us.
114
+ // We could do this inside the `wrapper` function defined below, which would simplify
115
+ // managing the callState, but then we'd be calling `wrapper` on each invocation, so
116
+ // instead we do it here, once.
117
+ const innerWrapped = wrapper(function (...args) {
118
+ // We need to stash the callState here because of recursion.
119
+ const callState = currentCallState
120
+ callState.startCall()
121
+ const retVal = original.apply(this, args)
122
+ if (isPromise(retVal)) {
123
+ retVal.then(callState.endCall.bind(callState))
124
+ } else {
125
+ callState.endCall(retVal)
126
+ }
127
+ return retVal
128
+ })
129
+
130
+ // This is the crux of what we're doing in safe mode. It handles errors
131
+ // that _we_ cause, by logging them, and transparently providing results
132
+ // as if no wrapping was done at all. That means detecting (via callState)
133
+ // whether the function has already run or not, and if it has, returning
134
+ // the result, and otherwise calling the original function unwrapped.
135
+ const handleError = function (args, callState, e) {
136
+ if (callState.completed) {
137
+ // error was thrown after original function returned/resolved, so
138
+ // it was us. log it.
139
+ log.error(e)
140
+ // original ran and returned something. return it.
141
+ return callState.retVal
142
+ }
143
+
144
+ if (!callState.called) {
145
+ // error was thrown before original function was called, so
146
+ // it was us. log it.
147
+ log.error(e)
148
+ // original never ran. call it unwrapped.
149
+ return original.apply(this, args)
150
+ }
151
+
152
+ // error was thrown during original function execution, so
153
+ // it was them. throw.
154
+ throw e
155
+ }
156
+
157
+ // The wrapped function is the one that will be called by the user.
158
+ // It calls our version of the original function, which manages the
159
+ // callState. That way when we use the errorHandler, it can tell where
160
+ // the error originated.
161
+ wrapped = function (...args) {
162
+ currentCallState = new CallState()
163
+ const errorHandler = handleError.bind(this, args, currentCallState)
164
+
165
+ try {
166
+ const retVal = innerWrapped.apply(this, args)
167
+ return isPromise(retVal) ? retVal.catch(errorHandler) : retVal
168
+ } catch (e) {
169
+ return errorHandler(e)
170
+ }
171
+ }
172
+ } else {
173
+ // In non-safe mode, we just wrap the original function directly.
174
+ wrapped = wrapper(original)
175
+ }
44
176
  const descriptor = Object.getOwnPropertyDescriptor(target, name)
45
177
 
46
178
  const attributes = {
@@ -48,7 +180,7 @@ function wrapMethod (target, name, wrapper) {
48
180
  ...descriptor
49
181
  }
50
182
 
51
- copyProperties(original, wrapped)
183
+ if (typeof original === 'function') copyProperties(original, wrapped)
52
184
 
53
185
  if (descriptor) {
54
186
  unwrappers.set(wrapped, () => Object.defineProperty(target, name, descriptor))
@@ -156,7 +288,9 @@ function assertNotClass (target) {
156
288
 
157
289
  module.exports = {
158
290
  wrap,
291
+ wrapFunction,
159
292
  massWrap,
160
293
  unwrap,
161
- massUnwrap
294
+ massUnwrap,
295
+ setSafe
162
296
  }
@@ -22,5 +22,7 @@ module.exports = {
22
22
  USER_ID: 'usr.id',
23
23
  WAF_CONTEXT_PROCESSOR: 'waf.context.processor',
24
24
 
25
- HTTP_OUTGOING_URL: 'server.io.net.url'
25
+ HTTP_OUTGOING_URL: 'server.io.net.url',
26
+ DB_STATEMENT: 'server.db.statement',
27
+ DB_SYSTEM: 'server.db.system'
26
28
  }
@@ -9,6 +9,8 @@ let templateHtml = blockedTemplates.html
9
9
  let templateJson = blockedTemplates.json
10
10
  let templateGraphqlJson = blockedTemplates.graphqlJson
11
11
 
12
+ let defaultBlockingActionParameters
13
+
12
14
  const responseBlockedSet = new WeakSet()
13
15
 
14
16
  const specificBlockingTypes = {
@@ -23,7 +25,7 @@ function addSpecificEndpoint (method, url, type) {
23
25
  detectedSpecificEndpoints[getSpecificKey(method, url)] = type
24
26
  }
25
27
 
26
- function getBlockWithRedirectData (rootSpan, actionParameters) {
28
+ function getBlockWithRedirectData (actionParameters) {
27
29
  let statusCode = actionParameters.status_code
28
30
  if (!statusCode || statusCode < 300 || statusCode >= 400) {
29
31
  statusCode = 303
@@ -32,10 +34,6 @@ function getBlockWithRedirectData (rootSpan, actionParameters) {
32
34
  Location: actionParameters.location
33
35
  }
34
36
 
35
- rootSpan.addTags({
36
- 'appsec.blocked': 'true'
37
- })
38
-
39
37
  return { headers, statusCode }
40
38
  }
41
39
 
@@ -49,7 +47,7 @@ function getSpecificBlockingData (type) {
49
47
  }
50
48
  }
51
49
 
52
- function getBlockWithContentData (req, specificType, rootSpan, actionParameters) {
50
+ function getBlockWithContentData (req, specificType, actionParameters) {
53
51
  let type
54
52
  let body
55
53
 
@@ -90,28 +88,28 @@ function getBlockWithContentData (req, specificType, rootSpan, actionParameters)
90
88
  'Content-Length': Buffer.byteLength(body)
91
89
  }
92
90
 
93
- rootSpan.addTags({
94
- 'appsec.blocked': 'true'
95
- })
96
-
97
91
  return { body, statusCode, headers }
98
92
  }
99
93
 
100
- function getBlockingData (req, specificType, rootSpan, actionParameters) {
94
+ function getBlockingData (req, specificType, actionParameters) {
101
95
  if (actionParameters?.location) {
102
- return getBlockWithRedirectData(rootSpan, actionParameters)
96
+ return getBlockWithRedirectData(actionParameters)
103
97
  } else {
104
- return getBlockWithContentData(req, specificType, rootSpan, actionParameters)
98
+ return getBlockWithContentData(req, specificType, actionParameters)
105
99
  }
106
100
  }
107
101
 
108
- function block (req, res, rootSpan, abortController, actionParameters) {
102
+ function block (req, res, rootSpan, abortController, actionParameters = defaultBlockingActionParameters) {
109
103
  if (res.headersSent) {
110
104
  log.warn('Cannot send blocking response when headers have already been sent')
111
105
  return
112
106
  }
113
107
 
114
- const { body, headers, statusCode } = getBlockingData(req, null, rootSpan, actionParameters)
108
+ const { body, headers, statusCode } = getBlockingData(req, null, actionParameters)
109
+
110
+ rootSpan.addTags({
111
+ 'appsec.blocked': 'true'
112
+ })
115
113
 
116
114
  for (const headerName of res.getHeaderNames()) {
117
115
  res.removeHeader(headerName)
@@ -125,7 +123,8 @@ function block (req, res, rootSpan, abortController, actionParameters) {
125
123
  }
126
124
 
127
125
  function getBlockingAction (actions) {
128
- return actions?.block_request || actions?.redirect_request
126
+ // waf only returns one action, but it prioritizes redirect over block
127
+ return actions?.redirect_request || actions?.block_request
129
128
  }
130
129
 
131
130
  function setTemplates (config) {
@@ -152,6 +151,12 @@ function isBlocked (res) {
152
151
  return responseBlockedSet.has(res)
153
152
  }
154
153
 
154
+ function setDefaultBlockingActionParameters (actions) {
155
+ const blockAction = actions?.find(action => action.id === 'block')
156
+
157
+ defaultBlockingActionParameters = blockAction?.parameters
158
+ }
159
+
155
160
  module.exports = {
156
161
  addSpecificEndpoint,
157
162
  block,
@@ -159,5 +164,6 @@ module.exports = {
159
164
  getBlockingData,
160
165
  getBlockingAction,
161
166
  setTemplates,
162
- isBlocked
167
+ isBlocked,
168
+ setDefaultBlockingActionParameters
163
169
  }
@@ -21,6 +21,8 @@ module.exports = {
21
21
  responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
22
22
  httpClientRequestStart: dc.channel('apm:http:client:request:start'),
23
23
  responseSetHeader: dc.channel('datadog:http:server:response:set-header:start'),
24
- setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start')
25
-
24
+ setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start'),
25
+ pgQueryStart: dc.channel('apm:pg:query:start'),
26
+ pgPoolQueryStart: dc.channel('datadog:pg:pool:query:start'),
27
+ wafRunFinished: dc.channel('datadog:waf:run:finish')
26
28
  }
@@ -94,11 +94,13 @@ function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) {
94
94
  const rootSpan = web.root(req)
95
95
  if (!rootSpan) return
96
96
 
97
- const blockingData = getBlockingData(req, specificBlockingTypes.GRAPHQL, rootSpan, requestData.wafAction)
97
+ const blockingData = getBlockingData(req, specificBlockingTypes.GRAPHQL, requestData.wafAction)
98
98
  abortData.statusCode = blockingData.statusCode
99
99
  abortData.headers = blockingData.headers
100
100
  abortData.message = blockingData.body
101
101
 
102
+ rootSpan.setTag('appsec.blocked', 'true')
103
+
102
104
  abortController?.abort()
103
105
  }
104
106
 
@@ -78,7 +78,8 @@ const iastLog = {
78
78
 
79
79
  errorAndPublish (data) {
80
80
  this.error(data)
81
- return this.publish(data, 'ERROR')
81
+ // publish is done automatically by log.error()
82
+ return this
82
83
  }
83
84
  }
84
85
 
@@ -0,0 +1,103 @@
1
+ 'use strict'
2
+
3
+ const web = require('../../plugins/util/web')
4
+ const { setUncaughtExceptionCaptureCallbackStart } = require('../channels')
5
+ const { block } = require('../blocking')
6
+ const ssrf = require('./ssrf')
7
+ const sqli = require('./sql_injection')
8
+
9
+ const { DatadogRaspAbortError } = require('./utils')
10
+
11
+ function removeAllListeners (emitter, event) {
12
+ const listeners = emitter.listeners(event)
13
+ emitter.removeAllListeners(event)
14
+
15
+ let cleaned = false
16
+ return function () {
17
+ if (cleaned === true) {
18
+ return
19
+ }
20
+ cleaned = true
21
+
22
+ for (let i = 0; i < listeners.length; ++i) {
23
+ emitter.on(event, listeners[i])
24
+ }
25
+ }
26
+ }
27
+
28
+ function findDatadogRaspAbortError (err, deep = 10) {
29
+ if (err instanceof DatadogRaspAbortError) {
30
+ return err
31
+ }
32
+
33
+ if (err.cause && deep > 0) {
34
+ return findDatadogRaspAbortError(err.cause, deep - 1)
35
+ }
36
+ }
37
+
38
+ function handleUncaughtExceptionMonitor (err) {
39
+ const abortError = findDatadogRaspAbortError(err)
40
+ if (!abortError) return
41
+
42
+ const { req, res, blockingAction } = abortError
43
+ block(req, res, web.root(req), null, blockingAction)
44
+
45
+ if (!process.hasUncaughtExceptionCaptureCallback()) {
46
+ const cleanUp = removeAllListeners(process, 'uncaughtException')
47
+ const handler = () => {
48
+ process.removeListener('uncaughtException', handler)
49
+ }
50
+
51
+ setTimeout(() => {
52
+ process.removeListener('uncaughtException', handler)
53
+ cleanUp()
54
+ })
55
+
56
+ process.on('uncaughtException', handler)
57
+ } else {
58
+ // uncaughtException event is not executed when hasUncaughtExceptionCaptureCallback is true
59
+ let previousCb
60
+ const cb = ({ currentCallback, abortController }) => {
61
+ setUncaughtExceptionCaptureCallbackStart.unsubscribe(cb)
62
+ if (!currentCallback) {
63
+ abortController.abort()
64
+ return
65
+ }
66
+
67
+ previousCb = currentCallback
68
+ }
69
+
70
+ setUncaughtExceptionCaptureCallbackStart.subscribe(cb)
71
+
72
+ process.setUncaughtExceptionCaptureCallback(null)
73
+
74
+ // For some reason, previous callback was defined before the instrumentation
75
+ // We can not restore it, so we let the app decide
76
+ if (previousCb) {
77
+ process.setUncaughtExceptionCaptureCallback(() => {
78
+ process.setUncaughtExceptionCaptureCallback(null)
79
+ process.setUncaughtExceptionCaptureCallback(previousCb)
80
+ })
81
+ }
82
+ }
83
+ }
84
+
85
+ function enable (config) {
86
+ ssrf.enable(config)
87
+ sqli.enable(config)
88
+
89
+ process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
90
+ }
91
+
92
+ function disable () {
93
+ ssrf.disable()
94
+ sqli.disable()
95
+
96
+ process.off('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
97
+ }
98
+
99
+ module.exports = {
100
+ enable,
101
+ disable,
102
+ handleUncaughtExceptionMonitor // exported only for testing purpose
103
+ }
@@ -0,0 +1,86 @@
1
+ 'use strict'
2
+
3
+ const { pgQueryStart, pgPoolQueryStart, wafRunFinished } = require('../channels')
4
+ const { storage } = require('../../../../datadog-core')
5
+ const addresses = require('../addresses')
6
+ const waf = require('../waf')
7
+ const { RULE_TYPES, handleResult } = require('./utils')
8
+
9
+ const DB_SYSTEM_POSTGRES = 'postgresql'
10
+ const reqQueryMap = new WeakMap() // WeakMap<Request, Set<querytext>>
11
+
12
+ let config
13
+
14
+ function enable (_config) {
15
+ config = _config
16
+
17
+ pgQueryStart.subscribe(analyzePgSqlInjection)
18
+ pgPoolQueryStart.subscribe(analyzePgSqlInjection)
19
+ wafRunFinished.subscribe(clearQuerySet)
20
+ }
21
+
22
+ function disable () {
23
+ if (pgQueryStart.hasSubscribers) pgQueryStart.unsubscribe(analyzePgSqlInjection)
24
+ if (pgPoolQueryStart.hasSubscribers) pgPoolQueryStart.unsubscribe(analyzePgSqlInjection)
25
+ if (wafRunFinished.hasSubscribers) wafRunFinished.unsubscribe(clearQuerySet)
26
+ }
27
+
28
+ function analyzePgSqlInjection (ctx) {
29
+ const query = ctx.query?.text
30
+ if (!query) return
31
+
32
+ const store = storage.getStore()
33
+ if (!store) return
34
+
35
+ const { req, res } = store
36
+
37
+ if (!req) return
38
+
39
+ let executedQueries = reqQueryMap.get(req)
40
+ if (executedQueries?.has(query)) return
41
+
42
+ // Do not waste time executing same query twice
43
+ // This also will prevent double calls in pg.Pool internal queries
44
+ if (!executedQueries) {
45
+ executedQueries = new Set()
46
+ reqQueryMap.set(req, executedQueries)
47
+ }
48
+ executedQueries.add(query)
49
+
50
+ const persistent = {
51
+ [addresses.DB_STATEMENT]: query,
52
+ [addresses.DB_SYSTEM]: DB_SYSTEM_POSTGRES
53
+ }
54
+
55
+ const result = waf.run({ persistent }, req, RULE_TYPES.SQL_INJECTION)
56
+
57
+ handleResult(result, req, res, ctx.abortController, config)
58
+ }
59
+
60
+ function hasInputAddress (payload) {
61
+ return hasAddressesObjectInputAddress(payload.ephemeral) || hasAddressesObjectInputAddress(payload.persistent)
62
+ }
63
+
64
+ function hasAddressesObjectInputAddress (addressesObject) {
65
+ return addressesObject && Object.keys(addressesObject)
66
+ .some(address => address.startsWith('server.request') || address.startsWith('graphql.server'))
67
+ }
68
+
69
+ function clearQuerySet ({ payload }) {
70
+ if (!payload) return
71
+
72
+ const store = storage.getStore()
73
+ if (!store) return
74
+
75
+ const { req } = store
76
+ if (!req) return
77
+
78
+ const executedQueries = reqQueryMap.get(req)
79
+ if (!executedQueries) return
80
+
81
+ if (hasInputAddress(payload)) {
82
+ executedQueries.clear()
83
+ }
84
+ }
85
+
86
+ module.exports = { enable, disable }