dd-trace 4.47.1 → 4.48.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 (84) hide show
  1. package/LICENSE-3rdparty.csv +0 -1
  2. package/ext/types.d.ts +1 -0
  3. package/ext/types.js +1 -0
  4. package/index.d.ts +26 -0
  5. package/package.json +6 -7
  6. package/packages/datadog-code-origin/index.js +38 -0
  7. package/packages/datadog-core/index.js +2 -2
  8. package/packages/datadog-instrumentations/src/avsc.js +37 -0
  9. package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
  10. package/packages/datadog-instrumentations/src/child_process.js +17 -8
  11. package/packages/datadog-instrumentations/src/express.js +37 -4
  12. package/packages/datadog-instrumentations/src/fastify.js +12 -1
  13. package/packages/datadog-instrumentations/src/fs.js +27 -7
  14. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  15. package/packages/datadog-instrumentations/src/jest.js +2 -1
  16. package/packages/datadog-instrumentations/src/mocha/common.js +1 -1
  17. package/packages/datadog-instrumentations/src/mysql2.js +220 -1
  18. package/packages/datadog-instrumentations/src/protobufjs.js +127 -0
  19. package/packages/datadog-instrumentations/src/winston.js +22 -0
  20. package/packages/datadog-plugin-avsc/src/index.js +9 -0
  21. package/packages/datadog-plugin-avsc/src/schema_iterator.js +169 -0
  22. package/packages/datadog-plugin-azure-functions/src/index.js +77 -0
  23. package/packages/datadog-plugin-fastify/src/code_origin.js +31 -0
  24. package/packages/datadog-plugin-fastify/src/index.js +10 -12
  25. package/packages/datadog-plugin-fastify/src/tracing.js +19 -0
  26. package/packages/datadog-plugin-protobufjs/src/index.js +14 -0
  27. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +180 -0
  28. package/packages/dd-trace/src/appsec/addresses.js +6 -1
  29. package/packages/dd-trace/src/appsec/channels.js +5 -1
  30. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +13 -1
  31. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +8 -1
  32. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
  33. package/packages/dd-trace/src/appsec/iast/index.js +3 -0
  34. package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +1 -0
  35. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +15 -0
  36. package/packages/dd-trace/src/appsec/index.js +58 -43
  37. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +99 -0
  38. package/packages/dd-trace/src/appsec/rasp/index.js +24 -10
  39. package/packages/dd-trace/src/appsec/rasp/lfi.js +112 -0
  40. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +24 -4
  41. package/packages/dd-trace/src/appsec/rasp/utils.js +2 -1
  42. package/packages/dd-trace/src/appsec/recommended.json +2 -4
  43. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +5 -1
  44. package/packages/dd-trace/src/appsec/remote_config/index.js +8 -0
  45. package/packages/dd-trace/src/appsec/reporter.js +12 -5
  46. package/packages/dd-trace/src/appsec/sdk/track_event.js +5 -0
  47. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +1 -1
  48. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +2 -14
  49. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +53 -0
  50. package/packages/dd-trace/src/config.js +12 -1
  51. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +25 -17
  52. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  53. package/packages/dd-trace/src/debugger/devtools_client/index.js +56 -5
  54. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +4 -4
  55. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -1
  56. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +153 -0
  57. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +30 -0
  58. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +241 -0
  59. package/packages/dd-trace/src/debugger/devtools_client/state.js +10 -4
  60. package/packages/dd-trace/src/exporters/common/request.js +8 -34
  61. package/packages/dd-trace/src/exporters/common/url-to-http-options-polyfill.js +31 -0
  62. package/packages/dd-trace/src/payload-tagging/index.js +1 -1
  63. package/packages/dd-trace/src/payload-tagging/jsonpath-plus.js +2094 -0
  64. package/packages/dd-trace/src/plugin_manager.js +4 -2
  65. package/packages/dd-trace/src/plugins/ci_plugin.js +2 -0
  66. package/packages/dd-trace/src/plugins/index.js +3 -0
  67. package/packages/dd-trace/src/plugins/log_plugin.js +1 -1
  68. package/packages/dd-trace/src/plugins/schema.js +35 -0
  69. package/packages/dd-trace/src/plugins/util/ci.js +23 -1
  70. package/packages/dd-trace/src/plugins/util/serverless.js +7 -0
  71. package/packages/dd-trace/src/plugins/util/stacktrace.js +94 -0
  72. package/packages/dd-trace/src/plugins/util/tags.js +7 -0
  73. package/packages/dd-trace/src/plugins/util/test.js +20 -22
  74. package/packages/dd-trace/src/plugins/util/web.js +6 -4
  75. package/packages/dd-trace/src/profiling/profiler.js +24 -14
  76. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  77. package/packages/dd-trace/src/profiling/profilers/wall.js +94 -66
  78. package/packages/dd-trace/src/proxy.js +12 -0
  79. package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
  80. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  81. package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
  82. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  83. package/packages/datadog-core/src/storage/async_resource.js +0 -108
  84. package/packages/datadog-core/src/storage/index.js +0 -5
