dd-trace 4.13.1 → 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.
Files changed (28) hide show
  1. package/index.d.ts +4 -4
  2. package/package.json +3 -3
  3. package/packages/datadog-instrumentations/src/graphql.js +5 -4
  4. package/packages/datadog-instrumentations/src/mocha.js +3 -0
  5. package/packages/datadog-instrumentations/src/mysql.js +39 -1
  6. package/packages/datadog-plugin-cypress/src/plugin.js +32 -5
  7. package/packages/datadog-plugin-graphql/src/resolve.js +27 -2
  8. package/packages/dd-trace/src/appsec/addresses.js +1 -0
  9. package/packages/dd-trace/src/appsec/channels.js +1 -0
  10. package/packages/dd-trace/src/appsec/iast/analyzers/ldap-injection-analyzer.js +7 -0
  11. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +28 -32
  12. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +19 -1
  13. package/packages/dd-trace/src/appsec/iast/path-line.js +1 -0
  14. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +48 -5
  15. package/packages/dd-trace/src/appsec/index.js +24 -2
  16. package/packages/dd-trace/src/appsec/recommended.json +107 -8
  17. package/packages/dd-trace/src/appsec/reporter.js +24 -0
  18. package/packages/dd-trace/src/appsec/telemetry.js +132 -0
  19. package/packages/dd-trace/src/appsec/waf/index.js +1 -1
  20. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +12 -4
  21. package/packages/dd-trace/src/appsec/waf/waf_manager.js +11 -14
  22. package/packages/dd-trace/src/dogstatsd.js +45 -5
  23. package/packages/dd-trace/src/plugins/tracing.js +1 -1
  24. package/packages/dd-trace/src/plugins/util/test.js +2 -2
  25. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -6
  26. package/packages/dd-trace/src/proxy.js +1 -11
  27. package/packages/dd-trace/src/runtime_metrics.js +1 -32
  28. 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 `[ '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.13.1",
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'",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@datadog/native-appsec": "^3.2.0",
71
- "@datadog/native-iast-rewriter": "2.0.1",
71
+ "@datadog/native-iast-rewriter": "2.1.3",
72
72
  "@datadog/native-iast-taint-tracking": "1.5.0",
73
73
  "@datadog/native-metrics": "^2.0.0",
74
74
  "@datadog/pprof": "3.2.0",
@@ -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
 
@@ -441,6 +441,9 @@ addHook({
441
441
  file: 'lib/cli/run-helpers.js'
442
442
  }, (run) => {
443
443
  shimmer.wrap(run, 'runMocha', runMocha => async function () {
444
+ if (!testStartCh.hasSubscribers) {
445
+ return runMocha.apply(this, arguments)
446
+ }
444
447
  const mocha = arguments[0]
445
448
  /**
446
449
  * This attaches `run` to the global context, which we'll call after
@@ -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
  })
@@ -25,6 +25,7 @@ const {
25
25
  } = require('../../dd-trace/src/plugins/util/test')
26
26
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
27
27
  const log = require('../../dd-trace/src/log')
28
+ const NoopTracer = require('../../dd-trace/src/noop/tracer')
28
29
 
29
30
  const TEST_FRAMEWORK_NAME = 'cypress'
30
31
 
@@ -119,10 +120,32 @@ function getSkippableTests (isSuitesSkippingEnabled, tracer, testConfiguration)
119
120
  })
120
121
  }
121
122
 
123
+ const noopTask = {
124
+ 'dd:testSuiteStart': () => {
125
+ return null
126
+ },
127
+ 'dd:beforeEach': () => {
128
+ return {}
129
+ },
130
+ 'dd:afterEach': () => {
131
+ return null
132
+ },
133
+ 'dd:addTags': () => {
134
+ return null
135
+ }
136
+ }
137
+
122
138
  module.exports = (on, config) => {
123
139
  let isTestsSkipped = false
124
140
  const skippedTests = []
125
141
  const tracer = require('../../dd-trace')
142
+
143
+ // The tracer was not init correctly for whatever reason (such as invalid DD_SITE)
144
+ if (tracer._tracer instanceof NoopTracer) {
145
+ // We still need to register these tasks or the support file will fail
146
+ return on('task', noopTask)
147
+ }
148
+
126
149
  const testEnvironmentMetadata = getTestEnvironmentMetadata(TEST_FRAMEWORK_NAME)
127
150
 
128
151
  const {
@@ -328,12 +351,16 @@ module.exports = (on, config) => {
328
351
  }
329
352
 
330
353
  return new Promise(resolve => {
331
- if (tracer._tracer._exporter.flush) {
332
- tracer._tracer._exporter.flush(() => {
354
+ const exporter = tracer._tracer._exporter
355
+ if (!exporter) {
356
+ return resolve(null)
357
+ }
358
+ if (exporter.flush) {
359
+ exporter.flush(() => {
333
360
  resolve(null)
334
361
  })
335
- } else {
336
- tracer._tracer._exporter._writer.flush(() => {
362
+ } else if (exporter._writer) {
363
+ exporter._writer.flush(() => {
337
364
  resolve(null)
338
365
  })
339
366
  }
@@ -375,7 +402,7 @@ module.exports = (on, config) => {
375
402
  'dd:afterEach': ({ test, coverage }) => {
376
403
  const { state, error, isRUMActive, testSourceLine, testSuite, testName } = test
377
404
  if (activeSpan) {
378
- if (coverage && tracer._tracer._exporter.exportCoverage && isCodeCoverageEnabled) {
405
+ if (coverage && isCodeCoverageEnabled && tracer._tracer._exporter && tracer._tracer._exporter.exportCoverage) {
379
406
  const coverageFiles = getCoveredFilenamesFromCoverage(coverage)
380
407
  const relativeCoverageFiles = coverageFiles.map(file => getTestSuitePath(file, rootDir))
381
408
  const { _traceId, _spanId } = testSuiteSpan.context()
@@ -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) {
@@ -6,6 +6,7 @@ const { addVulnerability } = require('../vulnerability-reporter')
6
6
  const { getIastContext } = require('../iast-context')
7
7
  const overheadController = require('../overhead-controller')
8
8
  const { SinkIastPlugin } = require('../iast-plugin')
9
+ const { getOriginalPathAndLineFromSourceMap } = require('../taint-tracking/rewriter')
9
10
 
10
11
  class Analyzer extends SinkIastPlugin {
11
12
  constructor (type) {
@@ -25,8 +26,9 @@ class Analyzer extends SinkIastPlugin {
25
26
  const evidence = this._getEvidence(value, context)
26
27
  const location = this._getLocation(value)
27
28
  if (!this._isExcluded(location)) {
29
+ const locationSourceMap = this._replaceLocationFromSourceMap(location)
28
30
  const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
29
- const vulnerability = this._createVulnerability(this._type, evidence, spanId, location)
31
+ const vulnerability = this._createVulnerability(this._type, evidence, spanId, locationSourceMap)
30
32
  addVulnerability(context, vulnerability)
31
33
  }
32
34
  }
@@ -47,6 +49,22 @@ class Analyzer extends SinkIastPlugin {
47
49
  return getFirstNonDDPathAndLine(this._getExcludedPaths())
48
50
  }
49
51
 
52
+ _replaceLocationFromSourceMap (location) {
53
+ if (location) {
54
+ const { path, line, column } = getOriginalPathAndLineFromSourceMap(location)
55
+ if (path) {
56
+ location.path = path
57
+ }
58
+ if (line) {
59
+ location.line = line
60
+ }
61
+ if (column) {
62
+ location.column = column
63
+ }
64
+ }
65
+ return location
66
+ }
67
+
50
68
  _getExcludedPaths () {}
51
69
 
52
70
  _isInvalidContext (store, iastContext) {
@@ -47,6 +47,7 @@ function getFirstNonDDPathAndLineFromCallsites (callsites, externallyExcludedPat
47
47
  return {
48
48
  path: path.relative(process.cwd(), filepath),
49
49
  line: callsite.getLineNumber(),
50
+ column: callsite.getColumnNumber(),
50
51
  isInternal: !path.isAbsolute(filepath)
51
52
  }
52
53
  }
@@ -10,12 +10,51 @@ const { getRewriteFunction } = require('./rewriter-telemetry')
10
10
 
11
11
  let rewriter
12
12
  let getPrepareStackTrace
13
+
14
+ let getRewriterOriginalPathAndLineFromSourceMap = function (path, line, column) {
15
+ return { path, line, column }
16
+ }
17
+
18
+ function isFlagPresent (flag) {
19
+ return process.env.NODE_OPTIONS?.includes(flag) ||
20
+ process.execArgv?.some(arg => arg.includes(flag))
21
+ }
22
+
23
+ function getGetOriginalPathAndLineFromSourceMapFunction (chainSourceMap, getOriginalPathAndLineFromSourceMap) {
24
+ if (chainSourceMap) {
25
+ return function (path, line, column) {
26
+ // if --enable-source-maps is present stacktraces of the rewritten files contain the original path, file and
27
+ // column because the sourcemap chaining is done during the rewriting process so we can skip it
28
+ if (isPrivateModule(path) && isNotLibraryFile(path)) {
29
+ return { path, line, column }
30
+ } else {
31
+ return getOriginalPathAndLineFromSourceMap(path, line, column)
32
+ }
33
+ }
34
+ } else {
35
+ return getOriginalPathAndLineFromSourceMap
36
+ }
37
+ }
38
+
13
39
  function getRewriter (telemetryVerbosity) {
14
40
  if (!rewriter) {
15
- const iastRewriter = require('@datadog/native-iast-rewriter')
16
- const Rewriter = iastRewriter.Rewriter
17
- getPrepareStackTrace = iastRewriter.getPrepareStackTrace
18
- rewriter = new Rewriter({ csiMethods, telemetryVerbosity: getName(telemetryVerbosity) })
41
+ try {
42
+ const iastRewriter = require('@datadog/native-iast-rewriter')
43
+ const Rewriter = iastRewriter.Rewriter
44
+ getPrepareStackTrace = iastRewriter.getPrepareStackTrace
45
+
46
+ const chainSourceMap = isFlagPresent('--enable-source-maps')
47
+ const getOriginalPathAndLineFromSourceMap = iastRewriter.getOriginalPathAndLineFromSourceMap
48
+ if (getOriginalPathAndLineFromSourceMap) {
49
+ getRewriterOriginalPathAndLineFromSourceMap =
50
+ getGetOriginalPathAndLineFromSourceMapFunction(chainSourceMap, getOriginalPathAndLineFromSourceMap)
51
+ }
52
+
53
+ rewriter = new Rewriter({ csiMethods, telemetryVerbosity: getName(telemetryVerbosity), chainSourceMap })
54
+ } catch (e) {
55
+ iastLog.error('Unable to initialize TaintTracking Rewriter')
56
+ .errorAndPublish(e)
57
+ }
19
58
  }
20
59
  return rewriter
21
60
  }
@@ -74,6 +113,10 @@ function disableRewriter () {
74
113
  Error.prepareStackTrace = originalPrepareStackTrace
75
114
  }
76
115
 
116
+ function getOriginalPathAndLineFromSourceMap ({ path, line, column }) {
117
+ return getRewriterOriginalPathAndLineFromSourceMap(path, line, column)
118
+ }
119
+
77
120
  module.exports = {
78
- enableRewriter, disableRewriter
121
+ enableRewriter, disableRewriter, getOriginalPathAndLineFromSourceMap
79
122
  }
@@ -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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "2.2",
3
3
  "metadata": {
4
- "rules_version": "1.7.1"
4
+ "rules_version": "1.7.2"
5
5
  },
6
6
  "rules": [
7
7
  {
@@ -1743,7 +1743,10 @@
1743
1743
  "sys/hypervisor",
1744
1744
  "sys/kernel",
1745
1745
  "sys/module",
1746
- "sys/power"
1746
+ "sys/power",
1747
+ "windows\\win.ini",
1748
+ "default\\ntuser.dat",
1749
+ "/var/run/secrets/kubernetes.io/serviceaccount"
1747
1750
  ]
1748
1751
  },
1749
1752
  "operator": "phrase_match"
@@ -2312,7 +2315,8 @@
2312
2315
  }
2313
2316
  ],
2314
2317
  "transformers": [
2315
- "lowercase"
2318
+ "lowercase",
2319
+ "cmdLine"
2316
2320
  ]
2317
2321
  },
2318
2322
  {
@@ -2950,7 +2954,7 @@
2950
2954
  "address": "grpc.server.request.message"
2951
2955
  }
2952
2956
  ],
2953
- "regex": "[\\s\\\"'`;\\/0-9=\\x0B\\x09\\x0C\\x3B\\x2C\\x28\\x3B]on(?:d(?:r(?:ag(?:en(?:ter|d)|leave|start|over)?|op)|urationchange|blclick)|s(?:e(?:ek(?:ing|ed)|arch|lect)|u(?:spend|bmit)|talled|croll|how)|m(?:ouse(?:(?:lea|mo)ve|o(?:ver|ut)|enter|down|up)|essage)|p(?:a(?:ge(?:hide|show)|(?:st|us)e)|lay(?:ing)?|rogress)|c(?:anplay(?:through)?|o(?:ntextmenu|py)|hange|lick|ut)|a(?:nimation(?:iteration|start|end)|(?:fterprin|bor)t)|t(?:o(?:uch(?:cancel|start|move|end)|ggle)|imeupdate)|f(?:ullscreen(?:change|error)|ocus(?:out|in)?)|(?:(?:volume|hash)chang|o(?:ff|n)lin)e|b(?:efore(?:unload|print)|lur)|load(?:ed(?:meta)?data|start)?|r(?:es(?:ize|et)|atechange)|key(?:press|down|up)|w(?:aiting|heel)|in(?:valid|put)|e(?:nded|rror)|unload)[\\s\\x0B\\x09\\x0C\\x3B\\x2C\\x28\\x3B]*?=[^=]",
2957
+ "regex": "\\bon(?:d(?:r(?:ag(?:en(?:ter|d)|leave|start|over)?|op)|urationchange|blclick)|s(?:e(?:ek(?:ing|ed)|arch|lect)|u(?:spend|bmit)|talled|croll|how)|m(?:ouse(?:(?:lea|mo)ve|o(?:ver|ut)|enter|down|up)|essage)|p(?:a(?:ge(?:hide|show)|(?:st|us)e)|lay(?:ing)?|rogress|aste|ointer(?:cancel|down|enter|leave|move|out|over|rawupdate|up))|c(?:anplay(?:through)?|o(?:ntextmenu|py)|hange|lick|ut)|a(?:nimation(?:iteration|start|end)|(?:fterprin|bor)t|uxclick|fterscriptexecute)|t(?:o(?:uch(?:cancel|start|move|end)|ggle)|imeupdate)|f(?:ullscreen(?:change|error)|ocus(?:out|in)?|inish)|(?:(?:volume|hash)chang|o(?:ff|n)lin)e|b(?:efore(?:unload|print)|lur)|load(?:ed(?:meta)?data|start|end)?|r(?:es(?:ize|et)|atechange)|key(?:press|down|up)|w(?:aiting|heel)|in(?:valid|put)|e(?:nded|rror)|unload)[\\s\\x0B\\x09\\x0C\\x3B\\x2C\\x28\\x3B]*?=[^=]",
2954
2958
  "options": {
2955
2959
  "min_length": 8
2956
2960
  }
