dd-trace 3.2.0 → 3.3.1

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/LICENSE-3rdparty.csv +2 -1
  2. package/README.md +4 -0
  3. package/package.json +5 -4
  4. package/packages/datadog-instrumentations/src/crypto.js +14 -12
  5. package/packages/datadog-instrumentations/src/grpc/server.js +15 -7
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  7. package/packages/datadog-instrumentations/src/jest.js +136 -14
  8. package/packages/datadog-instrumentations/src/mocha.js +77 -31
  9. package/packages/datadog-instrumentations/src/next.js +7 -3
  10. package/packages/datadog-plugin-jest/src/index.js +106 -6
  11. package/packages/datadog-plugin-mocha/src/index.js +15 -7
  12. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  13. package/packages/dd-trace/src/appsec/iast/analyzers/weak-cipher-analyzer.js +27 -0
  14. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +13 -5
  15. package/packages/dd-trace/src/appsec/recommended.json +1144 -275
  16. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  17. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -3
  18. package/packages/dd-trace/src/config.js +16 -2
  19. package/packages/dd-trace/src/encode/0.4.js +7 -1
  20. package/packages/dd-trace/src/encode/0.5.js +7 -1
  21. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +2 -2
  22. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +32 -20
  23. package/packages/dd-trace/src/exporters/common/request.js +3 -3
  24. package/packages/dd-trace/src/opentracing/span.js +6 -0
  25. package/packages/dd-trace/src/plugins/index.js +3 -0
  26. package/packages/dd-trace/src/plugins/util/ip_blocklist.js +30 -4
  27. package/packages/dd-trace/src/plugins/util/redis.js +0 -74
  28. package/packages/dd-trace/src/plugins/util/tx.js +0 -75
@@ -15,7 +15,7 @@ class Writer extends BaseWriter {
15
15
  constructor ({ url }) {
16
16
  super(...arguments)
17
17
  this._url = url
18
- this._encoder = new CoverageCIVisibilityEncoder()
18
+ this._encoder = new CoverageCIVisibilityEncoder(this)
19
19
  }
20
20
 
21
21
  _sendPayload (form, _, done) {
@@ -25,10 +25,10 @@ class AgentlessCiVisibilityExporter {
25
25
  })
26
26
  }
27
27
 
