dd-trace 2.31.0 → 2.32.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 (58) hide show
  1. package/package.json +6 -6
  2. package/packages/datadog-instrumentations/src/body-parser.js +15 -9
  3. package/packages/datadog-instrumentations/src/express.js +32 -0
  4. package/packages/datadog-instrumentations/src/http/server.js +2 -1
  5. package/packages/datadog-instrumentations/src/playwright.js +3 -0
  6. package/packages/datadog-plugin-amqp10/src/consumer.js +1 -3
  7. package/packages/datadog-plugin-amqp10/src/producer.js +1 -3
  8. package/packages/datadog-plugin-amqplib/src/client.js +4 -3
  9. package/packages/datadog-plugin-amqplib/src/consumer.js +1 -3
  10. package/packages/datadog-plugin-amqplib/src/producer.js +1 -3
  11. package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -3
  12. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +1 -3
  13. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -3
  14. package/packages/datadog-plugin-http/src/server.js +2 -2
  15. package/packages/datadog-plugin-http2/src/server.js +0 -5
  16. package/packages/datadog-plugin-kafkajs/src/consumer.js +1 -4
  17. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -3
  18. package/packages/datadog-plugin-rhea/src/consumer.js +1 -3
  19. package/packages/datadog-plugin-rhea/src/producer.js +1 -5
  20. package/packages/dd-trace/src/appsec/addresses.js +0 -3
  21. package/packages/dd-trace/src/appsec/blocked_templates.js +2 -9
  22. package/packages/dd-trace/src/appsec/blocking.js +1 -1
  23. package/packages/dd-trace/src/appsec/{gateway/channels.js → channels.js} +4 -4
  24. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -1
  25. package/packages/dd-trace/src/appsec/index.js +87 -79
  26. package/packages/dd-trace/src/appsec/recommended.json +448 -121
  27. package/packages/dd-trace/src/appsec/remote_config/apply_states.js +7 -0
  28. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -0
  29. package/packages/dd-trace/src/appsec/remote_config/index.js +29 -10
  30. package/packages/dd-trace/src/appsec/remote_config/manager.js +33 -12
  31. package/packages/dd-trace/src/appsec/reporter.js +27 -58
  32. package/packages/dd-trace/src/appsec/rule_manager.js +160 -32
  33. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +4 -12
  34. package/packages/dd-trace/src/appsec/waf/index.js +75 -0
  35. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +57 -0
  36. package/packages/dd-trace/src/appsec/waf/waf_manager.js +66 -0
  37. package/packages/dd-trace/src/config.js +17 -1
  38. package/packages/dd-trace/src/encode/0.4.js +12 -4
  39. package/packages/dd-trace/src/plugin_manager.js +2 -0
  40. package/packages/dd-trace/src/plugins/client.js +3 -2
  41. package/packages/dd-trace/src/plugins/consumer.js +17 -2
  42. package/packages/dd-trace/src/plugins/inbound.js +7 -0
  43. package/packages/dd-trace/src/plugins/{outgoing.js → outbound.js} +2 -2
  44. package/packages/dd-trace/src/plugins/producer.js +17 -2
  45. package/packages/dd-trace/src/plugins/server.js +2 -2
  46. package/packages/dd-trace/src/plugins/tracing.js +11 -0
  47. package/packages/dd-trace/src/service-naming/index.js +41 -0
  48. package/packages/dd-trace/src/service-naming/schemas/definition.js +28 -0
  49. package/packages/dd-trace/src/service-naming/schemas/index.js +6 -0
  50. package/packages/dd-trace/src/service-naming/schemas/v0.js +66 -0
  51. package/packages/dd-trace/src/service-naming/schemas/v1.js +58 -0
  52. package/packages/dd-trace/src/appsec/callbacks/ddwaf.js +0 -137
  53. package/packages/dd-trace/src/appsec/callbacks/index.js +0 -7
  54. package/packages/dd-trace/src/appsec/gateway/als.js +0 -6
  55. package/packages/dd-trace/src/appsec/gateway/engine/engine.js +0 -140
  56. package/packages/dd-trace/src/appsec/gateway/engine/index.js +0 -51
  57. package/packages/dd-trace/src/appsec/gateway/engine/runner.js +0 -42
  58. package/packages/dd-trace/src/plugins/incoming.js +0 -7
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ UNACKNOWLEDGED: 1,
5
+ ACKNOWLEDGED: 2,
6
+ ERROR: 3
7
+ }
@@ -4,5 +4,7 @@ module.exports = {
4
4
  ASM_ACTIVATION: 1n << 1n,
5
5
  ASM_IP_BLOCKING: 1n << 2n,
6
6
  ASM_DD_RULES: 1n << 3n,
7
+ ASM_EXCLUSIONS: 1n << 4n,
8
+ ASM_REQUEST_BLOCKING: 1n << 5n,
7
9
  ASM_USER_BLOCKING: 1n << 7n
8
10
  }
