dd-trace 5.64.0 → 5.66.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/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/azure_metadata.js +5 -4
- package/packages/dd-trace/src/config.js +12 -0
- package/packages/dd-trace/src/datastreams/pathway.js +1 -1
- package/packages/dd-trace/src/dogstatsd.js +17 -20
- 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/tagger.js +2 -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/runtime_metrics/runtime_metrics.js +175 -126
- 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 -1
|
@@ -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
|
|
|
@@ -76,10 +76,11 @@ function buildMetadata () {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function getAzureAppMetadata () {
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// See
|
|
82
|
-
|
|
79
|
+
// WEBSITE_SITE_NAME is the unique name of the website instance within Azure App Services. Its
|
|
80
|
+
// presence is used to determine if we are running in Azure App Service
|
|
81
|
+
// See equivalent in dd-trace-dotnet:
|
|
82
|
+
// https://github.com/DataDog/dd-trace-dotnet/blob/37030168b2996e549ba23231ae732874b53a37e6/tracer/src/Datadog.Trace/Util/EnvironmentHelpers.cs#L99-L155
|
|
83
|
+
if (getEnvironmentVariable('WEBSITE_SITE_NAME') !== undefined) {
|
|
83
84
|
return buildMetadata()
|
|
84
85
|
}
|
|
85
86
|
}
|
|
@@ -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)
|
|
@@ -17,7 +17,7 @@ const CONTEXT_PROPAGATION_KEY_BASE64 = 'dd-pathway-ctx-base64'
|
|
|
17
17
|
const logKeys = [CONTEXT_PROPAGATION_KEY, CONTEXT_PROPAGATION_KEY_BASE64]
|
|
18
18
|
|
|
19
19
|
function shaHash (checkpointString) {
|
|
20
|
-
const hash = crypto.createHash('
|
|
20
|
+
const hash = crypto.createHash('sha256').update(checkpointString).digest('hex').slice(0, 16)
|
|
21
21
|
return Buffer.from(hash, 'hex')
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -156,23 +156,19 @@ class DogStatsDClient {
|
|
|
156
156
|
return socket
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
static generateClientConfig (config
|
|
159
|
+
static generateClientConfig (config) {
|
|
160
160
|
const tags = []
|
|
161
161
|
|
|
162
162
|
if (config.tags) {
|
|
163
|
-
Object.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// Skip runtime-id unless enabled as cardinality may be too high
|
|
167
|
-
if (key !== 'runtime-id') return true
|
|
168
|
-
return config.runtimeMetricsRuntimeId
|
|
169
|
-
})
|
|
170
|
-
.forEach(key => {
|
|
163
|
+
for (const [key, value] of Object.entries(config.tags)) {
|
|
164
|
+
// Skip runtime-id unless enabled as cardinality may be too high
|
|
165
|
+
if (typeof value === 'string' && (key !== 'runtime-id' || config.runtimeMetricsRuntimeId)) {
|
|
171
166
|
// https://docs.datadoghq.com/tagging/#defining-tags
|
|
172
|
-
const
|
|
167
|
+
const valueStripped = value.replaceAll(/[^a-z0-9_:./-]/ig, '_')
|
|
173
168
|
|
|
174
|
-
tags.push(`${key}:${
|
|
175
|
-
}
|
|
169
|
+
tags.push(`${key}:${valueStripped}`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
176
172
|
}
|
|
177
173
|
|
|
178
174
|
const clientConfig = {
|
|
@@ -216,7 +212,7 @@ class MetricsAggregationClient {
|
|
|
216
212
|
this._histograms = new Map()
|
|
217
213
|
}
|
|
218
214
|
|
|
219
|
-
// TODO:
|
|
215
|
+
// TODO: Aggregate with a histogram and send the buckets to the client.
|
|
220
216
|
distribution (name, value, tags) {
|
|
221
217
|
this._client.distribution(name, value, tags)
|
|
222
218
|
}
|
|
@@ -352,9 +348,10 @@ class MetricsAggregationClient {
|
|
|
352
348
|
* @implements {DogStatsD}
|
|
353
349
|
*/
|
|
354
350
|
class CustomMetrics {
|
|
351
|
+
#client
|
|
355
352
|
constructor (config) {
|
|
356
353
|
const clientConfig = DogStatsDClient.generateClientConfig(config)
|
|
357
|
-
this
|
|
354
|
+
this.#client = new MetricsAggregationClient(new DogStatsDClient(clientConfig))
|
|
358
355
|
|
|
359
356
|
const flush = this.flush.bind(this)
|
|
360
357
|
|
|
@@ -365,27 +362,27 @@ class CustomMetrics {
|
|
|
365
362
|
}
|
|
366
363
|
|
|
367
364
|
increment (stat, value = 1, tags) {
|
|
368
|
-
this.
|
|
365
|
+
this.#client.increment(stat, value, CustomMetrics.tagTranslator(tags))
|
|
369
366
|
}
|
|
370
367
|
|
|
371
368
|
decrement (stat, value = 1, tags) {
|
|
372
|
-
this.
|
|
369
|
+
this.#client.decrement(stat, value, CustomMetrics.tagTranslator(tags))
|
|
373
370
|
}
|
|
374
371
|
|
|
375
372
|
gauge (stat, value, tags) {
|
|
376
|
-
this.
|
|
373
|
+
this.#client.gauge(stat, value, CustomMetrics.tagTranslator(tags))
|
|
377
374
|
}
|
|
378
375
|
|
|
379
376
|
distribution (stat, value, tags) {
|
|
380
|
-
this.
|
|
377
|
+
this.#client.distribution(stat, value, CustomMetrics.tagTranslator(tags))
|
|
381
378
|
}
|
|
382
379
|
|
|
383
380
|
histogram (stat, value, tags) {
|
|
384
|
-
this.
|
|
381
|
+
this.#client.histogram(stat, value, CustomMetrics.tagTranslator(tags))
|
|
385
382
|
}
|
|
386
383
|
|
|
387
384
|
flush () {
|
|
388
|
-
return this.
|
|
385
|
+
return this.#client.flush()
|
|
389
386
|
}
|
|
390
387
|
|
|
391
388
|
/**
|