From e3faa7b9b7216038e3c71c8ab78b59f36059e008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Mon, 28 Jul 2025 18:48:39 +0200 Subject: [PATCH 1/7] Only report ASM_DD, ASM_DATA and ASM capabilities when AppSec is enabled --- .../config/AppSecConfigServiceImpl.java | 83 ++++-- ...ppSecConfigServiceImplSpecification.groovy | 251 +++++------------- 2 files changed, 135 insertions(+), 199 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java index 4d6eb78f864..38cffe9dc10 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java @@ -60,11 +60,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import okio.Okio; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -97,12 +97,16 @@ public class AppSecConfigServiceImpl implements AppSecConfigService { .build() .adapter(Types.newParameterizedType(Map.class, String.class, Object.class)); - private boolean hasUserWafConfig; - private boolean defaultConfigActivated; - private final Set usedDDWafConfigKeys = new HashSet<>(); + private volatile boolean hasUserWafConfig; + private volatile boolean defaultConfigActivated; + private final AtomicBoolean subscribedToRulesAndData = new AtomicBoolean(); + private final Set usedDDWafConfigKeys = + Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set emptyConfigKeys = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final String DEFAULT_WAF_CONFIG_RULE = "DEFAULT_WAF_CONFIG"; private String currentRuleVersion; private List modulesToUpdateVersionIn; + private long rulesAndDataCapabilities; public AppSecConfigServiceImpl( Config tracerConfig, @@ -122,13 +126,15 @@ private void subscribeConfigurationPoller() { subscribeAsmFeatures(); if (!hasUserWafConfig) { - subscribeRulesAndData(); + updateRulesAndDataSubscription(); } else { log.debug("Will not subscribe to ASM, ASM_DD and ASM_DATA (AppSec custom rules in use)"); } this.configurationPoller.addConfigurationEndListener(applyRemoteConfigListener); + } + private long buildRulesAndDataCapabilities() { long capabilities = CAPABILITY_ASM_DD_RULES | CAPABILITY_ASM_IP_BLOCKING @@ -154,13 +160,36 @@ private void subscribeConfigurationPoller() { capabilities |= CAPABILITY_ASM_RASP_LFI; } } - this.configurationPoller.addCapabilities(capabilities); + return capabilities; + } + + private void updateRulesAndDataSubscription() { + if (hasUserWafConfig) { + return; // do nothing if the customer has custom rules + } + if (AppSecSystem.isActive()) { + subscribeRulesAndData(); + } else { + unsubscribeRulesAndData(); + } } private void subscribeRulesAndData() { - this.configurationPoller.addListener(Product.ASM_DD, new AppSecConfigChangesDDListener()); - this.configurationPoller.addListener(Product.ASM_DATA, new AppSecConfigChangesListener()); - this.configurationPoller.addListener(Product.ASM, new AppSecConfigChangesListener()); + if (subscribedToRulesAndData.compareAndSet(false, true)) { + this.configurationPoller.addListener(Product.ASM_DD, new AppSecConfigChangesDDListener()); + this.configurationPoller.addListener(Product.ASM_DATA, new AppSecConfigChangesListener()); + this.configurationPoller.addListener(Product.ASM, new AppSecConfigChangesListener()); + this.configurationPoller.addCapabilities(rulesAndDataCapabilities); + } + } + + private void unsubscribeRulesAndData() { + if (subscribedToRulesAndData.compareAndSet(true, false)) { + this.configurationPoller.removeListeners(Product.ASM_DD); + this.configurationPoller.removeListeners(Product.ASM_DATA); + this.configurationPoller.removeListeners(Product.ASM); + this.configurationPoller.removeCapabilities(rulesAndDataCapabilities); + } } public void modulesToUpdateVersionIn(List modules) { @@ -176,20 +205,26 @@ private class AppSecConfigChangesListener implements ProductListener { public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollingRateHinter) throws IOException { maybeInitializeDefaultConfig(); - + final String key = configKey.toString(); if (content == null) { - try { - wafBuilder.removeConfig(configKey.toString()); - } catch (UnclassifiedWafException e) { - throw new RuntimeException(e); + if (!emptyConfigKeys.remove(key)) { + try { + wafBuilder.removeConfig(key); + } catch (UnclassifiedWafException e) { + throw new RuntimeException(e); + } } } else { Map contentMap = ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content)))); - try { - handleWafUpdateResultReport(configKey.toString(), contentMap); - } catch (AppSecModule.AppSecModuleActivationException e) { - throw new RuntimeException(e); + if (contentMap == null || contentMap.isEmpty()) { + emptyConfigKeys.add(key); + } else { + try { + handleWafUpdateResultReport(key, contentMap); + } catch (AppSecModule.AppSecModuleActivationException e) { + throw new RuntimeException(e); + } } } } @@ -282,7 +317,6 @@ private void subscribeAsmFeatures() { Product.ASM_FEATURES, AppSecFeaturesDeserializer.INSTANCE, (configKey, newConfig, hinter) -> { - maybeInitializeDefaultConfig(); if (newConfig == null) { mergedAsmFeatures.removeConfig(configKey); } else { @@ -341,6 +375,8 @@ public void init() { } this.mergedAsmFeatures.clear(); this.usedDDWafConfigKeys.clear(); + this.emptyConfigKeys.clear(); + this.rulesAndDataCapabilities = buildRulesAndDataCapabilities(); if (wafConfig.isEmpty()) { throw new IllegalStateException("Expected default waf config to be available"); @@ -353,9 +389,12 @@ public void init() { } public void maybeSubscribeConfigPolling() { + final ProductActivation appSecActivation = tracerConfig.getAppSecActivation(); + if (appSecActivation == ProductActivation.FULLY_DISABLED) { + return; // shouldn't happen but just in case. + } if (this.configurationPoller != null) { - if (hasUserWafConfig - && tracerConfig.getAppSecActivation() == ProductActivation.FULLY_ENABLED) { + if (hasUserWafConfig && appSecActivation == ProductActivation.FULLY_ENABLED) { log.info( "AppSec will not use remote config because " + "there is a custom user configuration and AppSec is explicitly enabled"); @@ -494,6 +533,7 @@ public void close() { this.configurationPoller.removeListeners(Product.ASM); this.configurationPoller.removeListeners(Product.ASM_FEATURES); this.configurationPoller.removeConfigurationEndListener(applyRemoteConfigListener); + this.subscribedToRulesAndData.set(false); this.configurationPoller.stop(); if (this.wafBuilder != null) { this.wafBuilder.close(); @@ -526,6 +566,7 @@ private void setAppSecActivation(final AppSecFeatures.Asm asm) { if (AppSecSystem.isActive() != newState) { log.info("AppSec {} (runtime)", newState ? "enabled" : "disabled"); AppSecSystem.setActive(newState); + updateRulesAndDataSubscription(); } } diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy index 07fcae0da20..f88e7be103d 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy @@ -65,18 +65,17 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'maybeStartConfigPolling subscribes to the configuration poller'() { setup: appSecConfigService.init() + AppSecSystem.active = false + config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE when: appSecConfigService.maybeSubscribeConfigPolling() then: - 1 * config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE - 1 * poller.addListener(Product.ASM_DD, _) >> { - listeners.savedWafDataChangesListener = it[1] - } + 0 * poller.addListener(Product.ASM_DD, _) 1 * poller.addListener(Product.ASM_FEATURES, _, _) - 1 * poller.addListener(Product.ASM, _) - 1 * poller.addListener(Product.ASM_DATA, _) + 0 * poller.addListener(Product.ASM, _) + 0 * poller.addListener(Product.ASM_DATA, _) 1 * poller.addConfigurationEndListener(_) 1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) } @@ -84,12 +83,13 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'no subscription to ASM_FEATURES if appsec is fully enabled'() { setup: appSecConfigService.init() + AppSecSystem.active = true + config.getAppSecActivation() >> ProductActivation.FULLY_ENABLED when: appSecConfigService.maybeSubscribeConfigPolling() then: - 1 * config.getAppSecActivation() >> ProductActivation.FULLY_ENABLED 1 * poller.addListener(Product.ASM_DD, _) 1 * poller.addListener(Product.ASM_FEATURES, _, _) 1 * poller.addListener(Product.ASM, _) @@ -102,24 +102,22 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'no subscription to ASM_FEATURES if appsec is fully disabled'() { setup: appSecConfigService.init() + AppSecSystem.active = false + config.getAppSecActivation() >> ProductActivation.FULLY_DISABLED when: appSecConfigService.maybeSubscribeConfigPolling() then: - 1 * config.getAppSecActivation() >> ProductActivation.FULLY_DISABLED - 1 * poller.addListener(Product.ASM_DD, _) - 1 * poller.addListener(Product.ASM_FEATURES, _, _) - 1 * poller.addListener(Product.ASM, _) - 1 * poller.addListener(Product.ASM_DATA, _) - 1 * poller.addConfigurationEndListener(_) + 0 * poller.addConfigurationEndListener(_) 0 * poller.addListener(*_) - 0 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) + 0 * poller.addCapabilities(*_) } void 'no subscription to ASM ASM_DD ASM_DATA if custom rules are provided'() { setup: Path p = Paths.get(getClass().classLoader.getResource('test_multi_config_no_action.json').getPath()) + AppSecSystem.active = false when: appSecConfigService.init() @@ -196,6 +194,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { when: AppSecSystem.active = false + config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE appSecConfigService.init() appSecConfigService.maybeSubscribeConfigPolling() def configurer = appSecConfigService.createAppSecModuleConfigurer() @@ -203,9 +202,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { configurer.commit() then: - 1 * config.isAppSecRaspEnabled() >> true - 1 * config.getAppSecRulesFile() >> null - 2 * config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE 1 * poller.addListener(Product.ASM_FEATURES, _, _) >> { listeners.savedFeaturesDeserializer = it[1] listeners.savedFeaturesListener = it[2] @@ -213,8 +209,9 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { 1 * poller.addConfigurationEndListener(_) >> { listeners.savedConfEndListener = it[0] } - _ * poller._ - 0 * _._ + 1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) + 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) + 0 * poller._ when: listeners.savedFeaturesListener.accept( @@ -226,11 +223,17 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { then: 1 * subconfigListener.onNewSubconfig(_, _) AppSecSystem.active + 1 * poller.addListener(Product.ASM_DD, _) + 1 * poller.addListener(Product.ASM, _) + 1 * poller.addListener(Product.ASM_DATA, _) + 1 * poller.addCapabilities(_) } void 'provides updated configuration to waf subscription'() { AppSecModuleConfigurer.SubconfigListener subconfigListener = Mock() AppSecSystem.active = false + config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE + config.isAppSecRaspEnabled() >> true appSecConfigService.init() when: @@ -240,13 +243,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { configurer.commit() then: - 1 * config.isAppSecRaspEnabled() >> true - 2 * config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE - 1 * poller.addListener(Product.ASM_DD, _) >> { - listeners.savedWafDataChangesListener = it[1] - } - 1 * poller.addListener(Product.ASM_DATA, _) - 1 * poller.addListener(Product.ASM, _) 1 * poller.addListener(Product.ASM_FEATURES, _, _) >> { listeners.savedFeaturesDeserializer = it[1] listeners.savedFeaturesListener = it[2] @@ -256,6 +252,23 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { } 1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) + 0 * poller._ + + when: + listeners.savedFeaturesListener.accept( + 'asm_features_activation', + listeners.savedFeaturesDeserializer.deserialize( + '{"asm":{"enabled": true}}'.bytes), null) + listeners.savedConfEndListener.onConfigurationEnd() + + then: + 1 * subconfigListener.onNewSubconfig(_ as String, _) + AppSecSystem.active + 1 * poller.addListener(Product.ASM_DD, _) >> { + listeners.savedWafDataChangesListener = it[1] + } + 1 * poller.addListener(Product.ASM_DATA, _) + 1 * poller.addListener(Product.ASM, _) 1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES | CAPABILITY_ASM_IP_BLOCKING | CAPABILITY_ASM_EXCLUSIONS @@ -273,18 +286,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT | CAPABILITY_ASM_HEADER_FINGERPRINT) - 0 * _._ - - when: - listeners.savedFeaturesListener.accept( - 'asm_features_activation', - listeners.savedFeaturesDeserializer.deserialize( - '{"asm":{"enabled": true}}'.bytes), null) - listeners.savedConfEndListener.onConfigurationEnd() - - then: - 1 * subconfigListener.onNewSubconfig(_ as String, _) - AppSecSystem.active when: // AppSec is ACTIVE - rules trigger subscriptions @@ -374,15 +375,15 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { then: 'it is disabled ( == false)' !AppSecSystem.active - - cleanup: - AppSecSystem.active = true } void 'configuration pull out'() { AppSecModuleConfigurer.SubconfigListener subconfigListener = Mock() when: + AppSecSystem.active = false + config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE + config.isAppSecRaspEnabled() >> true appSecConfigService.init() appSecConfigService.maybeSubscribeConfigPolling() def configurer = appSecConfigService.createAppSecModuleConfigurer() @@ -390,14 +391,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { configurer.commit() then: - 1 * config.isAppSecRaspEnabled() >> true - 1 * config.getAppSecRulesFile() >> null - 2 * config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE - 1 * poller.addListener(Product.ASM_DD, _) >> { - listeners.savedWafDataChangesListener = it[1] - } - 1 * poller.addListener(Product.ASM_DATA, _) - 1 * poller.addListener(Product.ASM, _) 1 * poller.addListener(Product.ASM_FEATURES, _, _) >> { listeners.savedFeaturesDeserializer = it[1] listeners.savedFeaturesListener = it[2] @@ -407,6 +400,21 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { } 1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) + 0 * poller._ + + when: + listeners.savedFeaturesListener.accept('asm_features conf', + listeners.savedFeaturesDeserializer.deserialize('{"asm":{"enabled": true}}'.bytes), + NOOP) + listeners.savedConfEndListener.onConfigurationEnd() + + then: + AppSecSystem.active + 1 * poller.addListener(Product.ASM_DD, _) >> { + listeners.savedWafDataChangesListener = it[1] + } + 1 * poller.addListener(Product.ASM_DATA, _) + 1 * poller.addListener(Product.ASM, _) 1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES | CAPABILITY_ASM_IP_BLOCKING | CAPABILITY_ASM_EXCLUSIONS @@ -424,7 +432,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT | CAPABILITY_ASM_HEADER_FINGERPRINT) - 0 * _._ + 0 * poller._ when: listeners.savedWafDataChangesListener.accept( @@ -473,9 +481,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { } ] }'''.getBytes(), null) - listeners.savedFeaturesListener.accept('asm_features conf', - listeners.savedFeaturesDeserializer.deserialize('{"asm":{"enabled": true}}'.bytes), - NOOP) listeners.savedConfEndListener.onConfigurationEnd() then: @@ -561,11 +566,19 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { autoUserInstrum('yolo') | DISABLED } - void 'RASP capabilities for LFI is not sent when RASP is not fully enabled '() { + void 'RASP capabilities are not sent when RASP is not fully enabled '() { + setup: AppSecModuleConfigurer.SubconfigListener subconfigListener = Mock() + def raspCapabilities = [CAPABILITY_ASM_RASP_SQLI: CAPABILITY_ASM_RASP_SQLI, + CAPABILITY_ASM_RASP_SSRF: CAPABILITY_ASM_RASP_SSRF, + CAPABILITY_ASM_RASP_CMDI: CAPABILITY_ASM_RASP_CMDI, + CAPABILITY_ASM_RASP_SHI : CAPABILITY_ASM_RASP_SHI, + CAPABILITY_ASM_RASP_LFI : CAPABILITY_ASM_RASP_LFI] when: - AppSecSystem.active = false + AppSecSystem.active = true + config.getAppSecActivation() >> ProductActivation.FULLY_ENABLED + config.isAppSecRaspEnabled() >> false appSecConfigService.init() appSecConfigService.maybeSubscribeConfigPolling() def configurer = appSecConfigService.createAppSecModuleConfigurer() @@ -573,37 +586,11 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { configurer.commit() then: - 1 * config.isAppSecRaspEnabled() >> true - 1 * config.getAppSecRulesFile() >> null - 2 * config.getAppSecActivation() >> ProductActivation.FULLY_ENABLED - 1 * poller.addListener(Product.ASM_DD, _) - 1 * poller.addListener(Product.ASM_DATA, _) - 1 * poller.addListener(Product.ASM, _) - 1 * poller.addListener(Product.ASM_FEATURES, _, _) - 1 * poller.addConfigurationEndListener(_) - 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) - 1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES - | CAPABILITY_ASM_IP_BLOCKING - | CAPABILITY_ASM_EXCLUSIONS - | CAPABILITY_ASM_EXCLUSION_DATA - | CAPABILITY_ASM_REQUEST_BLOCKING - | CAPABILITY_ASM_USER_BLOCKING - | CAPABILITY_ASM_CUSTOM_RULES - | CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE - | CAPABILITY_ASM_TRUSTED_IPS - | CAPABILITY_ASM_RASP_SQLI - | CAPABILITY_ASM_RASP_SSRF - | CAPABILITY_ASM_RASP_CMDI - | CAPABILITY_ASM_RASP_SHI - | CAPABILITY_ASM_RASP_LFI - | CAPABILITY_ENDPOINT_FINGERPRINT - | CAPABILITY_ASM_SESSION_FINGERPRINT - | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT) - 0 * _._ - - cleanup: - AppSecSystem.active = true + poller.addCapabilities(_) >> { + final caps = it[0] as long + final found = raspCapabilities.findAll { (it.value & caps) > 0 }.collect { it.key } + assert found.isEmpty() + } } def 'test AppSecConfigChangesListener listener'() { @@ -622,98 +609,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { thrown RuntimeException } - void 'when AppSec is INACTIVE rules should not trigger subscriptions'() { - AppSecModuleConfigurer.SubconfigListener subconfigListener = Mock() - AppSecSystem.active = false - appSecConfigService.init() - - when: - appSecConfigService.maybeSubscribeConfigPolling() - def configurer = appSecConfigService.createAppSecModuleConfigurer() - configurer.addSubConfigListener("waf", subconfigListener) - configurer.commit() - - then: - 1 * config.isAppSecRaspEnabled() >> true - 2 * config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE - 1 * poller.addListener(Product.ASM_DD, _) >> { - listeners.savedWafDataChangesListener = it[1] - } - 1 * poller.addListener(Product.ASM_DATA, _) - 1 * poller.addListener(Product.ASM, _) - 1 * poller.addListener(Product.ASM_FEATURES, _, _) >> { - listeners.savedFeaturesDeserializer = it[1] - listeners.savedFeaturesListener = it[2] - } - 1 * poller.addConfigurationEndListener(_) >> { - listeners.savedConfEndListener = it[0] - } - 1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) - 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) - 1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES - | CAPABILITY_ASM_IP_BLOCKING - | CAPABILITY_ASM_EXCLUSIONS - | CAPABILITY_ASM_EXCLUSION_DATA - | CAPABILITY_ASM_REQUEST_BLOCKING - | CAPABILITY_ASM_USER_BLOCKING - | CAPABILITY_ASM_CUSTOM_RULES - | CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE - | CAPABILITY_ASM_TRUSTED_IPS - | CAPABILITY_ASM_RASP_SQLI - | CAPABILITY_ASM_RASP_SSRF - | CAPABILITY_ASM_RASP_CMDI - | CAPABILITY_ASM_RASP_SHI - | CAPABILITY_ENDPOINT_FINGERPRINT - | CAPABILITY_ASM_SESSION_FINGERPRINT - | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT) - 0 * _._ - - when: - // AppSec is INACTIVE - rules should not trigger subscriptions - listeners.savedWafDataChangesListener.accept( - 'ignored config key' as ConfigKey, - '''{ - "rules": [ - { - "id": "foo", - "name": "foo", - "tags": { - "type": "php_code_injection", - "crs_id": "933140", - "category": "attack_attempt", - "cwe": "94", - "capec": "1000/225/122/17/650", - "confidence": "1", - "module": "waf" - }, - "conditions": [ - { - "operator": "ip_match", - "parameters": { - "data": "suspicious_ips_data_id", - "inputs": [ - { - "address": "http.client_ip" - } - ] - } - } - ], - "type": "", - "data": [] - } - ] - }'''.getBytes(), null) - listeners.savedConfEndListener.onConfigurationEnd() - - then: - 0 * subconfigListener.onNewSubconfig(_, _) - - cleanup: - AppSecSystem.active = true - } - void 'InvalidRuleSetException is thrown when rules are not configured correctly' () { setup: // Mock WafMetricCollector From f76982e9b4040a16f0acf38501671c4506c90777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Mon, 28 Jul 2025 20:59:53 +0200 Subject: [PATCH 2/7] Add more tests --- .../config/AppSecConfigServiceImpl.java | 102 +++++++++--------- ...ppSecConfigServiceImplSpecification.groovy | 35 ++++++ .../dynamicconfig/AppSecApplication.java | 2 +- .../AppSecActivationSmokeTest.groovy | 50 +++++++-- .../smoketest/AbstractSmokeTest.groovy | 25 +++++ 5 files changed, 157 insertions(+), 57 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java index 38cffe9dc10..6b49a67767f 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java @@ -102,11 +102,12 @@ public class AppSecConfigServiceImpl implements AppSecConfigService { private final AtomicBoolean subscribedToRulesAndData = new AtomicBoolean(); private final Set usedDDWafConfigKeys = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private final Set emptyConfigKeys = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set ignoredConfigKeys = + Collections.newSetFromMap(new ConcurrentHashMap<>()); private final String DEFAULT_WAF_CONFIG_RULE = "DEFAULT_WAF_CONFIG"; private String currentRuleVersion; private List modulesToUpdateVersionIn; - private long rulesAndDataCapabilities; + private long rulesAndDataCapabilities = -1L; public AppSecConfigServiceImpl( Config tracerConfig, @@ -135,32 +136,34 @@ private void subscribeConfigurationPoller() { } private long buildRulesAndDataCapabilities() { - long capabilities = - CAPABILITY_ASM_DD_RULES - | CAPABILITY_ASM_IP_BLOCKING - | CAPABILITY_ASM_EXCLUSIONS - | CAPABILITY_ASM_EXCLUSION_DATA - | CAPABILITY_ASM_REQUEST_BLOCKING - | CAPABILITY_ASM_USER_BLOCKING - | CAPABILITY_ASM_CUSTOM_RULES - | CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE - | CAPABILITY_ASM_TRUSTED_IPS - | CAPABILITY_ENDPOINT_FINGERPRINT - | CAPABILITY_ASM_SESSION_FINGERPRINT - | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT; - if (tracerConfig.isAppSecRaspEnabled()) { - capabilities |= CAPABILITY_ASM_RASP_SQLI; - capabilities |= CAPABILITY_ASM_RASP_SSRF; - capabilities |= CAPABILITY_ASM_RASP_CMDI; - capabilities |= CAPABILITY_ASM_RASP_SHI; - // RASP LFI is only available in fully enabled mode as it's implemented using callsite - // instrumentation - if (tracerConfig.getAppSecActivation() == ProductActivation.FULLY_ENABLED) { - capabilities |= CAPABILITY_ASM_RASP_LFI; + if (rulesAndDataCapabilities == -1) { + rulesAndDataCapabilities = + CAPABILITY_ASM_DD_RULES + | CAPABILITY_ASM_IP_BLOCKING + | CAPABILITY_ASM_EXCLUSIONS + | CAPABILITY_ASM_EXCLUSION_DATA + | CAPABILITY_ASM_REQUEST_BLOCKING + | CAPABILITY_ASM_USER_BLOCKING + | CAPABILITY_ASM_CUSTOM_RULES + | CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE + | CAPABILITY_ASM_TRUSTED_IPS + | CAPABILITY_ENDPOINT_FINGERPRINT + | CAPABILITY_ASM_SESSION_FINGERPRINT + | CAPABILITY_ASM_NETWORK_FINGERPRINT + | CAPABILITY_ASM_HEADER_FINGERPRINT; + if (tracerConfig.isAppSecRaspEnabled()) { + rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_SQLI; + rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_SSRF; + rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_CMDI; + rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_SHI; + // RASP LFI is only available in fully enabled mode as it's implemented using callsite + // instrumentation + if (tracerConfig.getAppSecActivation() == ProductActivation.FULLY_ENABLED) { + rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_LFI; + } } } - return capabilities; + return rulesAndDataCapabilities; } private void updateRulesAndDataSubscription() { @@ -179,7 +182,7 @@ private void subscribeRulesAndData() { this.configurationPoller.addListener(Product.ASM_DD, new AppSecConfigChangesDDListener()); this.configurationPoller.addListener(Product.ASM_DATA, new AppSecConfigChangesListener()); this.configurationPoller.addListener(Product.ASM, new AppSecConfigChangesListener()); - this.configurationPoller.addCapabilities(rulesAndDataCapabilities); + this.configurationPoller.addCapabilities(buildRulesAndDataCapabilities()); } } @@ -188,7 +191,7 @@ private void unsubscribeRulesAndData() { this.configurationPoller.removeListeners(Product.ASM_DD); this.configurationPoller.removeListeners(Product.ASM_DATA); this.configurationPoller.removeListeners(Product.ASM); - this.configurationPoller.removeCapabilities(rulesAndDataCapabilities); + this.configurationPoller.removeCapabilities(buildRulesAndDataCapabilities()); } } @@ -204,27 +207,22 @@ private class AppSecConfigChangesListener implements ProductListener { @Override public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollingRateHinter) throws IOException { - maybeInitializeDefaultConfig(); final String key = configKey.toString(); if (content == null) { - if (!emptyConfigKeys.remove(key)) { - try { - wafBuilder.removeConfig(key); - } catch (UnclassifiedWafException e) { - throw new RuntimeException(e); - } - } + remove(configKey, pollingRateHinter); + return; + } + Map contentMap = + ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content)))); + if (contentMap == null || contentMap.isEmpty()) { + ignoredConfigKeys.add(key); } else { - Map contentMap = - ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content)))); - if (contentMap == null || contentMap.isEmpty()) { - emptyConfigKeys.add(key); - } else { - try { - handleWafUpdateResultReport(key, contentMap); - } catch (AppSecModule.AppSecModuleActivationException e) { - throw new RuntimeException(e); - } + ignoredConfigKeys.remove(key); + try { + maybeInitializeDefaultConfig(); + handleWafUpdateResultReport(key, contentMap); + } catch (AppSecModule.AppSecModuleActivationException e) { + throw new RuntimeException(e); } } } @@ -232,7 +230,16 @@ public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollin @Override public void remove(ConfigKey configKey, PollingRateHinter pollingRateHinter) throws IOException { - accept(configKey, null, pollingRateHinter); + final String key = configKey.toString(); + if (ignoredConfigKeys.remove(key)) { + return; + } + try { + maybeInitializeDefaultConfig(); + wafBuilder.removeConfig(key); + } catch (UnclassifiedWafException e) { + throw new RuntimeException(e); + } } @Override @@ -375,7 +382,6 @@ public void init() { } this.mergedAsmFeatures.clear(); this.usedDDWafConfigKeys.clear(); - this.emptyConfigKeys.clear(); this.rulesAndDataCapabilities = buildRulesAndDataCapabilities(); if (wafConfig.isEmpty()) { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy index f88e7be103d..f98529a30fc 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy @@ -715,6 +715,41 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { service.usedDDWafConfigKeys.empty } + void 'test that empty configurations are acknowledged'() { + given: + final key = new ParsedConfigKey('Test', '1234', 1, 'ASM_DD', 'ID') + + when: + AppSecSystem.active = true + config.getAppSecActivation() >> ProductActivation.FULLY_ENABLED + final service = new AppSecConfigServiceImpl(config, poller, reconf) + service.init() + service.maybeSubscribeConfigPolling() + + then: + 1 * poller.addListener(Product.ASM_DATA, _) >> { + listeners.savedWafDataChangesListener = it[1] + } + 1 * poller.addConfigurationEndListener(_) >> { + listeners.savedConfEndListener = it[0] + } + + when: + listeners.savedWafDataChangesListener.accept(key, '{}'.bytes, NOOP) + listeners.savedConfEndListener.onConfigurationEnd() + + then: + noExceptionThrown() + + when: + listeners.savedWafDataChangesListener.accept(key, null, NOOP) + listeners.savedConfEndListener.onConfigurationEnd() + + then: + noExceptionThrown() + } + + private static AppSecFeatures autoUserInstrum(String mode) { return new AppSecFeatures().tap { features -> features.autoUserInstrum = new AppSecFeatures.AutoUserInstrum().tap { instrum -> diff --git a/dd-smoke-tests/dynamic-config/src/main/java/datadog/smoketest/dynamicconfig/AppSecApplication.java b/dd-smoke-tests/dynamic-config/src/main/java/datadog/smoketest/dynamicconfig/AppSecApplication.java index 65367491985..0167449bed4 100644 --- a/dd-smoke-tests/dynamic-config/src/main/java/datadog/smoketest/dynamicconfig/AppSecApplication.java +++ b/dd-smoke-tests/dynamic-config/src/main/java/datadog/smoketest/dynamicconfig/AppSecApplication.java @@ -4,7 +4,7 @@ public class AppSecApplication { - public static final long TIMEOUT_IN_SECONDS = 10; + public static final long TIMEOUT_IN_SECONDS = 15; public static void main(String[] args) throws InterruptedException { // just wait as we want to test RC payloads diff --git a/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/AppSecActivationSmokeTest.groovy b/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/AppSecActivationSmokeTest.groovy index d7ed7a94699..a80177e35a9 100644 --- a/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/AppSecActivationSmokeTest.groovy +++ b/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/AppSecActivationSmokeTest.groovy @@ -1,7 +1,8 @@ package datadog.smoketest +import datadog.remoteconfig.Capabilities +import datadog.remoteconfig.Product import datadog.smoketest.dynamicconfig.AppSecApplication -import datadog.trace.test.util.Flaky class AppSecActivationSmokeTest extends AbstractSmokeTest { @@ -23,16 +24,25 @@ class AppSecActivationSmokeTest extends AbstractSmokeTest { processBuilder.directory(new File(buildDirectory)) } - @Flaky - void 'test activation config change is sent via RC'() { - when: + void 'test activation via RC workflow'() { + given: + final asmRuleProducts = [Product.ASM, Product.ASM_DD, Product.ASM_DATA] + + when: 'appsec is enabled but inactive' + final request = waitForRcClientRequest {req -> + decodeProducts(req).find { asmRuleProducts.contains(it) } == null + } + final capabilities = decodeCapabilities(request) + + then: 'only ASM_ACTIVATION capability should be reported' + assert hasCapability(capabilities, Capabilities.CAPABILITY_ASM_ACTIVATION) + assert !hasCapability(capabilities, Capabilities.CAPABILITY_ASM_CUSTOM_RULES) + + when: 'appsec is enabled via RC' setRemoteConfig('datadog/2/ASM_FEATURES/asm_features_activation/config', '{"asm":{"enabled":true}}') - then: + then: 'we should receive a product change for appsec' waitForTelemetryFlat { - if (it['request_type'] != 'app-client-configuration-change') { - return false - } final configurations = (List>) it?.payload?.configuration ?: [] final enabledConfig = configurations.find { it.name == 'appsec_enabled' } if (!enabledConfig) { @@ -40,5 +50,29 @@ class AppSecActivationSmokeTest extends AbstractSmokeTest { } return enabledConfig.value == 'true' && enabledConfig .origin == 'remote_config' } + + and: 'we should have set the capabilities for ASM rules and data' + final newRequest = waitForRcClientRequest {req -> + decodeProducts(req).containsAll(asmRuleProducts) + } + final newCapabilities = decodeCapabilities(newRequest) + assert hasCapability(newCapabilities, Capabilities.CAPABILITY_ASM_CUSTOM_RULES) + } + + private static Set decodeProducts(final Map request) { + return request.client.products.collect { Product.valueOf(it)} + } + + private static long decodeCapabilities(final Map request) { + final clientCapabilities = request.client.capabilities as byte[] + long capabilities = 0l + for (int i = 0; i < clientCapabilities.length; i++) { + capabilities |= (clientCapabilities[i] & 0xFFL) << ((clientCapabilities.length - i - 1) * 8) + } + return capabilities + } + + private static boolean hasCapability(final long capabilities, final long test) { + return (capabilities & test) > 0 } } diff --git a/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy b/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy index 4c2f95ddaea..3461913af4c 100644 --- a/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy +++ b/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy @@ -50,6 +50,12 @@ abstract class AbstractSmokeTest extends ProcessManager { @Shared protected TestHttpServer.Headers lastTraceRequestHeaders = null + @Shared + protected CopyOnWriteArrayList> rcClientMessages = new CopyOnWriteArrayList() + + @Shared + private Throwable rcClientDecodingFailure = null + @Shared protected final PollingConditions defaultPoll = new PollingConditions(timeout: 30, initialDelay: 0, delay: 1, factor: 1) @@ -129,6 +135,10 @@ abstract class AbstractSmokeTest extends ProcessManager { response.status(200).send() } prefix("/v0.7/config") { + if (request.getBody() != null) { + final msg = new JsonSlurper().parseText(new String(request.getBody(), StandardCharsets.UTF_8)) as Map + rcClientMessages.add(msg) + } response.status(200).send(remoteConfigResponse) } prefix("/telemetry/proxy/api/v2/apmtelemetry") { @@ -349,6 +359,21 @@ abstract class AbstractSmokeTest extends ProcessManager { } } + Map waitForRcClientRequest(final Function, Boolean> predicate) { + waitForRcClientRequest(defaultPoll, predicate) + } + + Map waitForRcClientRequest(final PollingConditions poll, final Function, Boolean> predicate) { + def message = null + poll.eventually { + if (rcClientDecodingFailure != null) { + throw rcClientDecodingFailure + } + assert (message = rcClientMessages.find { predicate.apply(it) }) != null + } + return message + } + List getTraces() { decodeTraces } From c926ae2a25dcbec6970fd38e20afe103cbf68c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Mon, 28 Jul 2025 21:08:52 +0200 Subject: [PATCH 3/7] Add exception handling to rc client requests --- .../groovy/datadog/smoketest/AbstractSmokeTest.groovy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy b/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy index 3461913af4c..8bc5e2f5a33 100644 --- a/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy +++ b/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy @@ -136,8 +136,12 @@ abstract class AbstractSmokeTest extends ProcessManager { } prefix("/v0.7/config") { if (request.getBody() != null) { - final msg = new JsonSlurper().parseText(new String(request.getBody(), StandardCharsets.UTF_8)) as Map - rcClientMessages.add(msg) + try { + final msg = new JsonSlurper().parseText(new String(request.getBody(), StandardCharsets.UTF_8)) as Map + rcClientMessages.add(msg) + } catch (Throwable t) { + rcClientDecodingFailure = t + } } response.status(200).send(remoteConfigResponse) } From 38a647cb4116e7ae1ab8cfbb727522967dbedba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Tue, 29 Jul 2025 09:09:51 +0200 Subject: [PATCH 4/7] Ensure no exceptions leak from the clinit of WafInitialization --- .../main/java/com/datadog/appsec/ddwaf/WafInitialization.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WafInitialization.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WafInitialization.java index 2e63300a764..3bbc177dad3 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WafInitialization.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WafInitialization.java @@ -15,7 +15,7 @@ private static boolean initWAF() { try { boolean simpleLoad = System.getProperty("POWERWAF_SIMPLE_LOAD") != null; Waf.initialize(simpleLoad); - } catch (Exception e) { + } catch (Throwable e) { Logger logger = LoggerFactory.getLogger(WafInitialization.class); logger.warn("Error initializing WAF library", e); StandardizedLogging.libddwafCannotBeLoaded(logger, getLibc()); From 24289d1fb64c9e5342df8666b6f1583f35c671b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Tue, 29 Jul 2025 09:20:42 +0200 Subject: [PATCH 5/7] Improve test --- .../config/AppSecConfigServiceImpl.java | 62 +++++++++---------- ...ppSecConfigServiceImplSpecification.groovy | 6 +- 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java index 6b49a67767f..31727872273 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java @@ -97,8 +97,8 @@ public class AppSecConfigServiceImpl implements AppSecConfigService { .build() .adapter(Types.newParameterizedType(Map.class, String.class, Object.class)); - private volatile boolean hasUserWafConfig; - private volatile boolean defaultConfigActivated; + private boolean hasUserWafConfig; + private boolean defaultConfigActivated; private final AtomicBoolean subscribedToRulesAndData = new AtomicBoolean(); private final Set usedDDWafConfigKeys = Collections.newSetFromMap(new ConcurrentHashMap<>()); @@ -107,7 +107,6 @@ public class AppSecConfigServiceImpl implements AppSecConfigService { private final String DEFAULT_WAF_CONFIG_RULE = "DEFAULT_WAF_CONFIG"; private String currentRuleVersion; private List modulesToUpdateVersionIn; - private long rulesAndDataCapabilities = -1L; public AppSecConfigServiceImpl( Config tracerConfig, @@ -135,35 +134,33 @@ private void subscribeConfigurationPoller() { this.configurationPoller.addConfigurationEndListener(applyRemoteConfigListener); } - private long buildRulesAndDataCapabilities() { - if (rulesAndDataCapabilities == -1) { - rulesAndDataCapabilities = - CAPABILITY_ASM_DD_RULES - | CAPABILITY_ASM_IP_BLOCKING - | CAPABILITY_ASM_EXCLUSIONS - | CAPABILITY_ASM_EXCLUSION_DATA - | CAPABILITY_ASM_REQUEST_BLOCKING - | CAPABILITY_ASM_USER_BLOCKING - | CAPABILITY_ASM_CUSTOM_RULES - | CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE - | CAPABILITY_ASM_TRUSTED_IPS - | CAPABILITY_ENDPOINT_FINGERPRINT - | CAPABILITY_ASM_SESSION_FINGERPRINT - | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT; - if (tracerConfig.isAppSecRaspEnabled()) { - rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_SQLI; - rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_SSRF; - rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_CMDI; - rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_SHI; - // RASP LFI is only available in fully enabled mode as it's implemented using callsite - // instrumentation - if (tracerConfig.getAppSecActivation() == ProductActivation.FULLY_ENABLED) { - rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_LFI; - } + private long getRulesAndDataCapabilities() { + long capabilities = + CAPABILITY_ASM_DD_RULES + | CAPABILITY_ASM_IP_BLOCKING + | CAPABILITY_ASM_EXCLUSIONS + | CAPABILITY_ASM_EXCLUSION_DATA + | CAPABILITY_ASM_REQUEST_BLOCKING + | CAPABILITY_ASM_USER_BLOCKING + | CAPABILITY_ASM_CUSTOM_RULES + | CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE + | CAPABILITY_ASM_TRUSTED_IPS + | CAPABILITY_ENDPOINT_FINGERPRINT + | CAPABILITY_ASM_SESSION_FINGERPRINT + | CAPABILITY_ASM_NETWORK_FINGERPRINT + | CAPABILITY_ASM_HEADER_FINGERPRINT; + if (tracerConfig.isAppSecRaspEnabled()) { + capabilities |= CAPABILITY_ASM_RASP_SQLI; + capabilities |= CAPABILITY_ASM_RASP_SSRF; + capabilities |= CAPABILITY_ASM_RASP_CMDI; + capabilities |= CAPABILITY_ASM_RASP_SHI; + // RASP LFI is only available in fully enabled mode as it's implemented using callsite + // instrumentation + if (tracerConfig.getAppSecActivation() == ProductActivation.FULLY_ENABLED) { + capabilities |= CAPABILITY_ASM_RASP_LFI; } } - return rulesAndDataCapabilities; + return capabilities; } private void updateRulesAndDataSubscription() { @@ -182,7 +179,7 @@ private void subscribeRulesAndData() { this.configurationPoller.addListener(Product.ASM_DD, new AppSecConfigChangesDDListener()); this.configurationPoller.addListener(Product.ASM_DATA, new AppSecConfigChangesListener()); this.configurationPoller.addListener(Product.ASM, new AppSecConfigChangesListener()); - this.configurationPoller.addCapabilities(buildRulesAndDataCapabilities()); + this.configurationPoller.addCapabilities(getRulesAndDataCapabilities()); } } @@ -191,7 +188,7 @@ private void unsubscribeRulesAndData() { this.configurationPoller.removeListeners(Product.ASM_DD); this.configurationPoller.removeListeners(Product.ASM_DATA); this.configurationPoller.removeListeners(Product.ASM); - this.configurationPoller.removeCapabilities(buildRulesAndDataCapabilities()); + this.configurationPoller.removeCapabilities(getRulesAndDataCapabilities()); } } @@ -382,7 +379,6 @@ public void init() { } this.mergedAsmFeatures.clear(); this.usedDDWafConfigKeys.clear(); - this.rulesAndDataCapabilities = buildRulesAndDataCapabilities(); if (wafConfig.isEmpty()) { throw new IllegalStateException("Expected default waf config to be available"); diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy index f98529a30fc..2daea19b89b 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy @@ -286,6 +286,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT | CAPABILITY_ASM_HEADER_FINGERPRINT) + 0 * poller._ when: // AppSec is ACTIVE - rules trigger subscriptions @@ -383,7 +384,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { when: AppSecSystem.active = false config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE - config.isAppSecRaspEnabled() >> true appSecConfigService.init() appSecConfigService.maybeSubscribeConfigPolling() def configurer = appSecConfigService.createAppSecModuleConfigurer() @@ -424,10 +424,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ASM_CUSTOM_RULES | CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE | CAPABILITY_ASM_TRUSTED_IPS - | CAPABILITY_ASM_RASP_SQLI - | CAPABILITY_ASM_RASP_SSRF - | CAPABILITY_ASM_RASP_CMDI - | CAPABILITY_ASM_RASP_SHI | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT From cd6b2489161e816e15256b0b2436cff5c54ca98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Tue, 29 Jul 2025 11:04:19 +0200 Subject: [PATCH 6/7] Fix failing tests --- .../config/AppSecConfigServiceImpl.java | 20 ++++++++++--------- .../ddwaf/WAFModuleSpecification.groovy | 16 ++++++++++++++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java index 31727872273..236a4103dcb 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java @@ -204,11 +204,11 @@ private class AppSecConfigChangesListener implements ProductListener { @Override public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollingRateHinter) throws IOException { - final String key = configKey.toString(); if (content == null) { remove(configKey, pollingRateHinter); return; } + final String key = configKey.toString(); Map contentMap = ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content)))); if (contentMap == null || contentMap.isEmpty()) { @@ -216,6 +216,7 @@ public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollin } else { ignoredConfigKeys.remove(key); try { + beforeApply(key, contentMap); maybeInitializeDefaultConfig(); handleWafUpdateResultReport(key, contentMap); } catch (AppSecModule.AppSecModuleActivationException e) { @@ -234,6 +235,7 @@ public void remove(ConfigKey configKey, PollingRateHinter pollingRateHinter) try { maybeInitializeDefaultConfig(); wafBuilder.removeConfig(key); + afterRemove(key); } catch (UnclassifiedWafException e) { throw new RuntimeException(e); } @@ -243,12 +245,15 @@ public void remove(ConfigKey configKey, PollingRateHinter pollingRateHinter) public void commit(PollingRateHinter pollingRateHinter) { // no action needed } + + protected void beforeApply(final String key, final Map contentMap) {} + + protected void afterRemove(final String key) {} } private class AppSecConfigChangesDDListener extends AppSecConfigChangesListener { @Override - public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollingRateHinter) - throws IOException { + protected void beforeApply(final String key, final Map config) { if (defaultConfigActivated) { // if we get any config, remove the default one log.debug("Removing default config"); try { @@ -258,15 +263,12 @@ public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollin } defaultConfigActivated = false; } - usedDDWafConfigKeys.add(configKey.toString()); - super.accept(configKey, content, pollingRateHinter); + usedDDWafConfigKeys.add(key); } @Override - public void remove(ConfigKey configKey, PollingRateHinter pollingRateHinter) - throws IOException { - super.remove(configKey, pollingRateHinter); - usedDDWafConfigKeys.remove(configKey.toString()); + protected void afterRemove(final String key) { + usedDDWafConfigKeys.remove(key); } } diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy index 7cddcd62523..aeed7c23d56 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy @@ -1,5 +1,6 @@ package com.datadog.appsec.ddwaf +import com.datadog.appsec.AppSecSystem import com.datadog.appsec.config.AppSecConfigService import com.datadog.appsec.config.AppSecConfigServiceImpl import com.datadog.appsec.config.AppSecModuleConfigurer @@ -30,6 +31,8 @@ import com.squareup.moshi.Types import datadog.appsec.api.blocking.BlockingContentType import datadog.communication.monitor.Monitoring import datadog.remoteconfig.ConfigurationPoller +import datadog.remoteconfig.PollerRequestFactory +import datadog.remoteconfig.PollingRateHinter import datadog.remoteconfig.Product import datadog.remoteconfig.state.ConfigKey import datadog.remoteconfig.state.ParsedConfigKey @@ -97,6 +100,7 @@ class WAFModuleSpecification extends DDSpecification { void setup() { WafMetricCollector.INSTANCE = wafMetricCollector AgentTracer.forceRegister(tracer) + AppSecSystem.active = true final configurationPoller = Stub(ConfigurationPoller) { addListener(Product.ASM_DD, _ as ProductListener) >> { @@ -972,7 +976,7 @@ class WAFModuleSpecification extends DDSpecification { void 'configuration can be given later'() { when: - initialRuleAddWithMap([waf: null]) + initialRuleAddWithMap([waf: new BadConfig()]) // empty configs are allowed now then: thrown RuntimeException @@ -1695,4 +1699,14 @@ class WAFModuleSpecification extends DDSpecification { throw new IllegalStateException("Unhandled WafErrorCode: $code") } } + + private static class BadConfig implements Map { + @Delegate + private Map delegate + + @Override + Set entrySet() { + throw new IllegalStateException("You tried to iterate!") + } + } } From 311bc7b1807a93fd28dc36001517f93f615ade29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Tue, 29 Jul 2025 12:54:39 +0200 Subject: [PATCH 7/7] Fix system-tests issue --- .../java/com/datadog/appsec/config/AppSecConfigServiceImpl.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java index 236a4103dcb..49d67e37211 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java @@ -379,8 +379,6 @@ public void init() { } else { hasUserWafConfig = true; } - this.mergedAsmFeatures.clear(); - this.usedDDWafConfigKeys.clear(); if (wafConfig.isEmpty()) { throw new IllegalStateException("Expected default waf config to be available");