@@ -4636,6 +4640,46 @@
4636
4640
  ],
4637
4641
  "transformers": []
4638
4642
  },
4643
+ {
4644
+ "id": "dog-913-008",
4645
+ "name": "Netsparker OOB domain",
4646
+ "tags": {
4647
+ "type": "commercial_scanner",
4648
+ "category": "attack_attempt",
4649
+ "tool_name": "Netsparker",
4650
+ "confidence": "0"
4651
+ },
4652
+ "conditions": [
4653
+ {
4654
+ "parameters": {
4655
+ "inputs": [
4656
+ {
4657
+ "address": "server.request.query"
4658
+ },
4659
+ {
4660
+ "address": "server.request.body"
4661
+ },
4662
+ {
4663
+ "address": "server.request.path_params"
4664
+ },
4665
+ {
4666
+ "address": "server.request.headers.no_cookies"
4667
+ },
4668
+ {
4669
+ "address": "grpc.server.request.message"
4670
+ }
4671
+ ],
4672
+ "regex": "\\b(?:\\.|(?:\\\\|&#)(?:0*46|x0*2e);)r87(?:\\.|(?:\\\\|&#)(?:0*46|x0*2e);)(?:me|com)\\b",
4673
+ "options": {
4674
+ "case_sensitive": false,
4675
+ "min_length": 7
4676
+ }
4677
+ },
4678
+ "operator": "match_regex"
4679
+ }
4680
+ ],
4681
+ "transformers": []
4682
+ },
4639
4683
  {
4640
4684
  "id": "dog-931-001",
4641
4685
  "name": "RFI: URL Payload to well known RFI target",
@@ -4699,6 +4743,56 @@
4699
4743
  ],
