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.
- package/index.d.ts +6 -0
- package/package.json +7 -7
- package/packages/datadog-code-origin/index.js +3 -0
- package/packages/datadog-instrumentations/src/apollo-server.js +14 -3
- package/packages/datadog-instrumentations/src/azure-functions.js +5 -0
- package/packages/datadog-instrumentations/src/azure-service-bus.js +38 -0
- package/packages/datadog-instrumentations/src/fastify.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/next.js +17 -18
- package/packages/datadog-instrumentations/src/openai.js +13 -114
- package/packages/datadog-instrumentations/src/sequelize.js +4 -14
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +6 -38
- package/packages/datadog-plugin-azure-functions/src/index.js +57 -28
- package/packages/datadog-plugin-azure-service-bus/src/index.js +15 -0
- package/packages/datadog-plugin-azure-service-bus/src/producer.js +36 -0
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -23
- package/packages/datadog-plugin-google-cloud-vertexai/src/tracing.js +3 -155
- package/packages/datadog-plugin-langchain/src/handlers/default.js +0 -18
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +0 -48
- package/packages/datadog-plugin-langchain/src/handlers/language_models.js +18 -0
- package/packages/datadog-plugin-langchain/src/tracing.js +5 -17
- package/packages/datadog-plugin-openai/src/stream-helpers.js +114 -0
- package/packages/datadog-plugin-openai/src/tracing.js +38 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +8 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +2 -2
- package/packages/dd-trace/src/appsec/iast/analyzers/missing-header-analyzer.js +11 -10
- package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +25 -18
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +13 -5
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +5 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/xcontenttype-header-missing-analyzer.js +2 -2
- package/packages/dd-trace/src/appsec/iast/iast-plugin.js +4 -0
- package/packages/dd-trace/src/appsec/iast/index.js +25 -7
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +79 -21
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -3
- package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +0 -4
- package/packages/dd-trace/src/appsec/reporter.js +3 -15
- package/packages/dd-trace/src/appsec/waf/index.js +20 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +2 -1
- package/packages/dd-trace/src/config.js +0 -16
- package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +4 -8
- package/packages/dd-trace/src/datastreams/schemas/schema_sampler.js +2 -4
- package/packages/dd-trace/src/debugger/config.js +16 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -6
- package/packages/dd-trace/src/debugger/devtools_client/index.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/log.js +19 -0
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
- package/packages/dd-trace/src/debugger/index.js +13 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/util/ci.js +23 -7
- package/packages/dd-trace/src/plugins/util/git.js +53 -18
- package/packages/dd-trace/src/plugins/util/tags.js +8 -6
- package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
- package/packages/dd-trace/src/profiling/profilers/space.js +4 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +5 -4
- package/packages/dd-trace/src/remote_config/capabilities.js +2 -1
- package/packages/dd-trace/src/remote_config/index.js +2 -0
- package/packages/dd-trace/src/remote_config/scheduler.js +2 -1
- package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +4 -0
- package/packages/dd-trace/src/supported-configurations.json +1 -0
- package/packages/datadog-plugin-langchain/src/handlers/chain.js +0 -50
- package/packages/datadog-plugin-langchain/src/handlers/language_models/chat_model.js +0 -101
- package/packages/datadog-plugin-langchain/src/handlers/language_models/index.js +0 -48
- 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 =
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
37
|
-
|
|
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 (
|
|
57
|
-
if (
|
|
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
|
-
|
|
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:
|
|
61
|
-
({
|
|
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 (
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
}
|