dd-trace 5.29.0 → 5.30.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.29.0",
3
+ "version": "5.30.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -13,6 +13,9 @@ const errorChannel = channel('apm:fs:operation:error')
13
13
  const ddFhSym = Symbol('ddFileHandle')
14
14
  let kHandle, kDirReadPromisified, kDirClosePromisified
15
15
 
16
+ // Update packages/dd-trace/src/profiling/profilers/event_plugins/fs.js if you make changes to param names in any of
17
+ // the following objects.
18
+
16
19
  const paramsByMethod = {
17
20
  access: ['path', 'mode'],
18
21
  appendFile: ['path', 'data', 'options'],
@@ -31,6 +31,7 @@ module.exports = {
31
31
  DB_STATEMENT: 'server.db.statement',
32
32
  DB_SYSTEM: 'server.db.system',
33
33
 
34
+ EXEC_COMMAND: 'server.sys.exec.cmd',
34
35
  SHELL_COMMAND: 'server.sys.shell.cmd',
35
36
 
36
37
  LOGIN_SUCCESS: 'server.business_logic.users.login.success',
@@ -25,19 +25,26 @@ function disable () {
25
25
  }
26
26
 
27
27
  function analyzeCommandInjection ({ file, fileArgs, shell, abortController }) {
28
- if (!file || !shell) return
28
+ if (!file) return
29
29
 
30
30
  const store = storage.getStore()
31
31
  const req = store?.req
32
32
  if (!req) return
33
33
 
34
- const commandParams = fileArgs ? [file, ...fileArgs] : file
35
-
36
- const persistent = {
37
- [addresses.SHELL_COMMAND]: commandParams
34
+ const persistent = {}
35
+ const raspRule = { type: RULE_TYPES.COMMAND_INJECTION }
36
+ const params = fileArgs ? [file, ...fileArgs] : file
37
+
38
+ if (shell) {
39
+ persistent[addresses.SHELL_COMMAND] = params
40
+ raspRule.variant = 'shell'
41
+ } else {
42
+ const commandParams = Array.isArray(params) ? params : [params]
43
+ persistent[addresses.EXEC_COMMAND] = commandParams
44
+ raspRule.variant = 'exec'
38
45
  }
39
46
 
40
- const result = waf.run({ persistent }, req, RULE_TYPES.COMMAND_INJECTION)
47
+ const result = waf.run({ persistent }, req, raspRule)
41
48
 
42
49
  const res = store?.res
43
50
  handleResult(result, req, res, abortController, config)
@@ -58,7 +58,9 @@ function analyzeLfi (ctx) {
58
58
  [FS_OPERATION_PATH]: path
59
59
  }
60
60
 
61
- const result = waf.run({ persistent }, req, RULE_TYPES.LFI)
61
+ const raspRule = { type: RULE_TYPES.LFI }
62
+
63
+ const result = waf.run({ persistent }, req, raspRule)
62
64
  handleResult(result, req, res, ctx.abortController, config)
63
65
  })
64
66
  }
@@ -72,7 +72,9 @@ function analyzeSqlInjection (query, dbSystem, abortController) {
72
72
  [addresses.DB_SYSTEM]: dbSystem
73
73
  }
74
74
 
75
- const result = waf.run({ persistent }, req, RULE_TYPES.SQL_INJECTION)
75
+ const raspRule = { type: RULE_TYPES.SQL_INJECTION }
76
+
77
+ const result = waf.run({ persistent }, req, raspRule)
76
78
 
77
79
  handleResult(result, req, res, abortController, config)
78
80
  }
@@ -29,7 +29,9 @@ function analyzeSsrf (ctx) {
29
29
  [addresses.HTTP_OUTGOING_URL]: outgoingUrl
30
30
  }
31
31
 
32
- const result = waf.run({ persistent }, req, RULE_TYPES.SSRF)
32
+ const raspRule = { type: RULE_TYPES.SSRF }
33
+
34
+ const result = waf.run({ persistent }, req, raspRule)
33
35
 
34
36
  const res = store?.res
35
37
  handleResult(result, req, res, ctx.abortController, config)