4700
4744
  "transformers": []
4701
4745
  },
4746
+ {
4747
+ "id": "dog-941-001",
4748
+ "name": "XSS in source property",
4749
+ "tags": {
4750
+ "type": "xss",
4751
+ "category": "attack_attempt",
4752
+ "confidence": "0"
4753
+ },
4754
+ "conditions": [
4755
+ {
4756
+ "parameters": {
4757
+ "inputs": [
4758
+ {
4759
+ "address": "server.request.headers.no_cookies",
4760
+ "key_path": [
4761
+ "user-agent"
4762
+ ]
4763
+ },
4764
+ {
4765
+ "address": "server.request.headers.no_cookies",
4766
+ "key_path": [
4767
+ "referer"
4768
+ ]
4769
+ },
4770
+ {
4771
+ "address": "server.request.query"
4772
+ },
4773
+ {
4774
+ "address": "server.request.body"
4775
+ },
4776
+ {
4777
+ "address": "server.request.path_params"
4778
+ },
4779
+ {
4780
+ "address": "grpc.server.request.message"
4781
+ }
4782
+ ],
4783
+ "regex": "<(?:iframe|esi:include)(?:(?:\\s|/)*\\w+=[\"'\\w]+)*(?:\\s|/)*src(?:doc)?=[\"']?(?:data:|javascript:|http:|//)[^\\s'\"]+['\"]?",
4784
+ "options": {
4785
+ "min_length": 14
4786
+ }
4787
+ },
4788
+ "operator": "match_regex"
4789
+ }
4790
+ ],
4791
+ "transformers": [
4792
+ "removeNulls",
4793
+ "urlDecodeUni"
4794
+ ]
4795
+ },
4702
4796
  {
4703
4797
  "id": "dog-942-001",
4704
4798
  "name": "Blind XSS callback domains",
@@ -5428,12 +5522,14 @@
5428
5522
  "address": "grpc.server.request.message"
5429
5523
  }
