dd-trace 5.59.0 → 5.61.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 (68) hide show
  1. package/index.d.ts +6 -0
  2. package/package.json +7 -7
  3. package/packages/datadog-code-origin/index.js +3 -0
  4. package/packages/datadog-instrumentations/src/apollo-server.js +14 -3
  5. package/packages/datadog-instrumentations/src/azure-functions.js +5 -0
  6. package/packages/datadog-instrumentations/src/azure-service-bus.js +38 -0
  7. package/packages/datadog-instrumentations/src/fastify.js +17 -0
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  9. package/packages/datadog-instrumentations/src/next.js +17 -18
  10. package/packages/datadog-instrumentations/src/openai.js +13 -114
  11. package/packages/datadog-instrumentations/src/sequelize.js +4 -14
  12. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +6 -38
  13. package/packages/datadog-plugin-azure-functions/src/index.js +57 -28
  14. package/packages/datadog-plugin-azure-service-bus/src/index.js +15 -0
  15. package/packages/datadog-plugin-azure-service-bus/src/producer.js +36 -0
  16. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -23
  17. package/packages/datadog-plugin-google-cloud-vertexai/src/tracing.js +3 -155
  18. package/packages/datadog-plugin-langchain/src/handlers/default.js +0 -18
  19. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +0 -48
  20. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +18 -0
  21. package/packages/datadog-plugin-langchain/src/tracing.js +5 -17
  22. package/packages/datadog-plugin-openai/src/stream-helpers.js +114 -0
  23. package/packages/datadog-plugin-openai/src/tracing.js +38 -0
  24. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +8 -1
  25. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +2 -2
  26. package/packages/dd-trace/src/appsec/iast/analyzers/missing-header-analyzer.js +11 -10
  27. package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +25 -18
  28. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +13 -5
  29. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +5 -1
  30. package/packages/dd-trace/src/appsec/iast/analyzers/xcontenttype-header-missing-analyzer.js +2 -2
  31. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +4 -0
  32. package/packages/dd-trace/src/appsec/iast/index.js +25 -7
  33. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +79 -21
  34. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -3
  35. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +0 -4
  36. package/packages/dd-trace/src/appsec/reporter.js +3 -15
  37. package/packages/dd-trace/src/appsec/waf/index.js +20 -1
  38. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +2 -1
  39. package/packages/dd-trace/src/config.js +0 -16
  40. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +4 -8
  41. package/packages/dd-trace/src/datastreams/schemas/schema_sampler.js +2 -4
  42. package/packages/dd-trace/src/debugger/config.js +16 -0
  43. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +1 -1
  44. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -6
  45. package/packages/dd-trace/src/debugger/devtools_client/index.js +1 -1
  46. package/packages/dd-trace/src/debugger/devtools_client/log.js +19 -0
  47. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +1 -1
  48. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
  49. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +1 -1
  50. package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -1
  51. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  52. package/packages/dd-trace/src/debugger/index.js +13 -3
  53. package/packages/dd-trace/src/plugins/index.js +1 -0
  54. package/packages/dd-trace/src/plugins/util/ci.js +23 -7
  55. package/packages/dd-trace/src/plugins/util/git.js +53 -18
  56. package/packages/dd-trace/src/plugins/util/tags.js +8 -6
  57. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  58. package/packages/dd-trace/src/profiling/profilers/space.js +4 -3
  59. package/packages/dd-trace/src/profiling/profilers/wall.js +5 -4
  60. package/packages/dd-trace/src/remote_config/capabilities.js +2 -1
  61. package/packages/dd-trace/src/remote_config/index.js +2 -0
  62. package/packages/dd-trace/src/remote_config/scheduler.js +2 -1
  63. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +4 -0
  64. package/packages/dd-trace/src/supported-configurations.json +1 -0
  65. package/packages/datadog-plugin-langchain/src/handlers/chain.js +0 -50
  66. package/packages/datadog-plugin-langchain/src/handlers/language_models/chat_model.js +0 -101
  67. package/packages/datadog-plugin-langchain/src/handlers/language_models/index.js +0 -48
  68. package/packages/datadog-plugin-langchain/src/handlers/language_models/llm.js +0 -58
