Skip to content

Commit 1a62486

Browse files
watsonnina9753
authored andcommitted
refactor(remote-config): replace kPreUpdate with batch handler API (DEBUG-4402) (#7121)
- Remove the leaky kPreUpdate hook and introduce a first-class batch API: setBatchHandler()/removeBatchHandler() - Introduce explicit product subscription via subscribeProducts()/ unsubscribeProducts(), and have setProductHandler()/ removeProductHandler() subscribe/unsubscribe for clarity - Centralize ASM/WAF RC product names in appsec/rc-products.js - Update AppSec WAF RC integration to use batched transaction (ack/error) instead of mutating configs - Improve JSDoc/TS inference for batch transaction/descriptors and WAF manager typing
1 parent a209665 commit 1a62486

10 files changed

Lines changed: 525 additions & 163 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict'
2+
3+
// Remote Config product names used by ASM/WAF.
4+
const ASM_WAF_PRODUCTS = ['ASM', 'ASM_DD', 'ASM_DATA']
5+
const ASM_WAF_PRODUCTS_SET = new Set(ASM_WAF_PRODUCTS)
6+
7+
module.exports = {
8+
ASM_WAF_PRODUCTS,
9+
ASM_WAF_PRODUCTS_SET
10+
}

packages/dd-trace/src/appsec/rule_manager.js

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,92 @@
11
'use strict'
22

3-
const fs = require('fs')
3+
const { readFileSync } = require('node:fs')
4+
45
const waf = require('./waf')
56
const { DIAGNOSTIC_KEYS } = require('./waf/diagnostics')
6-
const { ACKNOWLEDGED, ERROR } = require('../remote_config/apply_states')
7-
const Reporter = require('./reporter')
8-
97
const blocking = require('./blocking')
10-
11-
const ASM_PRODUCTS = new Set(['ASM', 'ASM_DD', 'ASM_DATA'])
8+
const Reporter = require('./reporter')
9+
const { ASM_WAF_PRODUCTS_SET } = require('./rc-products')
1210

1311
/*
1412
ASM Actions must be tracked in order to update the defaultBlockingActions in blocking. These actions are used
1513
by blockRequest method exposed in the user blocking SDK (see packages/dd-trace/src/appsec/sdk/user_blocking.js)
1614
*/
1715
let appliedActions = new Map()
1816

17+
/**
18+
* @typedef {object} AsmConfigFile
19+
* @property {Array<Record<string, unknown>>} [actions]
20+
*/
21+
22+
/**
23+
* @typedef {import('./waf').WAFConfig & { rules?: string }} AppSecConfig
24+
*/
25+
26+
/**
27+
* @param {AppSecConfig} config
28+
*/
1929
function loadRules (config) {
2030
const defaultRules = config.rules
21-
? JSON.parse(fs.readFileSync(config.rules))
31+
? JSON.parse(readFileSync(config.rules, 'utf8'))
2232
: require('./recommended.json')
2333

2434
waf.init(defaultRules, config)
2535

2636
blocking.setDefaultBlockingActionParameters(defaultRules?.actions)
2737
}
2838

29-
function updateWafFromRC ({ toUnapply, toApply, toModify }) {
39+
/**
40+
* Apply ASM remote-config updates to the WAF in a single batch.
41+
*
42+
* @param {import('../remote_config/manager').RcBatchUpdateTransaction} transaction
43+
*/
44+
function updateWafFromRC (transaction) {
45+
const { toUnapply, toApply, toModify } = transaction
46+
3047
const newActions = new SpyMap(appliedActions)
3148

3249
let wafUpdated = false
3350
let wafUpdatedFailed = false
3451

3552
for (const item of toUnapply) {
36-
if (!ASM_PRODUCTS.has(item.product)) continue
53+
if (!ASM_WAF_PRODUCTS_SET.has(item.product)) continue
3754

3855
try {
3956
waf.removeConfig(item.path)
4057

41-
item.apply_state = ACKNOWLEDGED
58+
transaction.ack(item.path)
4259
wafUpdated = true
4360

4461
// ASM actions
4562
if (item.product === 'ASM') {
4663
newActions.delete(item.id)
4764
}
4865
} catch (e) {
49-
item.apply_state = ERROR
50-
item.apply_error = e.toString()
66+
transaction.error(item.path, e)
5167
wafUpdatedFailed = true
5268
}
5369
}
5470

5571
for (const item of [...toApply, ...toModify]) {
56-
if (!ASM_PRODUCTS.has(item.product)) continue
72+
if (!ASM_WAF_PRODUCTS_SET.has(item.product)) continue
5773

5874
try {
5975
waf.updateConfig(item.product, item.id, item.path, item.file)
6076

61-
item.apply_state = ACKNOWLEDGED
77+
transaction.ack(item.path)
6278
wafUpdated = true
6379

6480
// ASM actions
65-
if (item.product === 'ASM' && item.file?.actions?.length) {
66-
newActions.set(item.id, item.file.actions)
81+
if (item.product === 'ASM') {
82+
const asmFile = /** @type {AsmConfigFile} */ (item.file)
83+
if (asmFile?.actions?.length) {
84+
newActions.set(item.id, asmFile.actions)
85+
}
6786
}
6887
} catch (e) {
69-
item.apply_state = ERROR
70-
item.apply_error = e instanceof waf.WafUpdateError
71-
? JSON.stringify(extractErrors(e.diagnosticErrors))
72-
: e.toString()
88+
const error = e instanceof waf.WafUpdateError ? JSON.stringify(extractErrors(e.diagnosticErrors)) : e
89+
transaction.error(item.path, error)
7390
wafUpdatedFailed = true
7491
}
7592
}

packages/dd-trace/src/appsec/waf/index.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@ class WafUpdateError extends Error {
1919

2020
let limiter = new Limiter(100)
2121

22+
/** @typedef {import('./waf_manager')} WAFManager */
23+
24+
/** @type {typeof import('./waf_manager') | null} */
25+
let WAFManager = null
26+
27+
/**
28+
* @typedef {import('./waf_manager').WAFManagerConfig & { rateLimit: number }} WAFConfig
29+
*/
30+
2231
const waf = {
32+
/** @type {WAFManager | null} */
2333
wafManager: null,
2434
init,
2535
destroy,
@@ -31,13 +41,17 @@ const waf = {
3141
WafUpdateError
3242
}
3343

44+
/**
45+
* @param {object} rules
46+
* @param {WAFConfig} config
47+
*/
3448
function init (rules, config) {
3549
destroy()
3650

3751
limiter = new Limiter(config.rateLimit)
3852

39-
// dirty require to make startup faster for serverless
40-
const WAFManager = require('./waf_manager')
53+
// Lazy loading improves the startup time
54+
WAFManager = require('./waf_manager')
4155

4256
waf.wafManager = new WAFManager(rules, config)
4357

@@ -70,7 +84,7 @@ function updateConfig (product, configId, configPath, config) {
7084

7185
try {
7286
if (product === 'ASM_DD') {
73-
waf.wafManager.removeConfig(waf.wafManager.constructor.defaultWafConfigPath)
87+
waf.wafManager.removeConfig((/** @type {NonNullable<typeof WAFManager>} */ (WAFManager)).defaultWafConfigPath)
7488
}
7589

7690
const updateSucceeded = waf.wafManager.updateConfig(configPath, config)

packages/dd-trace/src/appsec/waf/waf_manager.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,20 @@ const WAFContextWrapper = require('./waf_context_wrapper')
66

77
const contexts = new WeakMap()
88

9+
/**
10+
* @typedef {object} WAFManagerConfig
11+
* @property {number} wafTimeout - Maximum time in microseconds for WAF execution
12+
* @property {string} obfuscatorKeyRegex - Regex to redact sensitive data by key
13+
* @property {string} obfuscatorValueRegex - Regex to redact sensitive data by value
14+
*/
15+
916
class WAFManager {
1017
static defaultWafConfigPath = 'datadog/00/ASM_DD/default/config'
1118

19+
/**
20+
* @param {object} rules
21+
* @param {WAFManagerConfig} config
22+
*/
1223
constructor (rules, config) {
1324
this.config = config
1425
this.wafTimeout = config.wafTimeout

packages/dd-trace/src/remote_config/index.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ function enableOrDisableAppsec (action, rcConfig, config, appsec) {
8888
function enableWafUpdate (appsecConfig) {
8989
if (rc && appsecConfig && !appsecConfig.rules) {
9090
// dirty require to make startup faster for serverless
91+
const { ASM_WAF_PRODUCTS } = require('../appsec/rc-products')
9192
const RuleManager = require('../appsec/rule_manager')
9293

9394
rc.updateCapabilities(RemoteConfigCapabilities.ASM_IP_BLOCKING, true)
@@ -119,17 +120,14 @@ function enableWafUpdate (appsecConfig) {
119120
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, true)
120121
}
121122

122-
// TODO: delete noop handlers and kPreUpdate and replace with batched handlers
123-
rc.setProductHandler('ASM_DATA', noop)
124-
rc.setProductHandler('ASM_DD', noop)
125-
rc.setProductHandler('ASM', noop)
126-
127-
rc.on(RemoteConfigManager.kPreUpdate, RuleManager.updateWafFromRC)
123+
rc.subscribeProducts(...ASM_WAF_PRODUCTS)
124+
rc.setBatchHandler(ASM_WAF_PRODUCTS, RuleManager.updateWafFromRC)
128125
}
129126
}
130127

131128
function disableWafUpdate () {
132129
if (rc) {
130+
const { ASM_WAF_PRODUCTS } = require('../appsec/rc-products')
133131
const RuleManager = require('../appsec/rule_manager')
134132

135133
rc.updateCapabilities(RemoteConfigCapabilities.ASM_IP_BLOCKING, false)
@@ -158,16 +156,11 @@ function disableWafUpdate () {
158156
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, false)
159157
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, false)
160158

161-
rc.removeProductHandler('ASM_DATA')
162-
rc.removeProductHandler('ASM_DD')
163-
rc.removeProductHandler('ASM')
164-
165-
rc.off(RemoteConfigManager.kPreUpdate, RuleManager.updateWafFromRC)
159+
rc.unsubscribeProducts(...ASM_WAF_PRODUCTS)
160+
rc.removeBatchHandler(RuleManager.updateWafFromRC)
166161
}
167162
}
168163

169-
function noop () {}
170-
171164
module.exports = {
172165
enable,
173166
enableWafUpdate,

0 commit comments

Comments
 (0)