5430
5524
  ],
5431
- "regex": "(?i)[&|]\\s*cat\\s+\\/etc\\/[\\w\\.\\/]*passwd\\s*[&|]"
5525
+ "regex": "(?i)[&|]\\s*cat\\s*\\/etc\\/[\\w\\.\\/]*passwd\\s*[&|]"
5432
5526
  },
5433
5527
  "operator": "match_regex"
5434
5528
  }
5435
5529
  ],
5436
- "transformers": []
5530
+ "transformers": [
5531
+ "cmdLine"
5532
+ ]
5437
5533
  },
5438
5534
  {
5439
5535
  "id": "sqr-000-010",
@@ -7014,7 +7110,10 @@
7014
7110
  ]
7015
7111
  }
7016
7112
  ],
7017
- "regex": "mozilla/4\\.0 \\(compatible(; msie 6\\.0; win32)?\\)"
7113
+ "regex": "mozilla/4\\.0 \\(compatible(; msie (?:6\\.0; win32|4\\.0; Windows NT))?\\)",
7114
+ "options": {
7115
+ "case_sensitive": false
7116
+ }
7018
7117
  },
7019
7118
  "operator": "match_regex"
7020
7119
  }
@@ -7076,4 +7175,4 @@
7076
7175
  "transformers": []