@@ -25,5 +25,6 @@ module.exports = {
25
25
  ASM_AUTO_USER_INSTRUM_MODE: 1n << 31n,
26
26
  ASM_ENDPOINT_FINGERPRINT: 1n << 32n,
27
27
  ASM_NETWORK_FINGERPRINT: 1n << 34n,
28
- ASM_HEADER_FINGERPRINT: 1n << 35n
28
+ ASM_HEADER_FINGERPRINT: 1n << 35n,
29
+ ASM_RASP_CMDI: 1n << 37n
29
30
  }
@@ -101,6 +101,7 @@ function enableWafUpdate (appsecConfig) {
101
101
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, true)
102
102
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, true)
103
103
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, true)
104
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, true)
104
105
  }
105
106
 
106
107
  // TODO: delete noop handlers and kPreUpdate and replace with batched handlers
@@ -133,6 +134,7 @@ function disableWafUpdate () {
133
134
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, false)
134
135
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, false)
135
136
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, false)
137
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, false)
136
138
 
137
139
  rc.removeProductHandler('ASM_DATA')
138
140
  rc.removeProductHandler('ASM_DD')
@@ -101,7 +101,7 @@ function reportWafInit (wafVersion, rulesVersion, diagnosticsRules = {}) {
101
101
  incrementWafInitMetric(wafVersion, rulesVersion)
102
102
  }
103
103
 