28
- exportCoverage ({ testSpan, coverageFiles }) {
28
+ exportCoverage ({ span, coverageFiles }) {
29
29
  const formattedCoverage = {
30
- traceId: testSpan.context()._traceId,
31
- spanId: testSpan.context()._spanId,
30
+ traceId: span.context()._traceId,
31
+ spanId: span.context()._spanId,
32
32
  files: coverageFiles
33
33
  }
34
34
  this._coverageWriter.append(formattedCoverage)
@@ -14,7 +14,7 @@ const fromEntries = Object.fromEntries || (entries =>
14
14
  entries.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {}))
15
15
 
16
16
  // eslint-disable-next-line max-len
17
- const qsRegex = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:"|%22)(?:%2[^2]|%[^2]|[^"%])+(?:"|%22))|bearer(?:\\s|%20)+[a-z0-9\\._\\-]|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+\\/=-]|%3D|%2F|%2B)+)?|[\\-]{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY[\\-]{5}[^\\-]+[\\-]{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY|ssh-rsa(?:\\s|%20)*(?:[a-z0-9\\/\\.+]|%2F|%5C|%2B){100,}'
17
+ const qsRegex = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:"|%22)(?:%2[^2]|%[^2]|[^"%])+(?:"|%22))|bearer(?:\\s|%20)+[a-z0-9\\._\\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+\\/=-]|%3D|%2F|%2B)+)?|[\\-]{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY[\\-]{5}[^\\-]+[\\-]{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY|ssh-rsa(?:\\s|%20)*(?:[a-z0-9\\/\\.+]|%2F|%5C|%2B){100,}'
18
18
 
19
19
  function safeJsonParse (input) {
20
20
  try {
@@ -24,6 +24,16 @@ function safeJsonParse (input) {
24
24
  }
25
25
  }
26
26
 
27
+ // Shallow clone with property name remapping
28
+ function remapify (input, mappings) {
29
+ if (!input) return
30
+ const output = {}
31
+ for (const [key, value] of Object.entries(input)) {
32
+ output[key in mappings ? mappings[key] : key] = value
33
+ }
34
+ return output
35
+ }
36
+
27
37
  class Config {
28
38
  constructor (options) {
29
39
  options = options || {}
@@ -257,7 +267,11 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
257
267
  ingestion.sampleRate
258
268
  ),
259
269
  rateLimit: coalesce(options.rateLimit, process.env.DD_TRACE_RATE_LIMIT, ingestion.rateLimit),
260
- rules: coalesce(options.samplingRules, safeJsonParse(process.env.DD_TRACE_SAMPLING_RULES), [])
270
+ rules: coalesce(options.samplingRules, safeJsonParse(process.env.DD_TRACE_SAMPLING_RULES), []).map(rule => {
271
+ return remapify(rule, {
272
+ sample_rate: 'sampleRate'
273
+ })
274
+ })
261
275
  }
262
276
 
263
277
  const inAWSLambda = process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { truncateSpan, normalizeSpan } = require('./tags-processors')
3
4
  const Chunk = require('./chunk')
4
5
  const log = require('../log')
5
6
 
@@ -12,6 +13,10 @@ float64Array[0] = -1
12
13
 
13
14
  const bigEndian = uInt8Float64Array[7] === 0
14
15
 
16
+ function formatSpan (span) {
17
+ return normalizeSpan(truncateSpan(span))
18
+ }
19
+
15
20
  class AgentEncoder {
16
21
  constructor (writer, limit = SOFT_LIMIT) {
17
22
  this._limit = limit
@@ -66,7 +71,8 @@ class AgentEncoder {
66
71
  _encode (bytes, trace) {
67
72
  this._encodeArrayPrefix(bytes, trace)
68
73
 
69
- for (const span of trace) {
74
+ for (let span of trace) {
75
+ span = formatSpan(span)
70
76
  bytes.reserve(1)
71
77
 
72
78
  if (span.type) {
@@ -1,10 +1,15 @@
1
1
  'use strict'
2
2
 
3
+ const { truncateSpan, normalizeSpan } = require('./tags-processors')
3
4
  const { AgentEncoder: BaseEncoder } = require('./0.4')
4
5
 
5
6
  const ARRAY_OF_TWO = 0x92
6
7
  const ARRAY_OF_TWELVE = 0x9c
7
8
 
9
+ function formatSpan (span) {
10
+ return normalizeSpan(truncateSpan(span))
11
+ }
12
+
8
13
  class AgentEncoder extends BaseEncoder {
9
14
  makePayload () {
10
15
  const prefixSize = 1
@@ -27,7 +32,8 @@ class AgentEncoder extends BaseEncoder {
27
32
  _encode (bytes, trace) {
28
33
  this._encodeArrayPrefix(bytes, trace)
29
34
 
30
- for (const span of trace) {
35
+ for (let span of trace) {
36
+ span = formatSpan(span)
31
37
  this._encodeByte(bytes, ARRAY_OF_TWELVE)
32
38
  this._encodeString(bytes, span.service)
33
39
  this._encodeString(bytes, span.name)
@@ -134,11 +134,11 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
134
134
  */
135
135
  if (content.meta.test_session_id) {
136
136
  this._encodeString(bytes, 'test_session_id')
137
- this._encodeId(bytes, id(content.meta.test_session_id))
137
+ this._encodeId(bytes, id(content.meta.test_session_id, 10))
138
138
  delete content.meta.test_session_id
139
139
 
140
140
  this._encodeString(bytes, 'test_suite_id')
141
- this._encodeId(bytes, id(content.meta.test_suite_id))
141
+ this._encodeId(bytes, id(content.meta.test_suite_id, 10))
142
142
  delete content.meta.test_suite_id
143
143
  }
144
144
  this._encodeString(bytes, 'meta')
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
  const { AgentEncoder } = require('./0.4')
3
3
  const Chunk = require('./chunk')
4
+ const log = require('../log')
4
5
 
5
6
  const FormData = require('../exporters/common/form-data')
6
7
 
@@ -13,17 +14,34 @@ class CoverageCIVisibilityEncoder extends AgentEncoder {
13
14
  super(...arguments)
14
15
  this.codeCoverageBuffers = []
15
16
  this._coverageBytes = new Chunk()
17
+ this.form = new FormData()
18
+ this.fileIndex = 1
16
19
  this.reset()
17
20
  }
18
21
 
19
22
  count () {
20
- return this.codeCoverageBuffers.length
23
+ return this.fileIndex - 1
21
24
  }
22
25
 
23
26
  encode (coverage) {
24
27
  const bytes = this._coverageBytes
25
28
  const coverageBuffer = this.encodeCodeCoverage(bytes, coverage)
26
- this.codeCoverageBuffers.push(coverageBuffer)
29
+ const coverageFilename = `coverage${this.fileIndex++}`
30
+
31
+ this.form.append(
32
+ coverageFilename,
33
+ coverageBuffer,
34
+ {
35
+ filename: `${coverageFilename}.msgpack`,
36
+ contentType: 'application/msgpack'
37
+ }
38
+ )
39
+
40
+ if (this.fileIndex === MAXIMUM_NUM_COVERAGE_FILES) {
41
+ log.debug('Coverage buffer reached the limit, flushing')
42
+ this._writer.flush()
43
+ }
44
+
27
45
  this.reset()
28
46
  }
29
47
 
@@ -58,24 +76,18 @@ class CoverageCIVisibilityEncoder extends AgentEncoder {
58
76
  }
59
77
 
60
78
  makePayload () {
61
- const form = new FormData()
62
-
63
- let coverageFileIndex = 1
64
-
65
- for (const coverageBuffer of this.codeCoverageBuffers.slice(0, MAXIMUM_NUM_COVERAGE_FILES)) {
66
- const coverageFilename = `coverage${coverageFileIndex++}`
67
- form.append(
68
- coverageFilename,
69
- coverageBuffer,
70
- {
71
- filename: `${coverageFilename}.msgpack`,
72
- contentType: 'application/msgpack'
73
- }
74
- )
75
- }
76
- // 'event' is a backend requirement
77
- form.append('event', JSON.stringify({}), { filename: 'event.json', contentType: 'application/json' })
78
- this.codeCoverageBuffers = this.codeCoverageBuffers.slice(MAXIMUM_NUM_COVERAGE_FILES)
79
+ this.form.append(
80
+ 'event',
81
+ // The intake requires a populated dictionary here. Simply having {} is not valid.
82
+ // We use dummy: true but any other key/value pair would be valid.
83
+ JSON.stringify({ dummy: true }),
84
+ { filename: 'event.json', contentType: 'application/json' }
85
+ )
86
+
87
+ const form = this.form
88
+
89
+ this.form = new FormData()
90
+ this.fileIndex = 1
79
91
 
80
92
  return form
81
93
  }
@@ -11,10 +11,10 @@ const { storage } = require('../../../../datadog-core')
11
11
  const log = require('../../log')
12
12
 
13
13
  const keepAlive = true
14
- const maxTotalSockets = 1
14
+ const maxSockets = 1
15
15
  const maxActiveRequests = 8
16
- const httpAgent = new http.Agent({ keepAlive, maxTotalSockets })
17
- const httpsAgent = new https.Agent({ keepAlive, maxTotalSockets })
16
+ const httpAgent = new http.Agent({ keepAlive, maxSockets })
17
+ const httpsAgent = new https.Agent({ keepAlive, maxSockets })
18
18
  const containerId = docker.id()
19
19
 
20
20
  let activeRequests = 0
@@ -44,6 +44,9 @@ class DatadogSpan {
44
44
  metrics.increment('runtime.node.spans.unfinished')
45
45
  metrics.increment('runtime.node.spans.unfinished.by.name', `span_name:${operationName}`)
46
46
 
47
+ metrics.increment('runtime.node.spans.open') // unfinished for real
48
+ metrics.increment('runtime.node.spans.open.by.name', `span_name:${operationName}`)
49
+
47
50
  unfinishedRegistry.register(this, operationName, this)
48
51
  }
49
52
  }
@@ -121,6 +124,9 @@ class DatadogSpan {
121
124
  metrics.increment('runtime.node.spans.finished')
122
125
  metrics.increment('runtime.node.spans.finished.by.name', `span_name:${this._name}`)
123
126
 
127
+ metrics.decrement('runtime.node.spans.open') // unfinished for real
128
+ metrics.decrement('runtime.node.spans.open.by.name', `span_name:${this._name}`)
129
+
124
130
  unfinishedRegistry.unregister(this)
125
131
  finishedRegistry.register(this, this._name)
126
132
  }
@@ -7,6 +7,7 @@ module.exports = {
7
7
  get '@google-cloud/pubsub' () { return require('../../../datadog-plugin-google-cloud-pubsub/src') },
8
8
  get '@grpc/grpc-js' () { return require('../../../datadog-plugin-grpc/src') },
9
9
  get '@hapi/hapi' () { return require('../../../datadog-plugin-hapi/src') },
10
+ get '@jest/core' () { return require('../../../datadog-plugin-jest/src') },
10
11
  get '@koa/router' () { return require('../../../datadog-plugin-koa/src') },
11
12
  get '@node-redis/client' () { return require('../../../datadog-plugin-redis/src') },
12
13
  get 'amqp10' () { return require('../../../datadog-plugin-amqp10/src') },
@@ -29,6 +30,8 @@ module.exports = {
29
30
  get 'http2' () { return require('../../../datadog-plugin-http2/src') },
30
31
  get 'https' () { return require('../../../datadog-plugin-http/src') },
31
32
  get 'ioredis' () { return require('../../../datadog-plugin-ioredis/src') },
33
+ get 'jest-circus' () { return require('../../../datadog-plugin-jest/src') },
34
+ get 'jest-config' () { return require('../../../datadog-plugin-jest/src') },
32
35
  get 'jest-environment-node' () { return require('../../../datadog-plugin-jest/src') },
33
36
  get 'jest-environment-jsdom' () { return require('../../../datadog-plugin-jest/src') },
34
37
  get 'jest-jasmine2' () { return require('../../../datadog-plugin-jest/src') },
@@ -7,19 +7,45 @@ if (semver.satisfies(process.version, '>=14.18.0')) {
7
7
 
8
8
  module.exports = net.BlockList
9
9
  } else {
10
- const CIDRMatcher = require('cidr-matcher')
10
+ const ipaddr = require('ipaddr.js')
11
11
 
12
12
  module.exports = class BlockList {
13
13
  constructor () {
14
- this.matcher = new CIDRMatcher()
14
+ this.v4Ranges = []
15
+ this.v6Ranges = []
15
16
  }
16
17
 
17
18
  addSubnet (net, prefix, type) {
18
- this.matcher.addNetworkClass(`${net}/${prefix}`)
19
+ this[type === 'ipv4' ? 'v4Ranges' : 'v6Ranges'].push(ipaddr.parseCIDR(`${net}/${prefix}`))
19
20
  }
20
21
 
21
22
  check (address, type) {
22
- return this.matcher.contains(address)
23
+ try {
24
+ let ip = ipaddr.parse(address)
25
+
26
+ type = ip.kind()
27
+
28
+ if (type === 'ipv6') {
29
+ for (const range of this.v6Ranges) {
30
+ if (ip.match(range)) return true
31
+ }
32
+
33
+ if (ip.isIPv4MappedAddress()) {
34
+ ip = ip.toIPv4Address()
35
+ type = ip.kind()
36
+ }
37
+ }
38
+
39
+ if (type === 'ipv4') {
40
+ for (const range of this.v4Ranges) {
41
+ if (ip.match(range)) return true
42
+ }
43
+ }
44
+
45
+ return false
46
+ } catch {
47
+ return false
48
+ }
23
49
  }
24
50
  }
25
51
  }
@@ -1,74 +0,0 @@
1
- 'use strict'
2
-
3
- const analyticsSampler = require('../../analytics_sampler')
4
- const urlFilter = require('../util/urlfilter')
5
- const tx = require('./tx')
6
-
7
- const redis = {
8
- // Ensure the configuration has the correct structure and defaults.
9
- normalizeConfig (config) {
10
- const filter = urlFilter.getFilter(config)
11
-
12
- return Object.assign({}, config, {
13
- filter
14
- })
15
- },
16
-
17
- // Start a span for a Redis command.
18
- instrument (tracer, config, db, command, args) {
19
- const childOf = tracer.scope().active()
20
- const span = tracer.startSpan('redis.command', {
21
- childOf,
22
- tags: {
23
- 'span.kind': 'client',
24
- 'resource.name': command,
25
- 'span.type': 'redis',
26
- 'db.type': 'redis',
27
- 'db.name': db || '0',
28
- 'redis.raw_command': formatCommand(command, args)
29
- }
30
- })
31
-
32
- span.setTag('service.name', config.service || `${span.context()._tags['service.name']}-redis`)
33
-
34
- analyticsSampler.sample(span, config.measured)
35
-
36
- return span
37
- }
38
- }
39
-
40
- function formatCommand (command, args) {
41
- command = command.toUpperCase()
42
-
43
- if (!args || command === 'AUTH') return command
44
-
45
- for (let i = 0, l = args.length; i < l; i++) {
46
- if (typeof args[i] === 'function') continue
47
-
48
- command = `${command} ${formatArg(args[i])}`
49
-
50
- if (command.length > 1000) return trim(command, 1000)
51
- }
52
-
53
- return command
54
- }
55
-
56
- function formatArg (arg) {
57
- switch (typeof arg) {
58
- case 'string':
59
- case 'number':
60
- return trim(String(arg), 100)
61
- default:
62
- return '?'
63
- }
64
- }
65
-
66
- function trim (str, maxlen) {
67
- if (str.length > maxlen) {
68
- str = str.substr(0, maxlen - 3) + '...'
69
- }
70
-
71
- return str
72
- }
73
-
74
- module.exports = Object.assign({}, tx, redis)
@@ -1,75 +0,0 @@
1
- 'use strict'
2
-
3
- const tx = {
4
- // Set the outgoing host.
5
- setHost (span, hostname, port) {
6
- hostname && span.setTag('out.host', hostname)
7
- port && span.setTag('out.port', port)
8
- },
9
-
10
- // Wrap a promise or a callback to also finish the span.
11
- wrap (span, done) {
12
- if (typeof done === 'function' || !done) {
13
- return wrapCallback(span, done)
14
- } else if (isPromise(done)) {
15
- return wrapPromise(span, done)
16
- } else if (done && done.length) {
17
- return wrapArguments(span, done)
18
- }
19
- }
20
- }
21
-
22
- function wrapCallback (span, callback) {
23
- const scope = span.tracer().scope()
24
- const previous = scope.active()
25
-
26
- return function (err) {
27
- finish(span, err)
28
-
29
- if (callback) {
30
- return scope.activate(previous, () => callback.apply(this, arguments))
31
- }
32
- }
33
- }
34
-
35
- function wrapPromise (span, promise) {
36
- promise.then(
37
- () => finish(span),
38
- err => finish(span, err)
39
- )
40
-
41
- return promise
42
- }
43
-
44
- function wrapArguments (span, args) {
45
- const lastIndex = args.length - 1
46
- const callback = args[lastIndex]
47
-
48
- if (typeof callback === 'function') {
49
- args[lastIndex] = wrapCallback(span, args[lastIndex])
50
- }
51
-
52
- return args
53
- }
54
-
55
- function finish (span, error) {
56
- if (error) {
57
- span.addTags({
58
- 'error.type': error.name,
59
- 'error.msg': error.message,
60
- 'error.stack': error.stack
61
- })
62
- }
63
-
64
- span.finish()
65
- }
66
-
67
- function isPromise (obj) {
68
- return isObject(obj) && typeof obj.then === 'function'
69
- }
70
-
71
- function isObject (obj) {
72
- return typeof obj === 'object' && obj !== null
73
- }
74
-
75
- module.exports = tx