7077
7176
  }
7078
7177
  ]
7079
- }
7178
+ }
@@ -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()
@@ -5,6 +5,7 @@ const request = require('./exporters/common/request')
5
5
  const dgram = require('dgram')
6
6
  const isIP = require('net').isIP
7
7
  const log = require('./log')
8
+ const { URL, format } = require('url')
8
9
 
9
10
  const MAX_BUFFER_SIZE = 1024 // limit from the agent
10
11
 
@@ -13,9 +14,7 @@ const TYPE_GAUGE = 'g'
13
14
  const TYPE_DISTRIBUTION = 'd'
14
15
 
15
16
  class DogStatsDClient {
16
- constructor (options) {
17
- options = options || {}
18
-
17
+ constructor (options = {}) {
19
18
  if (options.metricsProxyUrl) {
20
19
  this._httpOptions = {
21
20
  url: options.metricsProxyUrl.toString(),
@@ -50,6 +49,8 @@ class DogStatsDClient {
50
49
  flush () {
51
50
  const queue = this._enqueue()
52
51
 
52
+ log.debug(`Flushing ${queue.length} metrics via ${this._httpOptions ? 'HTTP' : 'UDP'}`)
53
+
53
54
  if (this._queue.length === 0) return
54
55
 
55
56
  this._queue = []
@@ -141,6 +142,44 @@ class DogStatsDClient {
141
142
 
142
143
  return socket
143
144
  }
145
+
146
+ static generateClientConfig (config = {}) {
147
+ const tags = []
148
+
149
+ if (config.tags) {
150
+ Object.keys(config.tags)
151
+ .filter(key => typeof config.tags[key] === 'string')
152
+ .filter(key => {
153
+ // Skip runtime-id unless enabled as cardinality may be too high
154
+ if (key !== 'runtime-id') return true
155
+ return (config.experimental && config.experimental.runtimeId)
156
+ })
157
+ .forEach(key => {
158
+ // https://docs.datadoghq.com/tagging/#defining-tags
159
+ const value = config.tags[key].replace(/[^a-z0-9_:./-]/ig, '_')
160
+
161
+ tags.push(`${key}:${value}`)
162
+ })
163
+ }
164
+
165
+ const clientConfig = {
166
+ host: config.dogstatsd.hostname,
167
+ port: config.dogstatsd.port,
168
+ tags
169
+ }
170
+
171
+ if (config.url) {
172
+ clientConfig.metricsProxyUrl = config.url
173
+ } else if (config.port) {
174
+ clientConfig.metricsProxyUrl = new URL(format({
175
+ protocol: 'http:',
176
+ hostname: config.hostname || 'localhost',
177
+ port: config.port
178
+ }))
179
+ }
180
+
181
+ return clientConfig
182
+ }
144
183
  }
145
184
 
146
185
  class NoopDogStatsDClient {
@@ -155,8 +194,9 @@ class NoopDogStatsDClient {
155
194
 
156
195
  // This is a simplified user-facing proxy to the underlying DogStatsDClient instance
157
196
  class CustomMetrics {
158
- constructor (options) {
159
- this.dogstatsd = new DogStatsDClient(options)
197
+ constructor (config) {
198
+ const clientConfig = DogStatsDClient.generateClientConfig(config)
199
+ this.dogstatsd = new DogStatsDClient(clientConfig)
160
200
  }
161
201
 
162
202
  increment (stat, value = 1, tags) {
@@ -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) {
@@ -139,13 +139,13 @@ function removeInvalidMetadata (metadata) {
139
139
  return Object.keys(metadata).reduce((filteredTags, tag) => {
140
140
  if (tag === GIT_REPOSITORY_URL) {
141
141
  if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) {
142
- log.error('DD_GIT_REPOSITORY_URL must be a valid URL')
142
+ log.error(`Repository URL is not a valid repository URL: ${metadata[GIT_REPOSITORY_URL]}.`)
143
143
  return filteredTags
144
144
  }
145
145
  }
146
146
  if (tag === GIT_COMMIT_SHA) {
147
147
  if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) {
148
- log.error('DD_GIT_COMMIT_SHA must be a full-length git SHA')
148
+ log.error(`Git commit SHA must be a full-length git SHA: ${metadata[GIT_COMMIT_SHA]}.`)
149
149
  return filteredTags
150
150
  }
151
151
  }
@@ -6,9 +6,11 @@ const dc = require('../../../../diagnostics_channel')
6
6
  const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../../ext/tags')
7
7
  const { WEB } = require('../../../../../ext/types')
8
8
  const runtimeMetrics = require('../../runtime_metrics')
9
+ const telemetryMetrics = require('../../telemetry/metrics')
9
10
 
10
11
  const beforeCh = dc.channel('dd-trace:storage:before')
11
12
  const enterCh = dc.channel('dd-trace:storage:enter')
13
+ const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
12
14
 
13
15
  let kSampleCount
14
16
 
@@ -168,6 +170,16 @@ class NativeWallProfiler {
168
170
  }
169
171
  }
170
172
 
173
+ _reportV8bug (maybeBug) {
174
+ const tag = `v8_profiler_bug_workaround_enabled:${this._v8ProfilerBugWorkaroundEnabled}`
175
+ const metric = `v8_cpu_profiler${maybeBug ? '_maybe' : ''}_stuck_event_loop`
176
+ this._logger?.warn(`Wall profiler: ${maybeBug ? 'possible ' : ''}v8 profiler stuck event loop detected.`)
177
+ // report as runtime metric (can be removed in the future when telemetry is mature)
178
+ runtimeMetrics.increment(`runtime.node.profiler.${metric}`, tag, true)
179
+ // report as telemetry metric
180
+ profilerTelemetryMetrics.count(metric, [tag]).inc()
181
+ }
182
+
171
183
  _stop (restart) {
172
184
  if (!this._started) return
173
185
  if (this._codeHotspotsEnabled) {
@@ -178,12 +190,8 @@ class NativeWallProfiler {
178
190
  const profile = this._pprof.time.stop(restart, this._codeHotspotsEnabled ? generateLabels : undefined)
179
191
  if (restart) {
180
192
  const v8BugDetected = this._pprof.time.v8ProfilerStuckEventLoopDetected()
181
- if (v8BugDetected === 1) {
182
- this._logger?.warn('Wall profiler: possible v8 profiler stuck event loop detected.')
183
- runtimeMetrics.increment('runtime.node.profiler.v8_cpu_profiler_maybe_stuck_event_loop', undefined, true)
184
- } else if (v8BugDetected === 2) {
185
- this._logger?.warn('Wall profiler: v8 profiler stuck event loop detected.')
186
- runtimeMetrics.increment('runtime.node.profiler.v8_cpu_profiler_stuck_event_loop', undefined, true)
193
+ if (v8BugDetected !== 0) {
194
+ this._reportV8bug(v8BugDetected === 1)
187
195
  }
188
196
  }
189
197
  return profile
@@ -30,17 +30,7 @@ class Tracer extends NoopProxy {
30
30
 
31
31
  if (config.dogstatsd) {
32
32
  // Custom Metrics
33
- this.dogstatsd = new dogstatsd.CustomMetrics({
34
- host: config.dogstatsd.hostname,
35
- port: config.dogstatsd.port,
36
- tags: [
37
- // these are the Runtime Metrics default tags
38
- // Python also uses these as default Custom Metrics tags
39
- `service:${config.tags.service}`,
40
- `env:${config.tags.env}`,
41
- `version:${config.tags.version}`
42
- ]
43
- })
33
+ this.dogstatsd = new dogstatsd.CustomMetrics(config)
44
34
 
45
35
  setInterval(() => {
46
36
  this.dogstatsd.flush()
@@ -2,7 +2,6 @@
2
2
 
3
3
  // TODO: capture every second and flush every 10 seconds
4
4
 
5
- const { URL, format } = require('url')
6
5
  const v8 = require('v8')
7
6
  const os = require('os')
8
7
  const { DogStatsDClient } = require('./dogstatsd')
@@ -27,21 +26,7 @@ reset()
27
26
 
28
27
  module.exports = {
29
28
  start (config) {
30
- const tags = []
31
-
32
- Object.keys(config.tags)
33
- .filter(key => typeof config.tags[key] === 'string')
34
- .filter(key => {
35
- // Skip runtime-id unless enabled as cardinality may be too high
36
- if (key !== 'runtime-id') return true
37
- return (config.experimental && config.experimental.runtimeId)
38
- })
39
- .forEach(key => {
40
- // https://docs.datadoghq.com/tagging/#defining-tags
41
- const value = config.tags[key].replace(/[^a-z0-9_:./-]/ig, '_')
42
-
43
- tags.push(`${key}:${value}`)
44
- })
29
+ const clientConfig = DogStatsDClient.generateClientConfig(config)
45
30
 
46
31
  try {
47
32
  nativeMetrics = require('@datadog/native-metrics')
@@ -51,22 +36,6 @@ module.exports = {
51
36
  nativeMetrics = null
52
37
  }
53
38
 
54
- const clientConfig = {
55
- host: config.dogstatsd.hostname,
56
- port: config.dogstatsd.port,
57
- tags
58
- }
59
-
60
- if (config.url) {
61
- clientConfig.metricsProxyUrl = config.url
62
- } else if (config.port) {
63
- clientConfig.metricsProxyUrl = new URL(format({
64
- protocol: 'http:',
65
- hostname: config.hostname || 'localhost',
66
- port: config.port
67
- }))
68
- }
69
-
70
39
  client = new DogStatsDClient(clientConfig)
71
40
 
72
41
  time = process.hrtime()
@@ -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 = {