@@ -2,7 +2,6 @@
2
2
 
3
3
  const RemoteConfigManager = require('./manager')
4
4
  const RemoteConfigCapabilities = require('./capabilities')
5
- const RuleManager = require('../rule_manager')
6
5
 
7
6
  let rc
8
7
 
@@ -32,28 +31,48 @@ function enable (config) {
32
31
  }
33
32
  }
34
33
 
35
- function enableAsmData (appsecConfig) {
34
+ function enableWafUpdate (appsecConfig) {
36
35
  if (rc && appsecConfig && !appsecConfig.customRulesProvided) {
36
+ // dirty require to make startup faster for serverless
37
+ const RuleManager = require('../rule_manager')
38
+
37
39
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_IP_BLOCKING, true)
38
40
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_USER_BLOCKING, true)
39
- rc.on('ASM_DATA', _asmDataListener)
41
+ // TODO: we should have a different capability for rule override
42
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, true)
43
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, true)
44
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, true)
45
+
46
+ rc.on('ASM_DATA', noop)
47
+ rc.on('ASM_DD', noop)
48
+ rc.on('ASM', noop)
49
+
50
+ rc.on(RemoteConfigManager.kPreUpdate, RuleManager.updateWafFromRC)
40
51
  }
41
52
  }
42
53
 
43
- function disableAsmData () {
54
+ function disableWafUpdate () {
44
55
  if (rc) {
56
+ const RuleManager = require('../rule_manager')
57
+
45
58
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_IP_BLOCKING, false)
46
59
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_USER_BLOCKING, false)
47
- rc.off('ASM_DATA', _asmDataListener)
60
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, false)
61
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, false)
62
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, false)
63
+
64
+ rc.off('ASM_DATA', noop)
65
+ rc.off('ASM_DD', noop)
66
+ rc.off('ASM', noop)
67
+
68
+ rc.off(RemoteConfigManager.kPreUpdate, RuleManager.updateWafFromRC)
48
69
  }
49
70
  }
50
71
 
51
- function _asmDataListener (action, ruleData, ruleId) {
52
- RuleManager.updateAsmData(action, ruleData, ruleId)
53
- }
72
+ function noop () {}
54
73
 
55
74
  module.exports = {
56
75
  enable,
57
- enableAsmData,
58
- disableAsmData
76
+ enableWafUpdate,
77
+ disableWafUpdate
59
78
  }
@@ -6,14 +6,19 @@ const Scheduler = require('./scheduler')
6
6
  const tracerVersion = require('../../../../../package.json').version
7
7
  const request = require('../../exporters/common/request')
8
8
  const log = require('../../log')
9
+ const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('./apply_states')
9
10
 
10
11
  const clientId = uuid()
11
12
 
12
13
  const DEFAULT_CAPABILITY = Buffer.alloc(1).toString('base64') // 0x00
13
14
 
15
+ const kPreUpdate = Symbol('kPreUpdate')
16
+
14
17
  // There MUST NOT exist separate instances of RC clients in a tracer making separate ClientGetConfigsRequest
15
18
  // with their own separated Client.ClientState.