@@ -0,0 +1,114 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Combines legacy OpenAI streamed chunks into a single object.
5
+ * These legacy chunks are returned as buffers instead of individual objects.
6
+ * @param {readonly Uint8Array[]} chunks
7
+ * @returns {Array<Record<string, any>>}
8
+ */
9
+ function convertBuffersToObjects (chunks) {
10
+ return Buffer
11
+ .concat(chunks) // combine the buffers
12
+ .toString() // stringify
13
+ .split(/(?=data:)/) // split on "data:"
14
+ .map(chunk => chunk.replaceAll('\n', '').slice(6)) // remove newlines and 'data: ' from the front
15
+ .slice(0, -1) // remove the last [DONE] message
16
+ .map(JSON.parse) // parse all of the returned objects
17
+ }
18
+
19
+ /**
20
+ * Common function for combining chunks with n choices into a single response body.
21
+ * The shared logic will add a new choice index entry if it doesn't exist, and otherwise
22
+ * hand off to a onChoice handler to add that choice to the previously stored choice.
23
+ *
24
+ * @param {Array<Record<string, any>>} chunks
25
+ * @param {number} n
26
+ * @param {function(Record<string, any>, Record<string, any>): void} onChoice
27
+ * @returns {Record<string, any>}
28
+ */
29
+ function constructResponseFromStreamedChunks (chunks, n, onChoice) {
30
+ const body = { ...chunks[0], choices: Array.from({ length: n }) }
31
+
32
+ for (const chunk of chunks) {
33
+ body.usage = chunk.usage
34
+ for (const choice of chunk.choices) {
35
+ const choiceIdx = choice.index
36
+ const oldChoice = body.choices.find(choice => choice?.index === choiceIdx)
37
+
38
+ if (!oldChoice) {
39
+ body.choices[choiceIdx] = choice
40
+ continue
41
+ }
42
+
43
+ if (!oldChoice.finish_reason) {
44
+ oldChoice.finish_reason = choice.finish_reason
45
+ }
46
+
47
+ onChoice(choice, oldChoice)
48
+ }
49
+ }
50
+
51
+ return body
52
+ }
53
+
54
+ /**
55
+ * Constructs the entire response from a stream of OpenAI completion chunks,
56
+ * mainly combining the text choices of each chunk into a single string per choice.
57
+ * @param {Array<Record<string, any>>} chunks
58
+ * @param {number} n the number of choices to expect in the response
59
+ * @returns {Record<string, any>}
60
+ */
61
+ function constructCompletionResponseFromStreamedChunks (chunks, n) {
62
+ return constructResponseFromStreamedChunks(chunks, n, (choice, oldChoice) => {
63
+ const text = choice.text
64
+ if (text) {
65
+ if (oldChoice.text) {
66
+ oldChoice.text += text
67
+ } else {
68
+ oldChoice.text = text
69
+ }
70
+ }
71
+ })
72
+ }
73
+
74
+ /**
75
+ * Constructs the entire response from a stream of OpenAI chat completion chunks,
76
+ * mainly combining the text choices of each chunk into a single string per choice.
77
+ * @param {Array<Record<string, any>>} chunks
78
+ * @param {number} n the number of choices to expect in the response
79
+ * @returns {Record<string, any>}
80
+ */
81
+ function constructChatCompletionResponseFromStreamedChunks (chunks, n) {
82
+ return constructResponseFromStreamedChunks(chunks, n, (choice, oldChoice) => {
83
+ const delta = choice.delta
84
+ if (!delta) return
85
+
86
+ const content = delta.content
87
+ if (content) {
88
+ if (oldChoice.delta.content) {
89
+ oldChoice.delta.content += content
90
+ } else {
91
+ oldChoice.delta.content = content
92
+ }
93
+ }
94
+
95
+ const tools = delta.tool_calls
96
+ if (!tools) return
97
+
98
+ oldChoice.delta.tool_calls = tools.map((newTool, toolIdx) => {
99
+ const oldTool = oldChoice.delta.tool_calls?.[toolIdx]
100
+ if (oldTool) {
101
+ oldTool.function.arguments += newTool.function.arguments
102
+ return oldTool
103
+ }
104
+
105
+ return newTool
106
+ })
107
+ })
108
+ }
109
+
110
+ module.exports = {
111
+ convertBuffersToObjects,
112
+ constructCompletionResponseFromStreamedChunks,
113
+ constructChatCompletionResponseFromStreamedChunks
114
+ }
@@ -10,6 +10,11 @@ const { MEASURED } = require('../../../ext/tags')
10
10
  const { estimateTokens } = require('./token-estimator')
