dd-trace 5.65.0 → 5.67.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 +38 -0
- package/package.json +8 -8
- package/packages/datadog-instrumentations/src/express.js +3 -7
- package/packages/datadog-instrumentations/src/graphql.js +10 -6
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/register.js +10 -2
- package/packages/datadog-instrumentations/src/playwright.js +25 -9
- package/packages/datadog-instrumentations/src/prisma.js +8 -10
- package/packages/datadog-instrumentations/src/ws.js +136 -0
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +30 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +9 -6
- package/packages/datadog-plugin-aws-sdk/src/util.js +61 -1
- package/packages/datadog-plugin-express/src/code_origin.js +11 -0
- package/packages/datadog-plugin-graphql/src/index.js +3 -0
- package/packages/datadog-plugin-ws/src/close.js +69 -0
- package/packages/datadog-plugin-ws/src/index.js +26 -0
- package/packages/datadog-plugin-ws/src/producer.js +60 -0
- package/packages/datadog-plugin-ws/src/receiver.js +70 -0
- package/packages/datadog-plugin-ws/src/server.js +79 -0
- package/packages/datadog-shimmer/src/shimmer.js +11 -2
- package/packages/dd-trace/src/appsec/blocking.js +29 -0
- package/packages/dd-trace/src/appsec/channels.js +4 -2
- package/packages/dd-trace/src/appsec/index.js +7 -2
- package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +1 -0
- package/packages/dd-trace/src/appsec/rasp/index.js +25 -7
- package/packages/dd-trace/src/appsec/rasp/lfi.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/utils.js +13 -2
- package/packages/dd-trace/src/config.js +12 -0
- package/packages/dd-trace/src/guardrails/index.js +11 -3
- package/packages/dd-trace/src/guardrails/telemetry.js +15 -16
- package/packages/dd-trace/src/llmobs/index.js +7 -0
- package/packages/dd-trace/src/llmobs/sdk.js +28 -0
- package/packages/dd-trace/src/llmobs/span_processor.js +124 -28
- package/packages/dd-trace/src/llmobs/tagger.js +2 -1
- package/packages/dd-trace/src/llmobs/telemetry.js +7 -1
- package/packages/dd-trace/src/log/writer.js +1 -1
- package/packages/dd-trace/src/plugin_manager.js +8 -2
- package/packages/dd-trace/src/plugins/index.js +2 -1
- package/packages/dd-trace/src/plugins/util/ip_extractor.js +48 -45
- package/packages/dd-trace/src/profiling/profilers/wall.js +9 -3
- package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
- package/packages/dd-trace/src/service-naming/schemas/v0/websocket.js +30 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/websocket.js +30 -0
- package/packages/dd-trace/src/supported-configurations.json +3 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js')
|
|
4
|
+
|
|
5
|
+
class WSClosePlugin extends TracingPlugin {
|
|
6
|
+
static get id () { return 'ws' }
|
|
7
|
+
static get prefix () { return 'tracing:ws:close' }
|
|
8
|
+
static get type () { return 'websocket' }
|
|
9
|
+
static get kind () { return 'close' }
|
|
10
|
+
|
|
11
|
+
bindStart (ctx) {
|
|
12
|
+
const {
|
|
13
|
+
traceWebsocketMessagesEnabled,
|
|
14
|
+
traceWebsocketMessagesInheritSampling,
|
|
15
|
+
traceWebsocketMessagesSeparateTraces
|
|
16
|
+
} = this.config
|
|
17
|
+
if (!traceWebsocketMessagesEnabled) return
|
|
18
|
+
|
|
19
|
+
const { code, data, socket, isPeerClose } = ctx
|
|
20
|
+
if (!socket.spanContext) return
|
|
21
|
+
|
|
22
|
+
const spanKind = isPeerClose ? 'consumer' : 'producer'
|
|
23
|
+
const spanTags = socket.spanContext.spanTags
|
|
24
|
+
const path = spanTags['resource.name'].split(' ')[1]
|
|
25
|
+
const service = this.serviceName({ pluginConfig: this.config })
|
|
26
|
+
const span = this.startSpan(this.operationName(), {
|
|
27
|
+
service,
|
|
28
|
+
meta: {
|
|
29
|
+
'resource.name': `websocket ${path}`,
|
|
30
|
+
'span.type': 'websocket',
|
|
31
|
+
'span.kind': spanKind,
|
|
32
|
+
'websocket.close.code': code
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
}, ctx)
|
|
36
|
+
|
|
37
|
+
if (data?.toString().length > 0) {
|
|
38
|
+
span.setTag('websocket.close.reason', data.toString())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isPeerClose && traceWebsocketMessagesInheritSampling && traceWebsocketMessagesSeparateTraces) {
|
|
42
|
+
span.setTag('_dd.dm.service', spanTags['service.name'] || service)
|
|
43
|
+
span.setTag('_dd.dm.resource', spanTags['resource.name'] || `websocket ${path}`)
|
|
44
|
+
span.setTag('_dd.dm.inherited', 1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ctx.span = span
|
|
48
|
+
return ctx.currentStore
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bindAsyncStart (ctx) {
|
|
52
|
+
if (!ctx.isPeerClose) ctx.span.finish()
|
|
53
|
+
return ctx.parentStore
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
asyncStart (ctx) {
|
|
57
|
+
ctx.span.finish()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
end (ctx) {
|
|
61
|
+
if (!Object.hasOwn(ctx, 'result')) return
|
|
62
|
+
|
|
63
|
+
if (ctx.socket.spanContext) ctx.span.addLink(ctx.socket.spanContext)
|
|
64
|
+
|
|
65
|
+
ctx.span.finish()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = WSClosePlugin
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const CompositePlugin = require('../../dd-trace/src/plugins/composite')
|
|
4
|
+
|
|
5
|
+
const WSServerPlugin = require('./server')
|
|
6
|
+
const WSProducerPlugin = require('./producer')
|
|
7
|
+
const WSReceiverPlugin = require('./receiver')
|
|
8
|
+
const WSClosePlugin = require('./close')
|
|
9
|
+
|
|
10
|
+
class WSPlugin extends CompositePlugin {
|
|
11
|
+
static get id () { return 'ws' }
|
|
12
|
+
static get plugins () {
|
|
13
|
+
return {
|
|
14
|
+
server: WSServerPlugin,
|
|
15
|
+
producer: WSProducerPlugin,
|
|
16
|
+
receiver: WSReceiverPlugin,
|
|
17
|
+
close: WSClosePlugin
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
configure (config) {
|
|
22
|
+
return super.configure(config)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = WSPlugin
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js')
|
|
4
|
+
|
|
5
|
+
class WSProducerPlugin extends TracingPlugin {
|
|
6
|
+
static get id () { return 'ws' }
|
|
7
|
+
static get prefix () { return 'tracing:ws:send' }
|
|
8
|
+
static get type () { return 'websocket' }
|
|
9
|
+
static get kind () { return 'producer' }
|
|
10
|
+
|
|
11
|
+
bindStart (ctx) {
|
|
12
|
+
const messagesEnabled = this.config.traceWebsocketMessagesEnabled
|
|
13
|
+
if (!messagesEnabled) return
|
|
14
|
+
|
|
15
|
+
const { byteLength, socket, binary } = ctx
|
|
16
|
+
if (!socket.spanContext) return
|
|
17
|
+
|
|
18
|
+
const spanTags = socket.spanContext.spanTags
|
|
19
|
+
const path = spanTags['resource.name'].split(' ')[1]
|
|
20
|
+
const opCode = binary ? 'binary' : 'text'
|
|
21
|
+
const service = this.serviceName({ pluginConfig: this.config })
|
|
22
|
+
const span = this.startSpan(this.operationName(), {
|
|
23
|
+
service,
|
|
24
|
+
meta: {
|
|
25
|
+
'span.type': 'websocket',
|
|
26
|
+
'span.kind': 'producer',
|
|
27
|
+
'resource.name': `websocket ${path}`,
|
|
28
|
+
'websocket.message.type': opCode,
|
|
29
|
+
|
|
30
|
+
},
|
|
31
|
+
metrics: {
|
|
32
|
+
'websocket.message.length': byteLength
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}, ctx)
|
|
36
|
+
|
|
37
|
+
ctx.span = span
|
|
38
|
+
return ctx.currentStore
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
bindAsyncStart (ctx) {
|
|
42
|
+
ctx.span.finish()
|
|
43
|
+
return ctx.parentStore
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
asyncStart (ctx) {
|
|
47
|
+
ctx.span.finish()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
end (ctx) {
|
|
51
|
+
if (!Object.hasOwn(ctx, 'result')) return
|
|
52
|
+
|
|
53
|
+
if (ctx.socket.spanContext) ctx.span.addLink(ctx.socket.spanContext, { 'dd.kind': 'resuming' })
|
|
54
|
+
|
|
55
|
+
ctx.span.finish()
|
|
56
|
+
return ctx.parentStore
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = WSProducerPlugin
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js')
|
|
4
|
+
|
|
5
|
+
class WSReceiverPlugin extends TracingPlugin {
|
|
6
|
+
static get id () { return 'ws' }
|
|
7
|
+
static get prefix () { return 'tracing:ws:receive' }
|
|
8
|
+
static get type () { return 'websocket' }
|
|
9
|
+
static get kind () { return 'consumer' }
|
|
10
|
+
|
|
11
|
+
bindStart (ctx) {
|
|
12
|
+
const {
|
|
13
|
+
traceWebsocketMessagesEnabled,
|
|
14
|
+
traceWebsocketMessagesInheritSampling,
|
|
15
|
+
traceWebsocketMessagesSeparateTraces
|
|
16
|
+
} = this.config
|
|
17
|
+
if (!traceWebsocketMessagesEnabled) return
|
|
18
|
+
|
|
19
|
+
const { byteLength, socket, binary } = ctx
|
|
20
|
+
if (!socket.spanContext) return
|
|
21
|
+
|
|
22
|
+
const spanTags = socket.spanContext.spanTags
|
|
23
|
+
const path = spanTags['resource.name'].split(' ')[1]
|
|
24
|
+
const opCode = binary ? 'binary' : 'text'
|
|
25
|
+
|
|
26
|
+
const service = this.serviceName({ pluginConfig: this.config })
|
|
27
|
+
const span = this.startSpan(this.operationName(), {
|
|
28
|
+
service,
|
|
29
|
+
meta: {
|
|
30
|
+
'span.type': 'websocket',
|
|
31
|
+
'span.kind': 'consumer',
|
|
32
|
+
'resource.name': `websocket ${path}`,
|
|
33
|
+
'websocket.duration.style': 'handler',
|
|
34
|
+
'websocket.message.type': opCode,
|
|
35
|
+
},
|
|
36
|
+
metrics: {
|
|
37
|
+
'websocket.message.length': byteLength,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
}, ctx)
|
|
41
|
+
|
|
42
|
+
if (traceWebsocketMessagesInheritSampling && traceWebsocketMessagesSeparateTraces) {
|
|
43
|
+
span.setTag('_dd.dm.service', spanTags['service.name'] || service)
|
|
44
|
+
span.setTag('_dd.dm.resource', spanTags['resource.name'] || `websocket ${path}`)
|
|
45
|
+
span.setTag('_dd.dm.inherited', 1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ctx.span = span
|
|
49
|
+
return ctx.currentStore
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
bindAsyncStart (ctx) {
|
|
53
|
+
return ctx.parentStore
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
asyncStart (ctx) {
|
|
57
|
+
ctx.span.finish()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
end (ctx) {
|
|
61
|
+
if (!Object.hasOwn(ctx, 'result')) return
|
|
62
|
+
|
|
63
|
+
if (ctx.socket.spanContext) ctx.span.addLink(ctx.socket.spanContext, { 'dd.kind': 'executed_by' })
|
|
64
|
+
|
|
65
|
+
ctx.span.finish()
|
|
66
|
+
return ctx.parentStore
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = WSReceiverPlugin
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js')
|
|
4
|
+
const tags = require('../../../ext/tags.js')
|
|
5
|
+
|
|
6
|
+
const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE
|
|
7
|
+
|
|
8
|
+
class WSServerPlugin extends TracingPlugin {
|
|
9
|
+
static get id () { return 'ws' }
|
|
10
|
+
static get prefix () { return 'tracing:ws:server:connect' }
|
|
11
|
+
static get type () { return 'websocket' }
|
|
12
|
+
static get kind () { return 'request' }
|
|
13
|
+
|
|
14
|
+
bindStart (ctx) {
|
|
15
|
+
const req = ctx.req
|
|
16
|
+
|
|
17
|
+
const options = {}
|
|
18
|
+
const headers = Object.entries(req.headers)
|
|
19
|
+
options.headers = Object.fromEntries(headers)
|
|
20
|
+
options.method = req.method
|
|
21
|
+
|
|
22
|
+
const protocol = `${getRequestProtocol(req)}:`
|
|
23
|
+
const host = options.headers.host
|
|
24
|
+
const path = req.url
|
|
25
|
+
const uri = `${protocol}//${host}${path}`
|
|
26
|
+
|
|
27
|
+
ctx.args = { options }
|
|
28
|
+
|
|
29
|
+
const service = this.serviceName({ pluginConfig: this.config })
|
|
30
|
+
const span = this.startSpan(this.operationName(), {
|
|
31
|
+
service,
|
|
32
|
+
meta: {
|
|
33
|
+
'span.type': 'websocket',
|
|
34
|
+
'http.upgraded': 'websocket',
|
|
35
|
+
'http.method': options.method,
|
|
36
|
+
'http.url': uri,
|
|
37
|
+
'resource.name': `${options.method} ${path}`,
|
|
38
|
+
'span.kind': 'server'
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
}, ctx)
|
|
43
|
+
ctx.span = span
|
|
44
|
+
|
|
45
|
+
ctx.socket.spanContext = ctx.span._spanContext
|
|
46
|
+
ctx.socket.spanContext.spanTags = ctx.span._spanContext._tags
|
|
47
|
+
|
|
48
|
+
return ctx.currentStore
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bindAsyncStart (ctx) {
|
|
52
|
+
ctx.span.setTag(HTTP_STATUS_CODE, ctx.req.resStatus)
|
|
53
|
+
|
|
54
|
+
return ctx.parentStore
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
asyncStart (ctx) {
|
|
58
|
+
ctx.span.finish()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getRequestProtocol (req, fallback = 'ws') {
|
|
63
|
+
// 1. Check if the underlying TLS socket has 'encrypted'
|
|
64
|
+
if (req.socket && req.socket.encrypted) {
|
|
65
|
+
return 'wss'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Check for a trusted header set by a proxy
|
|
69
|
+
if (req.headers && req.headers['x-forwarded-proto']) {
|
|
70
|
+
const proto = req.headers['x-forwarded-proto'].split(',')[0].trim()
|
|
71
|
+
if (proto === 'https') return 'wss'
|
|
72
|
+
if (proto === 'http') return 'ws'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3. Fallback to ws
|
|
76
|
+
return fallback
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = WSServerPlugin
|
|
@@ -86,7 +86,7 @@ function wrapFunction (original, wrapper) {
|
|
|
86
86
|
/**
|
|
87
87
|
* Wraps a method of an object with a wrapper function.
|
|
88
88
|
*
|
|
89
|
-
* @param {Record<string | symbol, unknown> | Function} target - The target
|
|
89
|
+
* @param {Record<string | symbol, unknown> | Function | undefined} target - The target
|
|
90
90
|
* object.
|
|
91
91
|
* @param {string | symbol} name - The property key of the method to wrap.
|
|
92
92
|
* @param {(original: Function) => (...args) => any} wrapper - The wrapper function.
|
|
@@ -95,7 +95,7 @@ function wrapFunction (original, wrapper) {
|
|
|
95
95
|
* returns the earlier retrieved value. Use with care! This may only be done in
|
|
96
96
|
* case the getter absolutely has no side effect and no setter is defined for the
|
|
97
97
|
* property.
|
|
98
|
-
* @returns {Record<string | symbol, unknown> | Function} The target object with
|
|
98
|
+
* @returns {Record<string | symbol, unknown> | Function | undefined} The target object with
|
|
99
99
|
* the wrapped method.
|
|
100
100
|
*/
|
|
101
101
|
function wrap (target, name, wrapper, options) {
|
|
@@ -103,6 +103,15 @@ function wrap (target, name, wrapper, options) {
|
|
|
103
103
|
throw new TypeError(wrapper ? 'Target is not a function' : 'No function provided')
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
if (target == null) {
|
|
107
|
+
// TODO: Add logging. This is an indicator that the part of a module that we
|
|
108
|
+
// try to instrument changed.
|
|
109
|
+
// Accessing the properties directly is in itself also unsafe, so we could just
|
|
110
|
+
// pass through an array of properties that should be accessed and automatically
|
|
111
|
+
// handle the access in here.
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
106
115
|
// No descriptor means original was on the prototype. This is not totally
|
|
107
116
|
// safe, since we define the property on the target. That could have an impact
|
|
108
117
|
// in case e.g., the own keys are checks.
|
|
@@ -14,6 +14,8 @@ let defaultBlockingActionParameters
|
|
|
14
14
|
|
|
15
15
|
const responseBlockedSet = new WeakSet()
|
|
16
16
|
|
|
17
|
+
const blockDelegations = new WeakMap()
|
|
18
|
+
|
|
17
19
|
const specificBlockingTypes = {
|
|
18
20
|
GRAPHQL: 'graphql'
|
|
19
21
|
}
|
|
@@ -99,6 +101,9 @@ function getBlockingData (req, specificType, actionParameters) {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
function block (req, res, rootSpan, abortController, actionParameters = defaultBlockingActionParameters) {
|
|
104
|
+
// synchronous blocking overrides previously created delegations
|
|
105
|
+
blockDelegations.delete(res)
|
|
106
|
+
|
|
102
107
|
try {
|
|
103
108
|
if (res.headersSent) {
|
|
104
109
|
log.warn('[ASM] Cannot send blocking response when headers have already been sent')
|
|
@@ -127,11 +132,33 @@ function block (req, res, rootSpan, abortController, actionParameters = defaultB
|
|
|
127
132
|
rootSpan?.setTag('_dd.appsec.block.failed', 1)
|
|
128
133
|
log.error('[ASM] Blocking error', err)
|
|
129
134
|
|
|
135
|
+
// TODO: if blocking fails, then the response will never be sent ?
|
|
136
|
+
|
|
130
137
|
updateBlockFailureMetric(req)
|
|
131
138
|
return false
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
function registerBlockDelegation (req, res) {
|
|
143
|
+
const args = arguments
|
|
144
|
+
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
// ignore subsequent delegations by never calling their resolve()
|
|
147
|
+
if (blockDelegations.has(res)) return
|
|
148
|
+
|
|
149
|
+
blockDelegations.set(res, { args, resolve })
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function callBlockDelegation (res) {
|
|
154
|
+
const delegation = blockDelegations.get(res)
|
|
155
|
+
if (delegation) {
|
|
156
|
+
const result = block.apply(this, delegation.args)
|
|
157
|
+
delegation.resolve(result)
|
|
158
|
+
return result
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
135
162
|
function getBlockingAction (actions) {
|
|
136
163
|
// waf only returns one action, but it prioritizes redirect over block
|
|
137
164
|
return actions?.redirect_request || actions?.block_request
|
|
@@ -158,6 +185,8 @@ function setDefaultBlockingActionParameters (actions) {
|
|
|
158
185
|
module.exports = {
|
|
159
186
|
addSpecificEndpoint,
|
|
160
187
|
block,
|
|
188
|
+
registerBlockDelegation,
|
|
189
|
+
callBlockDelegation,
|
|
161
190
|
specificBlockingTypes,
|
|
162
191
|
getBlockingData,
|
|
163
192
|
getBlockingAction,
|
|
@@ -14,10 +14,11 @@ module.exports = {
|
|
|
14
14
|
expressProcessParams: dc.channel('datadog:express:process_params:start'),
|
|
15
15
|
expressSession: dc.channel('datadog:express-session:middleware:finish'),
|
|
16
16
|
fastifyBodyParser: dc.channel('datadog:fastify:body-parser:finish'),
|
|
17
|
-
fastifyResponseChannel: dc.channel('datadog:fastify:response:finish'),
|
|
18
|
-
fastifyQueryParams: dc.channel('datadog:fastify:query-params:finish'),
|
|
19
17
|
fastifyCookieParser: dc.channel('datadog:fastify-cookie:read:finish'),
|
|
18
|
+
fastifyMiddlewareError: dc.channel('apm:fastify:middleware:error'),
|
|
20
19
|
fastifyPathParams: dc.channel('datadog:fastify:path-params:finish'),
|
|
20
|
+
fastifyQueryParams: dc.channel('datadog:fastify:query-params:finish'),
|
|
21
|
+
fastifyResponseChannel: dc.channel('datadog:fastify:response:finish'),
|
|
21
22
|
fsOperationStart: dc.channel('apm:fs:operation:start'),
|
|
22
23
|
graphqlMiddlewareChannel: dc.tracingChannel('datadog:apollo:middleware'),
|
|
23
24
|
httpClientRequestStart: dc.channel('apm:http:client:request:start'),
|
|
@@ -36,6 +37,7 @@ module.exports = {
|
|
|
36
37
|
responseSetHeader: dc.channel('datadog:http:server:response:set-header:start'),
|
|
37
38
|
responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
|
|
38
39
|
routerParam: dc.channel('datadog:router:param:start'),
|
|
40
|
+
routerMiddlewareError: dc.channel('apm:router:middleware:error'),
|
|
39
41
|
setCookieChannel: dc.channel('datadog:iast:set-cookie'),
|
|
40
42
|
setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start'),
|
|
41
43
|
startGraphqlResolve: dc.channel('datadog:graphql:resolver:start'),
|
|
@@ -34,7 +34,7 @@ const apiSecuritySampler = require('./api_security_sampler')
|
|
|
34
34
|
const web = require('../plugins/util/web')
|
|
35
35
|
const { extractIp } = require('../plugins/util/ip_extractor')
|
|
36
36
|
const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
|
|
37
|
-
const { isBlocked, block, setTemplates, getBlockingAction } = require('./blocking')
|
|
37
|
+
const { isBlocked, block, callBlockDelegation, setTemplates, getBlockingAction } = require('./blocking')
|
|
38
38
|
const UserTracking = require('./user_tracking')
|
|
39
39
|
const { storage } = require('../../../datadog-core')
|
|
40
40
|
const graphql = require('./graphql')
|
|
@@ -306,8 +306,13 @@ function onResponseWriteHead ({ req, res, abortController, statusCode, responseH
|
|
|
306
306
|
storedResponseHeaders.set(req, responseHeaders)
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// TODO: do not call waf if inside block()
|
|
310
|
+
// if (isBlocking()) {
|
|
311
|
+
// return
|
|
312
|
+
// }
|
|
313
|
+
|
|
309
314
|
// avoid "write after end" error
|
|
310
|
-
if (isBlocked(res)) {
|
|
315
|
+
if (isBlocked(res) || callBlockDelegation(res)) {
|
|
311
316
|
abortController?.abort()
|
|
312
317
|
return
|
|
313
318
|
}
|
|
@@ -35,6 +35,7 @@ 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
|
+
// We might have to add the same subscribers for fastify later
|
|
38
39
|
|
|
39
40
|
super.configure(true)
|
|
40
41
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const web = require('../../plugins/util/web')
|
|
4
|
-
const {
|
|
5
|
-
|
|
4
|
+
const {
|
|
5
|
+
setUncaughtExceptionCaptureCallbackStart,
|
|
6
|
+
expressMiddlewareError,
|
|
7
|
+
fastifyMiddlewareError,
|
|
8
|
+
routerMiddlewareError
|
|
9
|
+
} = require('../channels')
|
|
10
|
+
const { block, registerBlockDelegation, isBlocked } = require('../blocking')
|
|
6
11
|
const ssrf = require('./ssrf')
|
|
7
12
|
const sqli = require('./sql_injection')
|
|
8
13
|
const lfi = require('./lfi')
|
|
@@ -39,7 +44,7 @@ function findDatadogRaspAbortError (err, deep = 10) {
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
function handleUncaughtExceptionMonitor (error) {
|
|
42
|
-
if (!blockOnDatadogRaspAbortError({ error })) return
|
|
47
|
+
if (!blockOnDatadogRaspAbortError({ error, isTopLevel: true })) return
|
|
43
48
|
|
|
44
49
|
if (process.hasUncaughtExceptionCaptureCallback()) {
|
|
45
50
|
// uncaughtException event is not executed when hasUncaughtExceptionCaptureCallback is true
|
|
@@ -81,15 +86,22 @@ function handleUncaughtExceptionMonitor (error) {
|
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
function blockOnDatadogRaspAbortError ({ error }) {
|
|
89
|
+
function blockOnDatadogRaspAbortError ({ error, isTopLevel }) {
|
|
85
90
|
const abortError = findDatadogRaspAbortError(error)
|
|
86
91
|
if (!abortError) return false
|
|
87
92
|
|
|
88
93
|
const { req, res, blockingAction, raspRule, ruleTriggered } = abortError
|
|
89
94
|
if (!isBlocked(res)) {
|
|
90
|
-
const
|
|
95
|
+
const blockFn = isTopLevel ? block : registerBlockDelegation
|
|
96
|
+
const blocked = blockFn(req, res, web.root(req), null, blockingAction)
|
|
91
97
|
if (ruleTriggered) {
|
|
92
|
-
|
|
98
|
+
// block() returns a bool, and registerBlockDelegation() returns a promise
|
|
99
|
+
// we use Promise.resolve() to handle both cases
|
|
100
|
+
Promise.resolve(blocked).then(blocked => {
|
|
101
|
+
// TODO: bug: this metric is not called when the raspAbortError is caught by user
|
|
102
|
+
// or on subsequent blockDelegations
|
|
103
|
+
updateRaspRuleMatchMetricTags(req, raspRule, true, blocked)
|
|
104
|
+
})
|
|
93
105
|
}
|
|
94
106
|
}
|
|
95
107
|
|
|
@@ -103,7 +115,10 @@ function enable (config) {
|
|
|
103
115
|
cmdi.enable(config)
|
|
104
116
|
|
|
105
117
|
process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
|
|
118
|
+
|
|
106
119
|
expressMiddlewareError.subscribe(blockOnDatadogRaspAbortError)
|
|
120
|
+
fastifyMiddlewareError.subscribe(blockOnDatadogRaspAbortError)
|
|
121
|
+
routerMiddlewareError.subscribe(blockOnDatadogRaspAbortError)
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
function disable () {
|
|
@@ -113,7 +128,10 @@ function disable () {
|
|
|
113
128
|
cmdi.disable()
|
|
114
129
|
|
|
115
130
|
process.off('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
|
|
116
|
-
|
|
131
|
+
|
|
132
|
+
expressMiddlewareError.unsubscribe(blockOnDatadogRaspAbortError)
|
|
133
|
+
fastifyMiddlewareError.unsubscribe(blockOnDatadogRaspAbortError)
|
|
134
|
+
routerMiddlewareError.unsubscribe(blockOnDatadogRaspAbortError)
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
module.exports = {
|
|
@@ -33,7 +33,7 @@ function disable () {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function onFirstReceivedRequest () {
|
|
36
|
-
//
|
|
36
|
+
// Node.js unsubscribe during publish bug: https://github.com/nodejs/node/pull/55116
|
|
37
37
|
process.nextTick(() => {
|
|
38
38
|
incomingHttpRequestStart.unsubscribe(onFirstReceivedRequest)
|
|
39
39
|
})
|
|
@@ -19,6 +19,11 @@ const RULE_TYPES = {
|
|
|
19
19
|
SSRF: 'ssrf'
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const ALLOWED_ROOTSPAN_NAMES = new Set([
|
|
23
|
+
'express.request',
|
|
24
|
+
'fastify.request'
|
|
25
|
+
])
|
|
26
|
+
|
|
22
27
|
class DatadogRaspAbortError extends Error {
|
|
23
28
|
constructor (req, res, blockingAction, raspRule, ruleTriggered) {
|
|
24
29
|
super('DatadogRaspAbortError')
|
|
@@ -28,6 +33,12 @@ class DatadogRaspAbortError extends Error {
|
|
|
28
33
|
this.blockingAction = blockingAction
|
|
29
34
|
this.raspRule = raspRule
|
|
30
35
|
this.ruleTriggered = ruleTriggered
|
|
36
|
+
|
|
37
|
+
// hide these props to not pollute app logs
|
|
38
|
+
Object.defineProperties(this, {
|
|
39
|
+
req: { enumerable: false },
|
|
40
|
+
res: { enumerable: false }
|
|
41
|
+
})
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
|
|
@@ -52,9 +63,9 @@ function handleResult (result, req, res, abortController, config, raspRule) {
|
|
|
52
63
|
|
|
53
64
|
if (abortController && !abortOnUncaughtException) {
|
|
54
65
|
const blockingAction = getBlockingAction(result?.actions)
|
|
66
|
+
const rootSpanName = rootSpan?.context?.()?._name
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
if (blockingAction && rootSpan?.context()._name === 'express.request') {
|
|
68
|
+
if (blockingAction && ALLOWED_ROOTSPAN_NAMES.has(rootSpanName)) {
|
|
58
69
|
const abortError = new DatadogRaspAbortError(req, res, blockingAction, raspRule, ruleTriggered)
|
|
59
70
|
abortController.abort(abortError)
|
|
60
71
|
|
|
@@ -630,6 +630,9 @@ class Config {
|
|
|
630
630
|
defaults['tracePropagationStyle.inject'] = ['datadog', 'tracecontext', 'baggage']
|
|
631
631
|
defaults['tracePropagationStyle.extract'] = ['datadog', 'tracecontext', 'baggage']
|
|
632
632
|
defaults['tracePropagationStyle.otelPropagators'] = false
|
|
633
|
+
defaults.traceWebsocketMessagesEnabled = true
|
|
634
|
+
defaults.traceWebsocketMessagesInheritSampling = true
|
|
635
|
+
defaults.traceWebsocketMessagesSeparateTraces = true
|
|
633
636
|
defaults.tracing = true
|
|
634
637
|
defaults.url = undefined
|
|
635
638
|
defaults.version = pkg.version
|
|
@@ -812,6 +815,9 @@ class Config {
|
|
|
812
815
|
DD_TRACE_SPAN_LEAK_DEBUG,
|
|
813
816
|
DD_TRACE_STARTUP_LOGS,
|
|
814
817
|
DD_TRACE_TAGS,
|
|
818
|
+
DD_TRACE_WEBSOCKET_MESSAGES_ENABLED,
|
|
819
|
+
DD_TRACE_WEBSOCKET_MESSAGES_INHERIT_SAMPLING,
|
|
820
|
+
DD_TRACE_WEBSOCKET_MESSAGES_SEPARATE_TRACES,
|
|
815
821
|
DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH,
|
|
816
822
|
DD_TRACING_ENABLED,
|
|
817
823
|
DD_VERSION,
|
|
@@ -1050,6 +1056,9 @@ class Config {
|
|
|
1050
1056
|
DD_TRACE_PROPAGATION_STYLE_EXTRACT
|
|
1051
1057
|
? false
|
|
1052
1058
|
: !!OTEL_PROPAGATORS)
|
|
1059
|
+
this._setBoolean(env, 'traceWebsocketMessagesEnabled', DD_TRACE_WEBSOCKET_MESSAGES_ENABLED)
|
|
1060
|
+
this._setBoolean(env, 'traceWebsocketMessagesInheritSampling', DD_TRACE_WEBSOCKET_MESSAGES_INHERIT_SAMPLING)
|
|
1061
|
+
this._setBoolean(env, 'traceWebsocketMessagesSeparateTraces', DD_TRACE_WEBSOCKET_MESSAGES_SEPARATE_TRACES)
|
|
1053
1062
|
this._setBoolean(env, 'tracing', DD_TRACING_ENABLED)
|
|
1054
1063
|
this._setString(env, 'version', DD_VERSION || tags.version)
|
|
1055
1064
|
this._setBoolean(env, 'inferredProxyServicesEnabled', DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED)
|
|
@@ -1215,6 +1224,9 @@ class Config {
|
|
|
1215
1224
|
this._setTags(opts, 'tags', tags)
|
|
1216
1225
|
this._setBoolean(opts, 'traceId128BitGenerationEnabled', options.traceId128BitGenerationEnabled)
|
|
1217
1226
|
this._setBoolean(opts, 'traceId128BitLoggingEnabled', options.traceId128BitLoggingEnabled)
|
|
1227
|
+
this._setBoolean(opts, 'traceWebsocketMessagesEnabled', options.traceWebsocketMessagesEnabled)
|
|
1228
|
+
this._setBoolean(opts, 'traceWebsocketMessagesInheritSampling', options.traceWebsocketMessagesInheritSampling)
|
|
1229
|
+
this._setBoolean(opts, 'traceWebsocketMessagesSeparateTraces', options.traceWebsocketMessagesSeparateTraces)
|
|
1218
1230
|
this._setString(opts, 'version', options.version || tags.version)
|
|
1219
1231
|
this._setBoolean(opts, 'inferredProxyServicesEnabled', options.inferredProxyServicesEnabled)
|
|
1220
1232
|
this._setBoolean(opts, 'graphqlErrorExtensions', options.graphqlErrorExtensions)
|
|
@@ -45,9 +45,13 @@ function guard (fn) {
|
|
|
45
45
|
telemetry([
|
|
46
46
|
{ name: 'abort', tags: ['reason:incompatible_runtime'] },
|
|
47
47
|
{ name: 'abort.runtime', tags: [] }
|
|
48
|
-
]
|
|
48
|
+
], undefined, {
|
|
49
|
+
result: 'abort',
|
|
50
|
+
result_class: 'incompatible_runtime',
|
|
51
|
+
result_reason: 'Incompatible runtime Node.js ' + version + ', supported runtimes: Node.js ' + engines.node
|
|
52
|
+
})
|
|
49
53
|
log.info('Aborting application instrumentation due to incompatible_runtime.')
|
|
50
|
-
log.info('Found incompatible runtime
|
|
54
|
+
log.info('Found incompatible runtime Node.js %s, Supported runtimes: Node.js %s.', version, engines.node)
|
|
51
55
|
if (forced) {
|
|
52
56
|
log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.')
|
|
53
57
|
}
|
|
@@ -56,7 +60,11 @@ function guard (fn) {
|
|
|
56
60
|
if (!clobberBailout && (!initBailout || forced)) {
|
|
57
61
|
// Ensure the instrumentation source is set for the current process and potential child processes.
|
|
58
62
|
var result = fn()
|
|
59
|
-
telemetry('complete', ['injection_forced:' + (forced && initBailout ? 'true' : 'false')]
|
|
63
|
+
telemetry('complete', ['injection_forced:' + (forced && initBailout ? 'true' : 'false')], {
|
|
64
|
+
result: 'success',
|
|
65
|
+
result_class: 'success',
|
|
66
|
+
result_reason: 'Successfully configured ddtrace package'
|
|
67
|
+
})
|
|
60
68
|
log.info('Application instrumentation bootstrapping complete')
|
|
61
69
|
return result
|
|
62
70
|
}
|