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 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 `[ 'foo:bar' ]`. Values are combined with config.tags.
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 `[ 'foo:bar' ]`. Values are combined with config.tags.
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 `[ 'foo:bar' ]`. Values are combined with config.tags.
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 `[ 'foo:bar' ]`. Values are combined with config.tags.
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.14.0",
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/test/appsec/**/*.@($(echo $PLUGINS)).plugin.spec.js\" -- npm run test:appsec:plugins",
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 ? arguments[0].sql : arguments[0]
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('datadog:sequelize:query:start', ({ sql, dialect }) => {
24
- const parentStore = storage.getStore()
25
- if (parentStore) {
26
- this.analyze(sql, dialect.toUpperCase(), parentStore)
27
-
28
- storage.enterWith({ ...parentStore, sqlAnalyzed: true, sequelizeParentStore: parentStore })
29
- }
30
- })
31
-
32
- this.addSub('datadog:sequelize:query:finish', () => {
33
- const store = storage.getStore()
34
- if (store && store.sequelizeParentStore) {
35
- storage.enterWith(store.sequelizeParentStore)
36
- }
37
- })
38
-
39
- this.addSub('datadog:pg:pool:query:start', ({ query }) => {
40
- const parentStore = storage.getStore()
41
- if (parentStore) {
42
- this.analyze(query.text, 'POSTGRES', parentStore)
43
-
44
- storage.enterWith({ ...parentStore, sqlAnalyzed: true, pgPoolParentStore: parentStore })
45
- }
46
- })
47
-
48
- this.addSub('datadog:pg:pool:query:finish', () => {
49
- const store = storage.getStore()
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.ddwaf.update(newRules)
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.rulesInfo = rulesInfo
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.rulesInfo.version
42
+ rulesVersion: this.rulesVersion,
43
+ ruleTriggered,
44
+ blockTriggered,
45
+ wafVersion: this.wafVersion,
46
+ wafTimeout: result.timeout
39
47
  })
40
48
 
41
- if (result.data && result.data !== '[]') {
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._reportMetrics()
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()
@@ -55,7 +55,7 @@ class TracingPlugin extends Plugin {
55
55
  start () {} // implemented by individual plugins
56
56
 
57
57
  finish () {
58
- this.activeSpan.finish()
58
+ this.activeSpan?.finish()
59
59
  }
60
60
 
61
61
  error (error) {
@@ -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 = {