11
11
 
12
12
  const makeUtilities = require('../../dd-trace/src/plugins/util/llm')
13
+ const {
14
+ convertBuffersToObjects,
15
+ constructCompletionResponseFromStreamedChunks,
16
+ constructChatCompletionResponseFromStreamedChunks
17
+ } = require('./stream-helpers')
13
18
 
14
19
  let normalize
15
20
 
@@ -48,6 +53,39 @@ class OpenAiTracingPlugin extends TracingPlugin {
48
53
 
49
54
  normalize = utilities.normalize
50
55
  }
56
+
57
+ this.addSub('apm:openai:request:chunk', ({ ctx, chunk, done }) => {
58
+ if (!ctx.chunks) ctx.chunks = []
59
+
60
+ if (chunk) ctx.chunks.push(chunk)
61
+ if (!done) return
62
+
63
+ let chunks = ctx.chunks
64
+ if (chunks.length === 0) return
65
+
66
+ const firstChunk = chunks[0]
67
+ // OpenAI in legacy versions returns chunked buffers instead of objects.
68
+ // These buffers will need to be combined and coalesced into a list of object chunks.
69
+ if (firstChunk instanceof Buffer) {
70
+ chunks = convertBuffersToObjects(chunks)
71
+ }
72
+
73
+ const methodName = ctx.currentStore.normalizedMethodName
74
+ let n = 1
75
+ const prompt = ctx.args[0].prompt
76
+ if (Array.isArray(prompt) && typeof prompt[0] !== 'number') {
77
+ n *= prompt.length
78
+ }
79
+
80
+ let response = {}
81
+ if (methodName === 'createCompletion') {
82
+ response = constructCompletionResponseFromStreamedChunks(chunks, n)
83
+ } else if (methodName === 'createChatCompletion') {
84
+ response = constructChatCompletionResponseFromStreamedChunks(chunks, n)
85
+ }
86
+
87
+ ctx.result = { data: response }
88
+ })
51
89
  }
52
90
 
