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..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 @@ -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; @@ -99,7 +99,11 @@ public class AppSecConfigServiceImpl implements AppSecConfigService { private boolean hasUserWafConfig; private boolean defaultConfigActivated; - private final Set usedDDWafConfigKeys = new HashSet<>(); + private final AtomicBoolean subscribedToRulesAndData = new AtomicBoolean(); + private final Set usedDDWafConfigKeys = + 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; @@ -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 getRulesAndDataCapabilities() { 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(getRulesAndDataCapabilities()); + } + } + + 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(getRulesAndDataCapabilities()); + } } public void modulesToUpdateVersionIn(List modules) { @@ -175,19 +204,21 @@ private class AppSecConfigChangesListener implements ProductListener { @Override public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollingRateHinter) throws IOException { - maybeInitializeDefaultConfig(); - if (content == null) { - try { - wafBuilder.removeConfig(configKey.toString()); - } catch (UnclassifiedWafException e) { - throw new RuntimeException(e); - } + 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()) { + ignoredConfigKeys.add(key); } else { - Map contentMap = - ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content)))); + ignoredConfigKeys.remove(key); try { - handleWafUpdateResultReport(configKey.toString(), contentMap); + beforeApply(key, contentMap); + maybeInitializeDefaultConfig(); + handleWafUpdateResultReport(key, contentMap); } catch (AppSecModule.AppSecModuleActivationException e) { throw new RuntimeException(e); } @@ -197,19 +228,32 @@ 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); + afterRemove(key); + } catch (UnclassifiedWafException e) { + throw new RuntimeException(e); + } } @Override 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 { @@ -219,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); } } @@ -282,7 +323,6 @@ private void subscribeAsmFeatures() { Product.ASM_FEATURES, AppSecFeaturesDeserializer.INSTANCE, (configKey, newConfig, hinter) -> { - maybeInitializeDefaultConfig(); if (newConfig == null) { mergedAsmFeatures.removeConfig(configKey); } else { @@ -339,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"); @@ -353,9 +391,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 +535,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 +568,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/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()); 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..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 @@ -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,7 @@ 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 + 0 * poller._ when: // AppSec is ACTIVE - rules trigger subscriptions @@ -374,15 +376,14 @@ 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 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 @@ -416,15 +424,11 @@ 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 | CAPABILITY_ASM_HEADER_FINGERPRINT) - 0 * _._ + 0 * poller._ when: listeners.savedWafDataChangesListener.accept( @@ -473,9 +477,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 +562,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 +582,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 +605,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 @@ -820,6 +711,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-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!") + } + } } 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..8bc5e2f5a33 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,14 @@ abstract class AbstractSmokeTest extends ProcessManager { response.status(200).send() } prefix("/v0.7/config") { + if (request.getBody() != null) { + 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) } prefix("/telemetry/proxy/api/v2/apmtelemetry") { @@ -349,6 +363,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 }