Skip to content

Commit b49c683

Browse files
committed
refactor(remote-config): replace kPreUpdate with batch handler API
- 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 tx (ack/error/markHandled) instead of mutating configs - Improve JSDoc/TS inference for batch transaction/descriptors and WAF manager typing
1 parent edfc7c6 commit b49c683

File tree

9 files changed

+542
-157
lines changed

9 files changed

+542
-157
lines changed
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: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,90 @@
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+
* @param {{ rules?: string, [key: string]: any }} config
19+
*/
1920
function loadRules (config) {
2021
const defaultRules = config.rules
21-
? JSON.parse(fs.readFileSync(config.rules))
22+
? JSON.parse(readFileSync(config.rules, 'utf8'))
2223
: require('./recommended.json')
2324

2425
waf.init(defaultRules, config)
2526

2627
blocking.setDefaultBlockingActionParameters(defaultRules?.actions)
2728
}
2829

29-
function updateWafFromRC ({ toUnapply, toApply, toModify }) {
30+
/**
31+
* Apply ASM remote-config updates to the WAF in a single batch.
32+
*
33+
* @param {import('../remote_config/manager').RcBatchUpdateTx} tx
34+
*/
35+
function updateWafFromRC (tx) {
36+
const { toUnapply, toApply, toModify } = tx
37+
3038
const newActions = new SpyMap(appliedActions)
3139

3240
let wafUpdated = false
3341
let wafUpdatedFailed = false
3442

3543
for (const item of toUnapply) {
36-
if (!ASM_PRODUCTS.has(item.product)) continue
44+
if (!ASM_WAF_PRODUCTS_SET.has(item.product)) continue
3745

3846
try {
3947
waf.removeConfig(item.path)
4048

41-
item.apply_state = ACKNOWLEDGED
49+
tx.ack(item.path)
50+
tx.markHandled(item.path)
4251
wafUpdated = true
4352

4453
// ASM actions
4554
if (item.product === 'ASM') {
4655
newActions.delete(item.id)
4756
}
4857
} catch (e) {
49-
item.apply_state = ERROR
50-
item.apply_error = e.toString()
58+
tx.error(item.path, e)
59+
tx.markHandled(item.path)
5160
wafUpdatedFailed = true
5261
}
5362
}
5463

5564
for (const item of [...toApply, ...toModify]) {
56-
if (!ASM_PRODUCTS.has(item.product)) continue
65+
if (!ASM_WAF_PRODUCTS_SET.has(item.product)) continue
5766

5867
try {
5968
waf.updateConfig(item.product, item.id, item.path, item.file)
6069

61-
item.apply_state = ACKNOWLEDGED
70+
tx.ack(item.path)
71+
tx.markHandled(item.path)
6272
wafUpdated = true
6373

6474
// ASM actions
6575
if (item.product === 'ASM' && item.file?.actions?.length) {
6676
newActions.set(item.id, item.file.actions)
6777
}
6878
} catch (e) {
69-
item.apply_state = ERROR
70-
item.apply_error = e instanceof waf.WafUpdateError
71-
? JSON.stringify(extractErrors(e.diagnosticErrors))
72-
: e.toString()
79+
tx.error(item.path, e instanceof waf.WafUpdateError ? JSON.stringify(extractErrors(e.diagnosticErrors)) : e)
80+
tx.markHandled(item.path)
7381
wafUpdatedFailed = true
7482
}
7583
}
7684

7785
waf.checkAsmDdFallback()
7886

79-
if (wafUpdated) {
87+
if (wafUpdated && waf.wafManager) {
8088
Reporter.reportWafUpdate(waf.wafManager.ddwafVersion, waf.wafManager.rulesVersion, !wafUpdatedFailed)
8189
}
8290

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ 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+
2227
const waf = {
28+
/** @type {WAFManager | null} */
2329
wafManager: null,
2430
init,
2531
destroy,
@@ -37,7 +43,7 @@ function init (rules, config) {
3743
limiter = new Limiter(config.rateLimit)
3844

3945
// dirty require to make startup faster for serverless
40-
const WAFManager = require('./waf_manager')
46+
WAFManager = require('./waf_manager')
4147

4248
waf.wafManager = new WAFManager(rules, config)
4349

@@ -50,6 +56,7 @@ function destroy () {
5056
waf.wafManager.destroy()
5157
waf.wafManager = null
5258
}
59+
WAFManager = null
5360

5461
waf.run = noop
5562
waf.disposeContext = noop
@@ -70,7 +77,7 @@ function updateConfig (product, configId, configPath, config) {
7077

7178
try {
7279
if (product === 'ASM_DD') {
73-
waf.wafManager.removeConfig(waf.wafManager.constructor.defaultWafConfigPath)
80+
waf.wafManager.removeConfig((/** @type {NonNullable<typeof WAFManager>} */ (WAFManager)).defaultWafConfigPath)
7481
}
7582

7683
const updateSucceeded = waf.wafManager.updateConfig(configPath, config)
@@ -97,6 +104,7 @@ function removeConfig (configPath) {
97104
}
98105

99106
function run (data, req, raspRule) {
107+
if (!waf.wafManager) return
100108
if (!req) {
101109
const store = storage('legacy').getStore()
102110
if (!store || !store.req) {
@@ -123,6 +131,7 @@ function run (data, req, raspRule) {
123131
}
124132

125133
function disposeContext (req) {
134+
if (!waf.wafManager) return
126135
const wafContext = waf.wafManager.getWAFContext(req)
127136

128137
if (wafContext && !wafContext.ddwafContext.disposed) {

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)