53
91
  configure (config) {
@@ -3,7 +3,14 @@
3
3
  const Analyzer = require('./vulnerability-analyzer')
4
4
  const { getNodeModulesPaths } = require('../path-line')
5
5
 
6
- const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
6
+ const EXCLUDED_PATHS = [
7
+ // Express
8
+ getNodeModulesPaths('express/lib/response.js'),
9
+ // Fastify
10
+ getNodeModulesPaths('fastify/lib/reply.js'),
11
+ getNodeModulesPaths('fastify/lib/hooks.js'),
12
+ getNodeModulesPaths('@fastify/cookie/plugin.js')
13
+ ]
7
14
 
8
15
  class CookieAnalyzer extends Analyzer {
9
16
  constructor (type, propertyToBeSafe) {
@@ -10,8 +10,8 @@ class HstsHeaderMissingAnalyzer extends MissingHeaderAnalyzer {
10
10
  super(HSTS_HEADER_MISSING, HSTS_HEADER_NAME)
11
11
  }
12
12
 
13
- _isVulnerableFromRequestAndResponse (req, res) {
14
- const headerValues = this._getHeaderValues(res, HSTS_HEADER_NAME)
13
+ _isVulnerableFromRequestAndResponse (req, res, storedHeaders) {
14
+ const headerValues = this._getHeaderValues(res, storedHeaders, HSTS_HEADER_NAME)
15
15
  return this._isHttpsProtocol(req) && (
16
16
  headerValues.length === 0 ||
17
17
  headerValues.some(headerValue => !this._isHeaderValid(headerValue))
@@ -28,8 +28,9 @@ class MissingHeaderAnalyzer extends Analyzer {
28
28
  }, (data) => this.analyze(data))
29
29
  }
30
30
 
31
- _getHeaderValues (res, headerName) {
32
- const headerValue = res.getHeader(headerName)
31
+ _getHeaderValues (res, storedHeaders, headerName) {
32
+ headerName = headerName.toLowerCase()
33
+ const headerValue = res.getHeader(headerName) || storedHeaders[headerName]
33
34
  if (Array.isArray(headerValue)) {
34
35
  return headerValue
35
36
  }
@@ -46,8 +47,8 @@ class MissingHeaderAnalyzer extends Analyzer {
46
47
  return `${type}:${this.config.tracerConfig.service}`
47
48
  }
48
49
 
49
- _getEvidence ({ res }) {
50
- const headerValues = this._getHeaderValues(res, this.headerName)
50
+ _getEvidence ({ res, storedHeaders }) {
51
+ const headerValues = this._getHeaderValues(res, storedHeaders, this.headerName)
51
52
  let value
52
53
  if (headerValues.length === 1) {
53
54
  value = headerValues[0]
@@ -57,19 +58,19 @@ class MissingHeaderAnalyzer extends Analyzer {
57
58
  return { value }
58
59
  }
59
60
 
60
- _isVulnerable ({ req, res }, context) {
61
- if (!IGNORED_RESPONSE_STATUS_LIST.has(res.statusCode) && this._isResponseHtml(res)) {
62
- return this._isVulnerableFromRequestAndResponse(req, res)
61
+ _isVulnerable ({ req, res, storedHeaders }, context) {
62
+ if (!IGNORED_RESPONSE_STATUS_LIST.has(res.statusCode) && this._isResponseHtml(res, storedHeaders)) {
63
+ return this._isVulnerableFromRequestAndResponse(req, res, storedHeaders)
63
64
  }
64
65
  return false
65
66
  }
66
67
 
67
- _isVulnerableFromRequestAndResponse (req, res) {
68
+ _isVulnerableFromRequestAndResponse (req, res, storedHeaders) {
68
69
  return false
69
70
  }
70
71
 
71
- _isResponseHtml (res) {
72
- const contentTypes = this._getHeaderValues(res, 'content-type')
72
+ _isResponseHtml (res, storedHeaders) {
73
+ const contentTypes = this._getHeaderValues(res, storedHeaders, 'content-type')
73
74
  return contentTypes.some(contentType => {
74
75
  return contentType && HTML_CONTENT_TYPES.some(htmlContentType => {
75
76
  return htmlContentType === contentType || contentType.startsWith(htmlContentType + ';')
@@ -7,25 +7,32 @@ class SetCookiesHeaderInterceptor extends Plugin {
7
7
  constructor () {
8
8
  super()
9
9
  this.cookiesInRequest = new WeakMap()
10
- this.addSub('datadog:http:server:response:set-header:finish', ({ name, value, res }) => {
11
- if (name.toLowerCase() === 'set-cookie') {
12
- let allCookies = value
13
- if (typeof value === 'string') {
14
- allCookies = [value]
15
- }
16
- const alreadyCheckedCookies = this._getAlreadyCheckedCookiesInResponse(res)
17
-
18
- let location
19
- allCookies.forEach(cookieString => {
20
- if (!alreadyCheckedCookies.includes(cookieString)) {
21
- alreadyCheckedCookies.push(cookieString)
22
- const parsedCookie = this._parseCookie(cookieString, location)
23
- setCookieChannel.publish(parsedCookie)
24
- location = parsedCookie.location
25
- }
26
- })
10
+
11
+ this.addSub('datadog:http:server:response:set-header:finish',
12
+ ({ name, value, res }) => this._handleCookies(name, value, res))
13
+
14
+ this.addSub('datadog:fastify:set-header:finish',
15
+ ({ name, value, res }) => this._handleCookies(name, value, res))
16
+ }
17
+
18
+ _handleCookies (name, value, res) {
19
+ if (name.toLowerCase() === 'set-cookie') {
20
+ let allCookies = value
21
+ if (typeof value === 'string') {
22
+ allCookies = [value]
27
23
  }
28
- })
24
+ const alreadyCheckedCookies = this._getAlreadyCheckedCookiesInResponse(res)
25
+
26
+ let location
27
+ allCookies.forEach(cookieString => {
28
+ if (!alreadyCheckedCookies.includes(cookieString)) {
29
+ alreadyCheckedCookies.push(cookieString)
30
+ const parsedCookie = this._parseCookie(cookieString, location)
31
+ setCookieChannel.publish(parsedCookie)
32
+ location = parsedCookie.location
33
+ }
34
+ })
35
+ }
29
36
  }
30
37
 
31
38
  _parseCookie (cookieString, location) {
@@ -18,31 +18,39 @@ class SqlInjectionAnalyzer extends StoredInjectionAnalyzer {
18
18
  this.addSub('datadog:mysql2:outerquery:start', ({ sql }) => this.analyze(sql, undefined, 'MYSQL'))
19
19
  this.addSub('apm:pg:query:start', ({ query }) => this.analyze(query.text, undefined, 'POSTGRES'))
20
20
 
21
- this.addSub(
21
+ this.addBind(
22
22
  'datadog:sequelize:query:start',
23
23
  ({ sql, dialect }) => this.getStoreAndAnalyze(sql, dialect.toUpperCase())
24
24
  )
25
25
  this.addSub('datadog:sequelize:query:finish', () => this.returnToParentStore())
26
26
 
27
- this.addSub('datadog:pg:pool:query:start', ({ query }) => this.getStoreAndAnalyze(query.text, 'POSTGRES'))
27
+ this.addSub('datadog:pg:pool:query:start', ({ query }) => this.setStoreAndAnalyze(query.text, 'POSTGRES'))
28
28
  this.addSub('datadog:pg:pool:query:finish', () => this.returnToParentStore())
29
29
 
30
- this.addSub('datadog:mysql:pool:query:start', ({ sql }) => this.getStoreAndAnalyze(sql, 'MYSQL'))
30
+ this.addSub('datadog:mysql:pool:query:start', ({ sql }) => this.setStoreAndAnalyze(sql, 'MYSQL'))
31
31
  this.addSub('datadog:mysql:pool:query:finish', () => this.returnToParentStore())
32
32
 
33
33
  this.addSub('datadog:knex:raw:start', ({ sql, dialect: knexDialect }) => {
34
34
  const dialect = this.normalizeKnexDialect(knexDialect)
35
- this.getStoreAndAnalyze(sql, dialect)
35
+ this.setStoreAndAnalyze(sql, dialect)
36
36
  })
37
37
  this.addSub('datadog:knex:raw:finish', () => this.returnToParentStore())
38
38
  }
39
39
 
40
+ setStoreAndAnalyze (query, dialect) {
41
+ const store = this.getStoreAndAnalyze(query, dialect)
42
+
43
+ if (store) {
44
+ storage('legacy').enterWith(store)
45
+ }
46
+ }
47
+
40
48
  getStoreAndAnalyze (query, dialect) {
41
49
  const parentStore = storage('legacy').getStore()
42
50
  if (parentStore) {
43
51
  this.analyze(query, parentStore, dialect)
44
52
 
45
- storage('legacy').enterWith({ ...parentStore, sqlAnalyzed: true, sqlParentStore: parentStore })
53
+ return { ...parentStore, sqlAnalyzed: true, sqlParentStore: parentStore }
46
54
  }
47
55
  }
48
56
 
@@ -9,7 +9,10 @@ const {
9
9
  HTTP_REQUEST_PARAMETER
10
10
  } = require('../taint-tracking/source-types')
11
11
 
12
- const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
12
+ const EXCLUDED_PATHS = [
13
+ getNodeModulesPaths('express/lib/response.js'),
14
+ getNodeModulesPaths('fastify/lib/reply.js'),
15
+ ]
13
16
 
14
17
  const VULNERABLE_SOURCE_TYPES = new Set([
15
18
  HTTP_REQUEST_BODY,
@@ -23,6 +26,7 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
23
26
 
24
27
  onConfigure () {
25
28
  this.addSub('datadog:http:server:response:set-header:finish', ({ name, value }) => this.analyze(name, value))
29
+ this.addSub('datadog:fastify:set-header:finish', ({ name, value }) => this.analyze(name, value))
26
30
  }
27
31
 
28
32
  analyze (name, value) {
@@ -10,8 +10,8 @@ class XcontenttypeHeaderMissingAnalyzer extends MissingHeaderAnalyzer {
10
10
  super(XCONTENTTYPE_HEADER_MISSING, XCONTENTTYPEOPTIONS_HEADER_NAME)
11
11
  }
12
12
 
13
- _isVulnerableFromRequestAndResponse (req, res) {
14
- const headerValues = this._getHeaderValues(res, XCONTENTTYPEOPTIONS_HEADER_NAME)
13
+ _isVulnerableFromRequestAndResponse (req, res, storedHeaders) {
14
+ const headerValues = this._getHeaderValues(res, storedHeaders, XCONTENTTYPEOPTIONS_HEADER_NAME)
15
15
  return headerValues.length === 0 || headerValues.some(headerValue => headerValue.trim().toLowerCase() !== 'nosniff')
16
16
  }
17
17
  }
@@ -47,6 +47,10 @@ class IastPluginSubscription {
47
47
  }
48
48
 
49
49
  matchesModuleInstrumented (name) {
50
+ // Remove node: prefix if present
51
+ if (name.startsWith('node:')) {
52
+ name = name.slice(5)
53
+ }
50
54
  // https module is a special case because it's events are published as http
51
55
  name = name === 'https' ? 'http' : name
52
56
  return this.moduleName === name
@@ -18,11 +18,12 @@ const { IAST_ENABLED_TAG_KEY } = require('./tags')
18
18
  const iastTelemetry = require('./telemetry')
19
19
  const { enable: enableFsPlugin, disable: disableFsPlugin, IAST_MODULE } = require('../rasp/fs-plugin')
20
20
  const securityControls = require('./security-controls')
21
+ const { incomingHttpRequestStart, incomingHttpRequestEnd, responseWriteHead } = require('../channels')
22
+
23
+ const collectedResponseHeaders = new WeakMap()
21
24
 
22
25
  // TODO Change to `apm:http:server:request:[start|close]` when the subscription
23
26
  // order of the callbacks can be enforce
24
- const requestStart = dc.channel('dd-trace:incomingHttpRequestStart')
25
- const requestClose = dc.channel('dd-trace:incomingHttpRequestEnd')
26
27
  const iastResponseEnd = dc.channel('datadog:iast:response-end')
27
28
  let isEnabled = false
28
29
 
@@ -33,8 +34,9 @@ function enable (config, _tracer) {
33
34
  enableFsPlugin(IAST_MODULE)
34
35
  enableAllAnalyzers(config)
35
36
  enableTaintTracking(config.iast, iastTelemetry.verbosity)
36
- requestStart.subscribe(onIncomingHttpRequestStart)
37
- requestClose.subscribe(onIncomingHttpRequestEnd)
37
+ incomingHttpRequestStart.subscribe(onIncomingHttpRequestStart)
38
+ incomingHttpRequestEnd.subscribe(onIncomingHttpRequestEnd)
39
+ responseWriteHead.subscribe(onResponseWriteHeadCollect)
38
40
  overheadController.configure(config.iast)
39
41
  overheadController.startGlobalContext()
40
42
  securityControls.configure(config.iast)
@@ -53,8 +55,9 @@ function disable () {
53
55
  disableAllAnalyzers()
54
56
  disableTaintTracking()
55
57
  overheadController.finishGlobalContext()
56
- if (requestStart.hasSubscribers) requestStart.unsubscribe(onIncomingHttpRequestStart)
57
- if (requestClose.hasSubscribers) requestClose.unsubscribe(onIncomingHttpRequestEnd)
58
+ if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(onIncomingHttpRequestStart)
59
+ if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(onIncomingHttpRequestEnd)
60
+ if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHeadCollect)
58
61
  vulnerabilityReporter.stop()
59
62
  }
60
63
 
@@ -89,7 +92,13 @@ function onIncomingHttpRequestEnd (data) {
89
92
  const topContext = web.getContext(data.req)
90
93
  const iastContext = iastContextFunctions.getIastContext(store, topContext)
91
94
  if (iastContext?.rootSpan) {
92
- iastResponseEnd.publish(data)
95
+ const storedHeaders = collectedResponseHeaders.get(data.res) || {}
96
+
97
+ iastResponseEnd.publish({ ...data, storedHeaders })
98
+
99
+ if (Object.keys(storedHeaders).length) {
100
+ collectedResponseHeaders.delete(data.res)
101
+ }
93
102
 
94
103
  const vulnerabilities = iastContext.vulnerabilities
95
104
  const rootSpan = iastContext.rootSpan
@@ -105,4 +114,13 @@ function onIncomingHttpRequestEnd (data) {
105
114
  }
106
115
  }
107
116
 
117
+ // Response headers are collected here because they are not available in the onIncomingHttpRequestEnd when using Fastify
118
+ function onResponseWriteHeadCollect ({ res, responseHeaders = {} }) {
119
+ if (!res) return
120
+
121
+ if (Object.keys(responseHeaders).length) {
122
+ collectedResponseHeaders.set(res, responseHeaders)
123
+ }
124
+ }
125
+
108
126
  module.exports = { enable, disable, onIncomingHttpRequestEnd, onIncomingHttpRequestStart }
@@ -38,6 +38,25 @@ class TaintTrackingPlugin extends SourceIastPlugin {
38
38
  }
39
39
 
40
40
  onConfigure () {
41
+ this.addBodyParsingSubscriptions()
42
+
43
+ this.addQueryParameterSubscriptions()
44
+
45
+ this.addCookieSubscriptions()
46
+
47
+ this.addDatabaseSubscriptions()
48
+
49
+ this.addPathParameterSubscriptions()
50
+
51
+ this.addGraphQLSubscriptions()
52
+
53
+ this.addURLParsingSubscriptions()
54
+
55
+ // this is a special case to increment INSTRUMENTED_SOURCE metric for header
56
+ this.addInstrumentedSource('http', [HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_HEADER_NAME])
57
+ }
58
+
59
+ addBodyParsingSubscriptions () {
41
60
  const onRequestBody = ({ req }) => {
42
61
  const iastContext = getIastContext(storage('legacy').getStore())
43
62
  if (iastContext && iastContext.body !== req.body) {
@@ -57,17 +76,13 @@ class TaintTrackingPlugin extends SourceIastPlugin {
57
76
  )
58
77
 
59
78
  this.addSub(
60
- { channelName: 'datadog:query:read:finish', tag: HTTP_REQUEST_PARAMETER },
61
- ({ query }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query)
62
- )
63
-
64
- this.addSub(
65
- { channelName: 'datadog:express:query:finish', tag: HTTP_REQUEST_PARAMETER },
66
- ({ query }) => {
79
+ { channelName: 'datadog:fastify:body-parser:finish', tag: HTTP_REQUEST_BODY },
80
+ ({ body }) => {
67
81
  const iastContext = getIastContext(storage('legacy').getStore())
68
- if (!iastContext || !query) return
69
-
70
- taintQueryWithCache(iastContext, query)
82
+ if (iastContext && iastContext.body !== body) {
83
+ this._taintTrackingHandler(HTTP_REQUEST_BODY, body)
84
+ iastContext.body = body
85
+ }
71
86
  }
72
87
  )
73
88
 
@@ -83,12 +98,45 @@ class TaintTrackingPlugin extends SourceIastPlugin {
83
98
  }
84
99
  }
85
100
  )
101
+ }
102
+
103
+ addQueryParameterSubscriptions () {
104
+ this.addSub(
105
+ { channelName: 'datadog:query:read:finish', tag: HTTP_REQUEST_PARAMETER },
106
+ ({ query }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query)
107
+ )
108
+
109
+ this.addSub(
110
+ { channelName: 'datadog:fastify:query-params:finish', tag: HTTP_REQUEST_PARAMETER },
111
+ ({ query }) => {
112
+ this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query)
113
+ }
114
+ )
86
115
 
116
+ this.addSub(
117
+ { channelName: 'datadog:express:query:finish', tag: HTTP_REQUEST_PARAMETER },
118
+ ({ query }) => {
119
+ const iastContext = getIastContext(storage('legacy').getStore())
120
+ if (!iastContext || !query) return
121
+
122
+ taintQueryWithCache(iastContext, query)
123
+ }
124
+ )
125
+ }
126
+
127
+ addCookieSubscriptions () {
87
128
  this.addSub(
88
129
  { channelName: 'datadog:cookie:parse:finish', tag: [HTTP_REQUEST_COOKIE_VALUE, HTTP_REQUEST_COOKIE_NAME] },
89
130
  ({ cookies }) => this._cookiesTaintTrackingHandler(cookies)
90
131
  )
91
132
 
133
+ this.addSub(
134
+ { channelName: 'datadog:fastify-cookie:read:finish', tag: [HTTP_REQUEST_COOKIE_VALUE, HTTP_REQUEST_COOKIE_NAME] },
135
+ ({ cookies }) => this._cookiesTaintTrackingHandler(cookies)
136
+ )
137
+ }
138
+
139
+ addDatabaseSubscriptions () {
92
140
  this.addSub(
93
141
  { channelName: 'datadog:sequelize:query:finish', tag: SQL_ROW_VALUE },
94
142
  ({ result }) => this._taintDatabaseResult(result, 'sequelize')
@@ -98,25 +146,36 @@ class TaintTrackingPlugin extends SourceIastPlugin {
98
146
  { channelName: 'apm:pg:query:finish', tag: SQL_ROW_VALUE },
99
147
  ({ result }) => this._taintDatabaseResult(result, 'pg')
100
148
  )
149
+ }
150
+
151
+ addPathParameterSubscriptions () {
152
+ const pathParamHandler = ({ req }) => {
153
+ if (req && req.params !== null && typeof req.params === 'object') {
154
+ this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, req, 'params')
155
+ }
156
+ }
101
157
 
102
158
  this.addSub(
103
159
  { channelName: 'datadog:express:process_params:start', tag: HTTP_REQUEST_PATH_PARAM },
104
- ({ req }) => {
105
- if (req && req.params !== null && typeof req.params === 'object') {
106
- this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, req, 'params')
107
- }
108
- }
160
+ pathParamHandler
109
161
  )
110
162
 
111
163
  this.addSub(
112
164
  { channelName: 'datadog:router:param:start', tag: HTTP_REQUEST_PATH_PARAM },
113
- ({ req }) => {
114
- if (req && req.params !== null && typeof req.params === 'object') {
115
- this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, req, 'params')
165
+ pathParamHandler
166
+ )
167
+
168
+ this.addSub(
169
+ { channelName: 'datadog:fastify:path-params:finish', tag: HTTP_REQUEST_PATH_PARAM },
170
+ ({ req, params }) => {
171
+ if (req) {
172
+ this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, params)
116
173
  }
117
174
  }
118
175
  )
176
+ }
119
177
 
178
+ addGraphQLSubscriptions () {
120
179
  this.addSub(
121
180
  { channelName: 'apm:graphql:resolve:start', tag: HTTP_REQUEST_BODY },
122
181
  (data) => {
@@ -128,7 +187,9 @@ class TaintTrackingPlugin extends SourceIastPlugin {
128
187
  }
129
188
  }
130
189
  )
190
+ }
131
191
 
192
+ addURLParsingSubscriptions () {
132
193
  const urlResultTaintedProperties = ['host', 'origin', 'hostname']
133
194
  this.addSub(
134
195
  { channelName: 'datadog:url:parse:finish' },
@@ -162,9 +223,6 @@ class TaintTrackingPlugin extends SourceIastPlugin {
162
223
  context.result =
163
224
  newTaintedString(iastContext, context.result, origRange.iinfo.parameterName, origRange.iinfo.type)
164
225
  })
165
-
166
- // this is a special case to increment INSTRUMENTED_SOURCE metric for header
167
- this.addInstrumentedSource('http', [HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_HEADER_NAME])
168
226
  }
169
227
 
170
228
  _taintTrackingHandler (type, target, property, iastContext = getIastContext(storage('legacy').getStore())) {
@@ -4,9 +4,7 @@ const sensitiveHandler = require('./evidence-redaction/sensitive-handler')
4
4
  const { stringifyWithRanges } = require('./utils')
5
5
 
6
6
  class VulnerabilityFormatter {
7
- constructor () {
8
- this._redactVulnearbilities = true
9
- }
7
+ _redactVulnearbilities = true
10
8
 
11
9
  setRedactVulnerabilities (shouldRedactVulnerabilities, redactionNamePattern, redactionValuePattern) {
12
10
  this._redactVulnearbilities = shouldRedactVulnerabilities
@@ -35,10 +35,6 @@ class AppsecFsPlugin extends Plugin {
35
35
  this.addBind('apm:fs:operation:finish', this._onFsOperationFinishOrRenderEnd)
36
36
  this.addBind('tracing:datadog:express:response:render:start', this._onResponseRenderStart)
37
37
  this.addBind('tracing:datadog:express:response:render:end', this._onFsOperationFinishOrRenderEnd)
38
- // TODO Remove this when dc-polyfill is fixed&updated
39
- // hack to node 18 and early 20.x
40
- // with dc-polyfill addBind is not enough to force a channel.hasSubscribers === true
41
- this.addSub('tracing:datadog:express:response:render:start', () => {})
42
38
 
43
39
  super.configure(true)
44
40
  }