104
- function reportMetrics (metrics, raspRuleType) {
104
+ function reportMetrics (metrics, raspRule) {
105
105
  const store = storage.getStore()
106
106
  const rootSpan = store?.req && web.root(store.req)
107
107
  if (!rootSpan) return
@@ -109,8 +109,8 @@ function reportMetrics (metrics, raspRuleType) {
109
109
  if (metrics.rulesVersion) {
110
110
  rootSpan.setTag('_dd.appsec.event_rules.version', metrics.rulesVersion)
111
111
  }
112
- if (raspRuleType) {
113
- updateRaspRequestsMetricTags(metrics, store.req, raspRuleType)
112
+ if (raspRule) {
113
+ updateRaspRequestsMetricTags(metrics, store.req, raspRule)
114
114
  } else {
115
115
  updateWafRequestsMetricTags(metrics, store.req)
116
116
  }
@@ -79,7 +79,7 @@ function getOrCreateMetricTags (store, versionsTags) {
79
79
  return metricTags
80
80
  }
81
81
 
82
- function updateRaspRequestsMetricTags (metrics, req, raspRuleType) {
82
+ function updateRaspRequestsMetricTags (metrics, req, raspRule) {
83
83
  if (!req) return
84
84
 
85
85
  const store = getStore(req)
@@ -89,7 +89,12 @@ function updateRaspRequestsMetricTags (metrics, req, raspRuleType) {
89
89
 
90
90
  if (!enabled) return
91
91
 
92
- const tags = { rule_type: raspRuleType, waf_version: metrics.wafVersion }
92
+ const tags = { rule_type: raspRule.type, waf_version: metrics.wafVersion }
93
+
94
+ if (raspRule.variant) {
95
+ tags.rule_variant = raspRule.variant
96
+ }
97
+
93
98
  appsecMetrics.count('rasp.rule.eval', tags).inc(1)
94
99
 
95
100
  if (metrics.wafTimeout) {
@@ -46,7 +46,7 @@ function update (newRules) {
46
46
  }
47
47
  }
48
48
 
49
- function run (data, req, raspRuleType) {
49
+ function run (data, req, raspRule) {
50
50
  if (!req) {
51
51
  const store = storage.getStore()
52
52
  if (!store || !store.req) {
@@ -59,7 +59,7 @@ function run (data, req, raspRuleType) {
59
59
 
60
60
  const wafContext = waf.wafManager.getWAFContext(req)
61
61
 
62
- return wafContext.run(data, raspRuleType)
62
+ return wafContext.run(data, raspRule)
63
63
  }
64
64
 
65
65
  function disposeContext (req) {
@@ -21,7 +21,7 @@ class WAFContextWrapper {
21
21
  this.knownAddresses = knownAddresses
22
22
  }
23
23
 
24
- run ({ persistent, ephemeral }, raspRuleType) {
24
+ run ({ persistent, ephemeral }, raspRule) {
25
25
  if (this.ddwafContext.disposed) {
26
26
  log.warn('[ASM] Calling run on a disposed context')
27
27
  return
@@ -87,7 +87,7 @@ class WAFContextWrapper {
87
87
  blockTriggered,
88
88
  wafVersion: this.wafVersion,
89
89
  wafTimeout: result.timeout
90
- }, raspRuleType)
90
+ }, raspRule)
91
91
 
92
92
  if (ruleTriggered) {
93
93
  Reporter.reportAttack(JSON.stringify(result.events))
@@ -3,7 +3,7 @@
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
5
  const Level = {
6
- trace: 20,
6
+ trace: 10,
7
7
  debug: 20,
8
8
  info: 30,
9
9
  warn: 40,
@@ -12,6 +12,7 @@ const Level = {
12
12
  off: 100
13
13
  }
14
14
 
15
+ const traceChannel = channel('datadog:log:trace')
15
16
  const debugChannel = channel('datadog:log:debug')
16
17
  const infoChannel = channel('datadog:log:info')
17
18
  const warnChannel = channel('datadog:log:warn')
@@ -31,6 +32,9 @@ class LogChannel {
31
32
  }
32
33
 
33
34
  subscribe (logger) {
35
+ if (Level.trace >= this._level) {
36
+ traceChannel.subscribe(logger.trace)
37
+ }
34
38
  if (Level.debug >= this._level) {
35
39
  debugChannel.subscribe(logger.debug)
36
40
  }
@@ -46,6 +50,9 @@ class LogChannel {
46
50
  }
47
51
 
48
52
  unsubscribe (logger) {
53
+ if (traceChannel.hasSubscribers) {
54
+ traceChannel.unsubscribe(logger.trace)
55
+ }
49
56
  if (debugChannel.hasSubscribers) {
50
57
  debugChannel.unsubscribe(logger.debug)
51
58
  }
@@ -63,7 +70,7 @@ class LogChannel {
63
70
 
64
71
  module.exports = {
65
72
  LogChannel,
66
-
73
+ traceChannel,
67
74
  debugChannel,
68
75
  infoChannel,
69
76
  warnChannel,
@@ -1,8 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const coalesce = require('koalas')
4
+ const { inspect } = require('util')
4
5
  const { isTrue } = require('../util')
5
- const { debugChannel, infoChannel, warnChannel, errorChannel } = require('./channels')
6
+ const { traceChannel, debugChannel, infoChannel, warnChannel, errorChannel } = require('./channels')
6
7
  const logWriter = require('./writer')
7
8
  const { Log } = require('./log')
8
9
 
@@ -56,6 +57,25 @@ const log = {
56
57
  return this
57
58
  },
58
59
 
60
+ trace (...args) {
61
+ if (traceChannel.hasSubscribers) {
62
+ const logRecord = {}
63
+
64
+ Error.captureStackTrace(logRecord, this.trace)
65
+
66
+ const fn = logRecord.stack.split('\n')[1].replace(/^\s+at ([^\s]+) .+/, '$1')
67
+ const params = args.map(a => {
68
+ return a && a.hasOwnProperty('toString') && typeof a.toString === 'function'
69
+ ? a.toString()
70
+ : inspect(a, { depth: 3, breakLength: Infinity, compact: true })
71
+ }).join(', ')
72
+ const formatted = logRecord.stack.replace('Error: ', `Trace: ${fn}(${params})`)
73
+
74
+ traceChannel.publish(Log.parse(formatted))
75
+ }
76
+ return this
77
+ },
78
+
59
79
  debug (...args) {
60
80
  if (debugChannel.hasSubscribers) {
61
81
  debugChannel.publish(Log.parse(...args))
@@ -23,7 +23,7 @@ function withNoop (fn) {
23
23
  }
24
24
 
25
25
  function unsubscribeAll () {
26
- logChannel.unsubscribe({ debug: onDebug, info: onInfo, warn: onWarn, error: onError })
26
+ logChannel.unsubscribe({ trace: onTrace, debug: onDebug, info: onInfo, warn: onWarn, error: onError })
27
27
  }
28
28
 
29
29
  function toggleSubscription (enable, level) {
@@ -31,7 +31,7 @@ function toggleSubscription (enable, level) {
31
31
 
32
32
  if (enable) {
33
33
  logChannel = new LogChannel(level)
34
- logChannel.subscribe({ debug: onDebug, info: onInfo, warn: onWarn, error: onError })
34
+ logChannel.subscribe({ trace: onTrace, debug: onDebug, info: onInfo, warn: onWarn, error: onError })
35
35
  }
36
36
  }
37
37
 
@@ -88,6 +88,14 @@ function onDebug (log) {
88
88
  if (cause) withNoop(() => logger.debug(cause))
89
89
  }
90
90
 
91
+ function onTrace (log) {
92
+ const { formatted, cause } = getErrorLog(log)
93
+ // Using logger.debug() because not all loggers have trace level,
94
+ // and console.trace() has a completely different meaning.
95
+ if (formatted) withNoop(() => logger.debug(formatted))
96
+ if (cause) withNoop(() => logger.debug(cause))
97
+ }
98
+
91
99
  function error (...args) {
92
100
  onError(Log.parse(...args))
93
101
  }
@@ -110,4 +118,8 @@ function debug (...args) {
110
118
  onDebug(Log.parse(...args))
111
119
  }
112
120
 
113
- module.exports = { use, toggle, reset, error, warn, info, debug }
121
+ function trace (...args) {
122
+ onTrace(Log.parse(...args))
123
+ }
124
+
125
+ module.exports = { use, toggle, reset, error, warn, info, debug, trace }
@@ -107,7 +107,7 @@ class DatadogSpan {
107
107
 
108
108
  toString () {
109
109
  const spanContext = this.context()
110
- const resourceName = spanContext._tags['resource.name']
110
+ const resourceName = spanContext._tags['resource.name'] || ''
111
111
  const resource = resourceName.length > 100
112
112
  ? `${resourceName.substring(0, 97)}...`
113
113
  : resourceName
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('./log')
3
4
  const RateLimiter = require('./rate_limiter')
4
5
  const Sampler = require('./sampler')
5
6
  const { setSamplingRules } = require('./startup-log')
@@ -44,16 +45,19 @@ class PrioritySampler {
44
45
  this.update({})
45
46
  }
46
47
 
47
- configure (env, { sampleRate, provenance = undefined, rateLimit = 100, rules = [] } = {}) {
48
+ configure (env, opts = {}) {
49
+ const { sampleRate, provenance = undefined, rateLimit = 100, rules = [] } = opts
48
50
  this._env = env
49
51
  this._rules = this._normalizeRules(rules, sampleRate, rateLimit, provenance)
50
52
  this._limiter = new RateLimiter(rateLimit)
51
53
 
54
+ log.trace(env, opts)
52
55
  setSamplingRules(this._rules)
53
56
  }
54
57
 
55
58
  isSampled (span) {
56
59
  const priority = this._getPriorityFromAuto(span)
60
+ log.trace(span)
57
61
  return priority === USER_KEEP || priority === AUTO_KEEP
58
62
  }
59
63
 
@@ -67,6 +71,8 @@ class PrioritySampler {
67
71
  if (context._sampling.priority !== undefined) return
68
72
  if (!root) return // noop span
69
73
 
74
+ log.trace(span, auto)
75
+
70
76
  const tag = this._getPriorityFromTags(context._tags, context)
71
77
 
72
78
  if (this.validate(tag)) {
@@ -94,6 +100,8 @@ class PrioritySampler {
94
100
  samplers[DEFAULT_KEY] = samplers[DEFAULT_KEY] || defaultSampler
95
101
 
96
102
  this._samplers = samplers
103
+
104
+ log.trace(rates)
97
105
  }
98
106
 
99
107
  validate (samplingPriority) {
@@ -117,6 +125,8 @@ class PrioritySampler {
117
125
  context._sampling.mechanism = mechanism
118
126
 
119
127
  const root = context._trace.started[0]
128
+
129
+ log.trace(span, samplingPriority, mechanism)
120
130
  this._addDecisionMaker(root)
121
131
  }
122
132
 
@@ -32,11 +32,11 @@ class EventPlugin extends TracingPlugin {
32
32
  if (!store) return
33
33
 
34
34
  const { startEvent, startTime, error } = store
35
- if (error) {
36
- return // don't emit perf events for failed operations
35
+ if (error || this.ignoreEvent(startEvent)) {
36
+ return // don't emit perf events for failed operations or ignored events
37
37
  }
38
- const duration = performance.now() - startTime
39
38
 
39
+ const duration = performance.now() - startTime
40
40
  const event = {
41
41
  entryType: this.entryType,
42
42
  startTime,
@@ -53,6 +53,10 @@ class EventPlugin extends TracingPlugin {
53
53
 
54
54
  this.eventHandler(this.extendEvent(event, startEvent))
55
55
  }
56
+
57
+ ignoreEvent () {
58
+ return false
59
+ }
56
60
  }
57
61
 
58
62
  module.exports = EventPlugin
@@ -0,0 +1,49 @@
1
+ const EventPlugin = require('./event')
2
+
3
+ // Values taken from parameter names in datadog-instrumentations/src/fs.js.
4
+ // Known param names that are disallowed because they can be strings and have arbitrary sizes:
5
+ // 'data'
6
+ // Known param names that are disallowed because they are never a string or number:
7
+ // 'buffer', 'buffers', 'listener'
8
+ const allowedParams = new Set([
9
+ 'atime', 'dest',
10
+ 'existingPath', 'fd', 'file',
11
+ 'flag', 'gid', 'len',
12
+ 'length', 'mode', 'mtime',
13
+ 'newPath', 'offset', 'oldPath',
14
+ 'operation', 'options', 'path',
15
+ 'position', 'prefix', 'src',
16
+ 'target', 'type', 'uid'
17
+ ])
18
+
19
+ class FilesystemPlugin extends EventPlugin {
20
+ static get id () {
21
+ return 'fs'
22
+ }
23
+
24
+ static get operation () {
25
+ return 'operation'
26
+ }
27
+
28
+ static get entryType () {
29
+ return 'fs'
30
+ }
31
+
32
+ ignoreEvent (event) {
33
+ // Don't care about sync events, they show up in the event loop samples anyway
34
+ return event.operation?.endsWith('Sync')
35
+ }
36
+
37
+ extendEvent (event, detail) {
38
+ const d = { ...detail }
39
+ Object.entries(d).forEach(([k, v]) => {
40
+ if (!(allowedParams.has(k) && (typeof v === 'string' || typeof v === 'number'))) {
41
+ delete d[k]
42
+ }
43
+ })
44
+ event.detail = d
45
+
46
+ return event
47
+ }
48
+ }
49
+ module.exports = FilesystemPlugin
@@ -133,11 +133,32 @@ class NetDecorator {
133
133
  }
134
134
  }
135
135
 
136
+ class FilesystemDecorator {
137
+ constructor (stringTable) {
138
+ this.stringTable = stringTable
139
+ }
140
+
141
+ decorateSample (sampleInput, item) {
142
+ const labels = sampleInput.label
143
+ const stringTable = this.stringTable
144
+ Object.entries(item.detail).forEach(([k, v]) => {
145
+ switch (typeof v) {
146
+ case 'string':
147
+ labels.push(labelFromStrStr(stringTable, k, v))
148
+ break
149
+ case 'number':
150
+ labels.push(new Label({ key: stringTable.dedup(k), num: v }))
151
+ }
152
+ })
153
+ }
154
+ }
155
+
136
156
  // Keys correspond to PerformanceEntry.entryType, values are constructor
137
157
  // functions for type-specific decorators.
138
158
  const decoratorTypes = {
139
- gc: GCDecorator,
159
+ fs: FilesystemDecorator,
140
160
  dns: DNSDecorator,
161
+ gc: GCDecorator,
141
162
  net: NetDecorator
142
163
  }
143
164
 
@@ -255,7 +276,7 @@ class NodeApiEventSource {
255
276
 
256
277
  class DatadogInstrumentationEventSource {
257
278
  constructor (eventHandler, eventFilter) {
258
- this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'net'].map(m => {
279
+ this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'fs', 'net'].map(m => {
259
280
  const Plugin = require(`./event_plugins/${m}`)
260
281
  return new Plugin(eventHandler, eventFilter)
261
282
  })