16
19
  class RemoteConfigManager extends EventEmitter {
20
+ static get kPreUpdate () { return kPreUpdate }
21
+
17
22
  constructor (config) {
18
23
  super()
19
24
 
@@ -78,9 +83,11 @@ class RemoteConfigManager extends EventEmitter {
78
83
  on (event, listener) {
79
84
  super.on(event, listener)
80
85
 
81
- this.state.client.products = this.eventNames()
86
+ this.updateProducts()
82
87
 
83
- this.scheduler.start()
88
+ if (this.state.client.products.length) {
89
+ this.scheduler.start()
90
+ }
84
91
 
85
92
  return this
86
93
  }
@@ -88,7 +95,7 @@ class RemoteConfigManager extends EventEmitter {
88
95
  off (event, listener) {
89
96
  super.off(event, listener)
90
97
 
91
- this.state.client.products = this.eventNames()
98
+ this.updateProducts()
92
99
 
93
100
  if (!this.state.client.products.length) {
94
101
  this.scheduler.stop()
@@ -97,6 +104,10 @@ class RemoteConfigManager extends EventEmitter {
97
104
  return this
98
105
  }
99
106
 
107
+ updateProducts () {
108
+ this.state.client.products = this.eventNames().filter(e => typeof e === 'string')
109
+ }
110
+
100
111
  poll (cb) {
101
112
  request(JSON.stringify(this.state), this.requestOptions, (err, data, statusCode) => {
102
113
  // 404 means RC is disabled, ignore it
@@ -181,7 +192,7 @@ class RemoteConfigManager extends EventEmitter {
181
192
  product,
182
193
  id,
183
194
  version: meta.custom.v,
184
- apply_state: 1,
195
+ apply_state: UNACKNOWLEDGED,
185
196
  apply_error: '',
186
197
  length: meta.length,
187
198
  hashes: meta.hashes,
@@ -194,6 +205,8 @@ class RemoteConfigManager extends EventEmitter {
194
205
  }
195
206
 
196
207
  if (toUnapply.length || toApply.length || toModify.length) {
208
+ this.emit(RemoteConfigManager.kPreUpdate, { toUnapply, toApply, toModify })
209
+
197
210
  this.dispatch(toUnapply, 'unapply')
198
211
  this.dispatch(toApply, 'apply')
199
212
  this.dispatch(toModify, 'modify')
@@ -221,14 +234,22 @@ class RemoteConfigManager extends EventEmitter {
221
234
 
222
235
  dispatch (list, action) {
223
236
  for (const item of list) {
224
- try {
225
- // TODO: do we want to pass old and new config ?
226
- this.emit(item.product, action, item.file, item.id)
227
-
228
- item.apply_state = 2
229
- } catch (err) {
230
- item.apply_state = 3
231
- item.apply_error = err.toString()
237
+ // TODO: we need a way to tell if unapply configs were handled by kPreUpdate or not, because they're always
238
+ // emitted unlike the apply and modify configs
239
+
240
+ // in case the item was already handled by kPreUpdate
241
+ if (item.apply_state === UNACKNOWLEDGED || action === 'unapply') {
242
+ try {
243
+ // TODO: do we want to pass old and new config ?
244
+ const hadListeners = this.emit(item.product, action, item.file, item.id)
245
+
246
+ if (hadListeners) {
247
+ item.apply_state = ACKNOWLEDGED
248
+ }
249
+ } catch (err) {
250
+ item.apply_state = ERROR
251
+ item.apply_error = err.toString()
252
+ }
232
253
  }
233
254
 
234
255
  if (action === 'unapply') {
@@ -1,12 +1,13 @@
1
1
  'use strict'
2
2
 
3
- const addresses = require('./addresses')
4
3
  const Limiter = require('../rate_limiter')
4
+ const { storage } = require('../../../datadog-core')
5
5
  const web = require('../plugins/util/web')
6
6
 
7
7
  // default limiter, configurable with setRateLimit()
8
8
  let limiter = new Limiter(100)
9
9
 
10
+ // TODO: use precomputed maps instead
10
11
  const REQUEST_HEADERS_PASSLIST = [
11
12
  'accept',
12
13
  'accept-encoding',
@@ -37,28 +38,6 @@ const RESPONSE_HEADERS_PASSLIST = [
37
38
 
38
39
  const metricsQueue = new Map()
39
40
 
40
- function resolveHTTPRequest (context) {
41
- if (!context) return {}
42
-
43
- const headers = context.resolve(addresses.HTTP_INCOMING_HEADERS)
44
-
45
- return {
46
- remote_ip: context.resolve(addresses.HTTP_INCOMING_REMOTE_IP),
47
- headers: filterHeaders(headers, REQUEST_HEADERS_PASSLIST, 'http.request.headers.')
48
- }
49
- }
50
-
51
- function resolveHTTPResponse (context) {
52
- if (!context) return {}
53
-
54
- const headers = context.resolve(addresses.HTTP_INCOMING_RESPONSE_HEADERS)
55
-
56
- return {
57
- endpoint: context.resolve(addresses.HTTP_INCOMING_ENDPOINT),
58
- headers: filterHeaders(headers, RESPONSE_HEADERS_PASSLIST, 'http.response.headers.')
59
- }
60
- }
61
-
62
41
  function filterHeaders (headers, passlist, prefix) {
63
42
  const result = {}
64
43
 
@@ -68,7 +47,7 @@ function filterHeaders (headers, passlist, prefix) {
68
47
  const headerName = passlist[i]
69
48
 
70
49
  if (headers[headerName]) {
71
- result[`${prefix}${formatHeaderName(headerName)}`] = headers[headerName] + ''
50
+ result[`${prefix}${formatHeaderName(headerName)}`] = '' + headers[headerName]
72
51
  }
73
52
  }
74
53
 
@@ -84,10 +63,11 @@ function formatHeaderName (name) {
84
63
  .toLowerCase()
85
64
  }
86
65
 
87
- function reportMetrics (metrics, store) {
88
- const req = store && store.get('req')
89
- const rootSpan = web.root(req)
90
- if (!rootSpan) return false
66
+ function reportMetrics (metrics) {
67
+ // TODO: metrics should be incremental, there already is an RFC to report metrics
68
+ const store = storage.getStore()
69
+ const rootSpan = store && store.req && web.root(store.req)
70
+ if (!rootSpan) return
91
71
 
92
72
  if (metrics.duration) {
93
73
  rootSpan.setTag('_dd.appsec.waf.duration', metrics.duration)
@@ -102,16 +82,17 @@ function reportMetrics (metrics, store) {
102
82
  }
103
83
  }
104
84
 
105
- function reportAttack (attackData, store) {
106
- const req = store && store.get('req')
85
+ function reportAttack (attackData) {
86
+ const store = storage.getStore()
87
+ const req = store && store.req
107
88
  const rootSpan = web.root(req)
108
- if (!rootSpan) return false
89
+ if (!rootSpan) return
109
90
 
110
91
  const currentTags = rootSpan.context()._tags
111
92
 
112
- const newTags = {
113
- 'appsec.event': 'true'
114
- }
93
+ const newTags = filterHeaders(req.headers, REQUEST_HEADERS_PASSLIST, 'http.request.headers.')
94
+
95
+ newTags['appsec.event'] = 'true'
115
96
 
116
97
  if (limiter.isAllowed()) {
117
98
  newTags['manual.keep'] = 'true' // TODO: figure out how to keep appsec traces with sampling revamp
@@ -126,32 +107,24 @@ function reportAttack (attackData, store) {
126
107
 
127
108
  // merge JSON arrays without parsing them
128
109
  if (currentJson) {
129
- newTags['_dd.appsec.json'] = currentJson.slice(0, -2) + ',' + attackData.slice(1, -1) + currentJson.slice(-2)
110
+ newTags['_dd.appsec.json'] = currentJson.slice(0, -2) + ',' + attackData.slice(1) + '}'
130
111
  } else {
131
112
  newTags['_dd.appsec.json'] = '{"triggers":' + attackData + '}'
132
113
  }
133
114
 
134
- const context = store.get('context')
135
-
136
- if (context) {
137
- const resolvedRequest = resolveHTTPRequest(context)
138
-
139
- Object.assign(newTags, resolvedRequest.headers)
140
-
141
- const ua = resolvedRequest.headers['http.request.headers.user-agent']
142
- if (ua) {
143
- newTags['http.useragent'] = ua
144
- }
145
-
146
- newTags['network.client.ip'] = resolvedRequest.remote_ip
115
+ const ua = newTags['http.request.headers.user-agent']
116
+ if (ua) {
117
+ newTags['http.useragent'] = ua
147
118
  }
148
119
 
120
+ newTags['network.client.ip'] = req.socket.remoteAddress
121
+
149
122
  rootSpan.addTags(newTags)
150
123
  }
151
124
 
152
- function finishRequest (req, context) {
125
+ function finishRequest (req, res) {
153
126
  const rootSpan = web.root(req)
154
- if (!rootSpan) return false
127
+ if (!rootSpan) return
155
128
 
156
129
  if (metricsQueue.size) {
157
130
  rootSpan.addTags(Object.fromEntries(metricsQueue))
@@ -159,14 +132,12 @@ function finishRequest (req, context) {
159
132
  metricsQueue.clear()
160
133
  }
161
134
 
162
- if (!context || !rootSpan.context()._tags['appsec.event']) return false
163
-
164
- const resolvedResponse = resolveHTTPResponse(context)
135
+ if (!rootSpan.context()._tags['appsec.event']) return
165
136
 
166
- const newTags = resolvedResponse.headers
137
+ const newTags = filterHeaders(res.getHeaders(), RESPONSE_HEADERS_PASSLIST, 'http.response.headers.')
167
138
 
168
- if (resolvedResponse.endpoint) {
169
- newTags['http.endpoint'] = resolvedResponse.endpoint
139
+ if (req.route && typeof req.route.path === 'string') {
140
+ newTags['http.endpoint'] = req.route.path
170
141
  }
171
142
 
172
143
  rootSpan.addTags(newTags)
@@ -178,8 +149,6 @@ function setRateLimit (rateLimit) {
178
149
 
179
150
  module.exports = {
180
151
  metricsQueue,
181
- resolveHTTPRequest,
182
- resolveHTTPResponse,
183
152
  filterHeaders,
184
153
  formatHeaderName,
185
154
  reportMetrics,
@@ -1,48 +1,175 @@
1
1
  'use strict'
2
2
 
3
- const callbacks = require('./callbacks')
4
- const Gateway = require('./gateway/engine')
3
+ const waf = require('./waf')
4
+ const { ACKNOWLEDGED, ERROR } = require('./remote_config/apply_states')
5
5
 
6
- const appliedCallbacks = new Map()
7
- const appliedAsmData = new Map()
6
+ let defaultRules
7
+
8
+ let appliedRulesData = new Map()
9
+ let appliedRulesetId
10
+ let appliedRulesOverride = new Map()
11
+ let appliedExclusions = new Map()
8
12
 
9
13
  function applyRules (rules, config) {
10
- if (appliedCallbacks.has(rules)) return
14
+ defaultRules = rules
15
+
16
+ waf.init(rules, config)
17
+ }
18
+
19
+ function updateWafFromRC ({ toUnapply, toApply, toModify }) {
20
+ const batch = new Set()
21
+
22
+ const newRulesData = new SpyMap(appliedRulesData)
23
+ let newRuleset
24
+ let newRulesetId
25
+ const newRulesOverride = new SpyMap(appliedRulesOverride)
26
+ const newExclusions = new SpyMap(appliedExclusions)
27
+
28
+ for (const item of toUnapply) {
29
+ const { product, id } = item
30
+
31
+ if (product === 'ASM_DATA') {
32
+ newRulesData.delete(id)
33
+ } else if (product === 'ASM_DD') {
34
+ if (appliedRulesetId === id) {
35
+ newRuleset = defaultRules
36
+ newRulesetId = null
37
+ }
38
+ } else if (product === 'ASM') {
39
+ newRulesOverride.delete(id)
40
+ newExclusions.delete(id)
41
+ }
42
+ }
43
+
44
+ for (const item of [...toApply, ...toModify]) {
45
+ const { product, id, file } = item
46
+
47
+ if (product === 'ASM_DATA') {
48
+ if (file && file.rules_data && file.rules_data.length) {
49
+ newRulesData.set(id, file.rules_data)
50
+ }
51
+
52
+ batch.add(item)
53
+ } else if (product === 'ASM_DD') {
54
+ if (appliedRulesetId && appliedRulesetId !== id) {
55
+ item.apply_state = ERROR
56
+ item.apply_error = 'Multiple ruleset received in ASM_DD'
57
+ } else {
58
+ if (file && file.rules && file.rules.length) {
59
+ const { version, metadata, rules } = file
60
+
61
+ newRuleset = { version, metadata, rules }
62
+ newRulesetId = id
63
+ }
64
+
65
+ batch.add(item)
66
+ }
67
+ } else if (product === 'ASM') {
68
+ if (file && file.rules_override && file.rules_override.length) {
69
+ newRulesOverride.set(id, file.rules_override)
70
+ }
71
+
72
+ if (file && file.exclusions && file.exclusions.length) {
73
+ newExclusions.set(id, file.exclusions)
74
+ }
75
+
76
+ batch.add(item)
77
+ }
78
+ }
11
79
 
12
- // for now there is only WAF
13
- const callback = new callbacks.DDWAF(rules, config)
80
+ let newApplyState = ACKNOWLEDGED
81
+ let newApplyError
14
82
 
15
- appliedCallbacks.set(rules, callback)
83
+ if (newRulesData.modified || newRuleset || newRulesOverride.modified || newExclusions.modified) {
84
+ const payload = newRuleset || {}
85
+
86
+ if (newRulesData.modified) {
87
+ payload.rules_data = mergeRulesData(newRulesData)
88
+ }
89
+ if (newRulesOverride.modified) {
90
+ payload.rules_override = concatArrays(newRulesOverride)
91
+ }
92
+ if (newExclusions.modified) {
93
+ payload.exclusions = concatArrays(newExclusions)
94
+ }
95
+
96
+ try {
97
+ waf.update(payload)
98
+
99
+ if (newRulesData.modified) {
100
+ appliedRulesData = newRulesData
101
+ }
102
+ if (newRuleset) {
103
+ appliedRulesetId = newRulesetId
104
+ }
105
+ if (newRulesOverride.modified) {
106
+ appliedRulesOverride = newRulesOverride
107
+ }
108
+ if (newExclusions.modified) {
109
+ appliedExclusions = newExclusions
110
+ }
111
+ } catch (err) {
112
+ newApplyState = ERROR
113
+ newApplyError = err.toString()
114
+ }
115
+ }
116
+
117
+ for (const config of batch) {
118
+ config.apply_state = newApplyState
119
+ if (newApplyError) config.apply_error = newApplyError
120
+ }
16
121
  }
17
122
 
18
- function updateAsmData (action, asmData, asmDataId) {
19
- if (action === 'unapply') {
20
- appliedAsmData.delete(asmDataId)
21
- } else {
22
- appliedAsmData.set(asmDataId, asmData)
123
+ // A Map with a new prop `modified`, a bool that indicates if the Map was modified
124
+ class SpyMap extends Map {
125
+ constructor (iterable) {
126
+ super(iterable)
127
+ this.modified = false
128
+ }
129
+
130
+ set (key, value) {
131
+ this.modified = true
132
+ return super.set(key, value)
133
+ }
134
+
135
+ delete (key) {
136
+ const result = super.delete(key)
137
+ if (result) this.modified = true
138
+ return result
23
139
  }
24
140
 
25
- const mergedRuleData = mergeRuleData(appliedAsmData.values())
26
- for (const callback of appliedCallbacks.values()) {
27
- callback.updateRuleData(mergedRuleData)
141
+ clear () {
142
+ this.modified = false
143
+ return super.clear()
28
144
  }
29
145
  }
30
146
 
31
- function mergeRuleData (asmDataValues) {
147
+ function concatArrays (files) {
148
+ return Array.from(files.values()).flat()
149
+ }
150
+
151
+ /*
152
+ ASM_DATA Merge strategy:
153
+ The merge should be based on the id and type. For any duplicate items, the longer expiration should be taken.
154
+ As a result, multiple Rule Data may use the same DATA_ID and DATA_TYPE. In this case, all values are considered part
155
+ of a set and are merged. For instance, a denylist customized by environment may use a global Rule Data for all
156
+ environments and a Rule Data per environment
157
+ */
158
+
159
+ function mergeRulesData (files) {
32
160
  const mergedRulesData = new Map()
33
- for (const asmData of asmDataValues) {
34
- if (!asmData.rules_data) continue
35
- for (const rulesData of asmData.rules_data) {
36
- const key = `${rulesData.id}+${rulesData.type}`
161
+ for (const [, file] of files) {
162
+ for (const ruleData of file) {
163
+ const key = `${ruleData.id}+${ruleData.type}`
37
164
  if (mergedRulesData.has(key)) {
38
165
  const existingRulesData = mergedRulesData.get(key)
39
- rulesData.data.reduce(rulesReducer, existingRulesData.data)
166
+ ruleData.data.reduce(rulesReducer, existingRulesData.data)
40
167
  } else {
41
- mergedRulesData.set(key, copyRulesData(rulesData))
168
+ mergedRulesData.set(key, copyRulesData(ruleData))
42
169
  }
43
170
  }
44
171
  }
45
- return [...mergedRulesData.values()]
172
+ return Array.from(mergedRulesData.values())
46
173
  }
47
174
 
48
175
  function rulesReducer (existingEntries, rulesDataEntry) {
@@ -69,19 +196,20 @@ function copyRulesData (rulesData) {
69
196
  }
70
197
  return copy
71
198
  }
199
+
72
200
  function clearAllRules () {
73
- Gateway.manager.clear()
201
+ waf.destroy()
74
202
 
75
- for (const [key, callback] of appliedCallbacks) {
76
- callback.clear()
203
+ defaultRules = null
77
204
 
78
- appliedCallbacks.delete(key)
79
- }
80
- appliedAsmData.clear()
205
+ appliedRulesData.clear()
206
+ appliedRulesetId = null
207
+ appliedRulesOverride.clear()
208
+ appliedExclusions.clear()
81
209
  }
82
210
 
83
211
  module.exports = {
84
212
  applyRules,
85
- clearAllRules,
86
- updateAsmData
213
+ updateWafFromRC,
214
+ clearAllRules
87
215
  }
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { USER_ID } = require('../addresses')
4
- const Gateway = require('../gateway/engine')
4
+ const waf = require('../waf')
5
5
  const { getRootSpan } = require('./utils')
6
6
  const { block } = require('../blocking')
7
7
  const { storage } = require('../../../../datadog-core')
@@ -9,19 +9,11 @@ const { setUserTags } = require('./set_user')
9
9
  const log = require('../../log')
10
10
 
11
11
  function isUserBlocked (user) {
12
- const results = Gateway.propagate({ [USER_ID]: user.id })
12
+ const actions = waf.run({ [USER_ID]: user.id })
13
13
 
14
- if (!results) {
15
- return false
16
- }
17
-
18
- for (const entry of results) {
19
- if (entry && entry.includes('block')) {
20
- return true
21
- }
22
- }
14
+ if (!actions) return false
23
15
 
24
- return false
16
+ return actions.includes('block')
25
17
  }
26
18
 
27
19
  function checkUserAndSetUser (tracer, user) {
@@ -0,0 +1,75 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../../../datadog-core')
4
+ const log = require('../../log')
5
+
6
+ const waf = {
7
+ wafManager: null,
8
+ init,
9
+ destroy,
10
+ update,
11
+ run: noop,
12
+ disposeContext: noop
13
+ }
14
+
15
+ function init (rules, config) {
16
+ destroy()
17
+
18
+ // dirty require to make startup faster for serverless
19
+ const WAFManager = require('./waf_manager')
20
+
21
+ waf.wafManager = new WAFManager(rules, config)
22
+
23
+ waf.run = run
24
+ waf.disposeContext = disposeContext
25
+ }
26
+
27
+ function destroy () {
28
+ if (waf.wafManager) {
29
+ waf.wafManager.destroy()
30
+ waf.wafManager = null
31
+ }
32
+
33
+ waf.run = noop
34
+ waf.disposeContext = noop
35
+ }
36
+
37
+ function update (newRules) {
38
+ // TODO: check race conditions between Appsec enable/disable and WAF updates, the whole RC state management in general
39
+ if (!waf.wafManager) throw new Error('Cannot update disabled WAF')
40
+
41
+ try {
42
+ waf.wafManager.ddwaf.update(newRules)
43
+ } catch (err) {
44
+ log.error('Could not apply rules from remote config')
45
+ throw err
46
+ }
47
+ }
48
+
49
+ function run (data, req) {
50
+ if (!req) {
51
+ const store = storage.getStore()
52
+ if (!store || !store.req) {
53
+ log.warn('Request object not available in waf.run')
54
+ return
55
+ }
56
+
57
+ req = store.req
58
+ }
59
+
60
+ const wafContext = waf.wafManager.getWAFContext(req)
61
+
62
+ return wafContext.run(data)
63
+ }
64
+
65
+ function disposeContext (req) {
66
+ const wafContext = waf.wafManager.getWAFContext(req)
67
+
68
+ if (wafContext && !wafContext.ddwafContext.disposed) {
69
+ wafContext.dispose()
70
+ }
71
+ }
72
+
73
+ function noop () {}
74
+
75
+ module.exports = waf