dd-trace 4.21.0 → 4.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +5 -0
- package/package.json +6 -6
- package/packages/datadog-instrumentations/src/apollo-server-core.js +41 -0
- package/packages/datadog-instrumentations/src/apollo-server.js +83 -0
- package/packages/datadog-instrumentations/src/child-process.js +4 -5
- package/packages/datadog-instrumentations/src/couchbase.js +5 -4
- package/packages/datadog-instrumentations/src/crypto.js +2 -1
- package/packages/datadog-instrumentations/src/dns.js +2 -1
- package/packages/datadog-instrumentations/src/graphql.js +18 -4
- package/packages/datadog-instrumentations/src/helpers/hooks.js +9 -2
- package/packages/datadog-instrumentations/src/helpers/instrument.js +8 -3
- package/packages/datadog-instrumentations/src/helpers/register.js +18 -2
- package/packages/datadog-instrumentations/src/http/client.js +4 -16
- package/packages/datadog-instrumentations/src/http/server.js +7 -4
- package/packages/datadog-instrumentations/src/http2/client.js +3 -1
- package/packages/datadog-instrumentations/src/http2/server.js +3 -1
- package/packages/datadog-instrumentations/src/jest.js +1 -1
- package/packages/datadog-instrumentations/src/net.js +10 -2
- package/packages/datadog-instrumentations/src/next.js +15 -5
- package/packages/datadog-instrumentations/src/rhea.js +15 -9
- package/packages/datadog-plugin-cucumber/src/index.js +34 -2
- package/packages/datadog-plugin-cypress/src/plugin.js +60 -8
- package/packages/datadog-plugin-graphql/src/resolve.js +26 -18
- package/packages/datadog-plugin-http/src/client.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +38 -4
- package/packages/datadog-plugin-mocha/src/index.js +32 -1
- package/packages/datadog-plugin-next/src/index.js +32 -6
- package/packages/datadog-plugin-playwright/src/index.js +17 -1
- package/packages/dd-trace/src/appsec/activation.js +29 -0
- package/packages/dd-trace/src/appsec/addresses.js +1 -0
- package/packages/dd-trace/src/appsec/api_security_sampler.js +48 -0
- package/packages/dd-trace/src/appsec/blocked_templates.js +4 -1
- package/packages/dd-trace/src/appsec/blocking.js +95 -43
- package/packages/dd-trace/src/appsec/channels.js +4 -1
- package/packages/dd-trace/src/appsec/graphql.js +146 -0
- package/packages/dd-trace/src/appsec/index.js +29 -40
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +6 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +40 -15
- package/packages/dd-trace/src/appsec/sdk/user_blocking.js +1 -1
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +25 -13
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +30 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +30 -1
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +36 -4
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +18 -1
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +26 -1
- package/packages/dd-trace/src/ci-visibility/telemetry.js +130 -0
- package/packages/dd-trace/src/config.js +104 -58
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +14 -1
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +14 -0
- package/packages/dd-trace/src/exporters/common/agent-info-exporter.js +4 -0
- package/packages/dd-trace/src/exporters/common/form-data.js +4 -0
- package/packages/dd-trace/src/opentracing/tracer.js +2 -2
- package/packages/dd-trace/src/plugins/ci_plugin.js +44 -8
- package/packages/dd-trace/src/plugins/index.js +5 -0
- package/packages/dd-trace/src/plugins/util/exec.js +23 -2
- package/packages/dd-trace/src/plugins/util/git.js +94 -19
- package/packages/dd-trace/src/plugins/util/user-provided-git.js +3 -2
- package/packages/dd-trace/src/priority_sampler.js +30 -38
- package/packages/dd-trace/src/profiling/exporters/agent.js +1 -0
- package/packages/dd-trace/src/profiling/profiler.js +7 -6
- package/packages/dd-trace/src/profiling/profilers/events.js +18 -13
- package/packages/dd-trace/src/profiling/profilers/shared.js +34 -4
- package/packages/dd-trace/src/profiling/profilers/space.js +2 -1
- package/packages/dd-trace/src/profiling/profilers/wall.js +17 -12
- package/packages/dd-trace/src/proxy.js +4 -0
- package/packages/dd-trace/src/sampling_rule.js +130 -0
- package/packages/dd-trace/src/span_sampler.js +6 -64
- package/packages/dd-trace/src/telemetry/index.js +43 -5
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Activation = {
|
|
4
|
+
ONECLICK: 'OneClick',
|
|
5
|
+
ENABLED: 'Enabled',
|
|
6
|
+
DISABLED: 'Disabled',
|
|
7
|
+
|
|
8
|
+
fromConfig (config) {
|
|
9
|
+
switch (config.appsec.enabled) {
|
|
10
|
+
// ASM is activated by an env var DD_APPSEC_ENABLED=true
|
|
11
|
+
case true:
|
|
12
|
+
return Activation.ENABLED
|
|
13
|
+
|
|
14
|
+
// ASM is disabled by an env var DD_APPSEC_ENABLED=false
|
|
15
|
+
case false:
|
|
16
|
+
return Activation.DISABLED
|
|
17
|
+
|
|
18
|
+
// ASM is activated by one click remote config
|
|
19
|
+
case undefined:
|
|
20
|
+
return Activation.ONECLICK
|
|
21
|
+
|
|
22
|
+
// Any other value should never occur
|
|
23
|
+
default:
|
|
24
|
+
return Activation.DISABLED
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = Activation
|
|
@@ -13,6 +13,7 @@ module.exports = {
|
|
|
13
13
|
HTTP_INCOMING_RESPONSE_HEADERS: 'server.response.headers.no_cookies',
|
|
14
14
|
// TODO: 'server.response.trailers',
|
|
15
15
|
HTTP_INCOMING_GRAPHQL_RESOLVERS: 'graphql.server.all_resolvers',
|
|
16
|
+
HTTP_INCOMING_GRAPHQL_RESOLVER: 'graphql.server.resolver',
|
|
16
17
|
|
|
17
18
|
HTTP_CLIENT_IP: 'http.client_ip',
|
|
18
19
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../log')
|
|
4
|
+
|
|
5
|
+
let enabled
|
|
6
|
+
let requestSampling
|
|
7
|
+
|
|
8
|
+
function configure ({ apiSecurity }) {
|
|
9
|
+
enabled = apiSecurity.enabled
|
|
10
|
+
setRequestSampling(apiSecurity.requestSampling)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function disable () {
|
|
14
|
+
enabled = false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function setRequestSampling (sampling) {
|
|
18
|
+
requestSampling = parseRequestSampling(sampling)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseRequestSampling (requestSampling) {
|
|
22
|
+
let parsed = parseFloat(requestSampling)
|
|
23
|
+
|
|
24
|
+
if (isNaN(parsed)) {
|
|
25
|
+
log.warn(`Incorrect API Security request sampling value: ${requestSampling}`)
|
|
26
|
+
|
|
27
|
+
parsed = 0
|
|
28
|
+
} else {
|
|
29
|
+
parsed = Math.min(1, Math.max(0, parsed))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parsed
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sampleRequest () {
|
|
36
|
+
if (!enabled || !requestSampling) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Math.random() <= requestSampling
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
configure,
|
|
45
|
+
disable,
|
|
46
|
+
setRequestSampling,
|
|
47
|
+
sampleRequest
|
|
48
|
+
}
|
|
@@ -5,7 +5,10 @@ const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta n
|
|
|
5
5
|
|
|
6
6
|
const json = `{"errors":[{"title":"You've been blocked","detail":"Sorry, you cannot access this page. Please contact the customer service team. Security provided by Datadog."}]}`
|
|
7
7
|
|
|
8
|
+
const graphqlJson = `{"errors":[{"message":"You've been blocked","extensions":{"detail":"Sorry, you cannot perform this operation. Please contact the customer service team. Security provided by Datadog."}}]}`
|
|
9
|
+
|
|
8
10
|
module.exports = {
|
|
9
11
|
html,
|
|
10
|
-
json
|
|
12
|
+
json,
|
|
13
|
+
graphqlJson
|
|
11
14
|
}
|
|
@@ -3,93 +3,142 @@
|
|
|
3
3
|
const log = require('../log')
|
|
4
4
|
const blockedTemplates = require('./blocked_templates')
|
|
5
5
|
|
|
6
|
+
const detectedSpecificEndpoints = {}
|
|
7
|
+
|
|
6
8
|
let templateHtml = blockedTemplates.html
|
|
7
9
|
let templateJson = blockedTemplates.json
|
|
10
|
+
let templateGraphqlJson = blockedTemplates.graphqlJson
|
|
8
11
|
let blockingConfiguration
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
})
|
|
13
|
+
const specificBlockingTypes = {
|
|
14
|
+
GRAPHQL: 'graphql'
|
|
15
|
+
}
|
|
14
16
|
|
|
17
|
+
function getSpecificKey (method, url) {
|
|
18
|
+
return `${method}+${url}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addSpecificEndpoint (method, url, type) {
|
|
22
|
+
detectedSpecificEndpoints[getSpecificKey(method, url)] = type
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getBlockWithRedirectData (rootSpan) {
|
|
15
26
|
let statusCode = blockingConfiguration.parameters.status_code
|
|
16
27
|
if (!statusCode || statusCode < 300 || statusCode >= 400) {
|
|
17
28
|
statusCode = 303
|
|
18
29
|
}
|
|
19
|
-
|
|
20
|
-
res.writeHead(statusCode, {
|
|
30
|
+
const headers = {
|
|
21
31
|
'Location': blockingConfiguration.parameters.location
|
|
22
|
-
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
rootSpan.addTags({
|
|
35
|
+
'appsec.blocked': 'true'
|
|
36
|
+
})
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
return { headers, statusCode }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getSpecificBlockingData (type) {
|
|
42
|
+
switch (type) {
|
|
43
|
+
case specificBlockingTypes.GRAPHQL:
|
|
44
|
+
return {
|
|
45
|
+
type: 'application/json',
|
|
46
|
+
body: templateGraphqlJson
|
|
47
|
+
}
|
|
26
48
|
}
|
|
27
49
|
}
|
|
28
50
|
|
|
29
|
-
function
|
|
51
|
+
function getBlockWithContentData (req, specificType, rootSpan) {
|
|
30
52
|
let type
|
|
31
53
|
let body
|
|
54
|
+
let statusCode
|
|
32
55
|
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
const specificBlockingType = specificType || detectedSpecificEndpoints[getSpecificKey(req.method, req.url)]
|
|
57
|
+
if (specificBlockingType) {
|
|
58
|
+
const specificBlockingContent = getSpecificBlockingData(specificBlockingType)
|
|
59
|
+
type = specificBlockingContent?.type
|
|
60
|
+
body = specificBlockingContent?.body
|
|
61
|
+
}
|
|
35
62
|
|
|
36
|
-
if (!
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
if (!type) {
|
|
64
|
+
// parse the Accept header, ex: Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8
|
|
65
|
+
const accept = req.headers.accept?.split(',').map((str) => str.split(';', 1)[0].trim())
|
|
66
|
+
|
|
67
|
+
if (!blockingConfiguration || blockingConfiguration.parameters.type === 'auto') {
|
|
68
|
+
if (accept?.includes('text/html') && !accept.includes('application/json')) {
|
|
69
|
+
type = 'text/html; charset=utf-8'
|
|
70
|
+
body = templateHtml
|
|
71
|
+
} else {
|
|
72
|
+
type = 'application/json'
|
|
73
|
+
body = templateJson
|
|
74
|
+
}
|
|
40
75
|
} else {
|
|
41
|
-
type
|
|
42
|
-
|
|
76
|
+
if (blockingConfiguration.parameters.type === 'html') {
|
|
77
|
+
type = 'text/html; charset=utf-8'
|
|
78
|
+
body = templateHtml
|
|
79
|
+
} else {
|
|
80
|
+
type = 'application/json'
|
|
81
|
+
body = templateJson
|
|
82
|
+
}
|
|
43
83
|
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (blockingConfiguration?.type === 'block_request' && blockingConfiguration.parameters.status_code) {
|
|
87
|
+
statusCode = blockingConfiguration.parameters.status_code
|
|
44
88
|
} else {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
89
|
+
statusCode = 403
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const headers = {
|
|
93
|
+
'Content-Type': type,
|
|
94
|
+
'Content-Length': Buffer.byteLength(body)
|
|
52
95
|
}
|
|
53
96
|
|
|
54
97
|
rootSpan.addTags({
|
|
55
98
|
'appsec.blocked': 'true'
|
|
56
99
|
})
|
|
57
100
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
res.statusCode = blockingConfiguration.parameters.status_code
|
|
61
|
-
} else {
|
|
62
|
-
res.statusCode = 403
|
|
63
|
-
}
|
|
64
|
-
res.setHeader('Content-Type', type)
|
|
65
|
-
res.setHeader('Content-Length', Buffer.byteLength(body))
|
|
66
|
-
res.end(body)
|
|
101
|
+
return { body, statusCode, headers }
|
|
102
|
+
}
|
|
67
103
|
|
|
68
|
-
|
|
69
|
-
|
|
104
|
+
function getBlockingData (req, specificType, rootSpan) {
|
|
105
|
+
if (blockingConfiguration?.type === 'redirect_request' && blockingConfiguration.parameters.location) {
|
|
106
|
+
return getBlockWithRedirectData(rootSpan)
|
|
107
|
+
} else {
|
|
108
|
+
return getBlockWithContentData(req, specificType, rootSpan)
|
|
70
109
|
}
|
|
71
110
|
}
|
|
72
111
|
|
|
73
|
-
function block (req, res, rootSpan, abortController) {
|
|
112
|
+
function block (req, res, rootSpan, abortController, type) {
|
|
74
113
|
if (res.headersSent) {
|
|
75
114
|
log.warn('Cannot send blocking response when headers have already been sent')
|
|
76
115
|
return
|
|
77
116
|
}
|
|
78
117
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
118
|
+
const { body, headers, statusCode } = getBlockingData(req, type, rootSpan)
|
|
119
|
+
|
|
120
|
+
res.writeHead(statusCode, headers).end(body)
|
|
121
|
+
|
|
122
|
+
abortController?.abort()
|
|
85
123
|
}
|
|
86
124
|
|
|
87
125
|
function setTemplates (config) {
|
|
88
126
|
if (config.appsec.blockedTemplateHtml) {
|
|
89
127
|
templateHtml = config.appsec.blockedTemplateHtml
|
|
128
|
+
} else {
|
|
129
|
+
templateHtml = blockedTemplates.html
|
|
90
130
|
}
|
|
131
|
+
|
|
91
132
|
if (config.appsec.blockedTemplateJson) {
|
|
92
133
|
templateJson = config.appsec.blockedTemplateJson
|
|
134
|
+
} else {
|
|
135
|
+
templateJson = blockedTemplates.json
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (config.appsec.blockedTemplateGraphql) {
|
|
139
|
+
templateGraphqlJson = config.appsec.blockedTemplateGraphql
|
|
140
|
+
} else {
|
|
141
|
+
templateGraphqlJson = blockedTemplates.graphqlJson
|
|
93
142
|
}
|
|
94
143
|
}
|
|
95
144
|
|
|
@@ -98,7 +147,10 @@ function updateBlockingConfiguration (newBlockingConfiguration) {
|
|
|
98
147
|
}
|
|
99
148
|
|
|
100
149
|
module.exports = {
|
|
150
|
+
addSpecificEndpoint,
|
|
101
151
|
block,
|
|
152
|
+
specificBlockingTypes,
|
|
153
|
+
getBlockingData,
|
|
102
154
|
setTemplates,
|
|
103
155
|
updateBlockingConfiguration
|
|
104
156
|
}
|
|
@@ -6,7 +6,10 @@ const dc = require('dc-polyfill')
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
bodyParser: dc.channel('datadog:body-parser:read:finish'),
|
|
8
8
|
cookieParser: dc.channel('datadog:cookie-parser:read:finish'),
|
|
9
|
-
|
|
9
|
+
startGraphqlResolve: dc.channel('datadog:graphql:resolver:start'),
|
|
10
|
+
graphqlMiddlewareChannel: dc.tracingChannel('datadog:apollo:middleware'),
|
|
11
|
+
apolloChannel: dc.tracingChannel('datadog:apollo:request'),
|
|
12
|
+
apolloServerCoreChannel: dc.tracingChannel('datadog:apollo-server-core:request'),
|
|
10
13
|
incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
|
|
11
14
|
incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
|
|
12
15
|
passportVerify: dc.channel('datadog:passport:verify:finish'),
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { storage } = require('../../../datadog-core')
|
|
4
|
+
const { addSpecificEndpoint, specificBlockingTypes, getBlockingData } = require('./blocking')
|
|
5
|
+
const waf = require('./waf')
|
|
6
|
+
const addresses = require('./addresses')
|
|
7
|
+
const web = require('../plugins/util/web')
|
|
8
|
+
const {
|
|
9
|
+
startGraphqlResolve,
|
|
10
|
+
graphqlMiddlewareChannel,
|
|
11
|
+
apolloChannel,
|
|
12
|
+
apolloServerCoreChannel
|
|
13
|
+
} = require('./channels')
|
|
14
|
+
|
|
15
|
+
const graphqlRequestData = new WeakMap()
|
|
16
|
+
|
|
17
|
+
function enable () {
|
|
18
|
+
enableApollo()
|
|
19
|
+
enableGraphql()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function disable () {
|
|
23
|
+
disableApollo()
|
|
24
|
+
disableGraphql()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function onGraphqlStartResolve ({ context, resolverInfo }) {
|
|
28
|
+
const req = storage.getStore()?.req
|
|
29
|
+
|
|
30
|
+
if (!req) return
|
|
31
|
+
|
|
32
|
+
if (!resolverInfo || typeof resolverInfo !== 'object') return
|
|
33
|
+
|
|
34
|
+
const actions = waf.run({ ephemeral: { [addresses.HTTP_INCOMING_GRAPHQL_RESOLVER]: resolverInfo } }, req)
|
|
35
|
+
if (actions?.includes('block')) {
|
|
36
|
+
const requestData = graphqlRequestData.get(req)
|
|
37
|
+
if (requestData?.isInGraphqlRequest) {
|
|
38
|
+
requestData.blocked = true
|
|
39
|
+
context?.abortController?.abort()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function enterInApolloMiddleware (data) {
|
|
45
|
+
const req = data?.req || storage.getStore()?.req
|
|
46
|
+
if (!req) return
|
|
47
|
+
|
|
48
|
+
graphqlRequestData.set(req, {
|
|
49
|
+
inApolloMiddleware: true,
|
|
50
|
+
blocked: false
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function enterInApolloServerCoreRequest () {
|
|
55
|
+
const req = storage.getStore()?.req
|
|
56
|
+
if (!req) return
|
|
57
|
+
|
|
58
|
+
graphqlRequestData.set(req, {
|
|
59
|
+
isInGraphqlRequest: true,
|
|
60
|
+
blocked: false
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function exitFromApolloMiddleware (data) {
|
|
65
|
+
const req = data?.req || storage.getStore()?.req
|
|
66
|
+
const requestData = graphqlRequestData.get(req)
|
|
67
|
+
if (requestData) requestData.inApolloMiddleware = false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function enterInApolloRequest () {
|
|
71
|
+
const req = storage.getStore()?.req
|
|
72
|
+
|
|
73
|
+
const requestData = graphqlRequestData.get(req)
|
|
74
|
+
if (requestData?.inApolloMiddleware) {
|
|
75
|
+
requestData.isInGraphqlRequest = true
|
|
76
|
+
addSpecificEndpoint(req.method, req.originalUrl || req.url, specificBlockingTypes.GRAPHQL)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) {
|
|
81
|
+
const req = storage.getStore()?.req
|
|
82
|
+
if (!req) return
|
|
83
|
+
|
|
84
|
+
const requestData = graphqlRequestData.get(req)
|
|
85
|
+
|
|
86
|
+
if (requestData?.blocked) {
|
|
87
|
+
const rootSpan = web.root(req)
|
|
88
|
+
if (!rootSpan) return
|
|
89
|
+
|
|
90
|
+
const blockingData = getBlockingData(req, specificBlockingTypes.GRAPHQL, rootSpan)
|
|
91
|
+
abortData.statusCode = blockingData.statusCode
|
|
92
|
+
abortData.headers = blockingData.headers
|
|
93
|
+
abortData.message = blockingData.body
|
|
94
|
+
|
|
95
|
+
abortController?.abort()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
graphqlRequestData.delete(req)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function enableApollo () {
|
|
102
|
+
graphqlMiddlewareChannel.subscribe({
|
|
103
|
+
start: enterInApolloMiddleware,
|
|
104
|
+
end: exitFromApolloMiddleware
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
apolloServerCoreChannel.subscribe({
|
|
108
|
+
start: enterInApolloServerCoreRequest,
|
|
109
|
+
asyncEnd: beforeWriteApolloGraphqlResponse
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
apolloChannel.subscribe({
|
|
113
|
+
start: enterInApolloRequest,
|
|
114
|
+
asyncEnd: beforeWriteApolloGraphqlResponse
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function disableApollo () {
|
|
119
|
+
graphqlMiddlewareChannel.unsubscribe({
|
|
120
|
+
start: enterInApolloMiddleware,
|
|
121
|
+
end: exitFromApolloMiddleware
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
apolloServerCoreChannel.unsubscribe({
|
|
125
|
+
start: enterInApolloServerCoreRequest,
|
|
126
|
+
asyncEnd: beforeWriteApolloGraphqlResponse
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
apolloChannel.unsubscribe({
|
|
130
|
+
start: enterInApolloRequest,
|
|
131
|
+
asyncEnd: beforeWriteApolloGraphqlResponse
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function enableGraphql () {
|
|
136
|
+
startGraphqlResolve.subscribe(onGraphqlStartResolve)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function disableGraphql () {
|
|
140
|
+
if (startGraphqlResolve.hasSubscribers) startGraphqlResolve.unsubscribe(onGraphqlStartResolve)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
enable,
|
|
145
|
+
disable
|
|
146
|
+
}
|
|
@@ -6,7 +6,6 @@ const remoteConfig = require('./remote_config')
|
|
|
6
6
|
const {
|
|
7
7
|
bodyParser,
|
|
8
8
|
cookieParser,
|
|
9
|
-
graphqlFinishExecute,
|
|
10
9
|
incomingHttpRequestStart,
|
|
11
10
|
incomingHttpRequestEnd,
|
|
12
11
|
passportVerify,
|
|
@@ -18,29 +17,24 @@ const waf = require('./waf')
|
|
|
18
17
|
const addresses = require('./addresses')
|
|
19
18
|
const Reporter = require('./reporter')
|
|
20
19
|
const appsecTelemetry = require('./telemetry')
|
|
20
|
+
const apiSecuritySampler = require('./api_security_sampler')
|
|
21
21
|
const web = require('../plugins/util/web')
|
|
22
22
|
const { extractIp } = require('../plugins/util/ip_extractor')
|
|
23
23
|
const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
|
|
24
24
|
const { block, setTemplates } = require('./blocking')
|
|
25
25
|
const { passportTrackEvent } = require('./passport')
|
|
26
26
|
const { storage } = require('../../../datadog-core')
|
|
27
|
+
const graphql = require('./graphql')
|
|
27
28
|
|
|
28
29
|
let isEnabled = false
|
|
29
30
|
let config
|
|
30
31
|
|
|
31
|
-
function sampleRequest ({ enabled, requestSampling }) {
|
|
32
|
-
if (!enabled || !requestSampling) {
|
|
33
|
-
return false
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return Math.random() <= requestSampling
|
|
37
|
-
}
|
|
38
|
-
|
|
39
32
|
function enable (_config) {
|
|
40
33
|
if (isEnabled) return
|
|
41
34
|
|
|
42
35
|
try {
|
|
43
36
|
appsecTelemetry.enable(_config.telemetry)
|
|
37
|
+
graphql.enable()
|
|
44
38
|
|
|
45
39
|
setTemplates(_config)
|
|
46
40
|
|
|
@@ -50,6 +44,8 @@ function enable (_config) {
|
|
|
50
44
|
|
|
51
45
|
Reporter.setRateLimit(_config.appsec.rateLimit)
|
|
52
46
|
|
|
47
|
+
apiSecuritySampler.configure(_config.appsec)
|
|
48
|
+
|
|
53
49
|
incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
|
|
54
50
|
incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
|
|
55
51
|
bodyParser.subscribe(onRequestBodyParsed)
|
|
@@ -57,7 +53,6 @@ function enable (_config) {
|
|
|
57
53
|
nextQueryParsed.subscribe(onRequestQueryParsed)
|
|
58
54
|
queryParser.subscribe(onRequestQueryParsed)
|
|
59
55
|
cookieParser.subscribe(onRequestCookieParser)
|
|
60
|
-
graphqlFinishExecute.subscribe(onGraphqlFinishExecute)
|
|
61
56
|
|
|
62
57
|
if (_config.appsec.eventTracking.enabled) {
|
|
63
58
|
passportVerify.subscribe(onPassportVerify)
|
|
@@ -88,21 +83,21 @@ function incomingHttpStartTranslator ({ req, res, abortController }) {
|
|
|
88
83
|
const requestHeaders = Object.assign({}, req.headers)
|
|
89
84
|
delete requestHeaders.cookie
|
|
90
85
|
|
|
91
|
-
const
|
|
86
|
+
const persistent = {
|
|
92
87
|
[addresses.HTTP_INCOMING_URL]: req.url,
|
|
93
88
|
[addresses.HTTP_INCOMING_HEADERS]: requestHeaders,
|
|
94
89
|
[addresses.HTTP_INCOMING_METHOD]: req.method
|
|
95
90
|
}
|
|
96
91
|
|
|
97
92
|
if (clientIp) {
|
|
98
|
-
|
|
93
|
+
persistent[addresses.HTTP_CLIENT_IP] = clientIp
|
|
99
94
|
}
|
|
100
95
|
|
|
101
|
-
if (sampleRequest(
|
|
102
|
-
|
|
96
|
+
if (apiSecuritySampler.sampleRequest()) {
|
|
97
|
+
persistent[addresses.WAF_CONTEXT_PROCESSOR] = { 'extract-schema': true }
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
const actions = waf.run(
|
|
100
|
+
const actions = waf.run({ persistent }, req)
|
|
106
101
|
|
|
107
102
|
handleResults(actions, req, res, rootSpan, abortController)
|
|
108
103
|
}
|
|
@@ -112,7 +107,7 @@ function incomingHttpEndTranslator ({ req, res }) {
|
|
|
112
107
|
const responseHeaders = Object.assign({}, res.getHeaders())
|
|
113
108
|
delete responseHeaders['set-cookie']
|
|
114
109
|
|
|
115
|
-
const
|
|
110
|
+
const persistent = {
|
|
116
111
|
[addresses.HTTP_INCOMING_RESPONSE_CODE]: '' + res.statusCode,
|
|
117
112
|
[addresses.HTTP_INCOMING_RESPONSE_HEADERS]: responseHeaders
|
|
118
113
|
}
|
|
@@ -120,24 +115,24 @@ function incomingHttpEndTranslator ({ req, res }) {
|
|
|
120
115
|
// we need to keep this to support other body parsers
|
|
121
116
|
// TODO: no need to analyze it if it was already done by the body-parser hook
|
|
122
117
|
if (req.body !== undefined && req.body !== null) {
|
|
123
|
-
|
|
118
|
+
persistent[addresses.HTTP_INCOMING_BODY] = req.body
|
|
124
119
|
}
|
|
125
120
|
|
|
126
121
|
// TODO: temporary express instrumentation, will use express plugin later
|
|
127
122
|
if (req.params && typeof req.params === 'object') {
|
|
128
|
-
|
|
123
|
+
persistent[addresses.HTTP_INCOMING_PARAMS] = req.params
|
|
129
124
|
}
|
|
130
125
|
|
|
131
126
|
// we need to keep this to support other cookie parsers
|
|
132
127
|
if (req.cookies && typeof req.cookies === 'object') {
|
|
133
|
-
|
|
128
|
+
persistent[addresses.HTTP_INCOMING_COOKIES] = req.cookies
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
if (req.query && typeof req.query === 'object') {
|
|
137
|
-
|
|
132
|
+
persistent[addresses.HTTP_INCOMING_QUERY] = req.query
|
|
138
133
|
}
|
|
139
134
|
|
|
140
|
-
waf.run(
|
|
135
|
+
waf.run({ persistent }, req)
|
|
141
136
|
|
|
142
137
|
waf.disposeContext(req)
|
|
143
138
|
|
|
@@ -156,7 +151,9 @@ function onRequestBodyParsed ({ req, res, body, abortController }) {
|
|
|
156
151
|
if (!rootSpan) return
|
|
157
152
|
|
|
158
153
|
const results = waf.run({
|
|
159
|
-
|
|
154
|
+
persistent: {
|
|
155
|
+
[addresses.HTTP_INCOMING_BODY]: body
|
|
156
|
+
}
|
|
160
157
|
}, req)
|
|
161
158
|
|
|
162
159
|
handleResults(results, req, res, rootSpan, abortController)
|
|
@@ -174,7 +171,9 @@ function onRequestQueryParsed ({ req, res, query, abortController }) {
|
|
|
174
171
|
if (!rootSpan) return
|
|
175
172
|
|
|
176
173
|
const results = waf.run({
|
|
177
|
-
|
|
174
|
+
persistent: {
|
|
175
|
+
[addresses.HTTP_INCOMING_QUERY]: query
|
|
176
|
+
}
|
|
178
177
|
}, req)
|
|
179
178
|
|
|
180
179
|
handleResults(results, req, res, rootSpan, abortController)
|
|
@@ -187,7 +186,9 @@ function onRequestCookieParser ({ req, res, abortController, cookies }) {
|
|
|
187
186
|
if (!rootSpan) return
|
|
188
187
|
|
|
189
188
|
const results = waf.run({
|
|
190
|
-
|
|
189
|
+
persistent: {
|
|
190
|
+
[addresses.HTTP_INCOMING_COOKIES]: cookies
|
|
191
|
+
}
|
|
191
192
|
}, req)
|
|
192
193
|
|
|
193
194
|
handleResults(results, req, res, rootSpan, abortController)
|
|
@@ -195,7 +196,7 @@ function onRequestCookieParser ({ req, res, abortController, cookies }) {
|
|
|
195
196
|
|
|
196
197
|
function onPassportVerify ({ credentials, user }) {
|
|
197
198
|
const store = storage.getStore()
|
|
198
|
-
const rootSpan = store
|
|
199
|
+
const rootSpan = store?.req && web.root(store.req)
|
|
199
200
|
|
|
200
201
|
if (!rootSpan) {
|
|
201
202
|
log.warn('No rootSpan found in onPassportVerify')
|
|
@@ -205,20 +206,6 @@ function onPassportVerify ({ credentials, user }) {
|
|
|
205
206
|
passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
function onGraphqlFinishExecute ({ context }) {
|
|
209
|
-
const store = storage.getStore()
|
|
210
|
-
const req = store?.req
|
|
211
|
-
|
|
212
|
-
if (!req) return
|
|
213
|
-
|
|
214
|
-
const resolvers = context?.resolvers
|
|
215
|
-
|
|
216
|
-
if (!resolvers || typeof resolvers !== 'object') return
|
|
217
|
-
|
|
218
|
-
// Don't collect blocking result because it only works in monitor mode.
|
|
219
|
-
waf.run({ [addresses.HTTP_INCOMING_GRAPHQL_RESOLVERS]: resolvers }, req)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
209
|
function handleResults (actions, req, res, rootSpan, abortController) {
|
|
223
210
|
if (!actions || !req || !res || !rootSpan || !abortController) return
|
|
224
211
|
|
|
@@ -234,12 +221,14 @@ function disable () {
|
|
|
234
221
|
RuleManager.clearAllRules()
|
|
235
222
|
|
|
236
223
|
appsecTelemetry.disable()
|
|
224
|
+
graphql.disable()
|
|
237
225
|
|
|
238
226
|
remoteConfig.disableWafUpdate()
|
|
239
227
|
|
|
228
|
+
apiSecuritySampler.disable()
|
|
229
|
+
|
|
240
230
|
// Channel#unsubscribe() is undefined for non active channels
|
|
241
231
|
if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
|
|
242
|
-
if (graphqlFinishExecute.hasSubscribers) graphqlFinishExecute.unsubscribe(onGraphqlFinishExecute)
|
|
243
232
|
if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator)
|
|
244
233
|
if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(incomingHttpEndTranslator)
|
|
245
234
|
if (queryParser.hasSubscribers) queryParser.unsubscribe(onRequestQueryParsed)
|
|
@@ -9,5 +9,10 @@ module.exports = {
|
|
|
9
9
|
ASM_USER_BLOCKING: 1n << 7n,
|
|
10
10
|
ASM_CUSTOM_RULES: 1n << 8n,
|
|
11
11
|
ASM_CUSTOM_BLOCKING_RESPONSE: 1n << 9n,
|
|
12
|
-
ASM_TRUSTED_IPS: 1n << 10n
|
|
12
|
+
ASM_TRUSTED_IPS: 1n << 10n,
|
|
13
|
+
ASM_API_SECURITY_SAMPLE_RATE: 1n << 11n,
|
|
14
|
+
APM_TRACING_SAMPLE_RATE: 1n << 12n,
|
|
15
|
+
APM_TRACING_LOGS_INJECTION: 1n << 13n,
|
|
16
|
+
APM_TRACING_HTTP_HEADER_TAGS: 1n << 14n,
|
|
17
|
+
APM_TRACING_CUSTOM_TAGS: 1n << 15n
|
|
13
18
|
}
|