@@ -1,18 +1,16 @@
1
1
  'use strict'
2
2
 
3
- const RouterPlugin = require('../../datadog-plugin-router/src')
3
+ const FastifyTracingPlugin = require('./tracing')
4
+ const FastifyCodeOriginForSpansPlugin = require('./code_origin')
5
+ const CompositePlugin = require('../../dd-trace/src/plugins/composite')
4
6
 
5
- class FastifyPlugin extends RouterPlugin {
6
- static get id () {
7
- return 'fastify'
8
- }
9
-
10
- constructor (...args) {
11
- super(...args)
12
-
13
- this.addSub('apm:fastify:request:handle', ({ req }) => {
14
- this.setFramework(req, 'fastify', this.config)
15
- })
7
+ class FastifyPlugin extends CompositePlugin {
8
+ static get id () { return 'fastify' }
9
+ static get plugins () {
10
+ return {
11
+ tracing: FastifyTracingPlugin,
12
+ codeOriginForSpans: FastifyCodeOriginForSpansPlugin
13
+ }
16
14
  }
17
15
  }
18
16
 
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const RouterPlugin = require('../../datadog-plugin-router/src')
4
+
5
+ class FastifyTracingPlugin extends RouterPlugin {
6
+ static get id () {
7
+ return 'fastify'
8
+ }
9
+
10
+ constructor (...args) {
11
+ super(...args)
12
+
13
+ this.addSub('apm:fastify:request:handle', ({ req }) => {
14
+ this.setFramework(req, 'fastify', this.config)
15
+ })
16
+ }
17
+ }
18
+
19
+ module.exports = FastifyTracingPlugin
@@ -0,0 +1,14 @@
1
+ const SchemaPlugin = require('../../dd-trace/src/plugins/schema')
2
+ const SchemaExtractor = require('./schema_iterator')
3
+
4
+ class ProtobufjsPlugin extends SchemaPlugin {
5
+ static get id () {
6
+ return 'protobufjs'
7
+ }
8
+
9
+ static get schemaExtractor () {
10
+ return SchemaExtractor
11
+ }
12
+ }
13
+
14
+ module.exports = ProtobufjsPlugin
@@ -0,0 +1,180 @@
1
+ const PROTOBUF = 'protobuf'
2
+ const {
3
+ SCHEMA_DEFINITION,
4
+ SCHEMA_ID,
5
+ SCHEMA_NAME,
6
+ SCHEMA_OPERATION,
7
+ SCHEMA_WEIGHT,
8
+ SCHEMA_TYPE
9
+ } = require('../../dd-trace/src/constants')
10
+ const log = require('../../dd-trace/src/log')
11
+ const {
12
+ SchemaBuilder
13
+ } = require('../../dd-trace/src/datastreams/schemas/schema_builder')
14
+
15
+ class SchemaExtractor {
16
+ constructor (schema) {
17
+ this.schema = schema
18
+ }
19
+
20
+ static getTypeAndFormat (type) {
21
+ const typeFormatMapping = {
22
+ int32: ['integer', 'int32'],
23
+ int64: ['integer', 'int64'],
24
+ uint32: ['integer', 'uint32'],
25
+ uint64: ['integer', 'uint64'],
26
+ sint32: ['integer', 'sint32'],
27
+ sint64: ['integer', 'sint64'],
28
+ fixed32: ['integer', 'fixed32'],
29
+ fixed64: ['integer', 'fixed64'],
30
+ sfixed32: ['integer', 'sfixed32'],
31
+ sfixed64: ['integer', 'sfixed64'],
32
+ float: ['number', 'float'],
33
+ double: ['number', 'double'],
34
+ bool: ['boolean', null],
35
+ string: ['string', null],
36
+ bytes: ['string', 'byte'],
37
+ Enum: ['enum', null],
38
+ Type: ['type', null],
39
+ map: ['map', null],
40
+ repeated: ['array', null]
41
+ }
42
+
43
+ return typeFormatMapping[type] || ['string', null]
44
+ }
45
+
46
+ static extractProperty (field, schemaName, fieldName, builder, depth) {
47
+ let array = false
48
+ let description
49
+ let ref
50
+ let enumValues
51
+
52
+ const resolvedType = field.resolvedType ? field.resolvedType.constructor.name : field.type
53
+
54
+ const isRepeatedField = field.rule === 'repeated'
55
+
56
+ let typeFormat = this.getTypeAndFormat(isRepeatedField ? 'repeated' : resolvedType)
57
+ let type = typeFormat[0]
58
+ let format = typeFormat[1]
59
+
60
+ if (type === 'array') {
61
+ array = true
62
+ typeFormat = this.getTypeAndFormat(resolvedType)
63
+ type = typeFormat[0]
64
+ format = typeFormat[1]
65
+ }
66
+
67
+ if (type === 'type') {
68
+ format = null
69
+ ref = `#/components/schemas/${removeLeadingPeriod(field.resolvedType.fullName)}`
70
+ // keep a reference to the original builder iterator since when we recurse this reference will get reset to
71
+ // deeper schemas
72
+ const originalSchemaExtractor = builder.iterator
73
+ if (!this.extractSchema(field.resolvedType, builder, depth, this)) {
74
+ return false
75
+ }
76
+ type = 'object'
77
+ builder.iterator = originalSchemaExtractor
78
+ } else if (type === 'enum') {
79
+ enumValues = []
80
+ let i = 0
81
+ while (field.resolvedType.valuesById[i]) {
82
+ enumValues.push(field.resolvedType.valuesById[i])
83
+ i += 1
84
+ }
85
+ }
86
+ return builder.addProperty(schemaName, fieldName, array, type, description, ref, format, enumValues)
87
+ }
88
+
89
+ static extractSchema (schema, builder, depth, extractor) {
90
+ depth += 1
91
+ const schemaName = removeLeadingPeriod(schema.resolvedType ? schema.resolvedType.fullName : schema.fullName)
92
+ if (extractor) {
93
+ // if we already have a defined extractor, this is a nested schema. create a new extractor for the nested
94
+ // schema, ensure it is added to our schema builder's cache, and replace the builders iterator with our
95
+ // nested schema iterator / extractor. Once complete, add the new schema to our builder's schemas.
96
+ const nestedSchemaExtractor = new SchemaExtractor(schema)
97
+ builder.iterator = nestedSchemaExtractor
98
+ const nestedSchema = SchemaBuilder.getSchema(schemaName, nestedSchemaExtractor, builder)
99
+ for (const nestedSubSchemaName in nestedSchema.components.schemas) {
100
+ if (nestedSchema.components.schemas.hasOwnProperty(nestedSubSchemaName)) {
101
+ builder.schema.components.schemas[nestedSubSchemaName] = nestedSchema.components.schemas[nestedSubSchemaName]
102
+ }
103
+ }
104
+ return true
105
+ } else {
106
+ if (!builder.shouldExtractSchema(schemaName, depth)) {
107
+ return false
108
+ }
109
+ for (const field of schema.fieldsArray) {
110
+ if (!this.extractProperty(field, schemaName, field.name, builder, depth)) {
111
+ log.warn(`DSM: Unable to extract field with name: ${field.name} from Avro schema with name: ${schemaName}`)
112
+ }
113
+ }
114
+ return true
115
+ }
116
+ }
117
+
118
+ static extractSchemas (descriptor, dataStreamsProcessor) {
119
+ const schemaName = removeLeadingPeriod(
120
+ descriptor.resolvedType ? descriptor.resolvedType.fullName : descriptor.fullName
121
+ )
122
+ return dataStreamsProcessor.getSchema(schemaName, new SchemaExtractor(descriptor))
123
+ }
124
+
125
+ iterateOverSchema (builder) {
126
+ this.constructor.extractSchema(this.schema, builder, 0)
127
+ }
128
+
129
+ static attachSchemaOnSpan (args, span, operation, tracer) {
130
+ const { messageClass } = args
131
+ const descriptor = messageClass.$type ?? messageClass
132
+
133
+ if (!descriptor || !span) {
134
+ return
135
+ }
136
+
137
+ if (span.context()._tags[SCHEMA_TYPE] && operation === 'serialization') {
138
+ // we have already added a schema to this span, this call is an encode of nested schema types
139
+ return
140
+ }
141
+
142
+ span.setTag(SCHEMA_TYPE, PROTOBUF)
143
+ span.setTag(SCHEMA_NAME, removeLeadingPeriod(descriptor.fullName))
144
+ span.setTag(SCHEMA_OPERATION, operation)
145
+
146
+ if (!tracer._dataStreamsProcessor.canSampleSchema(operation)) {
147
+ return
148
+ }
149
+
150
+ // if the span is unsampled, do not sample the schema
151
+ if (!tracer._prioritySampler.isSampled(span)) {
152
+ return
153
+ }
154
+
155
+ const weight = tracer._dataStreamsProcessor.trySampleSchema(operation)
156
+ if (weight === 0) {
157
+ return
158
+ }
159
+
160
+ const schemaData = SchemaBuilder.getSchemaDefinition(
161
+ this.extractSchemas(descriptor, tracer._dataStreamsProcessor)
162
+ )
163
+
164
+ span.setTag(SCHEMA_DEFINITION, schemaData.definition)
165
+ span.setTag(SCHEMA_WEIGHT, weight)
166
+ span.setTag(SCHEMA_ID, schemaData.id)
167
+ }
168
+ }
169
+
170
+ function removeLeadingPeriod (str) {
171
+ // Check if the first character is a period
172
+ if (str.charAt(0) === '.') {
173
+ // Remove the first character
174
+ return str.slice(1)
175
+ }
176
+ // Return the original string if the first character is not a period
177
+ return str
178
+ }
179
+
180
+ module.exports = SchemaExtractor
@@ -23,6 +23,11 @@ module.exports = {
23
23
  WAF_CONTEXT_PROCESSOR: 'waf.context.processor',
24
24
 
25
25
  HTTP_OUTGOING_URL: 'server.io.net.url',
26
+ FS_OPERATION_PATH: 'server.io.fs.file',
27
+
26
28
  DB_STATEMENT: 'server.db.statement',
27
- DB_SYSTEM: 'server.db.system'
29
+ DB_SYSTEM: 'server.db.system',
30
+
31
+ LOGIN_SUCCESS: 'server.business_logic.users.login.success',
32
+ LOGIN_FAILURE: 'server.business_logic.users.login.failure'
28
33
  }
@@ -17,6 +17,7 @@ module.exports = {
17
17
  setCookieChannel: dc.channel('datadog:iast:set-cookie'),
18
18
  nextBodyParsed: dc.channel('apm:next:body-parsed'),
19
19
  nextQueryParsed: dc.channel('apm:next:query-parsed'),
20
+ expressProcessParams: dc.channel('datadog:express:process_params:start'),
20
21
  responseBody: dc.channel('datadog:express:response:json:start'),
21
22
  responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
22
23
  httpClientRequestStart: dc.channel('apm:http:client:request:start'),
@@ -24,5 +25,8 @@ module.exports = {
24
25
  setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start'),
25
26
  pgQueryStart: dc.channel('apm:pg:query:start'),
26
27
  pgPoolQueryStart: dc.channel('datadog:pg:pool:query:start'),
27
- wafRunFinished: dc.channel('datadog:waf:run:finish')
28
+ mysql2OuterQueryStart: dc.channel('datadog:mysql2:outerquery:start'),
29
+ wafRunFinished: dc.channel('datadog:waf:run:finish'),
30
+ fsOperationStart: dc.channel('apm:fs:operation:start'),
31
+ expressMiddlewareError: dc.channel('apm:express:middleware:error')
28
32
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Analyzer = require('./vulnerability-analyzer')
4
4
  const { getNodeModulesPaths } = require('../path-line')
5
+ const iastLog = require('../iast-log')
5
6
 
6
7
  const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
7
8
 
@@ -11,7 +12,14 @@ class CookieAnalyzer extends Analyzer {
11
12
  this.propertyToBeSafe = propertyToBeSafe.toLowerCase()
12
13
  }
13
14
 
14
- onConfigure () {
15
+ onConfigure (config) {
16
+ try {
17
+ this.cookieFilterRegExp = new RegExp(config.iast.cookieFilterPattern)
18
+ } catch {
19
+ iastLog.error('Invalid regex in cookieFilterPattern')
20
+ this.cookieFilterRegExp = /.{32,}/
21
+ }
22
+
15
23
  this.addSub(
16
24
  { channelName: 'datadog:iast:set-cookie', moduleName: 'http' },
17
25
  (cookieInfo) => this.analyze(cookieInfo)
@@ -28,6 +36,10 @@ class CookieAnalyzer extends Analyzer {
28
36
  }
29
37
 
30
38
  _createHashSource (type, evidence, location) {
39
+ if (typeof evidence.value === 'string' && evidence.value.match(this.cookieFilterRegExp)) {
40
+ return 'FILTERED_' + this._type
41
+ }
42
+
31
43
  return `${type}:${evidence.value}`
32
44
  }
33
45
 
@@ -29,7 +29,14 @@ class PathTraversalAnalyzer extends InjectionAnalyzer {
29
29
 
30
30
  onConfigure () {
31
31
  this.addSub('apm:fs:operation:start', (obj) => {
32
- if (ignoredOperations.includes(obj.operation)) return
32
+ const store = storage.getStore()
33
+ const outOfReqOrChild = !store?.fs?.root
34
+
35
+ // we could filter out all the nested fs.operations based on store.fs.root
36
+ // but if we spect a store in the context to be present we are going to exclude
37
+ // all out_of_the_request fs.operations
38
+ // AppsecFsPlugin must be enabled
39
+ if (ignoredOperations.includes(obj.operation) || outOfReqOrChild) return
33
40
 
34
41
  const pathArguments = []
35
42
  if (obj.dest) {
@@ -127,7 +127,7 @@ class IastPlugin extends Plugin {
127
127
  config = { enabled: config }
128
128
  }
129
129
  if (config.enabled && !this.configured) {
130
- this.onConfigure()
130
+ this.onConfigure(config.tracerConfig)
131
131
  this.configured = true
132
132
  }
133
133
 
@@ -14,6 +14,7 @@ const {
14
14
  } = require('./taint-tracking')
15
15
  const { IAST_ENABLED_TAG_KEY } = require('./tags')
16
16
  const iastTelemetry = require('./telemetry')
17
+ const { enable: enableFsPlugin, disable: disableFsPlugin, IAST_MODULE } = require('../rasp/fs-plugin')
17
18
 
18
19
  // TODO Change to `apm:http:server:request:[start|close]` when the subscription
19
20
  // order of the callbacks can be enforce
@@ -27,6 +28,7 @@ function enable (config, _tracer) {
27
28
  if (isEnabled) return
28
29
 
29
30
  iastTelemetry.configure(config, config.iast?.telemetryVerbosity)
31
+ enableFsPlugin(IAST_MODULE)
30
32
  enableAllAnalyzers(config)
31
33
  enableTaintTracking(config.iast, iastTelemetry.verbosity)
32
34
  requestStart.subscribe(onIncomingHttpRequestStart)
@@ -44,6 +46,7 @@ function disable () {
44
46
  isEnabled = false
45
47
 
46
48
  iastTelemetry.stop()
49
+ disableFsPlugin(IAST_MODULE)
47
50
  disableAllAnalyzers()
48
51
  disableTaintTracking()
49
52
  overheadController.finishGlobalContext()
@@ -12,6 +12,7 @@ const csiMethods = [
12
12
  { src: 'substring' },
13
13
  { src: 'toLowerCase', dst: 'stringCase' },
14
14
  { src: 'toUpperCase', dst: 'stringCase' },
15
+ { src: 'tplOperator', operator: true },
15
16
  { src: 'trim' },
16
17
  { src: 'trimEnd' },
17
18
  { src: 'trimStart', dst: 'trim' },
@@ -29,6 +29,7 @@ const TaintTrackingNoop = {
29
29
  substr: noop,
30
30
  substring: noop,
31
31
  stringCase: noop,
32
+ tplOperator: noop,
32
33
  trim: noop,
33
34
  trimEnd: noop
34
35
  }
@@ -117,6 +118,20 @@ function csiMethodsOverrides (getContext) {
117
118
  return res
118
119
  },
119
120
 
121
+ tplOperator: function (res, ...rest) {
122
+ try {
123
+ const iastContext = getContext()
124
+ const transactionId = getTransactionId(iastContext)
125
+ if (transactionId) {
126
+ return TaintedUtils.concat(transactionId, res, ...rest)
127
+ }
128
+ } catch (e) {
129
+ iastLog.error('Error invoking CSI tplOperator')
130
+ .errorAndPublish(e)
131
+ }
132
+ return res
133
+ },
134
+
120
135
  stringCase: getCsiFn(
121
136
  (transactionId, res, target) => TaintedUtils.stringCase(transactionId, res, target),
122
137
  getContext,
@@ -12,6 +12,7 @@ const {
12
12
  queryParser,
13
13
  nextBodyParsed,
14
14
  nextQueryParsed,
15
+ expressProcessParams,
15
16
  responseBody,
16
17
  responseWriteHead,
17
18
  responseSetHeader
@@ -30,6 +31,8 @@ const { storage } = require('../../../datadog-core')
30
31
  const graphql = require('./graphql')
31
32
  const rasp = require('./rasp')
32
33
 
34
+ const responseAnalyzedSet = new WeakSet()
35
+
33
36
  let isEnabled = false
34
37
  let config
35
38
 
@@ -54,13 +57,14 @@ function enable (_config) {
54
57
 
55
58
  apiSecuritySampler.configure(_config.appsec)
56
59
 
60
+ bodyParser.subscribe(onRequestBodyParsed)
61
+ cookieParser.subscribe(onRequestCookieParser)
57
62
  incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
58
63
  incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
59
- bodyParser.subscribe(onRequestBodyParsed)
64
+ queryParser.subscribe(onRequestQueryParsed)
60
65
  nextBodyParsed.subscribe(onRequestBodyParsed)
61
66
  nextQueryParsed.subscribe(onRequestQueryParsed)
62
- queryParser.subscribe(onRequestQueryParsed)
63
- cookieParser.subscribe(onRequestCookieParser)
67
+ expressProcessParams.subscribe(onRequestProcessParams)
64
68
  responseBody.subscribe(onResponseBody)
65
69
  responseWriteHead.subscribe(onResponseWriteHead)
66
70
  responseSetHeader.subscribe(onResponseSetHeader)
@@ -79,6 +83,41 @@ function enable (_config) {
79
83
  }
80
84
  }
81
85
 
86
+ function onRequestBodyParsed ({ req, res, body, abortController }) {
87
+ if (body === undefined || body === null) return
88
+
89
+ if (!req) {
90
+ const store = storage.getStore()
91
+ req = store?.req
92
+ }
93
+
94
+ const rootSpan = web.root(req)
95
+ if (!rootSpan) return
96
+
97
+ const results = waf.run({
98
+ persistent: {
99
+ [addresses.HTTP_INCOMING_BODY]: body
100
+ }
101
+ }, req)
102
+
103
+ handleResults(results, req, res, rootSpan, abortController)
104
+ }
105
+
106
+ function onRequestCookieParser ({ req, res, abortController, cookies }) {
107
+ if (!cookies || typeof cookies !== 'object') return
108
+
109
+ const rootSpan = web.root(req)
110
+ if (!rootSpan) return
111
+
112
+ const results = waf.run({
113
+ persistent: {
114
+ [addresses.HTTP_INCOMING_COOKIES]: cookies
115
+ }
116
+ }, req)
117
+
118
+ handleResults(results, req, res, rootSpan, abortController)
119
+ }
120
+
82
121
  function incomingHttpStartTranslator ({ req, res, abortController }) {
83
122
  const rootSpan = web.root(req)
84
123
  if (!rootSpan) return
@@ -122,11 +161,6 @@ function incomingHttpEndTranslator ({ req, res }) {
122
161
  persistent[addresses.HTTP_INCOMING_BODY] = req.body
123
162
  }
124
163
 
125
- // TODO: temporary express instrumentation, will use express plugin later
126
- if (req.params !== null && typeof req.params === 'object') {
127
- persistent[addresses.HTTP_INCOMING_PARAMS] = req.params
128
- }
129
-
130
164
  // we need to keep this to support other cookie parsers
131
165
  if (req.cookies !== null && typeof req.cookies === 'object') {
132
166
  persistent[addresses.HTTP_INCOMING_COOKIES] = req.cookies
@@ -145,24 +179,16 @@ function incomingHttpEndTranslator ({ req, res }) {
145
179
  Reporter.finishRequest(req, res)
146
180
  }
147
181
 
148
- function onRequestBodyParsed ({ req, res, body, abortController }) {
149
- if (body === undefined || body === null) return
182
+ function onPassportVerify ({ credentials, user }) {
183
+ const store = storage.getStore()
184
+ const rootSpan = store?.req && web.root(store.req)
150
185
 
151
- if (!req) {
152
- const store = storage.getStore()
153
- req = store?.req
186
+ if (!rootSpan) {
187
+ log.warn('No rootSpan found in onPassportVerify')
188
+ return
154
189
  }
155
190
 
156
- const rootSpan = web.root(req)
157
- if (!rootSpan) return
158
-
159
- const results = waf.run({
160
- persistent: {
161
- [addresses.HTTP_INCOMING_BODY]: body
162
- }
163
- }, req)
164
-
165
- handleResults(results, req, res, rootSpan, abortController)
191
+ passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
166
192
  }
167
193
 
168
194
  function onRequestQueryParsed ({ req, res, query, abortController }) {
@@ -185,15 +211,15 @@ function onRequestQueryParsed ({ req, res, query, abortController }) {
185
211
  handleResults(results, req, res, rootSpan, abortController)
186
212
  }
187
213
 
188
- function onRequestCookieParser ({ req, res, abortController, cookies }) {
189
- if (!cookies || typeof cookies !== 'object') return
190
-
214
+ function onRequestProcessParams ({ req, res, abortController, params }) {
191
215
  const rootSpan = web.root(req)
192
216
  if (!rootSpan) return
193
217
 
218
+ if (!params || typeof params !== 'object' || !Object.keys(params).length) return
219
+
194
220
  const results = waf.run({
195
221
  persistent: {
196
- [addresses.HTTP_INCOMING_COOKIES]: cookies
222
+ [addresses.HTTP_INCOMING_PARAMS]: params
197
223
  }
198
224
  }, req)
199
225
 
@@ -212,20 +238,6 @@ function onResponseBody ({ req, body }) {
212
238
  }, req)
213
239
  }
214
240
 
215
- function onPassportVerify ({ credentials, user }) {
216
- const store = storage.getStore()
217
- const rootSpan = store?.req && web.root(store.req)
218
-
219
- if (!rootSpan) {
220
- log.warn('No rootSpan found in onPassportVerify')
221
- return
222
- }
223
-
224
- passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
225
- }
226
-
227
- const responseAnalyzedSet = new WeakSet()
228
-
229
241
  function onResponseWriteHead ({ req, res, abortController, statusCode, responseHeaders }) {
230
242
  // avoid "write after end" error
231
243
  if (isBlocked(res)) {
@@ -287,12 +299,15 @@ function disable () {
287
299
 
288
300
  // Channel#unsubscribe() is undefined for non active channels
289
301
  if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
302
+ if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser)
290
303
  if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator)
291
304
  if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(incomingHttpEndTranslator)
305
+ if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
292
306
  if (queryParser.hasSubscribers) queryParser.unsubscribe(onRequestQueryParsed)
293
- if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser)
307
+ if (nextBodyParsed.hasSubscribers) nextBodyParsed.unsubscribe(onRequestBodyParsed)
308
+ if (nextQueryParsed.hasSubscribers) nextQueryParsed.unsubscribe(onRequestQueryParsed)
309
+ if (expressProcessParams.hasSubscribers) expressProcessParams.unsubscribe(onRequestProcessParams)
294
310
  if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
295
- if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
296
311
  if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
297
312
  if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader)
298
313
  }
@@ -0,0 +1,99 @@
1
+ 'use strict'
2
+
3
+ const Plugin = require('../../plugins/plugin')
4
+ const { storage } = require('../../../../datadog-core')
5
+ const log = require('../../log')
6
+
7
+ const RASP_MODULE = 'rasp'
8
+ const IAST_MODULE = 'iast'
9
+
10
+ const enabledFor = {
11
+ [RASP_MODULE]: false,
12
+ [IAST_MODULE]: false
13
+ }
14
+
15
+ let fsPlugin
16
+
17
+ function enterWith (fsProps, store = storage.getStore()) {
18
+ if (store && !store.fs?.opExcluded) {
19
+ storage.enterWith({
20
+ ...store,
21
+ fs: {
22
+ ...store.fs,
23
+ ...fsProps,
24
+ parentStore: store
25
+ }
26
+ })
27
+ }
28
+ }
29
+
30
+ class AppsecFsPlugin extends Plugin {
31
+ enable () {
32
+ this.addSub('apm:fs:operation:start', this._onFsOperationStart)
33
+ this.addSub('apm:fs:operation:finish', this._onFsOperationFinishOrRenderEnd)
34
+ this.addSub('tracing:datadog:express:response:render:start', this._onResponseRenderStart)
35
+ this.addSub('tracing:datadog:express:response:render:end', this._onFsOperationFinishOrRenderEnd)
36
+
37
+ super.configure(true)
38
+ }
39
+
40
+ disable () {
41
+ super.configure(false)
42
+ }
43
+
44
+ _onFsOperationStart () {
45
+ const store = storage.getStore()
46
+ if (store) {
47
+ enterWith({ root: store.fs?.root === undefined }, store)
48
+ }
49
+ }
50
+
51
+ _onResponseRenderStart () {
52
+ enterWith({ opExcluded: true })
53
+ }
54
+
55
+ _onFsOperationFinishOrRenderEnd () {
56
+ const store = storage.getStore()
57
+ if (store?.fs?.parentStore) {
58
+ storage.enterWith(store.fs.parentStore)
59
+ }
60
+ }
61
+ }
62
+
63
+ function enable (mod) {
64
+ if (enabledFor[mod] !== false) return
65
+
66
+ enabledFor[mod] = true
67
+
68
+ if (!fsPlugin) {
69
+ fsPlugin = new AppsecFsPlugin()
70
+ fsPlugin.enable()
71
+ }
72
+
73
+ log.info(`Enabled AppsecFsPlugin for ${mod}`)
74
+ }
75
+
76
+ function disable (mod) {
77
+ if (!mod || !enabledFor[mod]) return
78
+
79
+ enabledFor[mod] = false
80
+
81
+ const allDisabled = Object.values(enabledFor).every(val => val === false)
82
+ if (allDisabled) {
83
+ fsPlugin?.disable()
84
+
85
+ fsPlugin = undefined
86
+ }
87
+
88
+ log.info(`Disabled AppsecFsPlugin for ${mod}`)
89
+ }
90
+
91
+ module.exports = {
92
+ enable,
93
+ disable,
94
+
95
+ AppsecFsPlugin,
96
+
97
+ RASP_MODULE,
98
+ IAST_MODULE
99
+ }