dd-trace 4.14.0 → 4.15.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 +4 -4
- package/package.json +2 -2
- package/packages/datadog-instrumentations/src/graphql.js +5 -4
- package/packages/datadog-instrumentations/src/mysql.js +39 -1
- package/packages/datadog-plugin-graphql/src/resolve.js +27 -2
- package/packages/dd-trace/src/appsec/addresses.js +1 -0
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/ldap-injection-analyzer.js +7 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +28 -32
- package/packages/dd-trace/src/appsec/index.js +24 -2
- package/packages/dd-trace/src/appsec/reporter.js +24 -0
- package/packages/dd-trace/src/appsec/telemetry.js +132 -0
- package/packages/dd-trace/src/appsec/waf/index.js +1 -1
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +12 -4
- package/packages/dd-trace/src/appsec/waf/waf_manager.js +11 -14
- package/packages/dd-trace/src/plugins/tracing.js +1 -1
- package/packages/dd-trace/src/util.js +1 -1
package/index.d.ts
CHANGED
|
@@ -649,7 +649,7 @@ export declare interface DogStatsD {
|
|
|
649
649
|
* Increments a metric by the specified value, optionally specifying tags.
|
|
650
650
|
* @param {string} stat The dot-separated metric name.
|
|
651
651
|
* @param {number} value The amount to increment the stat by.
|
|
652
|
-
* @param {[tag:string]:string|number} tags Tags to pass along, such as `
|
|
652
|
+
* @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags.
|
|
653
653
|
*/
|
|
654
654
|
increment(stat: string, value?: number, tags?: { [tag: string]: string|number }): void
|
|
655
655
|
|
|
@@ -657,7 +657,7 @@ export declare interface DogStatsD {
|
|
|
657
657
|
* Decrements a metric by the specified value, optionally specifying tags.
|
|
658
658
|
* @param {string} stat The dot-separated metric name.
|
|
659
659
|
* @param {number} value The amount to decrement the stat by.
|
|
660
|
-
* @param {[tag:string]:string|number} tags Tags to pass along, such as `
|
|
660
|
+
* @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags.
|
|
661
661
|
*/
|
|
662
662
|
decrement(stat: string, value?: number, tags?: { [tag: string]: string|number }): void
|
|
663
663
|
|
|
@@ -665,7 +665,7 @@ export declare interface DogStatsD {
|
|
|
665
665
|
* Sets a distribution value, optionally specifying tags.
|
|
666
666
|
* @param {string} stat The dot-separated metric name.
|
|
667
667
|
* @param {number} value The amount to increment the stat by.
|
|
668
|
-
* @param {[tag:string]:string|number} tags Tags to pass along, such as `
|
|
668
|
+
* @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags.
|
|
669
669
|
*/
|
|
670
670
|
distribution(stat: string, value?: number, tags?: { [tag: string]: string|number }): void
|
|
671
671
|
|
|
@@ -673,7 +673,7 @@ export declare interface DogStatsD {
|
|
|
673
673
|
* Sets a gauge value, optionally specifying tags.
|
|
674
674
|
* @param {string} stat The dot-separated metric name.
|
|
675
675
|
* @param {number} value The amount to increment the stat by.
|
|
676
|
-
* @param {[tag:string]:string|number} tags Tags to pass along, such as `
|
|
676
|
+
* @param {[tag:string]:string|number} tags Tags to pass along, such as `{ foo: 'bar' }`. Values are combined with config.tags.
|
|
677
677
|
*/
|
|
678
678
|
gauge(stat: string, value?: number, tags?: { [tag: string]: string|number }): void
|
|
679
679
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.15.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"test:appsec": "mocha --colors --exit -r \"packages/dd-trace/test/setup/mocha.js\" --exclude \"packages/dd-trace/test/appsec/**/*.plugin.spec.js\" \"packages/dd-trace/test/appsec/**/*.spec.js\"",
|
|
19
19
|
"test:appsec:ci": "nyc --no-clean --include \"packages/dd-trace/src/appsec/**/*.js\" --exclude \"packages/dd-trace/test/appsec/**/*.plugin.spec.js\" -- npm run test:appsec",
|
|
20
20
|
"test:appsec:plugins": "mocha --colors --exit -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/appsec/**/*.@($(echo $PLUGINS)).plugin.spec.js\"",
|
|
21
|
-
"test:appsec:plugins:ci": "yarn services && nyc --no-clean --include \"packages/dd-trace/
|
|
21
|
+
"test:appsec:plugins:ci": "yarn services && nyc --no-clean --include \"packages/dd-trace/src/appsec/**/*.js\" -- npm run test:appsec:plugins",
|
|
22
22
|
"test:trace:core": "tap packages/dd-trace/test/*.spec.js \"packages/dd-trace/test/{ci-visibility,encode,exporters,opentelemetry,opentracing,plugins,service-naming,telemetry}/**/*.spec.js\"",
|
|
23
23
|
"test:trace:core:ci": "npm run test:trace:core -- --coverage --nyc-arg=--include=\"packages/dd-trace/src/**/*.js\"",
|
|
24
24
|
"test:instrumentations": "mocha --colors -r 'packages/dd-trace/test/setup/mocha.js' 'packages/datadog-instrumentations/test/**/*.spec.js'",
|
|
@@ -188,7 +188,7 @@ function wrapExecute (execute) {
|
|
|
188
188
|
executeErrorCh.publish(error)
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
finishExecuteCh.publish({ res, args })
|
|
191
|
+
finishExecuteCh.publish({ res, args, context })
|
|
192
192
|
})
|
|
193
193
|
})
|
|
194
194
|
}
|
|
@@ -205,7 +205,7 @@ function wrapResolve (resolve) {
|
|
|
205
205
|
|
|
206
206
|
if (!context) return resolve.apply(this, arguments)
|
|
207
207
|
|
|
208
|
-
const field = assertField(context, info)
|
|
208
|
+
const field = assertField(context, info, args)
|
|
209
209
|
|
|
210
210
|
return callInAsyncScope(resolve, field.asyncResource, this, arguments, (err) => {
|
|
211
211
|
updateFieldCh.publish({ field, info, err })
|
|
@@ -250,7 +250,7 @@ function pathToArray (path) {
|
|
|
250
250
|
return flattened.reverse()
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
function assertField (context, info) {
|
|
253
|
+
function assertField (context, info, args) {
|
|
254
254
|
const pathInfo = info && info.path
|
|
255
255
|
|
|
256
256
|
const path = pathToArray(pathInfo)
|
|
@@ -272,7 +272,8 @@ function assertField (context, info) {
|
|
|
272
272
|
childResource.runInAsyncScope(() => {
|
|
273
273
|
startResolveCh.publish({
|
|
274
274
|
info,
|
|
275
|
-
context
|
|
275
|
+
context,
|
|
276
|
+
args
|
|
276
277
|
})
|
|
277
278
|
})
|
|
278
279
|
|
|
@@ -17,7 +17,7 @@ addHook({ name: 'mysql', file: 'lib/Connection.js', versions: ['>=2'] }, Connect
|
|
|
17
17
|
return query.apply(this, arguments)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const sql = arguments[0].sql
|
|
20
|
+
const sql = arguments[0].sql || arguments[0]
|
|
21
21
|
const conf = this.config
|
|
22
22
|
const payload = { sql, conf }
|
|
23
23
|
|
|
@@ -66,9 +66,47 @@ addHook({ name: 'mysql', file: 'lib/Connection.js', versions: ['>=2'] }, Connect
|
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
addHook({ name: 'mysql', file: 'lib/Pool.js', versions: ['>=2'] }, Pool => {
|
|
69
|
+
const startPoolQueryCh = channel('datadog:mysql:pool:query:start')
|
|
70
|
+
const finishPoolQueryCh = channel('datadog:mysql:pool:query:finish')
|
|
71
|
+
|
|
69
72
|
shimmer.wrap(Pool.prototype, 'getConnection', getConnection => function (cb) {
|
|
70
73
|
arguments[0] = AsyncResource.bind(cb)
|
|
71
74
|
return getConnection.apply(this, arguments)
|
|
72
75
|
})
|
|
76
|
+
|
|
77
|
+
shimmer.wrap(Pool.prototype, 'query', query => function () {
|
|
78
|
+
if (!startPoolQueryCh.hasSubscribers) {
|
|
79
|
+
return query.apply(this, arguments)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const asyncResource = new AsyncResource('bound-anonymous-fn')
|
|
83
|
+
|
|
84
|
+
const sql = arguments[0].sql || arguments[0]
|
|
85
|
+
|
|
86
|
+
return asyncResource.runInAsyncScope(() => {
|
|
87
|
+
startPoolQueryCh.publish({ sql })
|
|
88
|
+
|
|
89
|
+
const finish = asyncResource.bind(function () {
|
|
90
|
+
finishPoolQueryCh.publish()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const cb = arguments[arguments.length - 1]
|
|
94
|
+
if (typeof cb === 'function') {
|
|
95
|
+
arguments[arguments.length - 1] = shimmer.wrap(cb, function () {
|
|
96
|
+
finish()
|
|
97
|
+
return cb.apply(this, arguments)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const retval = query.apply(this, arguments)
|
|
102
|
+
|
|
103
|
+
if (retval && retval.then) {
|
|
104
|
+
retval.then(finish).catch(finish)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return retval
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
73
111
|
return Pool
|
|
74
112
|
})
|
|
@@ -8,13 +8,14 @@ class GraphQLResolvePlugin extends TracingPlugin {
|
|
|
8
8
|
static get id () { return 'graphql' }
|
|
9
9
|
static get operation () { return 'resolve' }
|
|
10
10
|
|
|
11
|
-
start ({ info, context }) {
|
|
11
|
+
start ({ info, context, args }) {
|
|
12
12
|
const path = getPath(info, this.config)
|
|
13
13
|
|
|
14
14
|
if (!shouldInstrument(this.config, path)) return
|
|
15
|
-
|
|
16
15
|
const computedPathString = path.join('.')
|
|
17
16
|
|
|
17
|
+
addResolver(context, info, args)
|
|
18
|
+
|
|
18
19
|
if (this.config.collapse) {
|
|
19
20
|
if (!context[collapsedPathSym]) {
|
|
20
21
|
context[collapsedPathSym] = {}
|
|
@@ -108,4 +109,28 @@ function withCollapse (responsePathAsArray) {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
function addResolver (context, info, args) {
|
|
113
|
+
if (info.rootValue && !info.rootValue[info.fieldName]) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!context.resolvers) {
|
|
118
|
+
context.resolvers = {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const resolvers = context.resolvers
|
|
122
|
+
|
|
123
|
+
if (!resolvers[info.fieldName]) {
|
|
124
|
+
if (args && Object.keys(args).length) {
|
|
125
|
+
resolvers[info.fieldName] = [args]
|
|
126
|
+
} else {
|
|
127
|
+
resolvers[info.fieldName] = []
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
if (args && Object.keys(args).length) {
|
|
131
|
+
resolvers[info.fieldName].push(args)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
111
136
|
module.exports = GraphQLResolvePlugin
|
|
@@ -12,6 +12,7 @@ module.exports = {
|
|
|
12
12
|
HTTP_INCOMING_RESPONSE_CODE: 'server.response.status',
|
|
13
13
|
HTTP_INCOMING_RESPONSE_HEADERS: 'server.response.headers.no_cookies',
|
|
14
14
|
// TODO: 'server.response.trailers',
|
|
15
|
+
HTTP_INCOMING_GRAPHQL_RESOLVERS: 'graphql.server.all_resolvers',
|
|
15
16
|
|
|
16
17
|
HTTP_CLIENT_IP: 'http.client_ip',
|
|
17
18
|
|
|
@@ -5,6 +5,7 @@ const dc = require('../../../diagnostics_channel')
|
|
|
5
5
|
// TODO: use TBD naming convention
|
|
6
6
|
module.exports = {
|
|
7
7
|
bodyParser: dc.channel('datadog:body-parser:read:finish'),
|
|
8
|
+
graphqlFinishExecute: dc.channel('apm:graphql:execute:finish'),
|
|
8
9
|
incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
|
|
9
10
|
incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
|
|
10
11
|
passportVerify: dc.channel('datadog:passport:verify:finish'),
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const InjectionAnalyzer = require('./injection-analyzer')
|
|
3
3
|
const { LDAP_INJECTION } = require('../vulnerabilities')
|
|
4
|
+
const { getNodeModulesPaths } = require('../path-line')
|
|
5
|
+
|
|
6
|
+
const EXCLUDED_PATHS = getNodeModulesPaths('ldapjs-promise')
|
|
4
7
|
|
|
5
8
|
class LdapInjectionAnalyzer extends InjectionAnalyzer {
|
|
6
9
|
constructor () {
|
|
@@ -10,6 +13,10 @@ class LdapInjectionAnalyzer extends InjectionAnalyzer {
|
|
|
10
13
|
onConfigure () {
|
|
11
14
|
this.addSub('datadog:ldapjs:client:search', ({ base, filter }) => this.analyzeAll(base, filter))
|
|
12
15
|
}
|
|
16
|
+
|
|
17
|
+
_getExcludedPaths () {
|
|
18
|
+
return EXCLUDED_PATHS
|
|
19
|
+
}
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
module.exports = new LdapInjectionAnalyzer()
|
|
@@ -8,7 +8,7 @@ const { getIastContext } = require('../iast-context')
|
|
|
8
8
|
const { addVulnerability } = require('../vulnerability-reporter')
|
|
9
9
|
const { getNodeModulesPaths } = require('../path-line')
|
|
10
10
|
|
|
11
|
-
const EXCLUDED_PATHS = getNodeModulesPaths('mysql2', 'sequelize', 'pg-pool')
|
|
11
|
+
const EXCLUDED_PATHS = getNodeModulesPaths('mysql', 'mysql2', 'sequelize', 'pg-pool')
|
|
12
12
|
|
|
13
13
|
class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
14
14
|
constructor () {
|
|
@@ -20,37 +20,33 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
|
20
20
|
this.addSub('apm:mysql2:query:start', ({ sql }) => this.analyze(sql, 'MYSQL'))
|
|
21
21
|
this.addSub('apm:pg:query:start', ({ query }) => this.analyze(query.text, 'POSTGRES'))
|
|
22
22
|
|
|
23
|
-
this.addSub(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.addSub('datadog:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (store && store.pgPoolParentStore) {
|
|
51
|
-
storage.enterWith(store.pgPoolParentStore)
|
|
52
|
-
}
|
|
53
|
-
})
|
|
23
|
+
this.addSub(
|
|
24
|
+
'datadog:sequelize:query:start',
|
|
25
|
+
({ sql, dialect }) => this.getStoreAndAnalyze(sql, dialect.toUpperCase())
|
|
26
|
+
)
|
|
27
|
+
this.addSub('datadog:sequelize:query:finish', () => this.returnToParentStore())
|
|
28
|
+
|
|
29
|
+
this.addSub('datadog:pg:pool:query:start', ({ query }) => this.getStoreAndAnalyze(query.text, 'POSTGRES'))
|
|
30
|
+
this.addSub('datadog:pg:pool:query:finish', () => this.returnToParentStore())
|
|
31
|
+
|
|
32
|
+
this.addSub('datadog:mysql:pool:query:start', ({ sql }) => this.getStoreAndAnalyze(sql, 'MYSQL'))
|
|
33
|
+
this.addSub('datadog:mysql:pool:query:finish', () => this.returnToParentStore())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getStoreAndAnalyze (query, dialect) {
|
|
37
|
+
const parentStore = storage.getStore()
|
|
38
|
+
if (parentStore) {
|
|
39
|
+
this.analyze(query, dialect, parentStore)
|
|
40
|
+
|
|
41
|
+
storage.enterWith({ ...parentStore, sqlAnalyzed: true, sqlParentStore: parentStore })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
returnToParentStore () {
|
|
46
|
+
const store = storage.getStore()
|
|
47
|
+
if (store && store.sqlParentStore) {
|
|
48
|
+
storage.enterWith(store.sqlParentStore)
|
|
49
|
+
}
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
_getEvidence (value, iastContext, dialect) {
|
|
@@ -4,15 +4,17 @@ const log = require('../log')
|
|
|
4
4
|
const RuleManager = require('./rule_manager')
|
|
5
5
|
const remoteConfig = require('./remote_config')
|
|
6
6
|
const {
|
|
7
|
+
bodyParser,
|
|
8
|
+
graphqlFinishExecute,
|
|
7
9
|
incomingHttpRequestStart,
|
|
8
10
|
incomingHttpRequestEnd,
|
|
9
|
-
bodyParser,
|
|
10
11
|
passportVerify,
|
|
11
12
|
queryParser
|
|
12
13
|
} = require('./channels')
|
|
13
14
|
const waf = require('./waf')
|
|
14
15
|
const addresses = require('./addresses')
|
|
15
16
|
const Reporter = require('./reporter')
|
|
17
|
+
const appsecTelemetry = require('./telemetry')
|
|
16
18
|
const web = require('../plugins/util/web')
|
|
17
19
|
const { extractIp } = require('../plugins/util/ip_extractor')
|
|
18
20
|
const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
|
|
@@ -35,10 +37,13 @@ function enable (_config) {
|
|
|
35
37
|
|
|
36
38
|
Reporter.setRateLimit(_config.appsec.rateLimit)
|
|
37
39
|
|
|
40
|
+
appsecTelemetry.enable(_config.telemetry)
|
|
41
|
+
|
|
38
42
|
incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
|
|
39
43
|
incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
|
|
40
44
|
bodyParser.subscribe(onRequestBodyParsed)
|
|
41
45
|
queryParser.subscribe(onRequestQueryParsed)
|
|
46
|
+
graphqlFinishExecute.subscribe(onGraphqlFinishExecute)
|
|
42
47
|
|
|
43
48
|
if (_config.appsec.eventTracking.enabled) {
|
|
44
49
|
passportVerify.subscribe(onPassportVerify)
|
|
@@ -158,6 +163,20 @@ function onPassportVerify ({ credentials, user }) {
|
|
|
158
163
|
passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
|
|
159
164
|
}
|
|
160
165
|
|
|
166
|
+
function onGraphqlFinishExecute ({ context }) {
|
|
167
|
+
const store = storage.getStore()
|
|
168
|
+
const req = store?.req
|
|
169
|
+
|
|
170
|
+
if (!req) return
|
|
171
|
+
|
|
172
|
+
const resolvers = context?.resolvers
|
|
173
|
+
|
|
174
|
+
if (!resolvers || typeof resolvers !== 'object') return
|
|
175
|
+
|
|
176
|
+
// Don't collect blocking result because it only works in monitor mode.
|
|
177
|
+
waf.run({ [addresses.HTTP_INCOMING_GRAPHQL_RESOLVERS]: resolvers }, req)
|
|
178
|
+
}
|
|
179
|
+
|
|
161
180
|
function handleResults (actions, req, res, rootSpan, abortController) {
|
|
162
181
|
if (!actions || !req || !res || !rootSpan || !abortController) return
|
|
163
182
|
|
|
@@ -172,12 +191,15 @@ function disable () {
|
|
|
172
191
|
|
|
173
192
|
RuleManager.clearAllRules()
|
|
174
193
|
|
|
194
|
+
appsecTelemetry.disable()
|
|
195
|
+
|
|
175
196
|
remoteConfig.disableWafUpdate()
|
|
176
197
|
|
|
177
198
|
// Channel#unsubscribe() is undefined for non active channels
|
|
199
|
+
if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
|
|
200
|
+
if (graphqlFinishExecute.hasSubscribers) graphqlFinishExecute.unsubscribe(onGraphqlFinishExecute)
|
|
178
201
|
if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator)
|
|
179
202
|
if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(incomingHttpEndTranslator)
|
|
180
|
-
if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
|
|
181
203
|
if (queryParser.hasSubscribers) queryParser.unsubscribe(onRequestQueryParsed)
|
|
182
204
|
if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
|
|
183
205
|
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
const Limiter = require('../rate_limiter')
|
|
4
4
|
const { storage } = require('../../../datadog-core')
|
|
5
5
|
const web = require('../plugins/util/web')
|
|
6
|
+
const {
|
|
7
|
+
incrementWafInitMetric,
|
|
8
|
+
updateWafRequestsMetricTags,
|
|
9
|
+
incrementWafUpdatesMetric,
|
|
10
|
+
incrementWafRequestsMetric
|
|
11
|
+
} = require('./telemetry')
|
|
6
12
|
|
|
7
13
|
// default limiter, configurable with setRateLimit()
|
|
8
14
|
let limiter = new Limiter(100)
|
|
@@ -63,6 +69,18 @@ function formatHeaderName (name) {
|
|
|
63
69
|
.toLowerCase()
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
function reportWafInit (wafVersion, rulesInfo) {
|
|
73
|
+
metricsQueue.set('_dd.appsec.waf.version', wafVersion)
|
|
74
|
+
|
|
75
|
+
metricsQueue.set('_dd.appsec.event_rules.loaded', rulesInfo.loaded)
|
|
76
|
+
metricsQueue.set('_dd.appsec.event_rules.error_count', rulesInfo.failed)
|
|
77
|
+
if (rulesInfo.failed) metricsQueue.set('_dd.appsec.event_rules.errors', JSON.stringify(rulesInfo.errors))
|
|
78
|
+
|
|
79
|
+
metricsQueue.set('manual.keep', 'true')
|
|
80
|
+
|
|
81
|
+
incrementWafInitMetric(wafVersion, rulesInfo.version)
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
function reportMetrics (metrics) {
|
|
67
85
|
// TODO: metrics should be incremental, there already is an RFC to report metrics
|
|
68
86
|
const store = storage.getStore()
|
|
@@ -80,6 +98,8 @@ function reportMetrics (metrics) {
|
|
|
80
98
|
if (metrics.rulesVersion) {
|
|
81
99
|
rootSpan.setTag('_dd.appsec.event_rules.version', metrics.rulesVersion)
|
|
82
100
|
}
|
|
101
|
+
|
|
102
|
+
updateWafRequestsMetricTags(metrics, store.req)
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
function reportAttack (attackData) {
|
|
@@ -132,6 +152,8 @@ function finishRequest (req, res) {
|
|
|
132
152
|
metricsQueue.clear()
|
|
133
153
|
}
|
|
134
154
|
|
|
155
|
+
incrementWafRequestsMetric(req)
|
|
156
|
+
|
|
135
157
|
if (!rootSpan.context()._tags['appsec.event']) return
|
|
136
158
|
|
|
137
159
|
const newTags = filterHeaders(res.getHeaders(), RESPONSE_HEADERS_PASSLIST, 'http.response.headers.')
|
|
@@ -151,8 +173,10 @@ module.exports = {
|
|
|
151
173
|
metricsQueue,
|
|
152
174
|
filterHeaders,
|
|
153
175
|
formatHeaderName,
|
|
176
|
+
reportWafInit,
|
|
154
177
|
reportMetrics,
|
|
155
178
|
reportAttack,
|
|
179
|
+
reportWafUpdate: incrementWafUpdatesMetric,
|
|
156
180
|
finishRequest,
|
|
157
181
|
setRateLimit
|
|
158
182
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const telemetryMetrics = require('../telemetry/metrics')
|
|
4
|
+
|
|
5
|
+
const appsecMetrics = telemetryMetrics.manager.namespace('appsec')
|
|
6
|
+
|
|
7
|
+
const DD_TELEMETRY_WAF_RESULT_TAGS = Symbol('_dd.appsec.telemetry.waf.result.tags')
|
|
8
|
+
|
|
9
|
+
const tags = {
|
|
10
|
+
REQUEST_BLOCKED: 'request_blocked',
|
|
11
|
+
RULE_TRIGGERED: 'rule_triggered',
|
|
12
|
+
WAF_TIMEOUT: 'waf_timeout',
|
|
13
|
+
WAF_VERSION: 'waf_version',
|
|
14
|
+
EVENT_RULES_VERSION: 'event_rules_version'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const metricsStoreMap = new WeakMap()
|
|
18
|
+
|
|
19
|
+
let enabled = false
|
|
20
|
+
|
|
21
|
+
function enable (telemetryConfig) {
|
|
22
|
+
enabled = telemetryConfig?.enabled && telemetryConfig.metrics
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function disable () {
|
|
26
|
+
enabled = false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getStore (req) {
|
|
30
|
+
let store = metricsStoreMap.get(req)
|
|
31
|
+
if (!store) {
|
|
32
|
+
store = {}
|
|
33
|
+
metricsStoreMap.set(req, store)
|
|
34
|
+
}
|
|
35
|
+
return store
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getVersionsTags (wafVersion, rulesVersion) {
|
|
39
|
+
return {
|
|
40
|
+
[tags.WAF_VERSION]: wafVersion,
|
|
41
|
+
[tags.EVENT_RULES_VERSION]: rulesVersion
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function trackWafDurations (metrics, versionsTags) {
|
|
46
|
+
if (metrics.duration) {
|
|
47
|
+
appsecMetrics.distribution('waf.duration', versionsTags).track(metrics.duration)
|
|
48
|
+
}
|
|
49
|
+
if (metrics.durationExt) {
|
|
50
|
+
appsecMetrics.distribution('waf.duration_ext', versionsTags).track(metrics.durationExt)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getOrCreateMetricTags ({ wafVersion, rulesVersion }, req, versionsTags) {
|
|
55
|
+
const store = getStore(req)
|
|
56
|
+
|
|
57
|
+
let metricTags = store[DD_TELEMETRY_WAF_RESULT_TAGS]
|
|
58
|
+
if (!metricTags) {
|
|
59
|
+
metricTags = {
|
|
60
|
+
[tags.REQUEST_BLOCKED]: false,
|
|
61
|
+
[tags.RULE_TRIGGERED]: false,
|
|
62
|
+
[tags.WAF_TIMEOUT]: false,
|
|
63
|
+
|
|
64
|
+
...versionsTags
|
|
65
|
+
}
|
|
66
|
+
store[DD_TELEMETRY_WAF_RESULT_TAGS] = metricTags
|
|
67
|
+
}
|
|
68
|
+
return metricTags
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function updateWafRequestsMetricTags (metrics, req) {
|
|
72
|
+
if (!req || !enabled) return
|
|
73
|
+
|
|
74
|
+
const versionsTags = getVersionsTags(metrics.wafVersion, metrics.rulesVersion)
|
|
75
|
+
|
|
76
|
+
trackWafDurations(metrics, versionsTags)
|
|
77
|
+
|
|
78
|
+
const metricTags = getOrCreateMetricTags(metrics, req, versionsTags)
|
|
79
|
+
|
|
80
|
+
const { blockTriggered, ruleTriggered, wafTimeout } = metrics
|
|
81
|
+
|
|
82
|
+
if (blockTriggered) {
|
|
83
|
+
metricTags[tags.REQUEST_BLOCKED] = blockTriggered
|
|
84
|
+
}
|
|
85
|
+
if (ruleTriggered) {
|
|
86
|
+
metricTags[tags.RULE_TRIGGERED] = ruleTriggered
|
|
87
|
+
}
|
|
88
|
+
if (wafTimeout) {
|
|
89
|
+
metricTags[tags.WAF_TIMEOUT] = wafTimeout
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return metricTags
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function incrementWafInitMetric (wafVersion, rulesVersion) {
|
|
96
|
+
if (!enabled) return
|
|
97
|
+
|
|
98
|
+
const versionsTags = getVersionsTags(wafVersion, rulesVersion)
|
|
99
|
+
|
|
100
|
+
appsecMetrics.count('waf.init', versionsTags).inc()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function incrementWafUpdatesMetric (wafVersion, rulesVersion) {
|
|
104
|
+
if (!enabled) return
|
|
105
|
+
|
|
106
|
+
const versionsTags = getVersionsTags(wafVersion, rulesVersion)
|
|
107
|
+
|
|
108
|
+
appsecMetrics.count('waf.updates', versionsTags).inc()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function incrementWafRequestsMetric (req) {
|
|
112
|
+
if (!req || !enabled) return
|
|
113
|
+
|
|
114
|
+
const store = getStore(req)
|
|
115
|
+
|
|
116
|
+
const metricTags = store[DD_TELEMETRY_WAF_RESULT_TAGS]
|
|
117
|
+
if (metricTags) {
|
|
118
|
+
appsecMetrics.count('waf.requests', metricTags).inc()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
metricsStoreMap.delete(req)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
enable,
|
|
126
|
+
disable,
|
|
127
|
+
|
|
128
|
+
updateWafRequestsMetricTags,
|
|
129
|
+
incrementWafInitMetric,
|
|
130
|
+
incrementWafUpdatesMetric,
|
|
131
|
+
incrementWafRequestsMetric
|
|
132
|
+
}
|
|
@@ -39,7 +39,7 @@ function update (newRules) {
|
|
|
39
39
|
if (!waf.wafManager) throw new Error('Cannot update disabled WAF')
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
|
-
waf.wafManager.
|
|
42
|
+
waf.wafManager.update(newRules)
|
|
43
43
|
} catch (err) {
|
|
44
44
|
log.error('Could not apply rules from remote config')
|
|
45
45
|
throw err
|
|
@@ -4,11 +4,12 @@ const log = require('../../log')
|
|
|
4
4
|
const Reporter = require('../reporter')
|
|
5
5
|
|
|
6
6
|
class WAFContextWrapper {
|
|
7
|
-
constructor (ddwafContext, requiredAddresses, wafTimeout, rulesInfo) {
|
|
7
|
+
constructor (ddwafContext, requiredAddresses, wafTimeout, rulesInfo, wafVersion) {
|
|
8
8
|
this.ddwafContext = ddwafContext
|
|
9
9
|
this.requiredAddresses = requiredAddresses
|
|
10
10
|
this.wafTimeout = wafTimeout
|
|
11
|
-
this.
|
|
11
|
+
this.rulesVersion = rulesInfo.version
|
|
12
|
+
this.wafVersion = wafVersion
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
run (params) {
|
|
@@ -32,13 +33,20 @@ class WAFContextWrapper {
|
|
|
32
33
|
|
|
33
34
|
const end = process.hrtime.bigint()
|
|
34
35
|
|
|
36
|
+
const ruleTriggered = !!result.data && result.data !== '[]'
|
|
37
|
+
const blockTriggered = result.actions?.includes('block')
|
|
38
|
+
|
|
35
39
|
Reporter.reportMetrics({
|
|
36
40
|
duration: result.totalRuntime / 1e3,
|
|
37
41
|
durationExt: parseInt(end - start) / 1e3,
|
|
38
|
-
rulesVersion: this.
|
|
42
|
+
rulesVersion: this.rulesVersion,
|
|
43
|
+
ruleTriggered,
|
|
44
|
+
blockTriggered,
|
|
45
|
+
wafVersion: this.wafVersion,
|
|
46
|
+
wafTimeout: result.timeout
|
|
39
47
|
})
|
|
40
48
|
|
|
41
|
-
if (
|
|
49
|
+
if (ruleTriggered) {
|
|
42
50
|
Reporter.reportAttack(result.data)
|
|
43
51
|
}
|
|
44
52
|
|
|
@@ -11,7 +11,9 @@ class WAFManager {
|
|
|
11
11
|
this.config = config
|
|
12
12
|
this.wafTimeout = config.wafTimeout
|
|
13
13
|
this.ddwaf = this._loadDDWAF(rules)
|
|
14
|
-
this.
|
|
14
|
+
this.ddwafVersion = this.ddwaf.constructor.version()
|
|
15
|
+
|
|
16
|
+
Reporter.reportWafInit(this.ddwafVersion, this.ddwaf.rulesInfo)
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
_loadDDWAF (rules) {
|
|
@@ -28,18 +30,6 @@ class WAFManager {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
_reportMetrics () {
|
|
32
|
-
Reporter.metricsQueue.set('_dd.appsec.waf.version', this.ddwaf.constructor.version())
|
|
33
|
-
|
|
34
|
-
const { loaded, failed, errors } = this.ddwaf.rulesInfo
|
|
35
|
-
|
|
36
|
-
Reporter.metricsQueue.set('_dd.appsec.event_rules.loaded', loaded)
|
|
37
|
-
Reporter.metricsQueue.set('_dd.appsec.event_rules.error_count', failed)
|
|
38
|
-
if (failed) Reporter.metricsQueue.set('_dd.appsec.event_rules.errors', JSON.stringify(errors))
|
|
39
|
-
|
|
40
|
-
Reporter.metricsQueue.set('manual.keep', 'true')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
33
|
getWAFContext (req) {
|
|
44
34
|
let wafContext = contexts.get(req)
|
|
45
35
|
|
|
@@ -48,7 +38,8 @@ class WAFManager {
|
|
|
48
38
|
this.ddwaf.createContext(),
|
|
49
39
|
this.ddwaf.requiredAddresses,
|
|
50
40
|
this.wafTimeout,
|
|
51
|
-
this.ddwaf.rulesInfo
|
|
41
|
+
this.ddwaf.rulesInfo,
|
|
42
|
+
this.ddwafVersion
|
|
52
43
|
)
|
|
53
44
|
contexts.set(req, wafContext)
|
|
54
45
|
}
|
|
@@ -56,6 +47,12 @@ class WAFManager {
|
|
|
56
47
|
return wafContext
|
|
57
48
|
}
|
|
58
49
|
|
|
50
|
+
update (newRules) {
|
|
51
|
+
this.ddwaf.update(newRules)
|
|
52
|
+
|
|
53
|
+
Reporter.reportWafUpdate(this.ddwafVersion, this.ddwaf.rulesInfo.version)
|
|
54
|
+
}
|
|
55
|
+
|
|
59
56
|
destroy () {
|
|
60
57
|
if (this.ddwaf) {
|
|
61
58
|
this.ddwaf.dispose()
|
|
@@ -66,7 +66,7 @@ function globMatch (pattern, subject) {
|
|
|
66
66
|
function calculateDDBasePath (dirname) {
|
|
67
67
|
const dirSteps = dirname.split(path.sep)
|
|
68
68
|
const packagesIndex = dirSteps.lastIndexOf('packages')
|
|
69
|
-
return dirSteps.slice(0, packagesIndex).join(path.sep) + path.sep
|
|
69
|
+
return dirSteps.slice(0, packagesIndex + 1).join(path.sep) + path.sep
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
module.exports = {
|