diff --git a/dd-java-agent/appsec/build.gradle b/dd-java-agent/appsec/build.gradle index 4f11cfc9af8..0d629598b5f 100644 --- a/dd-java-agent/appsec/build.gradle +++ b/dd-java-agent/appsec/build.gradle @@ -15,7 +15,7 @@ dependencies { implementation project(':internal-api') implementation project(':communication') implementation project(':telemetry') - implementation group: 'io.sqreen', name: 'libsqreen', version: '13.0.1' + implementation group: 'io.sqreen', name: 'libsqreen', version: '14.0.0' implementation libs.moshi testImplementation libs.bytebuddy @@ -68,9 +68,10 @@ ext { minimumBranchCoverage = 0.6 minimumInstructionCoverage = 0.8 excludedClassesCoverage = [ - 'com.datadog.appsec.config.MergedAsmData.InvalidAsmDataException', + 'com.datadog.appsec.config.AppSecConfigServiceImpl.AppSecConfigChangesListener', 'com.datadog.appsec.ddwaf.WafInitialization', 'com.datadog.appsec.ddwaf.WAFModule.WAFDataCallback', + 'com.datadog.appsec.config.AppSecModuleConfigurer.Reconfiguration', 'com.datadog.appsec.report.*', 'com.datadog.appsec.config.AppSecConfigServiceImpl.SubscribeFleetServiceRunnable.1', 'com.datadog.appsec.util.StandardizedLogging', @@ -82,6 +83,7 @@ ext { 'com.datadog.appsec.config.AppSecFeatures.Asm', 'com.datadog.appsec.config.AppSecFeatures.ApiSecurity', 'com.datadog.appsec.config.AppSecFeatures.AutoUserInstrum', + 'com.datadog.appsec.AppSecModule.AppSecModuleActivationException', 'com.datadog.appsec.event.ReplaceableEventProducerService', 'com.datadog.appsec.api.security.ApiSecuritySampler.NoOp', ] diff --git a/dd-java-agent/appsec/src/jmh/java/datadog/appsec/benchmark/WafBenchmark.java b/dd-java-agent/appsec/src/jmh/java/datadog/appsec/benchmark/WafBenchmark.java index a85f8a7d3d6..df43839203e 100644 --- a/dd-java-agent/appsec/src/jmh/java/datadog/appsec/benchmark/WafBenchmark.java +++ b/dd-java-agent/appsec/src/jmh/java/datadog/appsec/benchmark/WafBenchmark.java @@ -3,14 +3,16 @@ import static java.util.concurrent.TimeUnit.MICROSECONDS; import static java.util.concurrent.TimeUnit.SECONDS; -import com.datadog.appsec.config.AppSecConfig; -import com.datadog.appsec.config.AppSecConfigDeserializer; import com.datadog.appsec.event.data.KnownAddresses; import com.datadog.ddwaf.Waf; +import com.datadog.ddwaf.WafBuilder; import com.datadog.ddwaf.WafContext; import com.datadog.ddwaf.WafHandle; import com.datadog.ddwaf.WafMetrics; import com.datadog.ddwaf.exception.AbstractWafException; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -18,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import okio.Okio; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -38,45 +41,49 @@ @OutputTimeUnit(MICROSECONDS) @Fork(value = 3) public class WafBenchmark { + private static final JsonAdapter> ADAPTER = + new Moshi.Builder() + .build() + .adapter(Types.newParameterizedType(Map.class, String.class, Object.class)); static { BenchmarkUtil.disableLogging(); BenchmarkUtil.initializeWaf(); } - WafHandle ctx; + WafBuilder wafBuilder; + WafHandle wafHandle; + WafContext wafContext; Map wafData = new HashMap<>(); Waf.Limits limits = new Waf.Limits(50, 500, 1000, 5000000, 5000000); @Benchmark public void withMetrics() throws Exception { - WafMetrics metricsCollector = ctx.createMetrics(); - WafContext add = ctx.openContext(); + WafMetrics metricsCollector = new WafMetrics(); + wafContext = new WafContext(wafHandle); try { - add.run(wafData, limits, metricsCollector); + wafContext.run(wafData, limits, metricsCollector); } finally { - add.close(); + wafContext.close(); } } @Benchmark public void withoutMetrics() throws Exception { - WafContext add = ctx.openContext(); + wafContext = new WafContext(wafHandle); try { - add.run(wafData, limits, null); + wafContext.run(wafData, limits, null); } finally { - add.close(); + wafContext.close(); } } @Setup(Level.Trial) public void setUp() throws AbstractWafException, IOException { + wafBuilder = new WafBuilder(); InputStream stream = getClass().getClassLoader().getResourceAsStream("test_multi_config.json"); - Map cfg = - Collections.singletonMap("waf", AppSecConfigDeserializer.INSTANCE.deserialize(stream)); - AppSecConfig waf = cfg.get("waf"); - ctx = Waf.createHandle("waf", waf.getRawConfig()); - + wafBuilder.addOrUpdateConfig("waf", ADAPTER.fromJson(Okio.buffer(Okio.source(stream)))); + wafHandle = wafBuilder.buildWafHandleInstance(); wafData.put(KnownAddresses.REQUEST_METHOD.getKey(), "POST"); wafData.put( KnownAddresses.REQUEST_URI_RAW.getKey(), "/foo/bar?foo=bar&foo=xpto&foo=%3cscript%3e"); @@ -112,6 +119,14 @@ public void setUp() throws AbstractWafException, IOException { @TearDown(Level.Trial) public void teardown() { - ctx.close(); + if (wafHandle != null && wafHandle.isOnline()) { + wafHandle.close(); + } + if (wafContext != null && wafContext.isOnline()) { + wafContext.close(); + } + if (wafBuilder != null && wafBuilder.isOnline()) { + wafBuilder.close(); + } } } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecModule.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecModule.java index 6ac207979cb..ab458b3442c 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecModule.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecModule.java @@ -3,17 +3,24 @@ import com.datadog.appsec.config.AppSecModuleConfigurer; import com.datadog.appsec.event.DataListener; import com.datadog.appsec.event.data.Address; +import com.datadog.ddwaf.WafBuilder; import java.util.Collection; public interface AppSecModule { void config(AppSecModuleConfigurer appSecConfigService) throws AppSecModuleActivationException; + void setWafBuilder(WafBuilder wafBuilder); + + void setRuleVersion(String rulesetVersion); + String getName(); String getInfo(); Collection getDataSubscriptions(); + boolean isWafBuilderSet(); + abstract class DataSubscription implements DataListener { private final Collection> subscribedAddresses; private final Priority priority; diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecSystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecSystem.java index 461343252d7..992e7dddace 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecSystem.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecSystem.java @@ -76,8 +76,9 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s APP_SEC_CONFIG_SERVICE = new AppSecConfigServiceImpl( config, configurationPoller, () -> reloadSubscriptions(REPLACEABLE_EVENT_PRODUCER)); - APP_SEC_CONFIG_SERVICE.init(); - + if (appSecEnabledConfig == ProductActivation.FULLY_ENABLED) { + APP_SEC_CONFIG_SERVICE.init(); + } sco.createRemaining(config); GatewayBridge gatewayBridge = @@ -87,7 +88,8 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s () -> API_SECURITY_SAMPLER, APP_SEC_CONFIG_SERVICE.getTraceSegmentPostProcessors()); - loadModules(eventDispatcher, sco.monitoring); + loadModules( + eventDispatcher, sco.monitoring, appSecEnabledConfig == ProductActivation.FULLY_ENABLED); gatewayBridge.init(); STOP_SUBSCRIPTION_SERVICE = gatewayBridge::stop; @@ -136,20 +138,25 @@ public static void stop() { RESET_SUBSCRIPTION_SERVICE = null; } Blocking.setBlockingService(BlockingService.NOOP); - APP_SEC_CONFIG_SERVICE.close(); } - private static void loadModules(EventDispatcher eventDispatcher, Monitoring monitoring) { + private static void loadModules( + EventDispatcher eventDispatcher, Monitoring monitoring, boolean appSecEnabledConfig) { EventDispatcher.DataSubscriptionSet dataSubscriptionSet = new EventDispatcher.DataSubscriptionSet(); final List modules = Collections.singletonList(new WAFModule(monitoring)); + APP_SEC_CONFIG_SERVICE.modulesToUpdateVersionIn(modules); for (AppSecModule module : modules) { log.debug("Starting appsec module {}", module.getName()); try { - AppSecConfigService.TransactionalAppSecModuleConfigurer cfgObject; - cfgObject = APP_SEC_CONFIG_SERVICE.createAppSecModuleConfigurer(); + AppSecConfigService.TransactionalAppSecModuleConfigurer cfgObject = + APP_SEC_CONFIG_SERVICE.createAppSecModuleConfigurer(); + module.setRuleVersion(APP_SEC_CONFIG_SERVICE.getCurrentRuleVersion()); + if (appSecEnabledConfig) { + module.setWafBuilder(APP_SEC_CONFIG_SERVICE.getWafBuilder()); + } module.config(cfgObject); cfgObject.commit(); } catch (RuntimeException | AppSecModule.AppSecModuleActivationException t) { @@ -174,6 +181,7 @@ private static void reloadSubscriptions( EventDispatcher newEd = new EventDispatcher(); for (AppSecModule module : STARTED_MODULES_INFO.keySet()) { + module.setRuleVersion(APP_SEC_CONFIG_SERVICE.getCurrentRuleVersion()); for (AppSecModule.DataSubscription sub : module.getDataSubscriptions()) { dataSubscriptionSet.addSubscription(sub.getSubscribedAddresses(), sub); } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfig.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfig.java deleted file mode 100644 index dbacf30e715..00000000000 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfig.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.datadog.appsec.config; - -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public interface AppSecConfig { - - Moshi MOSHI = new Moshi.Builder().build(); - JsonAdapter ADAPTER_V1 = MOSHI.adapter(AppSecConfigV1.class); - JsonAdapter ADAPTER_V2 = MOSHI.adapter(AppSecConfigV2.class); - - String getVersion(); - - int getNumberOfRules(); - - Map getRawConfig(); - - static AppSecConfig valueOf(Map rawConfig) throws IOException { - if (rawConfig == null) { - return null; - } - - String version = String.valueOf(rawConfig.get("version")); - if (version == null) { - throw new IOException("Unable deserialize raw json config"); - } - - // For version 1.x - if (version.startsWith("1.")) { - AppSecConfigV1 config = ADAPTER_V1.fromJsonValue(rawConfig); - config.rawConfig = rawConfig; - return config; - } - - // For version 2.x - if (version.startsWith("2.")) { - AppSecConfigV2 config = ADAPTER_V2.fromJsonValue(rawConfig); - config.rawConfig = rawConfig; - return config; - } - - throw new IOException("Config version '" + version + "' is not supported"); - } - - class AppSecConfigV1 implements AppSecConfig { - - private String version; - private Map rawConfig; - - @Override - public String getVersion() { - return null; - } - - @Override - public int getNumberOfRules() { - return ((List) rawConfig.getOrDefault("rules", Collections.emptyList())).size(); - } - - @Override - public Map getRawConfig() { - return rawConfig; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AppSecConfigV1 that = (AppSecConfigV1) o; - return Objects.equals(version, that.version) && Objects.equals(rawConfig, that.rawConfig); - } - - @Override - public int hashCode() { - int hash = 1; - hash = 31 * hash + (version == null ? 0 : version.hashCode()); - hash = 31 * hash + (rawConfig == null ? 0 : rawConfig.hashCode()); - return hash; - } - } - - class AppSecConfigV2 implements AppSecConfig { - private String version; - // Note: the tendency is for new code to manipulate rawConfig directly - private Map rawConfig; - - @Override - public String getVersion() { - return version; - } - - @Override - public int getNumberOfRules() { - return ((List) rawConfig.getOrDefault("rules", Collections.emptyList())).size() - + ((List) rawConfig.getOrDefault("custom_rules", Collections.emptyList())).size(); - } - - @Override - public Map getRawConfig() { - return rawConfig; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AppSecConfigV2 that = (AppSecConfigV2) o; - return Objects.equals(version, that.version) && Objects.equals(rawConfig, that.rawConfig); - } - - @Override - public int hashCode() { - int hash = 1; - hash = 31 * hash + (version == null ? 0 : version.hashCode()); - hash = 31 * hash + (rawConfig == null ? 0 : rawConfig.hashCode()); - return hash; - } - } -} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigDeserializer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigDeserializer.java deleted file mode 100644 index ea085c99782..00000000000 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigDeserializer.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.datadog.appsec.config; - -import static com.datadog.appsec.config.AppSecConfig.MOSHI; - -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Types; -import datadog.remoteconfig.ConfigurationDeserializer; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; -import okio.Okio; - -public class AppSecConfigDeserializer implements ConfigurationDeserializer { - public static final AppSecConfigDeserializer INSTANCE = new AppSecConfigDeserializer(); - - private static final JsonAdapter> ADAPTER = - MOSHI.adapter(Types.newParameterizedType(Map.class, String.class, Object.class)); - - private AppSecConfigDeserializer() {} - - @Override - public AppSecConfig deserialize(byte[] content) throws IOException { - return deserialize(new ByteArrayInputStream(content)); - } - - public AppSecConfig deserialize(InputStream is) throws IOException { - Map configMap = ADAPTER.fromJson(Okio.buffer(Okio.source(is))); - return AppSecConfig.valueOf(configMap); - } -} 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 e8ea1e9295e..064e5eb557e 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 @@ -22,17 +22,33 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_USER_BLOCKING; import static datadog.remoteconfig.Capabilities.CAPABILITY_ENDPOINT_FINGERPRINT; +import com.datadog.appsec.AppSecModule; import com.datadog.appsec.AppSecSystem; import com.datadog.appsec.config.AppSecModuleConfigurer.SubconfigListener; -import com.datadog.appsec.config.CurrentAppSecConfig.DirtyStatus; +import com.datadog.appsec.ddwaf.WAFInitializationResultReporter; +import com.datadog.appsec.ddwaf.WAFStatsReporter; +import com.datadog.appsec.ddwaf.WafInitialization; import com.datadog.appsec.util.AbortStartupException; import com.datadog.appsec.util.StandardizedLogging; +import com.datadog.ddwaf.WafBuilder; +import com.datadog.ddwaf.WafConfig; +import com.datadog.ddwaf.WafDiagnostics; +import com.datadog.ddwaf.exception.InvalidRuleSetException; +import com.datadog.ddwaf.exception.UnclassifiedWafException; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; import datadog.remoteconfig.ConfigurationEndListener; import datadog.remoteconfig.ConfigurationPoller; +import datadog.remoteconfig.PollingRateHinter; import datadog.remoteconfig.Product; +import datadog.remoteconfig.state.ConfigKey; +import datadog.remoteconfig.state.ProductListener; import datadog.trace.api.Config; import datadog.trace.api.ProductActivation; import datadog.trace.api.UserIdCollectionMode; +import datadog.trace.api.telemetry.LogCollector; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -40,10 +56,12 @@ 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.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import okio.Okio; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,19 +70,13 @@ public class AppSecConfigServiceImpl implements AppSecConfigService { private static final Logger log = LoggerFactory.getLogger(AppSecConfigServiceImpl.class); private static final String DEFAULT_CONFIG_LOCATION = "default_config.json"; - private static AppSecConfig DEFAULT_WAF_CONFIG; private final ConfigurationPoller configurationPoller; + private WafBuilder wafBuilder; - // the only thread modifying currentAppSecConfig is the RC thread - // However, the initial state is set up by another thread, and this needs to be visible to the RC - // thread - private CurrentAppSecConfig currentAppSecConfig; private MergedAsmFeatures mergedAsmFeatures; private volatile boolean initialized; - // for new subconfig subscribers - private final ConcurrentHashMap lastConfig = new ConcurrentHashMap<>(); private final ConcurrentHashMap subconfigListeners = new ConcurrentHashMap<>(); private final Config tracerConfig; @@ -73,8 +85,21 @@ public class AppSecConfigServiceImpl implements AppSecConfigService { private final ConfigurationEndListener applyRemoteConfigListener = this::applyRemoteConfigListener; + private final WAFInitializationResultReporter initReporter = + new WAFInitializationResultReporter(); + private final WAFStatsReporter statsReporter = new WAFStatsReporter(); + + private static final JsonAdapter> ADAPTER = + new Moshi.Builder() + .build() + .adapter(Types.newParameterizedType(Map.class, String.class, Object.class)); private boolean hasUserWafConfig; + private boolean defaultConfigActivated; + private final Set usedDDWafConfigKeys = new HashSet<>(); + private final String DEFAULT_WAF_CONFIG_RULE = "DEFAULT_WAF_CONFIG"; + private String currentRuleVersion; + private List modulesToUpdateVersionIn; public AppSecConfigServiceImpl( Config tracerConfig, @@ -83,6 +108,10 @@ public AppSecConfigServiceImpl( this.tracerConfig = tracerConfig; this.configurationPoller = configurationPoller; this.reconfiguration = reconfig; + traceSegmentPostProcessors.add(initReporter); + if (tracerConfig.isAppSecWafMetrics()) { + traceSegmentPostProcessors.add(statsReporter); + } } private void subscribeConfigurationPoller() { @@ -126,57 +155,123 @@ private void subscribeConfigurationPoller() { } private void subscribeRulesAndData() { - this.configurationPoller.addListener( - Product.ASM_DD, - AppSecConfigDeserializer.INSTANCE, - (configKey, newConfig, hinter) -> { - // read initialized so that the state is currentAppSecConfig is visible - if (!initialized) { - throw new IllegalStateException(); - } - if (newConfig == null) { - if (DEFAULT_WAF_CONFIG == null) { - throw new IllegalStateException("Expected default waf config to be available"); - } - log.debug( - "AppSec config given by remote config was pulled. Restoring default WAF config"); - newConfig = DEFAULT_WAF_CONFIG; - } - this.currentAppSecConfig.setDdConfig(newConfig); - // base rules can contain all rules/data/exclusions/etc - this.currentAppSecConfig.dirtyStatus.markAllDirty(); - }); - this.configurationPoller.addListener( - Product.ASM_DATA, - AppSecDataDeserializer.INSTANCE, - (configKey, newConfig, hinter) -> { - if (!initialized) { - throw new IllegalStateException(); - } - if (newConfig == null) { - currentAppSecConfig.mergedAsmData.removeConfig(configKey); - } else { - currentAppSecConfig.mergedAsmData.addConfig(configKey, newConfig); - } - this.currentAppSecConfig.dirtyStatus.data = true; - }); - this.configurationPoller.addListener( - Product.ASM, - AppSecUserConfigDeserializer.INSTANCE, - (configKey, newConfig, hinter) -> { - if (!initialized) { - throw new IllegalStateException(); - } - DirtyStatus dirtyStatus; - if (newConfig == null) { - dirtyStatus = currentAppSecConfig.userConfigs.removeConfig(configKey); - } else { - AppSecUserConfig userCfg = newConfig.build(configKey); - dirtyStatus = currentAppSecConfig.userConfigs.addConfig(userCfg); - } + this.configurationPoller.addListener(Product.ASM_DD, new AppSecConfigChangesDDListener()); + this.configurationPoller.addListener(Product.ASM_DATA, new AppSecConfigChangesListener()); + this.configurationPoller.addListener(Product.ASM, new AppSecConfigChangesListener()); + } - this.currentAppSecConfig.dirtyStatus.mergeFrom(dirtyStatus); - }); + public void modulesToUpdateVersionIn(List modules) { + this.modulesToUpdateVersionIn = modules; + } + + public String getCurrentRuleVersion() { + return currentRuleVersion; + } + + private class AppSecConfigChangesListener implements ProductListener { + @Override + public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollingRateHinter) + throws IOException { + if (!initialized) { + throw new IllegalStateException(); + } + + if (content == null) { + try { + wafBuilder.removeConfig(configKey.toString()); + } 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); + } + } + } + + @Override + public void remove(ConfigKey configKey, PollingRateHinter pollingRateHinter) + throws IOException { + accept(configKey, null, pollingRateHinter); + } + + @Override + public void commit(PollingRateHinter pollingRateHinter) { + // no action needed + } + } + + private class AppSecConfigChangesDDListener extends AppSecConfigChangesListener { + @Override + public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollingRateHinter) + throws IOException { + if (defaultConfigActivated) { // if we get any config, remove the default one + log.debug("Removing default config"); + try { + wafBuilder.removeConfig(DEFAULT_WAF_CONFIG_RULE); + } catch (UnclassifiedWafException e) { + throw new RuntimeException(e); + } + defaultConfigActivated = false; + } + super.accept(configKey, content, pollingRateHinter); + usedDDWafConfigKeys.add(configKey.toString()); + } + + @Override + public void remove(ConfigKey configKey, PollingRateHinter pollingRateHinter) + throws IOException { + super.remove(configKey, pollingRateHinter); + usedDDWafConfigKeys.remove(configKey.toString()); + } + } + + private void handleWafUpdateResultReport(String configKey, Map rawConfig) + throws AppSecModule.AppSecModuleActivationException { + wafBuilder = getWafBuilder(); + if (modulesToUpdateVersionIn != null + && !modulesToUpdateVersionIn.isEmpty() + && !modulesToUpdateVersionIn.stream().findFirst().get().isWafBuilderSet()) { + modulesToUpdateVersionIn.forEach(module -> module.setWafBuilder(wafBuilder)); + } + try { + WafDiagnostics wafDiagnostics = wafBuilder.addOrUpdateConfig(configKey, rawConfig); + if (log.isInfoEnabled()) { + StandardizedLogging.numLoadedRules(log, configKey, countRules(rawConfig)); + } + + // TODO: Send diagnostics via telemetry + final LogCollector telemetryLogger = LogCollector.get(); + + initReporter.setReportForPublication(wafDiagnostics); + if (wafDiagnostics.rulesetVersion != null + && !wafDiagnostics.rulesetVersion.isEmpty() + && !wafDiagnostics.rules.getLoaded().isEmpty() + && (!defaultConfigActivated || currentRuleVersion == null)) { + currentRuleVersion = wafDiagnostics.rulesetVersion; + statsReporter.setRulesVersion(currentRuleVersion); + if (modulesToUpdateVersionIn != null) { + modulesToUpdateVersionIn.forEach(module -> module.setRuleVersion(currentRuleVersion)); + } + } + } catch (InvalidRuleSetException e) { + log.debug( + "Invalid rule during waf config update for config key {}: {}", + configKey, + e.wafDiagnostics); + + // TODO: Propagate diagostics back to remote config apply_error + + initReporter.setReportForPublication(e.wafDiagnostics); + throw new RuntimeException(e); + } catch (UnclassifiedWafException e) { + log.debug("Error during waf config update for config key {}: {}", configKey, e.getMessage()); + throw new RuntimeException(e); + } } private void subscribeAsmFeatures() { @@ -184,6 +279,10 @@ private void subscribeAsmFeatures() { Product.ASM_FEATURES, AppSecFeaturesDeserializer.INSTANCE, (configKey, newConfig, hinter) -> { + if (!hasUserWafConfig && !defaultConfigActivated) { + // features activated in runtime + init(); + } if (!initialized) { throw new IllegalStateException(); } @@ -202,15 +301,16 @@ private void subscribeAsmFeatures() { } private void distributeSubConfigurations( - Map newConfig, AppSecModuleConfigurer.Reconfiguration reconfiguration) { + String key, AppSecModuleConfigurer.Reconfiguration reconfiguration) { + if (usedDDWafConfigKeys.isEmpty() && !defaultConfigActivated && !hasUserWafConfig) { + // no config left in the WAF builder, add the default config + init(); + } for (Map.Entry entry : subconfigListeners.entrySet()) { - String key = entry.getKey(); - if (!newConfig.containsKey(key)) { - continue; - } SubconfigListener listener = entry.getValue(); try { - listener.onNewSubconfig(newConfig.get(key), reconfiguration); + listener.onNewSubconfig(key, reconfiguration); + reconfiguration.reloadSubscriptions(); } catch (Exception rte) { log.warn("Error updating configuration of app sec module listening on key {}", key, rte); } @@ -219,7 +319,7 @@ private void distributeSubConfigurations( @Override public void init() { - AppSecConfig wafConfig; + Map wafConfig; hasUserWafConfig = false; try { wafConfig = loadUserWafConfig(tracerConfig); @@ -230,6 +330,7 @@ public void init() { if (wafConfig == null) { try { wafConfig = loadDefaultWafConfig(); + defaultConfigActivated = true; } catch (IOException e) { log.error("Error loading default config", e); throw new AbortStartupException("Error loading default config", e); @@ -237,11 +338,17 @@ public void init() { } else { hasUserWafConfig = true; } - this.currentAppSecConfig = new CurrentAppSecConfig(); - this.currentAppSecConfig.setDdConfig(wafConfig); - this.lastConfig.put("waf", this.currentAppSecConfig); this.mergedAsmFeatures = new MergedAsmFeatures(); this.initialized = true; + + if (wafConfig.isEmpty()) { + throw new IllegalStateException("Expected default waf config to be available"); + } + try { + handleWafUpdateResultReport(DEFAULT_WAF_CONFIG_RULE, wafConfig); + } catch (AppSecModule.AppSecModuleActivationException e) { + throw new RuntimeException(e); + } } public void maybeSubscribeConfigPolling() { @@ -263,6 +370,18 @@ public List getTraceSegmentPostProcessors() { return traceSegmentPostProcessors; } + public WafBuilder getWafBuilder() throws AppSecModule.AppSecModuleActivationException { + if (!WafInitialization.ONLINE) { + log.debug("In-app WAF initialization failed. See previous log entries"); + throw new AppSecModule.AppSecModuleActivationException( + "In-app WAF initialization failed. See previous log entries"); + } + if (this.wafBuilder == null || !this.wafBuilder.isOnline()) { + this.wafBuilder = new WafBuilder(createWafConfig(Config.get())); + } + return this.wafBuilder; + } + /** * Implementation of {@link AppSecModuleConfigurer} that solves two problems: - Avoids the * submodules receiving configuration changes before their initial config is completed. - Avoid @@ -275,9 +394,8 @@ private class TransactionalAppSecModuleConfigurerImpl private final List postProcessors = new ArrayList<>(); @Override - public Optional addSubConfigListener(String key, SubconfigListener listener) { + public void addSubConfigListener(String key, SubconfigListener listener) { listenerMap.put(key, listener); - return Optional.ofNullable(lastConfig.get(key)); } @Override @@ -296,7 +414,8 @@ public TransactionalAppSecModuleConfigurer createAppSecModuleConfigurer() { return new TransactionalAppSecModuleConfigurerImpl(); } - private static AppSecConfig loadDefaultWafConfig() throws IOException { + private static Map loadDefaultWafConfig() throws IOException { + log.debug("Loading default waf config"); try (InputStream is = AppSecConfigServiceImpl.class .getClassLoader() @@ -305,25 +424,24 @@ private static AppSecConfig loadDefaultWafConfig() throws IOException { throw new IOException("Resource " + DEFAULT_CONFIG_LOCATION + " not found"); } - AppSecConfig ret = AppSecConfigDeserializer.INSTANCE.deserialize(is); + Map ret = ADAPTER.fromJson(Okio.buffer(Okio.source(is))); StandardizedLogging._initialConfigSourceAndLibddwafVersion(log, ""); if (log.isInfoEnabled()) { StandardizedLogging.numLoadedRules(log, "", countRules(ret)); } - - DEFAULT_WAF_CONFIG = ret; return ret; } } - private static AppSecConfig loadUserWafConfig(Config tracerConfig) throws IOException { + private static Map loadUserWafConfig(Config tracerConfig) throws IOException { + log.debug("Loading user waf config"); String filename = tracerConfig.getAppSecRulesFile(); if (filename == null) { return null; } try (InputStream is = new FileInputStream(filename)) { - AppSecConfig ret = AppSecConfigDeserializer.INSTANCE.deserialize(is); + Map ret = ADAPTER.fromJson(Okio.buffer(Okio.source(is))); StandardizedLogging._initialConfigSourceAndLibddwafVersion(log, filename); if (log.isInfoEnabled()) { @@ -340,8 +458,8 @@ private static AppSecConfig loadUserWafConfig(Config tracerConfig) throws IOExce } } - private static int countRules(AppSecConfig config) { - return config.getNumberOfRules(); + private static int countRules(Map config) { + return ((List) config.getOrDefault("rules", Collections.emptyList())).size(); } @Override @@ -376,6 +494,10 @@ public void close() { this.configurationPoller.removeListeners(Product.ASM_FEATURES); this.configurationPoller.removeConfigurationEndListener(applyRemoteConfigListener); this.configurationPoller.stop(); + if (this.wafBuilder != null) { + this.wafBuilder.close(); + this.wafBuilder = null; + } } private void applyRemoteConfigListener() { @@ -384,13 +506,11 @@ private void applyRemoteConfigListener() { setAppSecActivation(features.asm); setUserIdCollectionMode(features.autoUserInstrum); - if (!AppSecSystem.isActive() || !currentAppSecConfig.dirtyStatus.isAnyDirty()) { + if (!AppSecSystem.isActive()) { return; } - distributeSubConfigurations( - Collections.singletonMap("waf", currentAppSecConfig), reconfiguration); - currentAppSecConfig.dirtyStatus.clearDirty(); + distributeSubConfigurations("waf", reconfiguration); } private void setAppSecActivation(final AppSecFeatures.Asm asm) { @@ -403,11 +523,6 @@ private void setAppSecActivation(final AppSecFeatures.Asm asm) { if (AppSecSystem.isActive() != newState) { log.info("AppSec {} (runtime)", newState ? "enabled" : "disabled"); AppSecSystem.setActive(newState); - if (AppSecSystem.isActive()) { - // On remote activation, we need to re-distribute the last known configuration. - // This may trigger initializations, including WAF if it was lazy loaded. - this.currentAppSecConfig.dirtyStatus.markAllDirty(); - } } } @@ -423,4 +538,21 @@ private void setUserIdCollectionMode(final AppSecFeatures.AutoUserInstrum autoUs log.info("User ID collection mode changed via remote-config: {} -> {}", current, newMode); } } + + private static WafConfig createWafConfig(Config config) { + WafConfig wafConfig = new WafConfig(); + String keyRegexp = config.getAppSecObfuscationParameterKeyRegexp(); + if (keyRegexp != null) { + wafConfig.obfuscatorKeyRegex = keyRegexp; + } else { // reset + wafConfig.obfuscatorKeyRegex = WafConfig.DEFAULT_KEY_REGEX; + } + String valueRegexp = config.getAppSecObfuscationParameterValueRegexp(); + if (valueRegexp != null) { + wafConfig.obfuscatorValueRegex = valueRegexp; + } else { // reset + wafConfig.obfuscatorValueRegex = WafConfig.DEFAULT_VALUE_REGEX; + } + return wafConfig; + } } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecData.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecData.java deleted file mode 100644 index 32032fd1fa5..00000000000 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecData.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.datadog.appsec.config; - -import com.squareup.moshi.Json; -import java.util.List; -import java.util.Map; - -public class AppSecData { - - @Json(name = "rules_data") - private List> rules; - - @Json(name = "exclusion_data") - private List> exclusion; - - public List> getRules() { - return rules; - } - - public void setRules(List> rules) { - this.rules = rules; - } - - public List> getExclusion() { - return exclusion; - } - - public void setExclusion(List> exclusion) { - this.exclusion = exclusion; - } -} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecDataDeserializer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecDataDeserializer.java deleted file mode 100644 index b0b1043d8f5..00000000000 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecDataDeserializer.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.datadog.appsec.config; - -import static com.datadog.appsec.config.AppSecConfig.MOSHI; - -import com.squareup.moshi.JsonAdapter; -import datadog.remoteconfig.ConfigurationDeserializer; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import okio.Okio; - -public class AppSecDataDeserializer implements ConfigurationDeserializer { - public static final AppSecDataDeserializer INSTANCE = new AppSecDataDeserializer(); - - private static final JsonAdapter ADAPTER = MOSHI.adapter(AppSecData.class); - - private AppSecDataDeserializer() {} - - @Override - public AppSecData deserialize(byte[] content) throws IOException { - return deserialize(new ByteArrayInputStream(content)); - } - - private AppSecData deserialize(InputStream is) throws IOException { - return ADAPTER.fromJson(Okio.buffer(Okio.source(is))); - } -} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecFeaturesDeserializer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecFeaturesDeserializer.java index e900a42957c..2b9317bb25e 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecFeaturesDeserializer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecFeaturesDeserializer.java @@ -1,8 +1,7 @@ package com.datadog.appsec.config; -import static com.datadog.appsec.config.AppSecConfig.MOSHI; - import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; import datadog.remoteconfig.ConfigurationDeserializer; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -11,7 +10,8 @@ public class AppSecFeaturesDeserializer implements ConfigurationDeserializer { public static final AppSecFeaturesDeserializer INSTANCE = new AppSecFeaturesDeserializer(); - private static final JsonAdapter ADAPTER = MOSHI.adapter(AppSecFeatures.class); + private static final JsonAdapter ADAPTER = + new Moshi.Builder().build().adapter(AppSecFeatures.class); private AppSecFeaturesDeserializer() {} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecModuleConfigurer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecModuleConfigurer.java index c3e6a281f2c..e2a0c2d8b19 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecModuleConfigurer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecModuleConfigurer.java @@ -1,14 +1,14 @@ package com.datadog.appsec.config; import com.datadog.appsec.AppSecModule; -import java.util.Optional; +import com.datadog.ddwaf.exception.AbstractWafException; public interface AppSecModuleConfigurer { - Optional addSubConfigListener(String key, SubconfigListener listener); + void addSubConfigListener(String key, SubconfigListener listener); interface SubconfigListener { void onNewSubconfig(Object newConfig, Reconfiguration reconfiguration) - throws AppSecModule.AppSecModuleActivationException; + throws AppSecModule.AppSecModuleActivationException, AbstractWafException; } void addTraceSegmentPostProcessor(TraceSegmentPostProcessor interceptor); diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecUserConfig.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecUserConfig.java deleted file mode 100644 index 84c0689943c..00000000000 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecUserConfig.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.datadog.appsec.config; - -import com.datadog.appsec.config.CurrentAppSecConfig.DirtyStatus; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class AppSecUserConfig { - public final String configKey; - public final List> ruleOverrides; - public final List> actions; - public final List> exclusions; - public final List> customRules; - - public AppSecUserConfig( - String configKey, - List> ruleOverrides, - List> actions, - List> exclusions, - List> customRules) { - this.configKey = configKey; - this.ruleOverrides = ruleOverrides; - this.actions = actions; - this.exclusions = exclusions; - this.customRules = customRules; - } - - public DirtyStatus dirtyEffect() { - DirtyStatus ds = new DirtyStatus(); - if (!this.ruleOverrides.isEmpty()) { - ds.ruleOverrides = true; - } - if (!this.actions.isEmpty()) { - ds.actions = true; - } - if (!this.exclusions.isEmpty()) { - ds.exclusions = true; - } - if (!this.customRules.isEmpty()) { - ds.customRules = true; - } - // data not included here - return ds; - } - - public static class Builder { - public final List> ruleOverrides; - public final List> actions; - public final List> exclusions; - public final List> customRules; - - public Builder(Map>> userConfig) { - this.ruleOverrides = userConfig.getOrDefault("rules_override", Collections.EMPTY_LIST); - this.actions = userConfig.getOrDefault("actions", Collections.EMPTY_LIST); - this.exclusions = userConfig.getOrDefault("exclusions", Collections.EMPTY_LIST); - this.customRules = userConfig.getOrDefault("custom_rules", Collections.EMPTY_LIST); - } - - // configKey is unavailable on the deserializer - AppSecUserConfig build(String configKey) { - return new AppSecUserConfig(configKey, ruleOverrides, actions, exclusions, customRules); - } - } -} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecUserConfigDeserializer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecUserConfigDeserializer.java deleted file mode 100644 index 2d15ae60443..00000000000 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecUserConfigDeserializer.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.datadog.appsec.config; - -import static com.datadog.appsec.config.AppSecConfig.MOSHI; - -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Types; -import datadog.remoteconfig.ConfigurationDeserializer; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Map; -import okio.Okio; - -public class AppSecUserConfigDeserializer - implements ConfigurationDeserializer { - public static final AppSecUserConfigDeserializer INSTANCE = new AppSecUserConfigDeserializer(); - - private static final JsonAdapter>>> ADAPTER = - MOSHI.adapter( - Types.newParameterizedType( - Map.class, - String.class, - Types.newParameterizedType( - List.class, Types.newParameterizedType(Map.class, String.class, Object.class)))); - - private AppSecUserConfigDeserializer() {} - - @Override - public AppSecUserConfig.Builder deserialize(byte[] content) throws IOException { - return deserialize(new ByteArrayInputStream(content)); - } - - private AppSecUserConfig.Builder deserialize(InputStream is) throws IOException { - Map>> cfg = ADAPTER.fromJson(Okio.buffer(Okio.source(is))); - return new AppSecUserConfig.Builder(cfg); - } -} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/CollectedUserConfigs.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/CollectedUserConfigs.java deleted file mode 100644 index da136245e46..00000000000 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/CollectedUserConfigs.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.datadog.appsec.config; - -import com.datadog.appsec.config.CurrentAppSecConfig.DirtyStatus; -import java.util.AbstractList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - -class CollectedUserConfigs extends AbstractList { - private final List userConfigs = new LinkedList<>(); - - public DirtyStatus addConfig(AppSecUserConfig newUserConfig) { - if (newUserConfig == null) { - throw new NullPointerException("user config was null"); - } - DirtyStatus removedDirty = removeConfig(newUserConfig.configKey); - // it would be more accurate to actually compare the contents of the - // custom rules and the toggling instructions to see if anything changed - DirtyStatus newDirty = newUserConfig.dirtyEffect(); - this.userConfigs.add(newUserConfig); - - Collections.sort(userConfigs, Comparator.comparing(c -> c.configKey)); - - removedDirty.mergeFrom(newDirty); - return removedDirty; - } - - public DirtyStatus removeConfig(String cfgKey) { - Optional maybeRemovedElement = - userConfigs.stream() - .filter(cfg -> cfg.configKey.equals(cfgKey)) - .findAny() - .map( - cfg -> { - userConfigs.remove(cfg); - return cfg; - }); - - if (!maybeRemovedElement.isPresent()) { - return new DirtyStatus(); - } - AppSecUserConfig removedElement = maybeRemovedElement.get(); - return removedElement.dirtyEffect(); - } - - @Override - public AppSecUserConfig get(int index) { - return userConfigs.get(index); - } - - @Override - public int size() { - return userConfigs.size(); - } -} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/CurrentAppSecConfig.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/CurrentAppSecConfig.java deleted file mode 100644 index e33bf4515c3..00000000000 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/CurrentAppSecConfig.java +++ /dev/null @@ -1,276 +0,0 @@ -package com.datadog.appsec.config; - -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.collectingAndThen; -import static java.util.stream.Collectors.toMap; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CurrentAppSecConfig { - private static final Logger log = LoggerFactory.getLogger(CurrentAppSecConfig.class); - - private AppSecConfig ddConfig; // assume there's only one of these - CollectedUserConfigs userConfigs = new CollectedUserConfigs(); - MergedAsmData mergedAsmData = new MergedAsmData(new HashMap<>()); - public final DirtyStatus dirtyStatus = new DirtyStatus(); - - @SuppressWarnings("unchecked") - public void setDdConfig(AppSecConfig newConfig) { - this.ddConfig = newConfig; - - final Map rawConfig = newConfig.getRawConfig(); - List> rules = (List>) rawConfig.get("rules_data"); - List> exclusions = - (List>) rawConfig.get("exclusion_data"); - if (rules != null || exclusions != null) { - final AppSecData data = new AppSecData(); - data.setRules(rules); - data.setExclusion(exclusions); - mergedAsmData.addConfig(MergedAsmData.KEY_BUNDLED_DATA, data); - } else { - mergedAsmData.removeConfig(MergedAsmData.KEY_BUNDLED_DATA); - } - } - - public static class DirtyStatus { - public boolean rules; - public boolean customRules; - public boolean ruleOverrides; - public boolean actions; - public boolean data; - public boolean exclusions; - - public void mergeFrom(DirtyStatus o) { - rules = rules || o.rules; - customRules = customRules || o.customRules; - ruleOverrides = ruleOverrides || o.ruleOverrides; - actions = actions || o.actions; - data = data || o.data; - exclusions = exclusions || o.exclusions; - } - - public void clearDirty() { - rules = customRules = ruleOverrides = actions = data = exclusions = false; - } - - public void markAllDirty() { - rules = customRules = ruleOverrides = actions = data = exclusions = true; - } - - public boolean isAnyDirty() { - return isDirtyForActions() || isDirtyForDdwafUpdate(); - } - - public boolean isDirtyForDdwafUpdate() { - return rules || customRules || ruleOverrides || data || exclusions; - } - - public boolean isDirtyForActions() { - return actions; - } - } - - public AppSecConfig getMergedUpdateConfig() throws IOException { - if (!dirtyStatus.isAnyDirty()) { - throw new IllegalStateException( - "Can't call getMergedUpdateConfig without any dirty property"); - } - - Map mso = new HashMap<>(); - if (dirtyStatus.rules) { - mso.put("rules", ddConfig.getRawConfig().getOrDefault("rules", Collections.emptyList())); - mso.put( - "processors", - ddConfig.getRawConfig().getOrDefault("processors", Collections.emptyList())); - mso.put( - "scanners", ddConfig.getRawConfig().getOrDefault("scanners", Collections.emptyList())); - } - if (dirtyStatus.customRules) { - mso.put("custom_rules", getMergedCustomRules()); - } - if (dirtyStatus.exclusions) { - mso.put("exclusions", getMergedExclusions()); - } - if (dirtyStatus.ruleOverrides) { - mso.put("rules_override", getMergedRuleOverrides()); - } - if (dirtyStatus.data) { - final AppSecData data = mergedAsmData.getMergedData(); - mso.put("rules_data", data.getRules()); - mso.put("exclusion_data", data.getExclusion()); - } - if (dirtyStatus.actions) { - mso.put("actions", getMergedActions()); - } - - mso.put("version", ddConfig.getVersion() == null ? "2.1" : ddConfig.getVersion()); - - if (dirtyStatus.isAnyDirty()) { - mso.put("metadata", ddConfig.getRawConfig().getOrDefault("metadata", Collections.emptyMap())); - } - - if (log.isDebugEnabled()) { - log.debug( - "Providing WAF config with: " - + "rules: {}, custom_rules: {}, exclusions: {}, ruleOverrides: {}, rules_data: {}, exclusion_data: {}, actions: {}", - debugRuleSummary(mso), - debugCustomRuleSummary(mso), - debugExclusionsSummary(mso), - debugRuleOverridesSummary(mso), - debugRulesDataSummary(mso), - debugExclusionDataSummary(mso), - debugActionsSummary(mso)); - } - return AppSecConfig.valueOf(mso); - } - - private static String debugActionsSummary(Map mso) { - List> actions = (List>) mso.get("actions"); - if (actions == null) { - return ""; - } - return "[" - + actions.size() - + " actions with ids " - + actions.stream().map(rd -> String.valueOf(rd.get("id"))).collect(Collectors.joining(", ")) - + "]"; - } - - private static String debugRulesDataSummary(Map mso) { - List> rulesData = (List>) mso.get("rules_data"); - if (rulesData == null) { - return ""; - } - return "[" - + rulesData.size() - + " rules data sets with ids " - + rulesData.stream() - .map(rd -> String.valueOf(rd.get("id"))) - .collect(Collectors.joining(", ")) - + "]"; - } - - private static String debugExclusionDataSummary(Map mso) { - List> exclusionData = (List>) mso.get("exclusion_data"); - if (exclusionData == null) { - return ""; - } - return "[" - + exclusionData.size() - + " exclusion data sets with ids " - + exclusionData.stream() - .map(rd -> String.valueOf(rd.get("id"))) - .collect(Collectors.joining(", ")) - + "]"; - } - - private static String debugRuleOverridesSummary(Map mso) { - List> overrides = (List>) mso.get("rules_override"); - if (overrides == null) { - return ""; - } - return "[" + overrides.size() + " rule overrides]"; - } - - private static String debugExclusionsSummary(Map mso) { - List> exclusions = (List>) mso.get("exclusions"); - if (exclusions == null) { - return ""; - } - return "[" - + exclusions.size() - + " exclusions with ids " - + exclusions.stream() - .map(ex -> String.valueOf(ex.get("id"))) - .collect(Collectors.joining(", ")) - + "]"; - } - - private static String debugRuleSummary(Map mso) { - List> rules = (List>) mso.get("rules"); - if (rules == null) { - return ""; - } - return "[" + rules.size() + " rules]"; - } - - private static String debugCustomRuleSummary(Map mso) { - List> rules = (List>) mso.get("custom_rules"); - if (rules == null) { - return ""; - } - return "[" + rules.size() + " rules]"; - } - - // does not include default actions - private List> getMergedActions() { - List> actions = - (List>) - ddConfig.getRawConfig().getOrDefault("actions", Collections.EMPTY_LIST); - return userConfigs.stream() - .filter(userCfg -> !userCfg.actions.isEmpty()) - .reduce(actions, (a, b) -> b.actions, CurrentAppSecConfig::mergeMapsByIdKeepLatest); - } - - private List> getMergedCustomRules() { - return this.userConfigs.stream() - .map(uc -> uc.customRules) - .reduce(Collections.emptyList(), CurrentAppSecConfig::mergeMapsByIdKeepLatest); - } - - private List> getMergedExclusions() { - List> exclusions = - (List>) - ddConfig.getRawConfig().getOrDefault("exclusions", Collections.emptyList()); - - List userConfigs = this.userConfigs; - for (AppSecUserConfig userCfg : userConfigs) { - if (!userCfg.exclusions.isEmpty()) { - exclusions = mergeMapsByIdKeepLatest(exclusions, userCfg.exclusions); - } - } - - return exclusions; - } - - private List> getMergedRuleOverrides() { - List> ruleOverrides = - (List>) - ddConfig.getRawConfig().getOrDefault("rules_override", Collections.emptyList()); - - List userConfigs = this.userConfigs; - for (AppSecUserConfig userCfg : userConfigs) { - if (!userCfg.ruleOverrides.isEmpty()) { - // plain merge; overrides have no ids - ruleOverrides = - Stream.concat(ruleOverrides.stream(), userCfg.ruleOverrides.stream()) - .collect(Collectors.toList()); - } - } - - return ruleOverrides; - } - - private static List> mergeMapsByIdKeepLatest( - List> l1, List> l2) { - return Stream.concat(l1.stream(), l2.stream()) - .collect( - collectingAndThen( - toMap( - m -> String.valueOf(m.get("id")), - identity(), - (m1, m2) -> m2, - LinkedHashMap::new), - mapOfMaps -> new ArrayList<>(mapOfMaps.values()))); - } -} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/MergedAsmData.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/MergedAsmData.java deleted file mode 100644 index 6082c199bc1..00000000000 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/MergedAsmData.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.datadog.appsec.config; - -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class MergedAsmData { - public static final String KEY_BUNDLED_DATA = ""; - - private final Map configs; - private AppSecData mergedData; - - public MergedAsmData(Map configs) { - this.configs = configs; - } - - public void addConfig(String cfgKey, AppSecData config) { - this.configs.put(cfgKey, config); - this.mergedData = null; - } - - public void removeConfig(String cfgKey) { - this.configs.remove(cfgKey); - this.mergedData = null; - } - - /* - * Each config key is associated with a list of maps like this: - * - * - id: ip_data - * type: ip_with_expiration - * data: - * - value: 192.168.1.1 - * expiration: 555 - */ - public AppSecData getMergedData() throws InvalidAsmDataException { - if (mergedData != null) { - return mergedData; - } - - try { - this.mergedData = new AppSecData(); - this.mergedData.setRules(buildMergedData(groupById(AppSecData::getRules))); - this.mergedData.setExclusion(buildMergedData(groupById(AppSecData::getExclusion))); - } catch (InvalidAsmDataException iade) { - throw iade; - } catch (RuntimeException rte) { - throw new InvalidAsmDataException(rte); - } - - return this.mergedData; - } - - /** map of id -> list of maps across all the configs with such id */ - private Map>> groupById( - final Function>> property) { - return configs.values().stream() - .map(property) - .map(it -> it == null ? Collections.>emptyList() : it) - .flatMap(Collection::stream) - .collect(groupingBy(d -> (String) d.get("id"))); - } - - private List> buildMergedData( - Map>> dataPerId) { - return dataPerId.entrySet().stream() - .map( - idToMapList -> { - String id = idToMapList.getKey(); - List> mapList = idToMapList.getValue(); - if (mapList.size() == 0) { - return null; - } - Set types = - mapList.stream().map(m -> (String) m.get("type")).collect(Collectors.toSet()); - if (types.size() > 1) { - throw new InvalidAsmDataException( - "multiple types of data for data id " + id + ": " + types); - } - String type = types.iterator().next(); - - Stream> allDataEntries = - mapList.stream() - .flatMap(m -> ((List>) m.get("data")).stream()); - HashMap merged = new HashMap<>(); - merged.put("id", id); - merged.put("type", type); - - if ("ip_with_expiration".equals(type) || "data_with_expiration".equals(type)) { - merged.put("data", mergeExpirationData(allDataEntries)); - } else { - // just concatenate the data - List> allData = allDataEntries.collect(toList()); - merged.put("data", allData); - } - return merged; - }) - .collect(toList()); - } - - private List> mergeExpirationData(Stream> data) { - Map expirations = new HashMap<>(); - data.forEach( - d -> { - Object value = d.get("value"); - Long expiration = null; - Object expirationValue = d.get("expiration"); - if (expirationValue instanceof String) { - expirationValue = Long.parseLong((String) expirationValue); - } - if (expirationValue != null) { - expiration = ((Number) expirationValue).longValue(); - } - if (!expirations.containsKey(value)) { - expirations.put(value, expiration); - } else { - Long prevExpiration = expirations.get(value); - if (prevExpiration != null) { - if (expiration == null || prevExpiration < expiration) { - expirations.put(value, expiration); - } - } - } - }); - - return expirations.entrySet().stream() - .map( - e -> { - HashMap point = new HashMap<>(); - point.put("value", e.getKey()); - if (e.getValue() != null) { - point.put("expiration", e.getValue()); - } - return point; - }) - .collect(toList()); - } - - public class InvalidAsmDataException extends RuntimeException { - public InvalidAsmDataException(String s) { - super(s); - } - - public InvalidAsmDataException(RuntimeException rte) { - super(rte); - } - } -} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFInitializationResultReporter.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFInitializationResultReporter.java index ef73c33778d..c6587d167c6 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFInitializationResultReporter.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFInitializationResultReporter.java @@ -3,8 +3,8 @@ import com.datadog.appsec.config.TraceSegmentPostProcessor; import com.datadog.appsec.gateway.AppSecRequestContext; import com.datadog.appsec.report.AppSecEvent; -import com.datadog.ddwaf.RuleSetInfo; import com.datadog.ddwaf.Waf; +import com.datadog.ddwaf.WafDiagnostics; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; @@ -27,16 +27,16 @@ public class WAFInitializationResultReporter implements TraceSegmentPostProcesso .adapter( Types.newParameterizedType( Map.class, String.class, Types.newParameterizedType(List.class, String.class))); - private final AtomicReference pendingReportRef = new AtomicReference<>(); + private final AtomicReference pendingReportRef = new AtomicReference<>(); - public void setReportForPublication(RuleSetInfo report) { + public void setReportForPublication(WafDiagnostics report) { this.pendingReportRef.set(report); } @Override public void processTraceSegment( TraceSegment segment, AppSecRequestContext ctx, Collection collectedEvents) { - RuleSetInfo report = pendingReportRef.get(); + WafDiagnostics report = pendingReportRef.get(); if (report == null) { return; } @@ -45,9 +45,9 @@ public void processTraceSegment( return; } - segment.setTagTop(RULE_ERRORS, RULES_ERRORS_ADAPTER.toJson(report.getErrors())); - segment.setTagTop(RULES_LOADED, report.getNumRulesOK()); - segment.setTagTop(RULE_ERROR_COUNT, report.getNumRulesError()); + segment.setTagTop(RULE_ERRORS, RULES_ERRORS_ADAPTER.toJson(report.getAllErrors())); + segment.setTagTop(RULES_LOADED, report.getNumConfigOK()); + segment.setTagTop(RULE_ERROR_COUNT, report.getNumConfigError()); segment.setTagTop(WAF_VERSION, Waf.LIB_VERSION); segment.setTagTop(Tags.ASM_KEEP, true); diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java index 2954ed2eacc..609b2fdb64d 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java @@ -3,12 +3,9 @@ import static datadog.trace.util.stacktrace.StackTraceEvent.DEFAULT_LANGUAGE; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.toMap; import com.datadog.appsec.AppSecModule; -import com.datadog.appsec.config.AppSecConfig; import com.datadog.appsec.config.AppSecModuleConfigurer; -import com.datadog.appsec.config.CurrentAppSecConfig; import com.datadog.appsec.event.ChangeableFlow; import com.datadog.appsec.event.data.Address; import com.datadog.appsec.event.data.DataBundle; @@ -18,14 +15,12 @@ import com.datadog.appsec.gateway.RateLimiter; import com.datadog.appsec.report.AppSecEvent; import com.datadog.appsec.util.StandardizedLogging; -import com.datadog.ddwaf.RuleSetInfo; import com.datadog.ddwaf.Waf; -import com.datadog.ddwaf.WafConfig; +import com.datadog.ddwaf.WafBuilder; import com.datadog.ddwaf.WafContext; import com.datadog.ddwaf.WafHandle; import com.datadog.ddwaf.WafMetrics; import com.datadog.ddwaf.exception.AbstractWafException; -import com.datadog.ddwaf.exception.InvalidRuleSetException; import com.datadog.ddwaf.exception.TimeoutWafException; import com.datadog.ddwaf.exception.UnclassifiedWafException; import com.squareup.moshi.JsonAdapter; @@ -44,7 +39,6 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.Tags; -import datadog.trace.util.RandomUtils; import datadog.trace.util.stacktrace.StackTraceEvent; import datadog.trace.util.stacktrace.StackTraceFrame; import datadog.trace.util.stacktrace.StackUtils; @@ -65,7 +59,6 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -89,6 +82,9 @@ public class WAFModule implements AppSecModule { private static final Map DEFAULT_ACTIONS; private static final String EXPLOIT_DETECTED_MSG = "Exploit detected"; + private boolean init = true; + private String rulesetVersion; + private WafBuilder wafBuilder; private static class ActionInfo { final String type; @@ -103,15 +99,10 @@ private ActionInfo(String type, Map parameters) { private static class CtxAndAddresses { final Collection> addressesOfInterest; final WafHandle ctx; - final Map actionInfoMap; - private CtxAndAddresses( - Collection> addressesOfInterest, - WafHandle ctx, - Map actionInfoMap) { + private CtxAndAddresses(Collection> addressesOfInterest, WafHandle ctx) { this.addressesOfInterest = addressesOfInterest; this.ctx = ctx; - this.actionInfoMap = actionInfoMap; } } @@ -150,13 +141,8 @@ static void createLimitsObject() { private final boolean wafMetricsEnabled = Config.get().isAppSecWafMetrics(); // could be static if not for tests private final AtomicReference ctxAndAddresses = new AtomicReference<>(); - private final WAFInitializationResultReporter initReporter = - new WAFInitializationResultReporter(); - private final WAFStatsReporter statsReporter = new WAFStatsReporter(); private final RateLimiter rateLimiter; - private String currentRulesVersion; - public WAFModule() { this(null); } @@ -168,127 +154,65 @@ public WAFModule(Monitoring monitoring) { @Override public void config(AppSecModuleConfigurer appSecConfigService) throws AppSecModuleActivationException { - - Optional initialConfig = - appSecConfigService.addSubConfigListener("waf", this::applyConfig); + appSecConfigService.addSubConfigListener("waf", (key, reconf) -> applyConfig(reconf)); ProductActivation appSecEnabledConfig = Config.get().getAppSecActivation(); if (appSecEnabledConfig == ProductActivation.FULLY_ENABLED) { - if (!initialConfig.isPresent()) { - throw new AppSecModuleActivationException("No initial config for WAF"); - } - try { - applyConfig(initialConfig.get(), AppSecModuleConfigurer.Reconfiguration.NOOP); + applyConfig(AppSecModuleConfigurer.Reconfiguration.NOOP); } catch (ClassCastException e) { - throw new AppSecModuleActivationException("Config expected to be CurrentAppSecConfig", e); + throw new AppSecModuleActivationException("Config expected to be a map", e); + } catch (AbstractWafException e) { + throw new AppSecModuleActivationException("Could not apply config", e); } } + } - appSecConfigService.addTraceSegmentPostProcessor(initReporter); - if (wafMetricsEnabled) { - appSecConfigService.addTraceSegmentPostProcessor(statsReporter); - } + @Override + public void setWafBuilder(WafBuilder wafBuilder) { + this.wafBuilder = wafBuilder; } // this function is called from one thread in the beginning that's different // from the RC thread that calls it later on - private void applyConfig(Object config_, AppSecModuleConfigurer.Reconfiguration reconf) - throws AppSecModuleActivationException { - log.debug("Configuring WAF"); - - CurrentAppSecConfig config = (CurrentAppSecConfig) config_; - - CtxAndAddresses curCtxAndAddresses = this.ctxAndAddresses.get(); - - if (!WafInitialization.ONLINE) { - throw new AppSecModuleActivationException( - "In-app WAF initialization failed. See previous log entries"); - } - - if (curCtxAndAddresses == null) { - config.dirtyStatus.markAllDirty(); - } - + private void applyConfig(AppSecModuleConfigurer.Reconfiguration reconf) + throws AppSecModuleActivationException, AbstractWafException { boolean success = false; + if (init) { + log.debug("Initializing WAF"); + } else { + log.debug("Updating WAF"); + } try { - // ddwaf_init/update - success = initializeNewWafCtx(reconf, config, curCtxAndAddresses); + initOrUpdateWafHandle(reconf); + success = true; } catch (Exception e) { - throw new AppSecModuleActivationException("Could not initialize/update waf", e); + throw new AppSecModuleActivationException("Could not initialize waf handle", e); } finally { - if (curCtxAndAddresses == null) { - WafMetricCollector.get().wafInit(Waf.LIB_VERSION, currentRulesVersion, success); + if (init) { + WafMetricCollector.get().wafInit(Waf.LIB_VERSION, rulesetVersion, success); + init = false; } else { - WafMetricCollector.get().wafUpdates(currentRulesVersion, success); + WafMetricCollector.get().wafUpdates(rulesetVersion, success); } } } - private boolean initializeNewWafCtx( - AppSecModuleConfigurer.Reconfiguration reconf, - CurrentAppSecConfig config, - CtxAndAddresses prevContextAndAddresses) - throws AppSecModuleActivationException, IOException { - CtxAndAddresses newContextAndAddresses; - RuleSetInfo initReport = null; - - AppSecConfig ruleConfig = config.getMergedUpdateConfig(); - WafHandle newWafCtx = null; + private void initOrUpdateWafHandle(AppSecModuleConfigurer.Reconfiguration reconf) + throws AppSecModuleActivationException { + CtxAndAddresses prevContextAndAddresses = this.ctxAndAddresses.get(); + WafHandle newHandle; try { - String uniqueId = RandomUtils.randomUUID().toString(); - - if (prevContextAndAddresses == null) { - WafConfig pwConfig = createWafConfig(); - newWafCtx = Waf.createHandle(uniqueId, pwConfig, ruleConfig.getRawConfig()); - } else { - newWafCtx = prevContextAndAddresses.ctx.update(uniqueId, ruleConfig.getRawConfig()); - } - - initReport = newWafCtx.getRuleSetInfo(); - Collection> addresses = getUsedAddresses(newWafCtx); - - // Update current rules' version if you need - if (initReport != null && initReport.rulesetVersion != null) { - currentRulesVersion = initReport.rulesetVersion; - } - - if (initReport != null) { - log.info( - "Created {} WAF context with rules ({} OK, {} BAD), version {}", - prevContextAndAddresses == null ? "new" : "updated", - initReport.getNumRulesOK(), - initReport.getNumRulesError(), - initReport.rulesetVersion); - } else { - log.warn( - "Created {} WAF context without rules", - prevContextAndAddresses == null ? "new" : "updated"); - } - - Map actionInfoMap = - calculateEffectiveActions(prevContextAndAddresses, ruleConfig); - - newContextAndAddresses = new CtxAndAddresses(addresses, newWafCtx, actionInfoMap); - if (initReport != null) { - this.statsReporter.rulesVersion = initReport.rulesetVersion; - } - } catch (InvalidRuleSetException irse) { - initReport = irse.ruleSetInfo; - throw new AppSecModuleActivationException("Error creating WAF rules", irse); - } catch (RuntimeException | AbstractWafException e) { - if (newWafCtx != null) { - newWafCtx.close(); - } - throw new AppSecModuleActivationException("Error creating WAF rules", e); - } finally { - if (initReport != null) { - this.initReporter.setReportForPublication(initReport); - } + newHandle = wafBuilder.buildWafHandleInstance(); + } catch (AbstractWafException e) { + throw new AppSecModuleActivationException( + "Could not initialize waf handle, no rules were added!", e); } + Collection> addresses = getUsedAddresses(newHandle); + CtxAndAddresses newContextAndAddresses = new CtxAndAddresses(addresses, newHandle); if (!this.ctxAndAddresses.compareAndSet(prevContextAndAddresses, newContextAndAddresses)) { - newWafCtx.close(); + newHandle.close(); throw new AppSecModuleActivationException("Concurrent update of WAF configuration"); } @@ -297,52 +221,6 @@ private boolean initializeNewWafCtx( } reconf.reloadSubscriptions(); - return true; - } - - private Map calculateEffectiveActions( - CtxAndAddresses prevContextAndAddresses, AppSecConfig ruleConfig) { - Map actionInfoMap; - List> actions = - (List>) ruleConfig.getRawConfig().get("actions"); - if (actions == null) { - if (prevContextAndAddresses == null) { - // brand-new context; no actions provided: use default ones - actionInfoMap = DEFAULT_ACTIONS; - } else { - // in update, no changed actions; keep the old one - actionInfoMap = prevContextAndAddresses.actionInfoMap; - } - } else { - // actions were updated - actionInfoMap = new HashMap<>(DEFAULT_ACTIONS); - actionInfoMap.putAll( - ((List>) - ruleConfig.getRawConfig().getOrDefault("actions", Collections.emptyList())) - .stream() - .collect( - toMap( - m -> (String) m.get("id"), - m -> - new ActionInfo( - (String) m.get("type"), - (Map) m.get("parameters"))))); - } - return actionInfoMap; - } - - private WafConfig createWafConfig() { - WafConfig pwConfig = new WafConfig(); - Config config = Config.get(); - String keyRegexp = config.getAppSecObfuscationParameterKeyRegexp(); - if (keyRegexp != null) { - pwConfig.obfuscatorKeyRegex = keyRegexp; - } - String valueRegexp = config.getAppSecObfuscationParameterValueRegexp(); - if (valueRegexp != null) { - pwConfig.obfuscatorValueRegex = valueRegexp; - } - return pwConfig; } private static RateLimiter getRateLimiter(Monitoring monitoring) { @@ -360,6 +238,11 @@ private static RateLimiter getRateLimiter(Monitoring monitoring) { return rateLimiter; } + @Override + public void setRuleVersion(String rulesetVersion) { + this.rulesetVersion = rulesetVersion; + } + @Override public String getName() { return "ddwaf"; @@ -383,8 +266,13 @@ public Collection getDataSubscriptions() { return singletonList(new WAFDataCallback()); } - private static Collection> getUsedAddresses(WafHandle ctx) { - String[] usedAddresses = ctx.getUsedAddresses(); + @Override + public boolean isWafBuilderSet() { + return wafBuilder != null; + } + + private static Collection> getUsedAddresses(WafHandle wafHandle) { + String[] usedAddresses = wafHandle.getKnownAddresses(); Set> addressList = new HashSet<>(usedAddresses.length); for (String addrKey : usedAddresses) { Address address = KnownAddresses.forName(addrKey); @@ -412,7 +300,6 @@ public void onDataAvailable( log.debug("Skipped; the WAF is not configured"); return; } - if (reqCtx.isWafContextClosed()) { log.debug("Skipped; the WAF context is closed"); if (gwCtx.isRasp) { @@ -627,7 +514,6 @@ private Waf.ResultWithData doRunWaf( CtxAndAddresses ctxAndAddr, GatewayContext gwCtx) throws AbstractWafException { - WafContext wafContext = reqCtx.getOrCreateWafContext(ctxAndAddr.ctx, wafMetricsEnabled, gwCtx.isRasp); WafMetrics metrics; @@ -664,10 +550,10 @@ private static void incrementErrorCodeMetric( } private Waf.ResultWithData runWafTransient( - WafContext wafContext, WafMetrics metrics, DataBundle bundle, CtxAndAddresses ctxAndAddr) + WafContext wafContext, WafMetrics metrics, DataBundle newData, CtxAndAddresses ctxAndAddr) throws AbstractWafException { return wafContext.runEphemeral( - new DataBundleMapWrapper(ctxAndAddr.addressesOfInterest, bundle), LIMITS, metrics); + new DataBundleMapWrapper(ctxAndAddr.addressesOfInterest, newData), LIMITS, metrics); } private Collection buildEvents(Waf.ResultWithData actionWithData) { diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFStatsReporter.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFStatsReporter.java index d58d65a47c2..408a6b36a08 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFStatsReporter.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFStatsReporter.java @@ -19,7 +19,7 @@ public class WAFStatsReporter implements TraceSegmentPostProcessor { // XXX: if config is updated, this may not match the actual version run during this request // However, as of this point, we don't update rules at runtime. - volatile String rulesVersion; + private volatile String rulesVersion; @Override public void processTraceSegment( @@ -42,8 +42,7 @@ public void processTraceSegment( } } - String rulesVersion = this.rulesVersion; - if (rulesVersion != null) { + if (this.rulesVersion != null) { segment.setTagTop(RULE_FILE_VERSION, rulesVersion); } @@ -55,4 +54,8 @@ public void processTraceSegment( segment.setTagTop(RASP_TIMEOUT_TAG, ctx.getRaspTimeouts()); } } + + public void setRulesVersion(String rulesetVersion) { + this.rulesVersion = rulesetVersion; + } } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java index a73de84d7d0..741f03c17ac 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java @@ -239,14 +239,14 @@ public int getRaspTimeouts() { return raspTimeouts; } - public WafContext getOrCreateWafContext(WafHandle ctx, boolean createMetrics, boolean isRasp) { - + public WafContext getOrCreateWafContext( + WafHandle wafHandle, boolean createMetrics, boolean isRasp) { if (createMetrics) { if (wafMetrics == null) { - this.wafMetrics = ctx.createMetrics(); + this.wafMetrics = new WafMetrics(); } if (isRasp && raspMetrics == null) { - this.raspMetrics = ctx.createMetrics(); + this.raspMetrics = new WafMetrics(); } } @@ -256,7 +256,7 @@ public WafContext getOrCreateWafContext(WafHandle ctx, boolean createMetrics, bo if (curWafContext != null) { return curWafContext; } - curWafContext = ctx.openContext(); + curWafContext = new WafContext(wafHandle); this.wafContext = curWafContext; } return curWafContext; diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecModuleSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecModuleSpecification.groovy index fb06d1d33ad..79425af2774 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecModuleSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecModuleSpecification.groovy @@ -1,5 +1,6 @@ package com.datadog.appsec +import com.datadog.appsec.ddwaf.WAFModule import com.datadog.appsec.event.ChangeableFlow import com.datadog.appsec.event.OrderedCallback import com.datadog.appsec.event.data.Address @@ -10,19 +11,77 @@ import datadog.trace.test.util.DDSpecification import static com.datadog.appsec.event.OrderedCallback.Priority.DEFAULT import static com.datadog.appsec.event.OrderedCallback.Priority.HIGH +import static com.datadog.appsec.event.OrderedCallback.Priority.LOW class AppSecModuleSpecification extends DDSpecification { void 'data subscriptions are correctly ordered'() { def ds1 = new NoopDataSubscription('ds1', [], DEFAULT) def ds2 = new NoopDataSubscription('ds2', [], DEFAULT) def ds3 = new NoopDataSubscription('ds3', [], HIGH) + def ds4 = new NoopDataSubscription('ds4', [], LOW) when: - def list = [ds3, ds1, ds2] + def list = [ds3, ds1, ds2, ds4] list.sort(OrderedCallback.CallbackPriorityComparator.INSTANCE) then: - list == [ds3, ds1, ds2] + list == [ds3, ds1, ds2, ds4] + + when: 'sorting a list with same priorities' + def ds1a = new NoopDataSubscription('ds1a', [], DEFAULT) + def ds1b = new NoopDataSubscription('ds1b', [], DEFAULT) + def list2 = [ds1a, ds1b] + list2.sort(OrderedCallback.CallbackPriorityComparator.INSTANCE) + + then: 'insertion order should be maintained for items with same priority' + list2 == [ds1a, ds1b] + + when: 'sorting a different list with mixed priorities' + def mixedList = [ds1, ds3, ds2, ds4] + mixedList.sort(OrderedCallback.CallbackPriorityComparator.INSTANCE) + + then: 'items should be reordered by priority' + mixedList == [ds3, ds1, ds2, ds4] + } + + void 'subscribedAddresses are correctly tracked in DataSubscription'() { + given: 'mock addresses' + def address1 = new Address("one") + def address2 = new Address("two") + + when: 'creating a subscription with addresses' + def ds = new NoopDataSubscription('test', [address1, address2], DEFAULT) + + then: 'the addresses should be properly stored' + ds.subscribedAddresses.size() == 2 + ds.subscribedAddresses.contains(address1) + ds.subscribedAddresses.contains(address2) + + when: 'creating a subscription with empty addresses' + def emptyDs = new NoopDataSubscription('empty', [], DEFAULT) + + then: 'the subscription should have empty addresses' + assert emptyDs.subscribedAddresses.empty + } + + void 'WAFModule should correctly manage its data subscriptions'() { + given: 'a concrete WAFModule implementation' + def module = new WAFModule() + + when: 'getting data subscriptions before initialization' + def subscriptions = module.getDataSubscriptions() + + then: 'the module should return an empty list' + assert subscriptions.isEmpty() + } + + void 'null subscription addresses should be properly handled'() { + when: 'creating a subscription with null addresses' + final subscription = new NoopDataSubscription('nullTest', null, DEFAULT) + subscription + + then: 'an exception should be thrown or empty addresses set based on implementation' + assert subscription.subscribedAddresses == null } private static class NoopDataSubscription extends AppSecModule.DataSubscription { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy index e8cf47d988b..110458c1be2 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy @@ -1,6 +1,5 @@ package com.datadog.appsec -import com.datadog.appsec.config.AppSecConfig import com.datadog.appsec.event.EventProducerService import com.datadog.appsec.gateway.AppSecRequestContext import com.datadog.appsec.report.AppSecEvent @@ -8,17 +7,18 @@ import com.datadog.appsec.util.AbortStartupException import datadog.communication.ddagent.DDAgentFeaturesDiscovery import datadog.communication.ddagent.SharedCommunicationObjects import datadog.communication.monitor.Monitoring -import datadog.remoteconfig.ConfigurationChangesTypedListener import datadog.remoteconfig.ConfigurationEndListener import datadog.remoteconfig.ConfigurationPoller import datadog.remoteconfig.Product +import datadog.remoteconfig.state.ConfigKey +import datadog.remoteconfig.state.ProductListener import datadog.trace.api.Config -import datadog.trace.api.internal.TraceSegment import datadog.trace.api.gateway.Flow import datadog.trace.api.gateway.IGSpanInfo import datadog.trace.api.gateway.RequestContext import datadog.trace.api.gateway.RequestContextSlot import datadog.trace.api.gateway.SubscriptionService +import datadog.trace.api.internal.TraceSegment import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.test.util.DDSpecification import okhttp3.OkHttpClient @@ -53,7 +53,26 @@ class AppSecSystemSpecification extends DDSpecification { AppSecSystem.start(subService, sharedCommunicationObjects()) then: - thrown AbortStartupException + def exception = thrown(AbortStartupException) + exception.cause.toString().contains('/file/that/does/not/exist') + } + + void 'system should throw AbortStartupException when config file is not valid JSON'() { + given: 'a temporary file with invalid JSON content' + Path path = Files.createTempFile('dd-trace-', '.json') + path.toFile() << '{' // Invalid JSON - missing closing brace + injectSysConfig('dd.appsec.rules', path as String) + rebuildConfig() + + when: 'starting the AppSec system' + AppSecSystem.start(subService, sharedCommunicationObjects()) + + then: 'an AbortStartupException should be thrown' + def exception = thrown(AbortStartupException) + exception.cause instanceof IOException + + cleanup: 'delete the temporary file' + Files.deleteIfExists(path) } void 'honors appsec.ipheader'() { @@ -102,7 +121,7 @@ class AppSecSystemSpecification extends DDSpecification { } void 'updating configuration replaces the EventProducer'() { - ConfigurationChangesTypedListener savedAsmListener + ProductListener savedAsmListener ConfigurationEndListener savedConfEndListener when: @@ -110,40 +129,45 @@ class AppSecSystemSpecification extends DDSpecification { EventProducerService initialEPS = AppSecSystem.REPLACEABLE_EVENT_PRODUCER.cur then: - 1 * poller.addListener(Product.ASM_DD, _, _) >> { - savedAsmListener = it[2] + 1 * poller.addListener(Product.ASM_DD, _) >> { + savedAsmListener = it[1] } 1 * poller.addConfigurationEndListener(_) >> { savedConfEndListener = it[0] } when: - savedAsmListener.accept('ignored config key', - AppSecConfig.valueOf([version: '2.1', rules: [ - [ - id: 'foo', - name: 'foo', - conditions: [ - [ - operator: 'match_regex', - parameters: [ - inputs: [ - [ - address: 'my.addr', - key_path: ['kp'], - ] - ], - regex: 'foo', - ] - ] - ], - tags: [ - type: 't', - 'category': 'c', - ], - action: 'record', - ] - ]]), null) + def config = ''' + { + "version": "2.1", + "rules": [ + { + "id": "foo", + "name": "foo", + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "my.addr", + "key_path": ["kp"] + } + ], + "regex": "foo" + } + } + ], + "tags": { + "type": "t", + "category": "c" + }, + "action": "record" + } + ] + } + ''' + savedAsmListener.accept('ignored config key' as ConfigKey, config.getBytes(), null) savedConfEndListener.onConfigurationEnd() then: @@ -151,8 +175,7 @@ class AppSecSystemSpecification extends DDSpecification { } private SharedCommunicationObjects sharedCommunicationObjects() { - def sco = new SharedCommunicationObjects( - ) { + def sco = new SharedCommunicationObjects() { @Override ConfigurationPoller configurationPoller(Config config) { poller diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigDeserializerSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigDeserializerSpecification.groovy deleted file mode 100644 index 52775d4acb2..00000000000 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigDeserializerSpecification.groovy +++ /dev/null @@ -1,61 +0,0 @@ -package com.datadog.appsec.config - -import spock.lang.Specification - -import java.nio.charset.StandardCharsets - -class AppSecConfigDeserializerSpecification extends Specification { - - void "deserialize rule with unknown key"() { - given: - final deser = AppSecConfigDeserializer.INSTANCE - final input = """ - { - "version": "2.9999", - "metadata": { - "rules_version": "1.7.1" - }, - "exclusions": [ - { - "UNKNOWN_FIELD": "UNKNOWN_VALUE" - } - ], - "rules": [ - { - "UNKNOWN_FIELD": "UNKNOWN_VALUE", - "id": "blk-001-001", - "name": "Block IP Addresses", - "tags": { - "type": "block_ip", - "category": "security_response" - }, - "conditions": [ - { - "parameters": { - "inputs": [ - { - "address": "http.client_ip" - } - ], - "data": "blocked_ips" - }, - "operator": "ip_match" - } - ], - "transformers": [], - "on_match": [ - "block" - ] - } - ] - } - """ - - when: - def result = deser.deserialize(input.getBytes(StandardCharsets.UTF_8)) - - then: - result != null - result.getNumberOfRules() == 1 - } -} 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 56fd63f89f8..218f1c1f5a0 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 @@ -7,12 +7,17 @@ import datadog.remoteconfig.ConfigurationDeserializer import datadog.remoteconfig.ConfigurationEndListener import datadog.remoteconfig.ConfigurationPoller import datadog.remoteconfig.Product +import datadog.remoteconfig.state.ConfigKey +import datadog.remoteconfig.state.ParsedConfigKey +import datadog.remoteconfig.state.ProductListener +import datadog.trace.api.Config import datadog.trace.api.ProductActivation import datadog.trace.api.UserIdCollectionMode import datadog.trace.test.util.DDSpecification import java.nio.file.Files import java.nio.file.Path +import java.nio.file.Paths import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_ACTIVATION import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE @@ -24,32 +29,37 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSION_DATA import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_HEADER_FINGERPRINT import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT -import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_LFI import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_CMDI +import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_LFI import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SHI import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING +import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SESSION_FINGERPRINT import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRUSTED_IPS import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_USER_BLOCKING import static datadog.remoteconfig.Capabilities.CAPABILITY_ENDPOINT_FINGERPRINT -import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SESSION_FINGERPRINT import static datadog.remoteconfig.PollingHinterNoop.NOOP import static datadog.trace.api.UserIdCollectionMode.ANONYMIZATION import static datadog.trace.api.UserIdCollectionMode.DISABLED import static datadog.trace.api.UserIdCollectionMode.IDENTIFICATION class AppSecConfigServiceImplSpecification extends DDSpecification { - ConfigurationPoller poller = Mock() - def config = Mock(Class.forName('datadog.trace.api.Config')) + Config config = Mock(Class.forName('datadog.trace.api.Config')) as Config AppSecModuleConfigurer.Reconfiguration reconf = Stub() - AppSecConfigServiceImpl appSecConfigService = new AppSecConfigServiceImpl(config, poller, reconf) + AppSecConfigServiceImpl appSecConfigService + SavedListeners listeners void cleanup() { appSecConfigService?.close() } + void setup() { + appSecConfigService = new AppSecConfigServiceImpl(config, poller, reconf) + listeners = new SavedListeners() + } + void 'maybeStartConfigPolling subscribes to the configuration poller'() { setup: appSecConfigService.init() @@ -59,10 +69,12 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { then: 1 * config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE - 1 * poller.addListener(Product.ASM_DD, _, _) + 1 * poller.addListener(Product.ASM_DD, _) >> { + listeners.savedWafDataChangesListener = it[1] + } 1 * poller.addListener(Product.ASM_FEATURES, _, _) - 1 * poller.addListener(Product.ASM, _, _) - 1 * poller.addListener(Product.ASM_DATA, _, _) + 1 * poller.addListener(Product.ASM, _) + 1 * poller.addListener(Product.ASM_DATA, _) 1 * poller.addConfigurationEndListener(_) 1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) } @@ -76,10 +88,10 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { then: 1 * config.getAppSecActivation() >> ProductActivation.FULLY_ENABLED - 1 * poller.addListener(Product.ASM_DD, _, _) + 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.addListener(Product.ASM, _) + 1 * poller.addListener(Product.ASM_DATA, _) 1 * poller.addConfigurationEndListener(_) 0 * poller.addListener(*_) 0 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) @@ -94,10 +106,10 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { then: 1 * config.getAppSecActivation() >> ProductActivation.FULLY_DISABLED - 1 * poller.addListener(Product.ASM_DD, _, _) + 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.addListener(Product.ASM, _) + 1 * poller.addListener(Product.ASM_DATA, _) 1 * poller.addConfigurationEndListener(_) 0 * poller.addListener(*_) 0 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) @@ -105,8 +117,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'no subscription to ASM ASM_DD ASM_DATA if custom rules are provided'() { setup: - Path p = Files.createTempFile('appsec', '.json') - p.toFile() << '{"version":"2.0", "rules": []}' + Path p = Paths.get(getClass().classLoader.getResource('test_multi_config_no_action.json').getPath()) when: appSecConfigService.init() @@ -125,18 +136,16 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'can load from a different location'() { setup: - Path p = Files.createTempFile('appsec', '.json') - p.toFile() << '{"version":"2.0", "rules": []}' - AppSecModuleConfigurer.SubconfigListener listener = Stub() + Path p = Paths.get(getClass().classLoader.getResource('test_multi_config_no_action.json').getPath()) + String capturedPath = null when: appSecConfigService.init() then: - 1 * config.getAppSecRulesFile() >> (p as String) - def expected = AppSecConfig.valueOf([version: '2.0', rules: []]) - CurrentAppSecConfig actual = appSecConfigService.createAppSecModuleConfigurer().addSubConfigListener('waf', listener).get() - actual.ddConfig == expected + 1 * config.getAppSecRulesFile() >> { capturedPath = p.toString(); return p.toString() } + capturedPath == p.toString() + noExceptionThrown() } void 'aborts if alt config location does not exist'() { @@ -170,17 +179,11 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { expect: AppSecConfigService.TransactionalAppSecModuleConfigurer configurer = appSecConfigService.createAppSecModuleConfigurer() - configurer.addSubConfigListener("waf", listener).get() instanceof CurrentAppSecConfig - configurer.addSubConfigListener("waf2", listener) == Optional.empty() + configurer.addSubConfigListener("waf", listener) } static class SavedListeners { - ConfigurationDeserializer savedConfDeserializer - ConfigurationChangesTypedListener savedConfChangesListener - ConfigurationDeserializer>> savedWafDataDeserializer - ConfigurationChangesTypedListener>> savedWafDataChangesListener - ConfigurationDeserializer> savedWafRulesOverrideDeserializer - ConfigurationChangesTypedListener> savedWafRulesOverrideListener + ProductListener savedWafDataChangesListener ConfigurationDeserializer savedFeaturesDeserializer ConfigurationChangesTypedListener savedFeaturesListener ConfigurationEndListener savedConfEndListener @@ -188,7 +191,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'activation without custom config provides valid configuration'() { AppSecModuleConfigurer.SubconfigListener subconfigListener = Mock() - SavedListeners listeners = new SavedListeners() when: AppSecSystem.active = false @@ -206,7 +208,9 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { listeners.savedFeaturesDeserializer = it[1] listeners.savedFeaturesListener = it[2] } - 1 * poller.addConfigurationEndListener(_) >> { listeners.savedConfEndListener = it[0] } + 1 * poller.addConfigurationEndListener(_) >> { + listeners.savedConfEndListener = it[0] + } _ * poller._ 0 * _._ @@ -218,44 +222,36 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { listeners.savedConfEndListener.onConfigurationEnd() then: - 1 * subconfigListener.onNewSubconfig({ CurrentAppSecConfig casc -> casc.ddConfig != null }, _) - AppSecSystem.active == true + 1 * subconfigListener.onNewSubconfig(_, _) + AppSecSystem.active } void 'provides updated configuration to waf subscription'() { AppSecModuleConfigurer.SubconfigListener subconfigListener = Mock() - SavedListeners listeners = new SavedListeners() - Optional initialWafConfig - - when: AppSecSystem.active = false appSecConfigService.init() + + when: appSecConfigService.maybeSubscribeConfigPolling() def configurer = appSecConfigService.createAppSecModuleConfigurer() - initialWafConfig = configurer.addSubConfigListener("waf", subconfigListener) + configurer.addSubConfigListener("waf", subconfigListener) configurer.commit() then: 1 * config.isAppSecRaspEnabled() >> true - 1 * config.getAppSecRulesFile() >> null 2 * config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE - 1 * poller.addListener(Product.ASM_DD, _, _) >> { - listeners.savedConfDeserializer = it[1] - listeners.savedConfChangesListener = it[2] - } - 1 * poller.addListener(Product.ASM_DATA, _, _) >> { - listeners.savedWafDataDeserializer = it[1] - listeners.savedWafDataChangesListener = it[2] - } - 1 * poller.addListener(Product.ASM, _, _) >> { - listeners.savedWafRulesOverrideDeserializer = it[1] - listeners.savedWafRulesOverrideListener = it[2] + 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.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 @@ -276,17 +272,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ASM_NETWORK_FINGERPRINT | CAPABILITY_ASM_HEADER_FINGERPRINT) 0 * _._ - initialWafConfig.get() != null - - when: - // AppSec is INACTIVE - rules should not trigger subscriptions - listeners.savedConfChangesListener.accept( - 'ignored config key', - listeners.savedConfDeserializer.deserialize( - '{"version": "1.0"}'.bytes), null) - - then: - 0 * _._ when: listeners.savedFeaturesListener.accept( @@ -296,34 +281,52 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { listeners.savedConfEndListener.onConfigurationEnd() then: - 1 * subconfigListener.onNewSubconfig(_ as CurrentAppSecConfig, _) - AppSecSystem.active == true + 1 * subconfigListener.onNewSubconfig(_ as String, _) + AppSecSystem.active when: // AppSec is ACTIVE - rules trigger subscriptions - listeners.savedConfChangesListener.accept( - 'ignored config key', - listeners.savedConfDeserializer.deserialize( - '{"version": "2.0"}'.bytes), null) listeners.savedWafDataChangesListener.accept( - 'ignored config key', - listeners.savedWafDataDeserializer.deserialize('{"rules_data":[{"id":"foo","type":"","data":[]}]}'.bytes), null) - listeners.savedWafRulesOverrideListener.accept( - 'ignored config key', - listeners.savedWafRulesOverrideDeserializer.deserialize('{"rules_override": [{"rules_target":[{"rule_id": "foo"}], "enabled":false}]}'.bytes), null) + '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.savedWafDataChangesListener.accept( + 'ignored config key' as ConfigKey, + '''{"rules_override": [{"rules_target": [{"rule_id": "foo"}], "enabled": false}]}'''.getBytes(), null) listeners.savedConfEndListener.onConfigurationEnd() then: - 1 * subconfigListener.onNewSubconfig({ CurrentAppSecConfig casc -> - casc.ddConfig == AppSecConfig.valueOf([version: '2.0']) - casc.mergedUpdateConfig.rawConfig['rules_override'] == [ - [ - rules_target: [[rule_id: 'foo']], - enabled : false - ] - ] - casc.mergedAsmData.mergedData.rules == [[data: [], id: 'foo', type: '']] - }, _) + 1 * subconfigListener.onNewSubconfig(_, _) when: listeners.savedFeaturesListener.accept('asm_features_activation', @@ -332,7 +335,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { listeners.savedConfEndListener.onConfigurationEnd() then: - AppSecSystem.active == false + !AppSecSystem.active when: 'switch back to enabled' listeners.savedFeaturesListener.accept('asm_features_activation', @@ -341,7 +344,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { listeners.savedConfEndListener.onConfigurationEnd() then: 'it is enabled again' - AppSecSystem.active == true + AppSecSystem.active when: 'asm are not set' listeners.savedFeaturesListener.accept('asm_features_activation', @@ -350,7 +353,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { listeners.savedConfEndListener.onConfigurationEnd() then: 'it is disabled ( == false)' - AppSecSystem.active == false + !AppSecSystem.active when: 'switch back to enabled' listeners.savedFeaturesListener.accept('asm_features_activation', @@ -359,7 +362,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { listeners.savedConfEndListener.onConfigurationEnd() then: 'it is enabled again' - AppSecSystem.active == true + AppSecSystem.active when: 'asm features are not set' listeners.savedFeaturesListener.accept('asm_features_activation', @@ -368,7 +371,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { listeners.savedConfEndListener.onConfigurationEnd() then: 'it is disabled ( == false)' - AppSecSystem.active == false + !AppSecSystem.active cleanup: AppSecSystem.active = true @@ -376,9 +379,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'configuration pull out'() { AppSecModuleConfigurer.SubconfigListener subconfigListener = Mock() - SavedListeners listeners = new SavedListeners() - MergedAsmData mergedAsmData - AppSecConfig mergedUpdateConfig when: appSecConfigService.init() @@ -391,23 +391,18 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { 1 * config.isAppSecRaspEnabled() >> true 1 * config.getAppSecRulesFile() >> null 2 * config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE - 1 * poller.addListener(Product.ASM_DD, _, _) >> { - listeners.savedConfDeserializer = it[1] - listeners.savedConfChangesListener = it[2] - } - 1 * poller.addListener(Product.ASM_DATA, _, _) >> { - listeners.savedWafDataDeserializer = it[1] - listeners.savedWafDataChangesListener = it[2] - } - 1 * poller.addListener(Product.ASM, _, _) >> { - listeners.savedWafRulesOverrideDeserializer = it[1] - listeners.savedWafRulesOverrideListener = it[2] + 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.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 @@ -430,47 +425,69 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { 0 * _._ when: - listeners.savedConfChangesListener.accept( - 'asm_dd config', - listeners.savedConfDeserializer.deserialize( - '{"version": "2.0"}'.bytes), null) listeners.savedWafDataChangesListener.accept( - 'asm_data config', - listeners.savedWafDataDeserializer.deserialize('{"rules_data":[{"id":"foo","type":"","data":[]}]}'.bytes), null) - listeners.savedWafRulesOverrideListener.accept( - 'asm conf', - listeners.savedWafRulesOverrideDeserializer.deserialize('{"rules_override": [{"rules_target":[{"rule_id": "foo"}], "enabled":false}]}'.bytes), null) + new ParsedConfigKey('asm_dd config', 'null', 1, 'null', 'null'), + '''{ + "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.savedWafDataChangesListener.accept( + new ParsedConfigKey('asm conf', 'null', 1, 'null', 'null'), + '''{ + "rules_override": [ + { + "rules_target": [ + { "rule_id": "foo" } + ], + "enabled": false + } + ] + }'''.getBytes(), null) listeners.savedFeaturesListener.accept('asm_features conf', listeners.savedFeaturesDeserializer.deserialize('{"asm":{"enabled": true}}'.bytes), NOOP) listeners.savedConfEndListener.onConfigurationEnd() then: - 1 * subconfigListener.onNewSubconfig(_, _) >> { - CurrentAppSecConfig casc = it[0] - mergedAsmData = casc.mergedAsmData - mergedUpdateConfig = casc.mergedUpdateConfig - } - mergedUpdateConfig.numberOfRules == 0 - mergedUpdateConfig.rawConfig['rules_override'].isEmpty() == false - mergedAsmData.mergedData.rules.isEmpty() == false + 1 * subconfigListener.onNewSubconfig(_, _) when: - listeners.savedConfChangesListener.accept('asm_dd config', null, null) - listeners.savedWafDataChangesListener.accept('asm_data config', null, null) - listeners.savedWafRulesOverrideListener.accept('asm conf', null, null) + listeners.savedWafDataChangesListener.accept( + new ParsedConfigKey('asm_dd config', 'null', 1, 'null', 'null'), null, null) + listeners.savedWafDataChangesListener.accept( + new ParsedConfigKey('asm conf', 'null', 1, 'null', 'null'), null, null) listeners.savedConfEndListener.onConfigurationEnd() then: - 1 * subconfigListener.onNewSubconfig(_, _) >> { - CurrentAppSecConfig casc = it[0] - mergedAsmData = casc.mergedAsmData - mergedUpdateConfig = casc.mergedUpdateConfig - } - - mergedUpdateConfig.numberOfRules > 0 - mergedUpdateConfig.rawConfig['rules_override'].isEmpty() == true - mergedAsmData.mergedData.rules.isEmpty() == true + noExceptionThrown() } void 'stopping appsec unsubscribes from the poller'() { @@ -507,24 +524,6 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { 1 * poller.stop() } - void 'config should not be created'() { - def conf - - when: - conf = AppSecConfig.valueOf(null) - - then: - conf == null - } - - void 'unsupported config version'() { - when: - AppSecConfig.valueOf([version: '99.0']) - - then: - thrown IOException - } - void 'update auto user instrum mode via remote-config'() { given: def listeners = new SavedListeners() @@ -539,7 +538,9 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { listeners.savedFeaturesListener = it[2] } 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) - 1 * poller.addConfigurationEndListener(_) >> { listeners.savedConfEndListener = it[0] } + 1 * poller.addConfigurationEndListener(_) >> { + listeners.savedConfEndListener = it[0] + } when: listeners.savedFeaturesListener.accept('asm_auto_user_instrum', mode, null) @@ -560,38 +561,92 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'RASP capabilities for LFI is not sent when RASP is not fully enabled '() { AppSecModuleConfigurer.SubconfigListener subconfigListener = Mock() - SavedListeners listeners = new SavedListeners() - Optional initialWafConfig when: AppSecSystem.active = false appSecConfigService.init() appSecConfigService.maybeSubscribeConfigPolling() def configurer = appSecConfigService.createAppSecModuleConfigurer() - initialWafConfig = configurer.addSubConfigListener("waf", subconfigListener) + configurer.addSubConfigListener("waf", subconfigListener) configurer.commit() then: 1 * config.isAppSecRaspEnabled() >> true 1 * config.getAppSecRulesFile() >> null 2 * config.getAppSecActivation() >> ProductActivation.FULLY_ENABLED - 1 * poller.addListener(Product.ASM_DD, _, _) >> { - listeners.savedConfDeserializer = it[1] - listeners.savedConfChangesListener = it[2] - } - 1 * poller.addListener(Product.ASM_DATA, _, _) >> { - listeners.savedWafDataDeserializer = it[1] - listeners.savedWafDataChangesListener = it[2] - } - 1 * poller.addListener(Product.ASM, _, _) >> { - listeners.savedWafRulesOverrideDeserializer = it[1] - listeners.savedWafRulesOverrideListener = it[2] + 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 + } + + def 'test AppSecConfigChangesListener listener'() { + ProductListener listener = new AppSecConfigServiceImpl.AppSecConfigChangesListener() + when: + listener.remove('my_config' as ConfigKey, null) // unexisting config + + then: + thrown RuntimeException + + when: + def waf = [waf: null] as Map // wrong input + listener.accept('my_config' as ConfigKey, waf, null) + + then: + 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.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 @@ -606,13 +661,52 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | 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 * _._ - initialWafConfig.get() != null + + 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 diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecDataDeserializerSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecDataDeserializerSpecification.groovy deleted file mode 100644 index 54a4e641c7a..00000000000 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecDataDeserializerSpecification.groovy +++ /dev/null @@ -1,98 +0,0 @@ -package com.datadog.appsec.config - -import spock.lang.Specification - -import java.nio.charset.StandardCharsets - -class AppSecDataDeserializerSpecification extends Specification { - - void "deserialize IP denylist"() { - given: - final deser = AppSecDataDeserializer.INSTANCE - final input = """ -{ - "rules_data": [ - { - "data": [ - { - "expiration": 0, - "value": "1.1.1.1" - }, - { - "expiration": 1717083300, - "value": "2.2.2.2" - }, - { - "expiration": 9223372036854775807, - "value": "3.3.3.3" - } - ], - "id": "blocked_ips", - "type": "ip_with_expiration" - } - ] -} - """ - - when: - def result = deser.deserialize(input.getBytes(StandardCharsets.UTF_8)) - - then: - result != null - result.rules == [ - [ - data: [ - [ - expiration: 0.0, - value : "1.1.1.1" - ], - [ - expiration: 1717083300.0, - value : "2.2.2.2" - ], - [ - expiration: 9223372036854775807.0, - value : "3.3.3.3" - ] - ], - id : "blocked_ips", - type: "ip_with_expiration" - ] - ] - } - - void 'deserialize exclusions data'() { - final deser = AppSecDataDeserializer.INSTANCE - final input = """ -{ - "exclusion_data": [ - { - "id": "suspicious_ips_data_id", - "type": "ip_with_expiration", - "data": [ - { - "value": "34.65.27.85" - } - ] - } - ] -} - """ - - when: - def result = deser.deserialize(input.getBytes(StandardCharsets.UTF_8)) - - then: - result != null - result.exclusion == [ - [ - id : "suspicious_ips_data_id", - type: "ip_with_expiration", - data: [[ - value: "34.65.27.85" - ]], - - ] - ] - } -} diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecUserConfigDeserializerSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecUserConfigDeserializerSpecification.groovy deleted file mode 100644 index 82c31204620..00000000000 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecUserConfigDeserializerSpecification.groovy +++ /dev/null @@ -1,35 +0,0 @@ -package com.datadog.appsec.config - -import spock.lang.Specification - -class AppSecUserConfigDeserializerSpecification extends Specification { - - void 'all the components'() { - when: - def res = AppSecUserConfigDeserializer.INSTANCE.deserialize(''' - { - "rules_override": [{"a": 0}], - "custom_rules": [{"b": 1}], - "exclusions": [{"c": 2}], - "actions": [{"d": 3}] - }'''.bytes).build('cfg key') - - then: - res.ruleOverrides == [[a: 0]] - res.customRules == [[b: 1]] - res.exclusions == [[c: 2]] - res.actions == [[d: 3]] - } - - void 'none of the components'() { - when: - def res = AppSecUserConfigDeserializer.INSTANCE.deserialize('{}'.bytes).build('cfg key') - - then: - res.ruleOverrides == [] - res.customRules == [] - res.exclusions == [] - res.actions == [] - res.configKey == 'cfg key' - } -} diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/MergedAsmDataSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/MergedAsmDataSpecification.groovy deleted file mode 100644 index ff285aa2e2f..00000000000 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/MergedAsmDataSpecification.groovy +++ /dev/null @@ -1,135 +0,0 @@ -package com.datadog.appsec.config - -import spock.lang.Specification - -import java.util.function.Function - -class MergedAsmDataSpecification extends Specification { - void 'merge of data without expiration: #test'() { - setup: - def cfg1 = test.set.apply([ - [ - id: 'id1', - type: 'type1', - data: [[value: 'foobar1']] - ], - [ - id: 'id1', - type: 'type1', - data: [[value: 'foobar2']] - ] - ]) - def cfg2 = test.set.apply([ - [ - id: 'id2', - type: 'type1', - data: [[value: 'foobar4']] - ], - [ - id: 'id1', - type: 'type1', - data: [[value: 'foobar3']] - ], - ]) - def md = new MergedAsmData([cfg1: cfg1, cfg2: cfg2]).mergedData - test.get.apply(md).sort({ a, b -> a['id'] <=> b['id'] }) - - expect: - test.get.apply(md) == [ - [ - id: 'id1', - type: 'type1', - data: [[value: 'foobar1'], [value: 'foobar2'], [value: 'foobar3'],] - ], - [ - id: 'id2', - type: 'type1', - data: [[value: 'foobar4']] - ] - ] - - where: - test << testSuite() - } - - void 'merge of data with expiration: #test'() { - setup: - def cfg1 = test.set.apply([ - [ - id: 'id1', - type: 'data_with_expiration', - data: [ - [value: 'foobar1', expiration: 20], - [value: 'foobar1', expiration: 5], - ] - ], - [ - id: 'id1', - type: 'data_with_expiration', - data: [[value: 'foobar1', expiration: 50], [value: 'foobar2'],] - ] - ]) - def cfg2 = test.set.apply([ - [ - id: 'id1', - type: 'data_with_expiration', - data: [[value: 'foobar2', expiration: 100]] - ], - ]) - def md = new MergedAsmData([cfg1: cfg1, cfg2: cfg2]).mergedData - test.get.apply(md).sort({ a, b -> a['id'] <=> b['id'] }) - - expect: - test.get.apply(md) == [ - [ - id: 'id1', - type: 'data_with_expiration', - data: [[value: 'foobar2'], [value: 'foobar1', expiration: 50]] - ] - ] - - where: - test << testSuite() - } - - void 'error due to mismatched type: #test'() { - when: - new MergedAsmData([ - cfg: test.set.apply([ - [ - id: 'foo', - // type: null, - data: [] - ], - [ - 'id': 'foo', - type: 'bar', - data: [] - ] - ]) - ]).mergedData - then: - thrown MergedAsmData.InvalidAsmDataException - - where: - test << testSuite() - } - - private static List testSuite() { - return [ - new Test(descr: 'rules_data', set: { payload -> new AppSecData(rules: payload) }, get: { AppSecData data -> data.rules }), - new Test(descr: 'exclusion_data', set: { payload -> new AppSecData(exclusion: payload) }, get: { AppSecData data -> data.exclusion }) - ] - } - - private static class Test { - String descr - Function>, AppSecData> set - Function>> get - - @Override - String toString() { - return descr - } - } -} 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 773c624704c..7cddcd62523 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,14 +1,8 @@ package com.datadog.appsec.ddwaf -import com.datadog.ddwaf.WafErrorCode as LibWafErrorCode -import datadog.trace.api.telemetry.WafMetricCollector.WafErrorCode as InternalWafErrorCode - -import com.datadog.appsec.AppSecModule -import com.datadog.appsec.config.AppSecConfig -import com.datadog.appsec.config.AppSecData +import com.datadog.appsec.config.AppSecConfigService +import com.datadog.appsec.config.AppSecConfigServiceImpl import com.datadog.appsec.config.AppSecModuleConfigurer -import com.datadog.appsec.config.AppSecUserConfig -import com.datadog.appsec.config.CurrentAppSecConfig import com.datadog.appsec.config.TraceSegmentPostProcessor import com.datadog.appsec.event.ChangeableFlow import com.datadog.appsec.event.DataListener @@ -20,27 +14,38 @@ import com.datadog.appsec.event.data.MapDataBundle import com.datadog.appsec.gateway.AppSecRequestContext import com.datadog.appsec.gateway.GatewayContext import com.datadog.appsec.report.AppSecEvent +import com.datadog.ddwaf.Waf +import com.datadog.ddwaf.WafContext +import com.datadog.ddwaf.WafErrorCode +import com.datadog.ddwaf.WafHandle +import com.datadog.ddwaf.WafMetrics import com.datadog.ddwaf.exception.AbstractWafException import com.datadog.ddwaf.exception.InternalWafException import com.datadog.ddwaf.exception.InvalidArgumentWafException import com.datadog.ddwaf.exception.InvalidObjectWafException import com.datadog.ddwaf.exception.UnclassifiedWafException -import datadog.trace.api.telemetry.RuleType -import datadog.trace.util.stacktrace.StackTraceEvent -import com.datadog.appsec.test.StubAppSecConfigService +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import datadog.appsec.api.blocking.BlockingContentType import datadog.communication.monitor.Monitoring +import datadog.remoteconfig.ConfigurationPoller +import datadog.remoteconfig.Product +import datadog.remoteconfig.state.ConfigKey +import datadog.remoteconfig.state.ParsedConfigKey +import datadog.remoteconfig.state.ProductListener +import datadog.trace.api.Config import datadog.trace.api.ConfigDefaults -import datadog.trace.api.internal.TraceSegment -import datadog.appsec.api.blocking.BlockingContentType import datadog.trace.api.gateway.Flow +import datadog.trace.api.internal.TraceSegment +import datadog.trace.api.telemetry.RuleType import datadog.trace.api.telemetry.WafMetricCollector +import datadog.trace.api.telemetry.WafMetricCollector.WafErrorCode as InternalWafErrorCode import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.AgentTracer import datadog.trace.test.util.DDSpecification -import com.datadog.ddwaf.WafContext -import com.datadog.ddwaf.Waf -import com.datadog.ddwaf.WafHandle -import com.datadog.ddwaf.WafMetrics +import datadog.trace.util.stacktrace.StackTraceEvent +import okio.Okio import spock.lang.Shared import spock.lang.Unroll @@ -57,6 +62,10 @@ class WAFModuleSpecification extends DDSpecification { @Shared protected static final ORIGINAL_METRIC_COLLECTOR = WafMetricCollector.get() + private static final JsonAdapter> ADAPTER = + new Moshi.Builder() + .build() + .adapter(Types.newParameterizedType(Map, String, Object)) private static final DataBundle ATTACK_BUNDLE = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, new CaseInsensitiveMap>(['user-agent': 'Arachni/v0'])) @@ -72,148 +81,119 @@ class WAFModuleSpecification extends DDSpecification { AppSecRequestContext ctx = Spy() GatewayContext gwCtx = new GatewayContext(false) - StubAppSecConfigService service + AppSecConfigServiceImpl service WAFModule wafModule = new WAFModule() DataListener dataListener WafContext wafContext - WafMetrics metrics + WafMetrics metrics = new WafMetrics() WafMetricCollector wafMetricCollector = Mock(WafMetricCollector) + AppSecConfigService.TransactionalAppSecModuleConfigurer cfg + ProductListener listener + + AppSecModuleConfigurer.Reconfiguration reconf = Mock() void setup() { WafMetricCollector.INSTANCE = wafMetricCollector AgentTracer.forceRegister(tracer) + + final configurationPoller = Stub(ConfigurationPoller) { + addListener(Product.ASM_DD, _ as ProductListener) >> { + Product _, ProductListener l -> + listener = l + } + } + service = new AppSecConfigServiceImpl(Config.get(), configurationPoller, () -> {}) + service.init() + service.maybeSubscribeConfigPolling() + assert listener != null + + cfg = service.createAppSecModuleConfigurer() + cfg.commit() } void cleanup() { WafMetricCollector.INSTANCE = ORIGINAL_METRIC_COLLECTOR AgentTracer.forceRegister(ORIGINAL_TRACER) + service.close() wafContext?.close() - release wafModule } - private static void release(WAFModule wafModule) { - wafModule?.ctxAndAddresses?.get()?.ctx?.close() + private void send(String configKey, Object map){ + accept(configKey, map as Map) } - private void setupWithStubConfigService(String location = "test_multi_config.json") { - service = new StubAppSecConfigService(location) - service.init() - wafModule.config(service) + private void initialRuleAdd(String location = "test_multi_config.json") { + def stream = getClass().classLoader.getResourceAsStream(location) + accept('initial_waf', ADAPTER.fromJson(Okio.buffer(Okio.source(stream)))) + wafModule.setWafBuilder(service.getWafBuilder()) + wafModule.config(cfg) dataListener = wafModule.dataSubscriptions.first() } - void 'use default actions if none defined in config'() { - when: - setupWithStubConfigService'no_actions_config.json' - - then: - wafModule.ctxAndAddresses.get().actionInfoMap.size() == 1 - wafModule.ctxAndAddresses.get().actionInfoMap.get('block') != null - wafModule.ctxAndAddresses.get().actionInfoMap.get('block').parameters == [ - status_code: 403, - type:'auto', - grpc_status_code: 10 - ] - } - - void 'override default actions by config'() { - when: - setupWithStubConfigService('override_actions_config.json') - - then: - wafModule.ctxAndAddresses.get().actionInfoMap.size() == 1 - wafModule.ctxAndAddresses.get().actionInfoMap.get('block') != null - wafModule.ctxAndAddresses.get().actionInfoMap.get('block').parameters == [ - status_code: 500, - type:'html', - ] + void initialRuleAddWithMap(Map definition) { + accept('initial_waf', definition) + wafModule.setWafBuilder(service.getWafBuilder()) + wafModule.config(cfg) + dataListener = wafModule.dataSubscriptions.first() } - void 'override actions through reconfiguration'() { - when: - setupWithStubConfigService('override_actions_config.json') - - def actions = [ - [ - id: 'block', - type: 'block_request', - parameters: [ - status_code: 501, - type: 'json' - ] - ] - ] - AppSecModuleConfigurer.Reconfiguration reconf = Stub() - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('b', [], actions, [], [])) - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() + private void accept(String configKey, Map map) { + ConfigKey config = new ParsedConfigKey(configKey, 'null', 1, 'null', 'null') + if(map == null) { + listener.remove(config, null) + return } - - then: - wafModule.ctxAndAddresses.get().actionInfoMap.size() == 1 - wafModule.ctxAndAddresses.get().actionInfoMap.get('block') != null - wafModule.ctxAndAddresses.get().actionInfoMap.get('block').parameters == [ - status_code: 501, - type: 'json', - ] + def json = ADAPTER.toJson(map) + listener.accept(config, json.getBytes(), null) } void 'override on_match through reconfiguration'() { ChangeableFlow flow = Mock() - AppSecModuleConfigurer.Reconfiguration reconf = Mock() when: - setupWithStubConfigService('override_actions_config.json') - dataListener = wafModule.dataSubscriptions.first() + initialRuleAdd('override_actions_config.json') - def actions = [ + Map actions = + [actions: [ - id: 'block2', - type: 'block_request', - parameters: [ - status_code: 501, - type: 'json' + [ + id: 'block2', + type: 'block_request', + parameters: [ + status_code: 501, + type: 'json' + ] ] ] - ] - def ruleOverrides = [ - [ - rules_target: [[ - rule_id: 'ip_match_rule', - ],], - on_match: ['block2'] - ] - ] - def ipData = new AppSecData(rules: [ + ,rules_override: [ - id : 'ip_data', - type: 'ip_with_expiration', - data: [[ - value : '1.2.3.4', - expiration: '0', - ]] - ] - ]) - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('b', ruleOverrides, actions, [], [])) - mergedAsmData.addConfig('c', ipData) - it.dirtyStatus.data = true - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } - def newBundle = MapDataBundle.of( - KnownAddresses.REQUEST_INFERRED_CLIENT_IP, - '1.2.3.4' - ) + [ + rules_target: [[ + rule_id: 'ip_match_rule', + ],], + on_match: ['block2'] + ] + ]] + + Map ipData = [ + rules_data :[ + [ + id : 'ip_data', + type: 'data_with_expiration', + data: [[ + value : '1.2.3.4', + expiration: '0', + ]] + ] + ]] + + send('b', actions) + send('c', ipData) + + wafModule.applyConfig(reconf) + def newBundle = MapDataBundle.of(KnownAddresses.REQUEST_INFERRED_CLIENT_IP, '1.2.3.4') dataListener.onDataAvailable(flow, ctx, newBundle, gwCtx) ctx.closeWafContext() @@ -223,11 +203,10 @@ class WAFModuleSpecification extends DDSpecification { 1 * reconf.reloadSubscriptions() 1 * flow.setAction({ Flow.Action.RequestBlockingAction rba -> rba.statusCode == 501 && - rba.blockingContentType == BlockingContentType.JSON + rba.blockingContentType == BlockingContentType.JSON }) - 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) >> { - wafContext = it[0].openContext() - } + + 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 2 * ctx.getWafMetrics() @@ -243,56 +222,49 @@ class WAFModuleSpecification extends DDSpecification { AppSecModuleConfigurer.Reconfiguration reconf = Mock() when: - setupWithStubConfigService('rules_with_data_config.json') - dataListener = wafModule.dataSubscriptions.first() - ctx.closeWafContext() - + initialRuleAdd('rules_with_data_config.json') def bundle = MapDataBundle.of( - KnownAddresses.USER_ID, - 'user-to-block-1' - ) + KnownAddresses.USER_ID, + 'user-to-block-1' + ) + wafModule.applyConfig(service.reconfiguration) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() then: 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) + 1 * wafMetricCollector.wafUpdates(_, true) 1 * flow.setAction({ Flow.Action.RequestBlockingAction rba -> rba.statusCode == 403 && - rba.blockingContentType == BlockingContentType.AUTO + rba.blockingContentType == BlockingContentType.AUTO }) 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) >> { - wafContext = it[0].openContext() + wafContext = new WafContext(it[0]) } 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false - 2 * ctx.closeWafContext() + 1 * ctx.closeWafContext() 1 * flow.isBlocking() 1 * ctx.isThrottled(null) 0 * _ when: 'merges new waf data with the one in the rules config' - def newData = new AppSecData(rules: [ - [ - id : 'blocked_users', - type: 'data_with_expiration', - data: [ - [ - value : 'user-to-block-2', - expiration: '0', + def newData = [rules_data: [ + [ + id : 'blocked_users', + type: 'data_with_expiration', + data: [ + [ + value : 'user-to-block-2', + expiration: '0', + ] ] ] - ] - ]) - service.currentAppSecConfig.with { - mergedAsmData.addConfig('c', newData) - it.dirtyStatus.data = true - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } - + ]] + send('c', newData) + wafModule.applyConfig(reconf) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() @@ -301,11 +273,9 @@ class WAFModuleSpecification extends DDSpecification { 1 * reconf.reloadSubscriptions() 1 * flow.setAction({ Flow.Action.RequestBlockingAction rba -> rba.statusCode == 403 && - rba.blockingContentType == BlockingContentType.AUTO + rba.blockingContentType == BlockingContentType.AUTO }) - 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 2 * ctx.getWafMetrics() @@ -317,20 +287,18 @@ class WAFModuleSpecification extends DDSpecification { when: bundle = MapDataBundle.of( - KnownAddresses.USER_ID, - 'user-to-block-2' - ) + KnownAddresses.USER_ID, + 'user-to-block-2' + ) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() then: 1 * flow.setAction({ Flow.Action.RequestBlockingAction rba -> rba.statusCode == 403 && - rba.blockingContentType == BlockingContentType.AUTO + rba.blockingContentType == BlockingContentType.AUTO }) - 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 2 * ctx.getWafMetrics() @@ -362,20 +330,15 @@ class WAFModuleSpecification extends DDSpecification { ], on_match: ['block'] ] ], ] - - service.currentAppSecConfig.with { - setDdConfig(AppSecConfig.valueOf(newCfg)) - dirtyStatus.markAllDirty() - - service.listeners['waf'].onNewSubconfig(it, reconf) - dirtyStatus.clearDirty() - } + send('initial_waf', null) + send('waf', newCfg) + wafModule.applyConfig(reconf) and: bundle = MapDataBundle.of( - KnownAddresses.USER_ID, - 'user-to-block-2' - ) + KnownAddresses.USER_ID, + 'user-to-block-2' + ) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() @@ -384,11 +347,9 @@ class WAFModuleSpecification extends DDSpecification { 1 * reconf.reloadSubscriptions() 1 * flow.setAction({ Flow.Action.RequestBlockingAction rba -> rba.statusCode == 403 && - rba.blockingContentType == BlockingContentType.AUTO + rba.blockingContentType == BlockingContentType.AUTO }) - 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 2 * ctx.getWafMetrics() @@ -400,16 +361,14 @@ class WAFModuleSpecification extends DDSpecification { when: bundle = MapDataBundle.of( - KnownAddresses.USER_ID, - 'user-to-block-1' - ) + KnownAddresses.USER_ID, + 'user-to-block-1' + ) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() then: - 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() @@ -418,43 +377,37 @@ class WAFModuleSpecification extends DDSpecification { void 'add exclusions through reconfiguration'() { ChangeableFlow flow = new ChangeableFlow() - AppSecModuleConfigurer.Reconfiguration reconf = Mock() when: - setupWithStubConfigService() - - def exclusions = [ + initialRuleAdd() + def exclusions = [ exclusions: [ - id : '1', - rules_target: [ - [ - tags: [ - type : 'security_scanner', + [ + id : '1', + rules_target: [ + [ + tags: [ + type : 'security_scanner', + ] ] - ] - ], - conditions : [ - [ - operator : 'exact_match', - parameters: [ - inputs: [[ - address: 'http.client_ip' - ]], - list : ['192.168.0.1'] + ], + conditions : [ + [ + operator : 'exact_match', + parameters: [ + inputs: [[ + address: 'http.client_ip' + ]], + list : ['192.168.0.1'] + ] ] ] ] ] ] - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('b', [], [], exclusions, [])) - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } + send('b', exclusions) + wafModule.applyConfig(reconf) then: 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) @@ -467,32 +420,28 @@ class WAFModuleSpecification extends DDSpecification { ctx.closeWafContext() then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false - 1 * ctx.closeWafContext() >> { wafContext.close() } + 1 * ctx.closeWafContext() 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 0 * _ when: def newBundle = MapDataBundle.of( - KnownAddresses.HEADERS_NO_COOKIES, - new CaseInsensitiveMap>(['user-agent': 'Arachni/v0']), - KnownAddresses.REQUEST_INFERRED_CLIENT_IP, - '192.168.0.1' - ) + KnownAddresses.HEADERS_NO_COOKIES, + new CaseInsensitiveMap>(['user-agent': 'Arachni/v0']), + KnownAddresses.REQUEST_INFERRED_CLIENT_IP, + '192.168.0.1' + ) dataListener.onDataAvailable(flow, ctx, newBundle, gwCtx) ctx.closeWafContext() then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() @@ -501,45 +450,40 @@ class WAFModuleSpecification extends DDSpecification { void 'add custom rule through reconfiguration'() { ChangeableFlow flow = new ChangeableFlow() - AppSecModuleConfigurer.Reconfiguration reconf = Mock() when: - setupWithStubConfigService() + initialRuleAdd() - def customRules = [ + def customRules = [ custom_rules: [ - id: 'ua0-600-12x-copy', - name: 'Arachni', - tags: [ - category: 'attack_attempt', - type: 'security_scanner2' - ], - conditions: [ - [ - operator: 'match_regex', - parameters: [ - inputs: [ - [ - address: 'server.request.headers.no_cookies', - key_path:['user-agent'] - ] - ], - regex: '^Arachni/v' + [ + id: 'ua0-600-12x-copy', + name: 'Arachni', + tags: [ + category: 'attack_attempt', + type: 'security_scanner2' + ], + conditions: [ + [ + operator: 'match_regex', + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path:['user-agent'] + ] + ], + regex: '^Arachni/v' + ] ] - ] - ], - on_match: ['block'] + ], + on_match: ['block'] + ] ] ] - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('b', [], [], [], customRules)) - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } + send('b', customRules) + wafModule.applyConfig(reconf) then: 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) @@ -552,9 +496,7 @@ class WAFModuleSpecification extends DDSpecification { ctx.closeWafContext() then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(hasSize(1)) 2 * ctx.getWafMetrics() @@ -565,63 +507,49 @@ class WAFModuleSpecification extends DDSpecification { 0 * _ } - void 'append actions in addition to default'() { - when: - WAFModule powerWAFModule = new WAFModule() - StubAppSecConfigService confService = new StubAppSecConfigService("another_actions_config.json") - confService.init() - powerWAFModule.config(confService) - - then: - powerWAFModule.ctxAndAddresses.get().actionInfoMap.size() == 2 - powerWAFModule.ctxAndAddresses.get().actionInfoMap.get('block') != null - powerWAFModule.ctxAndAddresses.get().actionInfoMap.get('block').parameters == [ - status_code: 403, - type:'auto', - grpc_status_code: 10 - ] - powerWAFModule.ctxAndAddresses.get().actionInfoMap.get('test') != null - powerWAFModule.ctxAndAddresses.get().actionInfoMap.get('test').parameters == [ - status_code: 302, - type:'xxx' - ] - - cleanup: - release powerWAFModule - } - void 'replace actions through runtime configuration'() { ChangeableFlow flow = Mock() - AppSecModuleConfigurer.Reconfiguration reconf = Mock() when: - setupWithStubConfigService() - // first initialization to exercise the update path - service.listeners['waf'].onNewSubconfig(service.currentAppSecConfig, reconf) - service.currentAppSecConfig.dirtyStatus.clearDirty() + initialRuleAdd('test_multi_config_no_action.json') - def actions = [ + // original action + def action1 = [ + actions: [ - id: 'block', - type: 'block_request', - parameters: [ - status_code: 401, + [ + id : 'block', + type : 'block_request', + parameters: [ + status_code: 418, + type : 'html' + ] ] ] ] - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('new config', [], actions, [], [])) - it.dirtyStatus.mergeFrom(dirtyStatus) - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } + def action2 = [ + actions: + [ + [ + id : 'block', + type : 'block_request', + parameters: [ + status_code: 401 + ] + ] + ] + ] + + send('original config', action1) + send('original config', null) + send('new config', action2) + wafModule.applyConfig(reconf) then: 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) - 2 * wafMetricCollector.wafUpdates(_, true) - 2 * reconf.reloadSubscriptions() + 1 * wafMetricCollector.wafUpdates(_, true) + 1 * reconf.reloadSubscriptions() 0 * _ when: @@ -632,9 +560,9 @@ class WAFModuleSpecification extends DDSpecification { // original rule is replaced; no attack 1 * flow.setAction({ Flow.Action.RequestBlockingAction rba -> rba.statusCode == 401 && - rba.blockingContentType == BlockingContentType.AUTO + rba.blockingContentType == BlockingContentType.AUTO }) - 1 * ctx.getOrCreateWafContext(_, true, false) >> { it[0].openContext() } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 2 * ctx.getWafMetrics() @@ -647,20 +575,16 @@ class WAFModuleSpecification extends DDSpecification { void 'redirect actions are correctly processed expected variant redirect#variant'(int variant, int statusCode) { when: - setupWithStubConfigService('redirect_actions.json') + initialRuleAdd('redirect_actions.json') + wafModule.applyConfig(reconf) DataBundle bundle = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, - new CaseInsensitiveMap>(['user-agent': 'redirect' + variant])) + new CaseInsensitiveMap>(['user-agent': 'redirect' + variant])) def flow = new ChangeableFlow() dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - WafHandle wafHandle = it[0] as WafHandle - wafContext = wafHandle.openContext() - metrics = wafHandle.createMetrics() - wafContext - } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * ctx.getWafMetrics() >> metrics 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() @@ -668,7 +592,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 0 * ctx._(*_) - flow.blocking == true + flow.blocking flow.action instanceof Flow.Action.RequestBlockingAction with(flow.action as Flow.Action.RequestBlockingAction) { assert it.statusCode == statusCode @@ -682,7 +606,7 @@ class WAFModuleSpecification extends DDSpecification { 3 | 303 } - void 'is named powerwaf'() { + void 'is named ddwaf'() { expect: wafModule.name == 'ddwaf' } @@ -691,15 +615,16 @@ class WAFModuleSpecification extends DDSpecification { setup: TraceSegment segment = Mock() TraceSegmentPostProcessor pp + initialRuleAdd() + wafModule.applyConfig(reconf) when: - setupWithStubConfigService() pp = service.traceSegmentPostProcessors.first() pp.processTraceSegment(segment, ctx, []) then: 1 * segment.setTagTop('_dd.appsec.waf.version', _ as String) - 1 * segment.setTagTop('_dd.appsec.event_rules.loaded', 116) + 1 * segment.setTagTop('_dd.appsec.event_rules.loaded', 117) 1 * segment.setTagTop('_dd.appsec.event_rules.error_count', 1) 1 * segment.setTagTop('_dd.appsec.event_rules.errors', { it =~ /\{"[^"]+":\["bad rule"\]\}/}) 1 * segment.setTagTop('asm.keep', true) @@ -713,20 +638,16 @@ class WAFModuleSpecification extends DDSpecification { } void 'triggers a rule through the user agent header'() { - setupWithStubConfigService() + initialRuleAdd() ChangeableFlow flow = new ChangeableFlow() + wafModule.applyConfig(reconf) when: dataListener.onDataAvailable(flow, ctx, ATTACK_BUNDLE, gwCtx) ctx.closeWafContext() then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - WafHandle wafHandle = it[0] as WafHandle - wafContext = wafHandle.openContext() - metrics = wafHandle.createMetrics() - wafContext - } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * ctx.getWafMetrics() >> metrics 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() @@ -734,26 +655,26 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 0 * ctx._(*_) - flow.blocking == true + flow.blocking flow.action.statusCode == 418 flow.action.blockingContentType == BlockingContentType.HTML } void 'no metrics are set if waf metrics are off'() { setup: + metrics = null injectSysConfig('appsec.waf.metrics', 'false') wafModule = new WAFModule() // replace the one created too soon - setupWithStubConfigService() + initialRuleAdd() ChangeableFlow flow = new ChangeableFlow() + wafModule.applyConfig(reconf) when: dataListener.onDataAvailable(flow, ctx, ATTACK_BUNDLE, gwCtx) ctx.closeWafContext() then: - 1 * ctx.getOrCreateWafContext(_, false, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_, false, false) 2 * ctx.getWafMetrics() >> null 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() @@ -769,21 +690,16 @@ class WAFModuleSpecification extends DDSpecification { TraceSegment segment = Mock() TraceSegmentPostProcessor pp Flow flow = new ChangeableFlow() + initialRuleAdd() when: - setupWithStubConfigService() pp = service.traceSegmentPostProcessors[1] dataListener.onDataAvailable(flow, ctx, ATTACK_BUNDLE, gwCtx) ctx.closeWafContext() pp.processTraceSegment(segment, ctx, []) then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - WafHandle wafHandle = it[0] as WafHandle - wafContext = wafHandle.openContext() - metrics = wafHandle.createMetrics() - wafContext - } + 1 * ctx.getOrCreateWafContext(_, true, false) 1 * ctx.closeWafContext() 3 * ctx.getWafMetrics() >> { metrics.with { @@ -802,47 +718,41 @@ class WAFModuleSpecification extends DDSpecification { } void 'can trigger a nonwafContext waf run'() { - setupWithStubConfigService() + initialRuleAdd() ChangeableFlow flow = new ChangeableFlow() + wafModule.applyConfig(reconf) when: dataListener.onDataAvailable(flow, ctx, ATTACK_BUNDLE, gwCtx) then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - WafHandle wafHandle = it[0] as WafHandle - wafContext = wafHandle.openContext() - metrics = wafHandle.createMetrics() - wafContext - } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * ctx.getWafMetrics() >> metrics 1 * ctx.reportEvents(*_) 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 1 * ctx.isWafContextClosed() >> false 0 * ctx._(*_) - flow.blocking == true + flow.blocking } void 'reports events'() { setup: - setupWithStubConfigService() + initialRuleAdd() + wafModule.applyConfig(reconf) AppSecEvent event StackTraceEvent stackTrace - wafModule = new WAFModule() // replace the one created too soon def attackBundle = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, - new CaseInsensitiveMap>(['user-agent': 'Arachni/generate-stacktrace'])) + new CaseInsensitiveMap>(['user-agent': 'Arachni/generate-stacktrace'])) when: dataListener.onDataAvailable(Stub(ChangeableFlow), ctx, attackBundle, gwCtx) ctx.closeWafContext() then: - ctx.getOrCreateWafContext(_, true) >> { - wafContext = it[0].openContext() - } - ctx.reportEvents(_ as Collection) >> { event = it[0].iterator().next() } - ctx.reportStackTrace(_ as StackTraceEvent) >> { stackTrace = it[0] } + 1 * ctx.getOrCreateWafContext(_, true, _) + 1 * ctx.reportEvents(_ as Collection) >> { event = it[0].iterator().next() } + 1 * ctx.reportStackTrace(_ as StackTraceEvent) >> { stackTrace = it[0] } event.rule.id == 'generate-stacktrace-on-scanner' event.rule.name == 'Arachni' @@ -866,19 +776,17 @@ class WAFModuleSpecification extends DDSpecification { } void 'redaction with default settings'() { - setupWithStubConfigService() + initialRuleAdd() AppSecEvent event when: def bundle = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, - new CaseInsensitiveMap>(['user-agent': [password: 'Arachni/v0']])) + new CaseInsensitiveMap>(['user-agent': [password: 'Arachni/v0']])) dataListener.onDataAvailable(Stub(ChangeableFlow), ctx, bundle, gwCtx) ctx.closeWafContext() then: - ctx.getOrCreateWafContext(_, true) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_, true, _) ctx.reportEvents(_ as Collection) >> { event = it[0].iterator().next() } event.ruleMatches[0].parameters[0].address == 'server.request.headers.no_cookies' @@ -889,19 +797,17 @@ class WAFModuleSpecification extends DDSpecification { void 'disabling of key regex'() { injectSysConfig(APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP, '') - setupWithStubConfigService() + setup() + initialRuleAdd() AppSecEvent event when: def bundle = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, - new CaseInsensitiveMap>(['user-agent': [password: 'Arachni/v0']])) + new CaseInsensitiveMap>(['user-agent': [password: 'Arachni/v0']])) dataListener.onDataAvailable(Stub(ChangeableFlow), ctx, bundle, gwCtx) ctx.closeWafContext() then: - ctx.getOrCreateWafContext(_, true) >> { - wafContext = it[0].openContext() - } ctx.reportEvents(_ as Collection) >> { event = it[0].iterator().next() } event.ruleMatches[0].parameters[0].address == 'server.request.headers.no_cookies' @@ -912,8 +818,8 @@ class WAFModuleSpecification extends DDSpecification { void 'redaction of values'() { injectSysConfig(APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP, 'Arachni') - - setupWithStubConfigService() + setup() + initialRuleAdd() AppSecEvent event when: @@ -921,9 +827,7 @@ class WAFModuleSpecification extends DDSpecification { ctx.closeWafContext() then: - ctx.getOrCreateWafContext(_, true) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_, true, _) ctx.reportEvents(_ as Collection) >> { event = it[0].iterator().next() } event.ruleMatches[0].parameters[0].address == 'server.request.headers.no_cookies' @@ -933,52 +837,48 @@ class WAFModuleSpecification extends DDSpecification { } void 'triggers no rule'() { - setupWithStubConfigService() + initialRuleAdd() ChangeableFlow flow = new ChangeableFlow() DataBundle db = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, - new CaseInsensitiveMap>(['user-agent': 'Harmless'])) + new CaseInsensitiveMap>(['user-agent': 'Harmless'])) when: dataListener.onDataAvailable(flow, ctx, db, gwCtx) then: - ctx.getOrCreateWafContext(_, true) >> { - wafContext = it[0].openContext() - } - flow.blocking == false + 1 * ctx.getOrCreateWafContext(_, true, _) + !flow.blocking } void 'non-string types work'() { - setupWithStubConfigService() + initialRuleAdd() ChangeableFlow flow = new ChangeableFlow() DataBundle db = MapDataBundle.of(KnownAddresses.REQUEST_BODY_OBJECT, - [ - [key: [ - true, - (byte)1, - (short)2, - (int)3, - (long)4, - (float)5.0, - (double)6.0, - (char)'7', - (BigDecimal)8.0G, - (BigInteger)9.0G - ]] - ]) + [ + [key: [ + true, + (byte)1, + (short)2, + (int)3, + (long)4, + (float)5.0, + (double)6.0, + (char)'7', + (BigDecimal)8.0G, + (BigInteger)9.0G + ]] + ]) when: dataListener.onDataAvailable(flow, ctx, db, gwCtx) then: - ctx.getOrCreateWafContext(_, true) >> { - wafContext = it[0].openContext() - } - flow.blocking == false + 1 * ctx.getOrCreateWafContext(_, true, _) + !flow.blocking } - void 'powerwaf exceptions do not propagate'() { - setupWithStubConfigService() + void 'waf exceptions do not propagate'() { + initialRuleAdd() ChangeableFlow flow = new ChangeableFlow() DataBundle db = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, [get: { null }] as List) @@ -986,9 +886,11 @@ class WAFModuleSpecification extends DDSpecification { dataListener.onDataAvailable(flow, ctx, db, gwCtx) then: - ctx.getOrCreateWafContext(_, true) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_, true, false) + 2 * ctx.getWafMetrics() + 1 * ctx.setWafErrors() + 1 * wafMetricCollector.wafErrorCode(-127) + 2 * ctx.isWafContextClosed() assert !flow.blocking } @@ -996,9 +898,9 @@ class WAFModuleSpecification extends DDSpecification { setup: injectSysConfig('appsec.waf.timeout', '1') WAFModule.createLimitsObject() - setupWithStubConfigService() + initialRuleAdd() DataBundle db = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, - new CaseInsensitiveMap>(['user-agent': 'Arachni/v' + ('a' * 4000)])) + new CaseInsensitiveMap>(['user-agent': 'Arachni/v' + ('a' * 4000)])) ChangeableFlow flow = new ChangeableFlow() TraceSegment segment = Mock() @@ -1008,12 +910,9 @@ class WAFModuleSpecification extends DDSpecification { dataListener.onDataAvailable(flow, ctx, db, gwCtx) then: - ctx.getOrCreateWafContext(_, true) >> { - wafContext = it[0].openContext() } assert !flow.blocking 1 * ctx.isWafContextClosed() - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * ctx.getWafMetrics() 1 * ctx.increaseWafTimeouts() 0 * _ @@ -1034,9 +933,9 @@ class WAFModuleSpecification extends DDSpecification { setup: injectSysConfig('appsec.waf.timeout', '1') WAFModule.createLimitsObject() - setupWithStubConfigService() + initialRuleAdd() DataBundle db = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, - new CaseInsensitiveMap>(['user-agent': 'Arachni/v' + ('a' * 4000)])) + new CaseInsensitiveMap>(['user-agent': 'Arachni/v' + ('a' * 4000)])) ChangeableFlow flow = new ChangeableFlow() TraceSegment segment = Mock() @@ -1048,12 +947,9 @@ class WAFModuleSpecification extends DDSpecification { dataListener.onDataAvailable(flow, ctx, db, gwCtx) then: - ctx.getOrCreateWafContext(_, true) >> { - wafContext = it[0].openContext() } assert !flow.blocking + 1 * ctx.getOrCreateWafContext(_, true, true) 1 * ctx.isWafContextClosed() - 1 * ctx.getOrCreateWafContext(_, true, true) >> { - wafContext = it[0].openContext() } 1 * ctx.getRaspMetrics() 1 * ctx.getRaspMetricsCounter() 1 * ctx.increaseRaspTimeouts() @@ -1075,26 +971,21 @@ class WAFModuleSpecification extends DDSpecification { } void 'configuration can be given later'() { - def cfgService = new StubAppSecConfigService([waf: null]) - AppSecModuleConfigurer.Reconfiguration reconf = Mock() - when: - cfgService.init() - wafModule.config(cfgService) + initialRuleAddWithMap([waf: null]) then: - thrown AppSecModule.AppSecModuleActivationException + thrown RuntimeException 0 * _ when: - cfgService.listeners['waf'].onNewSubconfig(defaultConfig['waf'], reconf) - dataListener = wafModule.dataSubscriptions.first() + // default config + initialRuleAdd() dataListener.onDataAvailable(Stub(ChangeableFlow), ctx, ATTACK_BUNDLE, gwCtx) ctx.closeWafContext() then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() } + 1 * ctx.getOrCreateWafContext(_, true, false) 1 * ctx.reportEvents(_ as Collection) 1 * ctx.isWafContextClosed() 2 * ctx.getWafMetrics() @@ -1102,34 +993,28 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() 2 * tracer.activeSpan() 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) - 1 * reconf.reloadSubscriptions() 0 * _ } void 'rule data given through configuration'() { - setupWithStubConfigService() - AppSecModuleConfigurer.Reconfiguration reconf = Mock() + initialRuleAdd() ChangeableFlow flow = Mock() - def ipData = new AppSecData(rules: [ + def ipData = [ rules_data : [ - id : 'ip_data', - type: 'ip_with_expiration', - data: [[ - value : '1.2.3.4', - expiration: '0', - ]] + [ + id : 'ip_data', + type: 'ip_with_expiration', + data: [[ + value : '1.2.3.4', + expiration: '0', + ]] + ] ] - ]) + ] when: - service.currentAppSecConfig.with { - mergedAsmData.addConfig('my_config', ipData) - it.dirtyStatus.data = true - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } - - dataListener = wafModule.dataSubscriptions.first() + send('my_config', ipData) + wafModule.applyConfig(reconf) def bundle = MapDataBundle.of(KnownAddresses.REQUEST_INFERRED_CLIENT_IP, '1.2.3.4') dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() @@ -1137,7 +1022,7 @@ class WAFModuleSpecification extends DDSpecification { then: 1 * wafMetricCollector.wafUpdates(_, true) 1 * reconf.reloadSubscriptions() - 1 * ctx.getOrCreateWafContext(_, true, false) >> { wafContext = it[0].openContext() } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 2 * ctx.getWafMetrics() @@ -1149,45 +1034,57 @@ class WAFModuleSpecification extends DDSpecification { 0 * _ } - private static List toggleById(String id, boolean enabled) { - [ + private static Object toggleById(String id, boolean enabled) { + [ rules_override: [ - rules_target: [[ - rule_id: id - ]], - enabled: enabled + [ + rules_target: [[ + rule_id: id + ]], + enabled: enabled + ] ] ] } void 'reloading rules clears waf data and rule toggling'() { - setupWithStubConfigService() - AppSecModuleConfigurer.Reconfiguration reconf = Mock() + initialRuleAdd() ChangeableFlow flow = Mock() - def ipData = new AppSecData(rules: [ + def ipData = [ + rules_data : [ - id : 'ip_data', - type: 'ip_with_expiration', - data: [[ - value : '1.2.3.4', - expiration: '0', - ]] + [ + id : 'ip_data', + name: 'IP data', + conditions: [ + [ + parameters: [ + inputs: [[ address: 'http.client_ip' ]], + data: 'blocked_users' + ], + operator: 'exact_match' + ] + ], + tags: [ + type: 'test', + category: 'test', + confidence: '1', + ], + type: 'ip_with_expiration', + on_match: ['block'], + data: [[ + value : '1.2.3.4', + expiration: '0', + ]] + ] ] - ]) + ] when: 'reconfigure with data and toggling' - service.currentAppSecConfig.with { - mergedAsmData.addConfig('my_config', ipData) - it.dirtyStatus.data = true - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('my_config', toggleById('ip_match_rule', false), [], [], [])) - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } + send('my_config', ipData) + send('my_config', toggleById('ip_match_rule', false)) - dataListener = wafModule.dataSubscriptions.first() + wafModule.applyConfig(reconf) def bundle = MapDataBundle.of(KnownAddresses.REQUEST_INFERRED_CLIENT_IP, '1.2.3.4') dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() @@ -1195,63 +1092,48 @@ class WAFModuleSpecification extends DDSpecification { then: 'no match; rule is disabled' 1 * wafMetricCollector.wafUpdates(_, true) 1 * reconf.reloadSubscriptions() - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false - 1 * ctx.closeWafContext() >> { wafContext.close() } + 1 * ctx.closeWafContext() _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ when: 'removing data and override config' - service.currentAppSecConfig.with { - mergedAsmData.removeConfig('my_config') - it.dirtyStatus.data = true - def dirtyStatus = userConfigs.removeConfig('my_config') - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } + service.getWafBuilder().removeConfig("my_config") + wafModule.applyConfig(reconf) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() then: 'no match; data was cleared (though rule is no longer disabled)' - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() } - 2 * ctx.getWafMetrics() + 1 * ctx.getOrCreateWafContext(_, true, false) 1 * ctx.isWafContextClosed() >> false - 1 * ctx.closeWafContext() >> {wafContext.close()} 1 * wafMetricCollector.wafUpdates(_, true) 1 * reconf.reloadSubscriptions() + 1 * ctx.closeWafContext() + 2 * ctx.getWafMetrics() _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ - when: 'data is readded' - service.currentAppSecConfig.with { - mergedAsmData.addConfig('my_config', ipData) - it.dirtyStatus.data = true - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } - + when: 'data is read' + send('my_config', ipData) + wafModule.applyConfig(reconf) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() then: 'now we have match' 1 * wafMetricCollector.wafUpdates(_, true) 1 * reconf.reloadSubscriptions() - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 2 * ctx.getWafMetrics() 1 * flow.setAction({ it.blocking }) 1 * ctx.isWafContextClosed() >> false - 1 * ctx.closeWafContext() >> {wafContext.close()} + 1 * ctx.closeWafContext() 1 * flow.isBlocking() 1 * ctx.isThrottled(null) _ * ctx.increaseWafTimeouts() @@ -1259,22 +1141,15 @@ class WAFModuleSpecification extends DDSpecification { 0 * _ when: 'toggling the rule off' - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('my_config', toggleById('ip_match_rule', false), [], [], [])) - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } - + send('my_config', toggleById('ip_match_rule', false)) + wafModule.applyConfig(reconf) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() then: 'nothing again; we disabled the rule' 1 * wafMetricCollector.wafUpdates(_, true) 1 * reconf.reloadSubscriptions() - 1 * ctx.getOrCreateWafContext(_, true, false) >> { wafContext = it[0].openContext() } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() @@ -1284,46 +1159,35 @@ class WAFModuleSpecification extends DDSpecification { } void 'rule toggling data given through configuration'() { - setupWithStubConfigService() - AppSecModuleConfigurer.Reconfiguration reconf = Mock() ChangeableFlow flow = Mock() + initialRuleAdd() + WafContext wafContext when: 'rule disabled in config b' - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('b', toggleById('ua0-600-12x', false), [], [], [])) - - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } - dataListener = wafModule.dataSubscriptions.first() + send('b', toggleById('ua0-600-12x', false)) + wafModule.applyConfig(reconf) dataListener.onDataAvailable(flow, ctx, ATTACK_BUNDLE, gwCtx) ctx.closeWafContext() then: - 1 * wafMetricCollector.wafUpdates(_, true) + 1 * wafMetricCollector.wafUpdates(null, true) 1 * reconf.reloadSubscriptions() // no attack - 1 * ctx.getOrCreateWafContext(_, true, false) >> { wafContext = it[0].openContext() } + 1 * ctx.getOrCreateWafContext(_, true, false) >> { + WafHandle wafHandle = it[0] as WafHandle + wafContext = new WafContext(wafHandle)} 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false - 1 * ctx.closeWafContext() >> {wafContext.close()} + 1 * ctx.closeWafContext() >> { + wafContext.close() + } _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ when: 'rule enabled in config a has no effect' - // later configurations have precedence (b > a) - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('a', toggleById('ua0-600-12x', true), [], [], [])) - - it.dirtyStatus.mergeFrom(dirtyStatus) - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } + send('a', toggleById('ua0-600-12x', true)) + wafModule.applyConfig(reconf) dataListener.onDataAvailable(flow, ctx, ATTACK_BUNDLE, gwCtx) ctx.closeWafContext() @@ -1332,24 +1196,21 @@ class WAFModuleSpecification extends DDSpecification { 1 * reconf.reloadSubscriptions() // no attack 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() } + WafHandle wafHandle = it[0] as WafHandle + wafContext = new WafContext(wafHandle)} 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false - 1 * ctx.closeWafContext() >> {wafContext.close()} + 1 * ctx.closeWafContext() >> { + wafContext.close() + } _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ when: 'rule enabled in config c overrides b' - // later configurations have precedence (c > a) - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.addConfig( - new AppSecUserConfig('c', toggleById('ua0-600-12x', true), [], [], [])) - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } + send('b', null) + send('c', toggleById('ua0-600-12x', true)) + wafModule.applyConfig(reconf) dataListener.onDataAvailable(flow, ctx, ATTACK_BUNDLE, gwCtx) ctx.closeWafContext() @@ -1358,27 +1219,27 @@ class WAFModuleSpecification extends DDSpecification { 1 * reconf.reloadSubscriptions() // attack found 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() } + WafHandle wafHandle = it[0] as WafHandle + wafContext = new WafContext(wafHandle)} 2 * ctx.getWafMetrics() 1 * flow.isBlocking() 1 * flow.setAction({ it.blocking }) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) 1 * ctx.isWafContextClosed() >> false - 1 * ctx.closeWafContext() >> {wafContext.close()} + 1 * ctx.closeWafContext() >> { + wafContext.close() + } _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 1 * ctx.isThrottled(null) 0 * _ - when: 'removing c restores the state before c was added (rule disabled)' - service.currentAppSecConfig.with { - def dirtyStatus = userConfigs.removeConfig('c') - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } + when: 'removing c and a removes c and a, allows earlier toggle to take effect' + send('b', toggleById('ua0-600-12x', false)) + send('c', null) + send('a', null) + wafModule.applyConfig(reconf) dataListener.onDataAvailable(flow, ctx, ATTACK_BUNDLE, gwCtx) ctx.closeWafContext() @@ -1387,10 +1248,13 @@ class WAFModuleSpecification extends DDSpecification { 1 * reconf.reloadSubscriptions() // no attack 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() } + WafHandle wafHandle = it[0] as WafHandle + wafContext = new WafContext(wafHandle)} 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false - 1 * ctx.closeWafContext() + 1 * ctx.closeWafContext() >> { + wafContext.close() + } _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ @@ -1398,9 +1262,8 @@ class WAFModuleSpecification extends DDSpecification { void 'initial configuration has unknown addresses'() { Address doesNotExistAddress = new Address<>("server.request.headers.does-not-exist") - def cfgService = new StubAppSecConfigService(waf: - new CurrentAppSecConfig( - ddConfig: AppSecConfig.valueOf([ + def waf = + [ version: '2.1', rules: [ [ @@ -1425,11 +1288,11 @@ class WAFModuleSpecification extends DDSpecification { ], ] ] - ]))) + ] + when: - cfgService.init() - wafModule.config(cfgService) + initialRuleAddWithMap(waf as Map) then: 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) @@ -1438,27 +1301,25 @@ class WAFModuleSpecification extends DDSpecification { } void 'bad initial configuration is given results in no subscriptions'() { - def cfgService = new StubAppSecConfigService([waf: [:]]) + def waf = [waf: [:]] when: - cfgService.init() - wafModule.config(cfgService) + initialRuleAddWithMap(waf) then: - thrown AppSecModule.AppSecModuleActivationException + thrown RuntimeException wafModule.dataSubscriptions.empty 0 * _ } void 'rule data not a config'() { - def confService = new StubAppSecConfigService(waf: []) + Map waf = [waf: [:]] when: - confService.init() - wafModule.config(confService) + initialRuleAddWithMap(waf) then: - thrown AppSecModule.AppSecModuleActivationException + thrown RuntimeException wafModule.ctxAndAddresses.get() == null 0 * _ } @@ -1488,19 +1349,19 @@ class WAFModuleSpecification extends DDSpecification { } void 'ephemeral and persistent addresses'() { - setupWithStubConfigService() + initialRuleAdd() + wafModule.applyConfig(reconf) ChangeableFlow flow = Mock() when: def transientBundle = MapDataBundle.of( - KnownAddresses.REQUEST_BODY_OBJECT, - '/cybercop' - ) + KnownAddresses.REQUEST_BODY_OBJECT, + '/cybercop' + ) dataListener.onDataAvailable(flow, ctx, transientBundle, gwCtx) then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext = it[0].openContext() } + 1 * ctx.getOrCreateWafContext(_, true, false) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) >> { it[0].iterator().next().ruleMatches[0].parameters[0].value == '/cybercop' @@ -1516,8 +1377,7 @@ class WAFModuleSpecification extends DDSpecification { ctx.closeWafContext() then: - 1 * ctx.getOrCreateWafContext(_, true, false) >> { - wafContext } + 1 * ctx.getOrCreateWafContext(_, true, false) 1 * flow.setAction({ it.blocking }) 2 * tracer.activeSpan() 1 * ctx.reportEvents(_ as Collection) >> { @@ -1537,7 +1397,7 @@ class WAFModuleSpecification extends DDSpecification { */ @Unroll("test repeated #n time") void 'parallel REQUEST_END should not cause race condition'() { - setupWithStubConfigService() + initialRuleAdd() ChangeableFlow flow = new ChangeableFlow() AppSecRequestContext ctx = new AppSecRequestContext() @@ -1581,52 +1441,42 @@ class WAFModuleSpecification extends DDSpecification { void 'suspicious attacker blocking'() { given: final flow = Mock(ChangeableFlow) - final reconf = Mock(AppSecModuleConfigurer.Reconfiguration) final suspiciousIp = '34.65.27.85' - setupWithStubConfigService('rules_suspicious_attacker_blocking.json') - dataListener = wafModule.dataSubscriptions.first() - ctx.closeWafContext() + initialRuleAdd('rules_suspicious_attacker_blocking.json') + wafModule.applyConfig(reconf) + final bundle = MapDataBundle.of( - KnownAddresses.REQUEST_INFERRED_CLIENT_IP, - suspiciousIp, - KnownAddresses.HEADERS_NO_COOKIES, - new CaseInsensitiveMap>(['user-agent': ['Arachni/v1.5.1']]) - ) + KnownAddresses.REQUEST_INFERRED_CLIENT_IP, + suspiciousIp, + KnownAddresses.HEADERS_NO_COOKIES, + new CaseInsensitiveMap>(['user-agent': ['Arachni/v1.5.1']])) when: dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() then: - 1 * ctx.isWafContextClosed() - 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * ctx.getWafMetrics() 1 * ctx.isThrottled(null) 1 * ctx.reportEvents(_ as Collection) 1 * ctx.closeWafContext() + 1 * ctx.isWafContextClosed() 2 * tracer.activeSpan() 1 * flow.isBlocking() 0 * flow.setAction(_) 0 * _ when: - final ipData = new AppSecData(exclusion: [ - [ - id : 'suspicious_ips_data_id', - type: 'ip_with_expiration', - data: [[value: suspiciousIp]] - ] - ]) - service.currentAppSecConfig.with { - mergedAsmData.addConfig('suspicious_ips', ipData) - it.dirtyStatus.data = true - it.dirtyStatus.mergeFrom(dirtyStatus) - - service.listeners['waf'].onNewSubconfig(it, reconf) - it.dirtyStatus.clearDirty() - } + final ipData = [exclusion_data : [ + [ + id : 'suspicious_ips_data_id', + type: 'ip_with_expiration', + data: [[value: suspiciousIp]] + ] + ]] + send('suspicious_ips', ipData) + wafModule.applyConfig(reconf) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) ctx.closeWafContext() @@ -1638,9 +1488,7 @@ class WAFModuleSpecification extends DDSpecification { }) 1 * flow.isBlocking() 1 * ctx.isWafContextClosed() >> false - 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) >> { - wafContext = it[0].openContext() - } + 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * ctx.getWafMetrics() 1 * ctx.isThrottled(null) 1 * ctx.reportEvents(_ as Collection) @@ -1653,8 +1501,7 @@ class WAFModuleSpecification extends DDSpecification { given: final flow = Mock(ChangeableFlow) final fingerprint = '_dd.appsec.fp.http.endpoint' - setupWithStubConfigService 'fingerprint_config.json' - dataListener = wafModule.dataSubscriptions.first() + initialRuleAdd 'fingerprint_config.json' ctx.closeWafContext() final bundle = MapDataBundle.ofDelegate([ (KnownAddresses.WAF_CONTEXT_PROCESSOR): [fingerprint: true], @@ -1664,6 +1511,7 @@ class WAFModuleSpecification extends DDSpecification { (KnownAddresses.REQUEST_QUERY): [name: ['test']], (KnownAddresses.HEADERS_NO_COOKIES): new CaseInsensitiveMap>(['user-agent': ['Arachni/v1.5.1']]) ]) + wafModule.applyConfig(reconf) when: dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) @@ -1681,8 +1529,8 @@ class WAFModuleSpecification extends DDSpecification { final flow = Mock(ChangeableFlow) final fingerprint = '_dd.appsec.fp.session' final sessionId = UUID.randomUUID().toString() - setupWithStubConfigService 'fingerprint_config.json' - dataListener = wafModule.dataSubscriptions.first() + initialRuleAdd 'fingerprint_config.json' + wafModule.applyConfig(reconf) ctx.closeWafContext() final bundle = MapDataBundle.ofDelegate([ (KnownAddresses.WAF_CONTEXT_PROCESSOR): [fingerprint: true], @@ -1703,9 +1551,10 @@ class WAFModuleSpecification extends DDSpecification { void 'retrieve used addresses'() { when: - setupWithStubConfigService('small_config.json') - def ctx0 = wafModule.ctxAndAddresses.get().ctx - def addresses = wafModule.getUsedAddresses(ctx0) + initialRuleAdd('small_config.json') + wafModule.applyConfig(reconf) + def ctx0 = wafModule.ctxAndAddresses.get() + def addresses = ctx0.addressesOfInterest then: addresses.size() == 6 @@ -1721,13 +1570,13 @@ class WAFModuleSpecification extends DDSpecification { ChangeableFlow flow = Mock() when: - setupWithStubConfigService('rules_with_data_config.json') + initialRuleAdd('rules_with_data_config.json') dataListener = wafModule.dataSubscriptions.first() def bundle = MapDataBundle.of( - KnownAddresses.USER_ID, - 'legit-user' - ) + KnownAddresses.USER_ID, + 'legit-user' + ) ctx.closeWafContext() dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) @@ -1744,13 +1593,13 @@ class WAFModuleSpecification extends DDSpecification { GatewayContext gwCtxMock = new GatewayContext(false, RuleType.SQL_INJECTION) when: - setupWithStubConfigService('rules_with_data_config.json') + initialRuleAdd('rules_with_data_config.json') dataListener = wafModule.dataSubscriptions.first() def bundle = MapDataBundle.of( - KnownAddresses.USER_ID, - 'legit-user' - ) + KnownAddresses.USER_ID, + 'legit-user' + ) ctx.closeWafContext() dataListener.onDataAvailable(flow, ctx, bundle, gwCtxMock) @@ -1762,35 +1611,34 @@ class WAFModuleSpecification extends DDSpecification { 0 * _ } - void 'test raspErrorCode metric is increased when waf call throws #wafErrorCode '() { + void 'test raspErrorCode metric is increased when waf call throws #wafErrorCode '() { setup: ChangeableFlow flow = Mock() GatewayContext gwCtxMock = new GatewayContext(false, RuleType.SQL_INJECTION) WafContext wafContext = Mock() when: - setupWithStubConfigService('rules_with_data_config.json') - dataListener = wafModule.dataSubscriptions.first() + initialRuleAdd('rules_with_data_config.json') def bundle = MapDataBundle.of( - KnownAddresses.USER_ID, - 'legit-user' - ) + KnownAddresses.USER_ID, + 'legit-user' + ) dataListener.onDataAvailable(flow, ctx, bundle, gwCtxMock) then: (1..2) * ctx.isWafContextClosed() >> false // if UnclassifiedWafException it's called twice 1 * ctx.getOrCreateWafContext(_, true, true) >> wafContext 1 * wafMetricCollector.raspRuleEval(RuleType.SQL_INJECTION) - 1 * wafContext.run(_, _, _) >> { throw createWafException(wafErrorCode) } + 1 * wafContext.run(_, _, _) >> { throw createWafException(wafErrorCode as WafErrorCode) } 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) 1 * ctx.getRaspMetrics() 1 * ctx.getRaspMetricsCounter() - 1 * wafMetricCollector.raspErrorCode(RuleType.SQL_INJECTION, wafErrorCode.code) + 1 * wafMetricCollector.raspErrorCode(RuleType.SQL_INJECTION, _) 0 * _ where: - wafErrorCode << LibWafErrorCode.values() + wafErrorCode << WafErrorCode.values() } void 'test wafErrorCode metric is increased when waf call throws #wafErrorCode '() { @@ -1799,32 +1647,31 @@ class WAFModuleSpecification extends DDSpecification { WafContext wafContext = Mock() when: - setupWithStubConfigService('rules_with_data_config.json') - dataListener = wafModule.dataSubscriptions.first() + initialRuleAdd('rules_with_data_config.json') def bundle = MapDataBundle.of( - KnownAddresses.USER_ID, - 'legit-user' - ) + KnownAddresses.USER_ID, + 'legit-user' + ) dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) then: (1..2) * ctx.isWafContextClosed() >> false // if UnclassifiedWafException it's called twice 1 * ctx.getOrCreateWafContext(_, true, false) >> wafContext - 1 * wafContext.run(_, _, _) >> { throw createWafException(wafErrorCode) } + 1 * wafContext.run(_, _, _) >> { throw createWafException(wafErrorCode as WafErrorCode) } 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) 2 * ctx.getWafMetrics() - 1 * wafMetricCollector.wafErrorCode(wafErrorCode.code) + 1 * wafMetricCollector.wafErrorCode(_) 1 * ctx.setWafErrors() 0 * _ where: - wafErrorCode << LibWafErrorCode.values() + wafErrorCode << WafErrorCode.values() } def 'internal-api WafErrorCode enum matches libddwaf-java by name and code'() { given: - def libddwaf = LibWafErrorCode.values().collectEntries { [it.name(), it.code] } + def libddwaf = WafErrorCode.values().collectEntries { [it.name(), it.code] } def internal = InternalWafErrorCode.values().collectEntries { [it.name(), it.code] } expect: @@ -1834,24 +1681,18 @@ class WAFModuleSpecification extends DDSpecification { /** * Helper to return a concrete Waf exception for each WafErrorCode */ - static AbstractWafException createWafException(LibWafErrorCode code) { + static AbstractWafException createWafException(WafErrorCode code) { switch (code) { - case LibWafErrorCode.INVALID_ARGUMENT: - return new InvalidArgumentWafException(code.code) - case LibWafErrorCode.INVALID_OBJECT: - return new InvalidObjectWafException(code.code) - case LibWafErrorCode.INTERNAL_ERROR: - return new InternalWafException(code.code) - case LibWafErrorCode.BINDING_ERROR: - return new UnclassifiedWafException(code.code) + case WafErrorCode.INVALID_ARGUMENT: + return new InvalidArgumentWafException(code.code) + case WafErrorCode.INVALID_OBJECT: + return new InvalidObjectWafException(code.code) + case WafErrorCode.INTERNAL_ERROR: + return new InternalWafException(code.code) + case WafErrorCode.BINDING_ERROR: + return new UnclassifiedWafException(code.code) default: - throw new IllegalStateException("Unhandled WafErrorCode: $code") + throw new IllegalStateException("Unhandled WafErrorCode: $code") } } - - private Map getDefaultConfig() { - def service = new StubAppSecConfigService() - service.init() - service.lastConfig - } } diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy index c3604d812c1..0b3b2c91bd7 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy @@ -1,23 +1,31 @@ package com.datadog.appsec.gateway - -import com.datadog.appsec.config.CurrentAppSecConfig +import com.datadog.appsec.ddwaf.WafInitialization import com.datadog.appsec.event.data.KnownAddresses import com.datadog.appsec.event.data.MapDataBundle import com.datadog.appsec.report.AppSecEvent +import com.datadog.ddwaf.Waf +import com.datadog.ddwaf.WafBuilder +import com.datadog.ddwaf.WafContext +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import datadog.trace.api.telemetry.LogCollector import datadog.trace.test.logging.TestLogCollector -import datadog.trace.util.stacktrace.StackTraceEvent -import com.datadog.appsec.test.StubAppSecConfigService import datadog.trace.test.util.DDSpecification +import datadog.trace.util.stacktrace.StackTraceEvent import datadog.trace.util.stacktrace.StackTraceFrame -import com.datadog.ddwaf.WafContext -import com.datadog.ddwaf.Waf -import com.datadog.ddwaf.WafHandle +import okio.Okio class AppSecRequestContextSpecification extends DDSpecification { + private static final JsonAdapter> ADAPTER = + new Moshi.Builder() + .build() + .adapter(Types.newParameterizedType(Map, String, Object)) + AppSecRequestContext ctx = new AppSecRequestContext() + WafBuilder wafBuilder void 'implements DataBundle'() { when: @@ -204,14 +212,13 @@ class AppSecRequestContextSpecification extends DDSpecification { } private WafContext createWafContext() { + WafInitialization.ONLINE Waf.initialize false - def service = new StubAppSecConfigService() - service.init() - CurrentAppSecConfig config = service.lastConfig['waf'] - String uniqueId = UUID.randomUUID() as String - config.dirtyStatus.markAllDirty() - WafHandle context = Waf.createHandle(uniqueId, config.mergedUpdateConfig.rawConfig) - new WafContext(context) + wafBuilder?.close() + wafBuilder = new WafBuilder() + def stream = getClass().classLoader.getResourceAsStream("test_multi_config.json") + wafBuilder.addOrUpdateConfig("test", ADAPTER.fromJson(Okio.buffer(Okio.source(stream)))) + new WafContext(wafBuilder.buildWafHandleInstance()) } void 'close closes the wafContext'() { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/test/StubAppSecConfigService.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/test/StubAppSecConfigService.groovy deleted file mode 100644 index 54961ee56c7..00000000000 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/test/StubAppSecConfigService.groovy +++ /dev/null @@ -1,66 +0,0 @@ -package com.datadog.appsec.test - -import com.datadog.appsec.config.AppSecConfigDeserializer -import com.datadog.appsec.config.AppSecConfigService -import com.datadog.appsec.config.AppSecModuleConfigurer -import com.datadog.appsec.config.CurrentAppSecConfig -import com.datadog.appsec.config.TraceSegmentPostProcessor - -class StubAppSecConfigService implements AppSecConfigService, AppSecConfigService.TransactionalAppSecModuleConfigurer { - Map listeners = [:] - - Map lastConfig - final String location - final traceSegmentPostProcessors = [] - - private final Map hardcodedConfig - - StubAppSecConfigService(String location = "test_multi_config.json") { - this.location = location - } - - StubAppSecConfigService(Map config) { - this.hardcodedConfig = config - } - - CurrentAppSecConfig getCurrentAppSecConfig() { - lastConfig['waf'] - } - - @Override - void init() { - if (hardcodedConfig) { - lastConfig = hardcodedConfig - } else { - def loader = getClass().classLoader - def stream = loader.getResourceAsStream(location) - def casc = new CurrentAppSecConfig() - casc.ddConfig = AppSecConfigDeserializer.INSTANCE.deserialize(stream) - lastConfig = Collections.singletonMap('waf', casc) - } - } - - @Override - Optional addSubConfigListener(String key, AppSecModuleConfigurer.SubconfigListener listener) { - listeners[key] = listener - - Optional.ofNullable(lastConfig[key]) - } - - @Override - void addTraceSegmentPostProcessor(TraceSegmentPostProcessor interceptor) { - traceSegmentPostProcessors << interceptor - } - - @Override - void close() {} - - @Override - TransactionalAppSecModuleConfigurer createAppSecModuleConfigurer() { - this - } - - @Override - void commit() { - } -} diff --git a/dd-java-agent/appsec/src/test/resources/test_multi_config_no_action.json b/dd-java-agent/appsec/src/test/resources/test_multi_config_no_action.json new file mode 100644 index 00000000000..78d528e4495 --- /dev/null +++ b/dd-java-agent/appsec/src/test/resources/test_multi_config_no_action.json @@ -0,0 +1,4950 @@ +{ + "version": "2.1", + "metadata": { + "rules_version": "0.42.0" + }, + "rules": [ + { + "id": "bad rule" + }, + { + "id": "ip_match_rule", + "name": "rule1", + "tags": { + "type": "flow1", + "category": "category1" + }, + "conditions": [ + { + "operator": "ip_match", + "parameters": { + "inputs": [ + { + "address": "http.client_ip" + } + ], + "data": "ip_data" + } + } + ], + "on_match": ["block"] + }, + { + "id": "crs-913-110", + "name": "Found request header associated with Acunetix security scanner", + "tags": { + "type": "security_scanner", + "crs_id": "913110", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies" + } + ], + "list": [ + "acunetix-product", + "(acunetix web vulnerability scanner", + "acunetix-scanning-agreement", + "acunetix-user-agreement" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-913-120", + "name": "Found request filename/argument associated with security scanner", + "tags": { + "type": "security_scanner", + "crs_id": "913120", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "list": [ + "/.adsensepostnottherenonobook", + "/hello.html", + "/actsensepostnottherenonotive", + "/acunetix-wvs-test-for-some-inexistent-file", + "/antidisestablishmentarianism", + "/appscan_fingerprint/mac_address", + "/arachni-", + "/cybercop", + "/nessus_is_probing_you_", + "/nessustest", + "/netsparker-", + "/rfiinc.txt", + "/thereisnowaythat-you-canbethere", + "/w3af/remotefileinclude.html", + "appscan_fingerprint", + "w00tw00t.at.isc.sans.dfind", + "w00tw00t.at.blackhats.romanian.anti-sec" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-920-260", + "name": "Unicode Full/Half Width Abuse Attack Attempt", + "tags": { + "type": "http_protocol_violation", + "crs_id": "920260", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + } + ], + "regex": "\\%u[fF]{2}[0-9a-fA-F]{2}", + "options": { + "case_sensitive": true, + "min_length": 6 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-921-110", + "name": "HTTP Request Smuggling Attack", + "tags": { + "type": "http_protocol_violation", + "crs_id": "921110", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?:get|post|head|options|connect|put|delete|trace|track|patch|propfind|propatch|mkcol|copy|move|lock|unlock)\\s+[^\\s]+\\s+http/\\d", + "options": { + "case_sensitive": true, + "min_length": 12 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-921-140", + "name": "HTTP Header Injection Attack via headers", + "tags": { + "type": "http_protocol_violation", + "crs_id": "921140", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "[\\n\\r]", + "options": { + "case_sensitive": true, + "min_length": 1 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-921-160", + "name": "HTTP Header Injection Attack via payload (CR/LF and header-name detected)", + "tags": { + "type": "http_protocol_violation", + "crs_id": "921160", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "[\\n\\r]+(?:\\s|location|refresh|(?:set-)?cookie|(?:x-)?(?:forwarded-(?:for|host|server)|host|via|remote-ip|remote-addr|originating-IP))\\s*:", + "options": { + "case_sensitive": true, + "min_length": 3 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-930-100", + "name": "Obfuscated Path Traversal Attack (/../)", + "tags": { + "type": "lfi", + "crs_id": "930100", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "(?:\\x5c|(?:%(?:c(?:0%(?:[2aq]f|5c|9v)|1%(?:[19p]c|8s|af))|2(?:5(?:c(?:0%25af|1%259c)|2f|5c)|%46|f)|(?:(?:f(?:8%8)?0%8|e)0%80%a|bg%q)f|%3(?:2(?:%(?:%6|4)6|F)|5%%63)|u(?:221[56]|002f|EFC8|F025)|1u|5c)|0x(?:2f|5c)|/))(?:%(?:(?:f(?:(?:c%80|8)%8)?0%8|e)0%80%ae|2(?:(?:5(?:c0%25a|2))?e|%45)|u(?:(?:002|ff0)e|2024)|%32(?:%(?:%6|4)5|E)|c0(?:%[256aef]e|\\.))|\\.(?:%0[01]|\\?)?|\\?\\.?|0x2e){2}(?:\\x5c|(?:%(?:c(?:0%(?:[2aq]f|5c|9v)|1%(?:[19p]c|8s|af))|2(?:5(?:c(?:0%25af|1%259c)|2f|5c)|%46|f)|(?:(?:f(?:8%8)?0%8|e)0%80%a|bg%q)f|%3(?:2(?:%(?:%6|4)6|F)|5%%63)|u(?:221[56]|002f|EFC8|F025)|1u|5c)|0x(?:2f|5c)|/))", + "options": { + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-930-110", + "name": "Simple Path Traversal Attack (/../)", + "tags": { + "type": "lfi", + "crs_id": "930110", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.uri.raw" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "(?:(?:^|[\\\\/])\\.\\.[\\\\/]|[\\\\/]\\.\\.(?:[\\\\/]|$))", + "options": { + "case_sensitive": true, + "min_length": 3 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-930-120", + "name": "OS File Access Attempt", + "tags": { + "type": "lfi", + "crs_id": "930120", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "list": [ + ".htaccess", + ".htdigest", + ".htpasswd", + ".addressbook", + ".aptitude/config", + ".bash_config", + ".bash_history", + ".bash_logout", + ".bash_profile", + ".bashrc", + ".cache/notify-osd.log", + ".config/odesk/odesk team.conf", + ".cshrc", + ".dockerignore", + ".drush/", + ".eslintignore", + ".fbcindex", + ".forward", + ".gitattributes", + ".gitconfig", + ".gnupg/", + ".hplip/hplip.conf", + ".ksh_history", + ".lesshst", + ".lftp/", + ".lhistory", + ".lldb-history", + ".local/share/mc/", + ".lynx_cookies", + ".my.cnf", + ".mysql_history", + ".nano_history", + ".node_repl_history", + ".pearrc", + ".php_history", + ".pinerc", + ".pki/", + ".proclog", + ".procmailrc", + ".psql_history", + ".python_history", + ".rediscli_history", + ".rhistory", + ".rhosts", + ".sh_history", + ".sqlite_history", + ".ssh/authorized_keys", + ".ssh/config", + ".ssh/id_dsa", + ".ssh/id_dsa.pub", + ".ssh/id_rsa", + ".ssh/id_rsa.pub", + ".ssh/identity", + ".ssh/identity.pub", + ".ssh/known_hosts", + ".subversion/auth", + ".subversion/config", + ".subversion/servers", + ".tconn/tconn.conf", + ".tcshrc", + ".vidalia/vidalia.conf", + ".viminfo", + ".vimrc", + ".www_acl", + ".wwwacl", + ".xauthority", + ".zhistory", + ".zshrc", + ".zsh_history", + ".nsconfig", + "etc/redis.conf", + "etc/redis-sentinel.conf", + "etc/php.ini", + "bin/php.ini", + "etc/httpd/php.ini", + "usr/lib/php.ini", + "usr/lib/php/php.ini", + "usr/local/etc/php.ini", + "usr/local/lib/php.ini", + "usr/local/php/lib/php.ini", + "usr/local/php4/lib/php.ini", + "usr/local/php5/lib/php.ini", + "usr/local/apache/conf/php.ini", + "etc/php4.4/fcgi/php.ini", + "etc/php4/apache/php.ini", + "etc/php4/apache2/php.ini", + "etc/php5/apache/php.ini", + "etc/php5/apache2/php.ini", + "etc/php/php.ini", + "etc/php/php4/php.ini", + "etc/php/apache/php.ini", + "etc/php/apache2/php.ini", + "web/conf/php.ini", + "usr/local/zend/etc/php.ini", + "opt/xampp/etc/php.ini", + "var/local/www/conf/php.ini", + "etc/php/cgi/php.ini", + "etc/php4/cgi/php.ini", + "etc/php5/cgi/php.ini", + "home2/bin/stable/apache/php.ini", + "home/bin/stable/apache/php.ini", + "etc/httpd/conf.d/php.conf", + "php5/php.ini", + "php4/php.ini", + "php/php.ini", + "windows/php.ini", + "winnt/php.ini", + "apache/php/php.ini", + "xampp/apache/bin/php.ini", + "netserver/bin/stable/apache/php.ini", + "volumes/macintosh_hd1/usr/local/php/lib/php.ini", + "etc/mono/1.0/machine.config", + "etc/mono/2.0/machine.config", + "etc/mono/2.0/web.config", + "etc/mono/config", + "usr/local/cpanel/logs/stats_log", + "usr/local/cpanel/logs/access_log", + "usr/local/cpanel/logs/error_log", + "usr/local/cpanel/logs/license_log", + "usr/local/cpanel/logs/login_log", + "var/cpanel/cpanel.config", + "var/log/sw-cp-server/error_log", + "usr/local/psa/admin/logs/httpsd_access_log", + "usr/local/psa/admin/logs/panel.log", + "var/log/sso/sso.log", + "usr/local/psa/admin/conf/php.ini", + "etc/sw-cp-server/applications.d/plesk.conf", + "usr/local/psa/admin/conf/site_isolation_settings.ini", + "usr/local/sb/config", + "etc/sw-cp-server/applications.d/00-sso-cpserver.conf", + "etc/sso/sso_config.ini", + "etc/mysql/conf.d/old_passwords.cnf", + "var/log/mysql/mysql-bin.log", + "var/log/mysql/mysql-bin.index", + "var/log/mysql/data/mysql-bin.index", + "var/log/mysql.log", + "var/log/mysql.err", + "var/log/mysqlderror.log", + "var/log/mysql/mysql.log", + "var/log/mysql/mysql-slow.log", + "var/log/mysql-bin.index", + "var/log/data/mysql-bin.index", + "var/mysql.log", + "var/mysql-bin.index", + "var/data/mysql-bin.index", + "program files/mysql/mysql server 5.0/data/{host}.err", + "program files/mysql/mysql server 5.0/data/mysql.log", + "program files/mysql/mysql server 5.0/data/mysql.err", + "program files/mysql/mysql server 5.0/data/mysql-bin.log", + "program files/mysql/mysql server 5.0/data/mysql-bin.index", + "program files/mysql/data/{host}.err", + "program files/mysql/data/mysql.log", + "program files/mysql/data/mysql.err", + "program files/mysql/data/mysql-bin.log", + "program files/mysql/data/mysql-bin.index", + "mysql/data/{host}.err", + "mysql/data/mysql.log", + "mysql/data/mysql.err", + "mysql/data/mysql-bin.log", + "mysql/data/mysql-bin.index", + "usr/local/mysql/data/mysql.log", + "usr/local/mysql/data/mysql.err", + "usr/local/mysql/data/mysql-bin.log", + "usr/local/mysql/data/mysql-slow.log", + "usr/local/mysql/data/mysqlderror.log", + "usr/local/mysql/data/{host}.err", + "usr/local/mysql/data/mysql-bin.index", + "var/lib/mysql/my.cnf", + "etc/mysql/my.cnf", + "etc/my.cnf", + "program files/mysql/mysql server 5.0/my.ini", + "program files/mysql/mysql server 5.0/my.cnf", + "program files/mysql/my.ini", + "program files/mysql/my.cnf", + "mysql/my.ini", + "mysql/my.cnf", + "mysql/bin/my.ini", + "var/postgresql/log/postgresql.log", + "var/log/postgresql/postgresql.log", + "var/log/postgres/pg_backup.log", + "var/log/postgres/postgres.log", + "var/log/postgresql.log", + "var/log/pgsql/pgsql.log", + "var/log/postgresql/postgresql-8.1-main.log", + "var/log/postgresql/postgresql-8.3-main.log", + "var/log/postgresql/postgresql-8.4-main.log", + "var/log/postgresql/postgresql-9.0-main.log", + "var/log/postgresql/postgresql-9.1-main.log", + "var/log/pgsql8.log", + "var/log/postgresql/postgres.log", + "var/log/pgsql_log", + "var/log/postgresql/main.log", + "var/log/cron/var/log/postgres.log", + "usr/internet/pgsql/data/postmaster.log", + "usr/local/pgsql/data/postgresql.log", + "usr/local/pgsql/data/pg_log", + "postgresql/log/pgadmin.log", + "var/lib/pgsql/data/postgresql.conf", + "var/postgresql/db/postgresql.conf", + "var/nm2/postgresql.conf", + "usr/local/pgsql/data/postgresql.conf", + "usr/local/pgsql/data/pg_hba.conf", + "usr/internet/pgsql/data/pg_hba.conf", + "usr/local/pgsql/data/passwd", + "usr/local/pgsql/bin/pg_passwd", + "etc/postgresql/postgresql.conf", + "etc/postgresql/pg_hba.conf", + "home/postgres/data/postgresql.conf", + "home/postgres/data/pg_version", + "home/postgres/data/pg_ident.conf", + "home/postgres/data/pg_hba.conf", + "program files/postgresql/8.3/data/pg_hba.conf", + "program files/postgresql/8.3/data/pg_ident.conf", + "program files/postgresql/8.3/data/postgresql.conf", + "program files/postgresql/8.4/data/pg_hba.conf", + "program files/postgresql/8.4/data/pg_ident.conf", + "program files/postgresql/8.4/data/postgresql.conf", + "program files/postgresql/9.0/data/pg_hba.conf", + "program files/postgresql/9.0/data/pg_ident.conf", + "program files/postgresql/9.0/data/postgresql.conf", + "program files/postgresql/9.1/data/pg_hba.conf", + "program files/postgresql/9.1/data/pg_ident.conf", + "program files/postgresql/9.1/data/postgresql.conf", + "wamp/logs/access.log", + "wamp/logs/apache_error.log", + "wamp/logs/genquery.log", + "wamp/logs/mysql.log", + "wamp/logs/slowquery.log", + "wamp/bin/apache/apache2.2.22/logs/access.log", + "wamp/bin/apache/apache2.2.22/logs/error.log", + "wamp/bin/apache/apache2.2.21/logs/access.log", + "wamp/bin/apache/apache2.2.21/logs/error.log", + "wamp/bin/mysql/mysql5.5.24/data/mysql-bin.index", + "wamp/bin/mysql/mysql5.5.16/data/mysql-bin.index", + "wamp/bin/apache/apache2.2.21/conf/httpd.conf", + "wamp/bin/apache/apache2.2.22/conf/httpd.conf", + "wamp/bin/apache/apache2.2.21/wampserver.conf", + "wamp/bin/apache/apache2.2.22/wampserver.conf", + "wamp/bin/apache/apache2.2.22/conf/wampserver.conf", + "wamp/bin/mysql/mysql5.5.24/my.ini", + "wamp/bin/mysql/mysql5.5.24/wampserver.conf", + "wamp/bin/mysql/mysql5.5.16/my.ini", + "wamp/bin/mysql/mysql5.5.16/wampserver.conf", + "wamp/bin/php/php5.3.8/php.ini", + "wamp/bin/php/php5.4.3/php.ini", + "xampp/apache/logs/access.log", + "xampp/apache/logs/error.log", + "xampp/mysql/data/mysql-bin.index", + "xampp/mysql/data/mysql.err", + "xampp/mysql/data/{host}.err", + "xampp/sendmail/sendmail.log", + "xampp/apache/conf/httpd.conf", + "xampp/filezillaftp/filezilla server.xml", + "xampp/mercurymail/mercury.ini", + "xampp/php/php.ini", + "xampp/phpmyadmin/config.inc.php", + "xampp/sendmail/sendmail.ini", + "xampp/webalizer/webalizer.conf", + "opt/lampp/etc/httpd.conf", + "xampp/htdocs/aca.txt", + "xampp/htdocs/admin.php", + "xampp/htdocs/leer.txt", + "usr/local/apache/logs/audit_log", + "usr/local/apache2/logs/audit_log", + "logs/security_debug_log", + "logs/security_log", + "usr/local/apache/conf/modsec.conf", + "usr/local/apache2/conf/modsec.conf", + "winnt/system32/logfiles/msftpsvc", + "winnt/system32/logfiles/msftpsvc1", + "winnt/system32/logfiles/msftpsvc2", + "windows/system32/logfiles/msftpsvc", + "windows/system32/logfiles/msftpsvc1", + "windows/system32/logfiles/msftpsvc2", + "etc/logrotate.d/proftpd", + "www/logs/proftpd.system.log", + "var/log/proftpd", + "var/log/proftpd/xferlog.legacy", + "var/log/proftpd.access_log", + "var/log/proftpd.xferlog", + "etc/pam.d/proftpd", + "etc/proftp.conf", + "etc/protpd/proftpd.conf", + "etc/vhcs2/proftpd/proftpd.conf", + "etc/proftpd/modules.conf", + "var/log/vsftpd.log", + "etc/vsftpd.chroot_list", + "etc/logrotate.d/vsftpd.log", + "etc/vsftpd/vsftpd.conf", + "etc/vsftpd.conf", + "etc/chrootusers", + "var/log/xferlog", + "var/adm/log/xferlog", + "etc/wu-ftpd/ftpaccess", + "etc/wu-ftpd/ftphosts", + "etc/wu-ftpd/ftpusers", + "var/log/pure-ftpd/pure-ftpd.log", + "logs/pure-ftpd.log", + "var/log/pureftpd.log", + "usr/sbin/pure-config.pl", + "usr/etc/pure-ftpd.conf", + "etc/pure-ftpd/pure-ftpd.conf", + "usr/local/etc/pure-ftpd.conf", + "usr/local/etc/pureftpd.pdb", + "usr/local/pureftpd/etc/pureftpd.pdb", + "usr/local/pureftpd/sbin/pure-config.pl", + "usr/local/pureftpd/etc/pure-ftpd.conf", + "etc/pure-ftpd.conf", + "etc/pure-ftpd/pure-ftpd.pdb", + "etc/pureftpd.pdb", + "etc/pureftpd.passwd", + "etc/pure-ftpd/pureftpd.pdb", + "usr/ports/ftp/pure-ftpd/pure-ftpd.conf", + "usr/ports/ftp/pure-ftpd/pureftpd.pdb", + "usr/ports/ftp/pure-ftpd/pureftpd.passwd", + "usr/ports/net/pure-ftpd/pure-ftpd.conf", + "usr/ports/net/pure-ftpd/pureftpd.pdb", + "usr/ports/net/pure-ftpd/pureftpd.passwd", + "usr/pkgsrc/net/pureftpd/pure-ftpd.conf", + "usr/pkgsrc/net/pureftpd/pureftpd.pdb", + "usr/pkgsrc/net/pureftpd/pureftpd.passwd", + "usr/ports/contrib/pure-ftpd/pure-ftpd.conf", + "usr/ports/contrib/pure-ftpd/pureftpd.pdb", + "usr/ports/contrib/pure-ftpd/pureftpd.passwd", + "var/log/muddleftpd", + "usr/sbin/mudlogd", + "etc/muddleftpd/mudlog", + "etc/muddleftpd.com", + "etc/muddleftpd/mudlogd.conf", + "etc/muddleftpd/muddleftpd.conf", + "var/log/muddleftpd.conf", + "usr/sbin/mudpasswd", + "etc/muddleftpd/muddleftpd.passwd", + "etc/muddleftpd/passwd", + "var/log/ftp-proxy/ftp-proxy.log", + "var/log/ftp-proxy", + "var/log/ftplog", + "etc/logrotate.d/ftp", + "etc/ftpchroot", + "etc/ftphosts", + "etc/ftpusers", + "var/log/exim_mainlog", + "var/log/exim/mainlog", + "var/log/maillog", + "var/log/exim_paniclog", + "var/log/exim/paniclog", + "var/log/exim/rejectlog", + "var/log/exim_rejectlog", + "winnt/system32/logfiles/smtpsvc", + "winnt/system32/logfiles/smtpsvc1", + "winnt/system32/logfiles/smtpsvc2", + "winnt/system32/logfiles/smtpsvc3", + "winnt/system32/logfiles/smtpsvc4", + "winnt/system32/logfiles/smtpsvc5", + "windows/system32/logfiles/smtpsvc", + "windows/system32/logfiles/smtpsvc1", + "windows/system32/logfiles/smtpsvc2", + "windows/system32/logfiles/smtpsvc3", + "windows/system32/logfiles/smtpsvc4", + "windows/system32/logfiles/smtpsvc5", + "etc/osxhttpd/osxhttpd.conf", + "system/library/webobjects/adaptors/apache2.2/apache.conf", + "etc/apache2/sites-available/default", + "etc/apache2/sites-available/default-ssl", + "etc/apache2/sites-enabled/000-default", + "etc/apache2/sites-enabled/default", + "etc/apache2/apache2.conf", + "etc/apache2/ports.conf", + "usr/local/etc/apache/httpd.conf", + "usr/pkg/etc/httpd/httpd.conf", + "usr/pkg/etc/httpd/httpd-default.conf", + "usr/pkg/etc/httpd/httpd-vhosts.conf", + "etc/httpd/mod_php.conf", + "etc/httpd/extra/httpd-ssl.conf", + "etc/rc.d/rc.httpd", + "usr/local/apache/conf/httpd.conf.default", + "usr/local/apache/conf/access.conf", + "usr/local/apache22/conf/httpd.conf", + "usr/local/apache22/httpd.conf", + "usr/local/etc/apache22/conf/httpd.conf", + "usr/local/apps/apache22/conf/httpd.conf", + "etc/apache22/conf/httpd.conf", + "etc/apache22/httpd.conf", + "opt/apache22/conf/httpd.conf", + "usr/local/etc/apache2/vhosts.conf", + "usr/local/apache/conf/vhosts.conf", + "usr/local/apache2/conf/vhosts.conf", + "usr/local/apache/conf/vhosts-custom.conf", + "usr/local/apache2/conf/vhosts-custom.conf", + "etc/apache/default-server.conf", + "etc/apache2/default-server.conf", + "usr/local/apache2/conf/extra/httpd-ssl.conf", + "usr/local/apache2/conf/ssl.conf", + "etc/httpd/conf.d", + "usr/local/etc/apache22/httpd.conf", + "usr/local/etc/apache2/httpd.conf", + "etc/apache2/httpd2.conf", + "etc/apache2/ssl-global.conf", + "etc/apache2/vhosts.d/00_default_vhost.conf", + "apache/conf/httpd.conf", + "etc/apache/httpd.conf", + "etc/httpd/conf", + "http/httpd.conf", + "usr/local/apache1.3/conf/httpd.conf", + "usr/local/etc/httpd/conf", + "var/apache/conf/httpd.conf", + "var/www/conf", + "www/apache/conf/httpd.conf", + "www/conf/httpd.conf", + "etc/init.d", + "etc/apache/access.conf", + "etc/rc.conf", + "www/logs/freebsddiary-error.log", + "www/logs/freebsddiary-access_log", + "library/webserver/documents/index.html", + "library/webserver/documents/index.htm", + "library/webserver/documents/default.html", + "library/webserver/documents/default.htm", + "library/webserver/documents/index.php", + "library/webserver/documents/default.php", + "var/log/webmin/miniserv.log", + "usr/local/etc/webmin/miniserv.conf", + "etc/webmin/miniserv.conf", + "usr/local/etc/webmin/miniserv.users", + "etc/webmin/miniserv.users", + "winnt/system32/logfiles/w3svc/inetsvn1.log", + "winnt/system32/logfiles/w3svc1/inetsvn1.log", + "winnt/system32/logfiles/w3svc2/inetsvn1.log", + "winnt/system32/logfiles/w3svc3/inetsvn1.log", + "windows/system32/logfiles/w3svc/inetsvn1.log", + "windows/system32/logfiles/w3svc1/inetsvn1.log", + "windows/system32/logfiles/w3svc2/inetsvn1.log", + "windows/system32/logfiles/w3svc3/inetsvn1.log", + "var/log/httpd/access_log", + "var/log/httpd/error_log", + "apache/logs/error.log", + "apache/logs/access.log", + "apache2/logs/error.log", + "apache2/logs/access.log", + "logs/error.log", + "logs/access.log", + "etc/httpd/logs/access_log", + "etc/httpd/logs/access.log", + "etc/httpd/logs/error_log", + "etc/httpd/logs/error.log", + "usr/local/apache/logs/access_log", + "usr/local/apache/logs/access.log", + "usr/local/apache/logs/error_log", + "usr/local/apache/logs/error.log", + "usr/local/apache2/logs/access_log", + "usr/local/apache2/logs/access.log", + "usr/local/apache2/logs/error_log", + "usr/local/apache2/logs/error.log", + "var/www/logs/access_log", + "var/www/logs/access.log", + "var/www/logs/error_log", + "var/www/logs/error.log", + "var/log/httpd/access.log", + "var/log/httpd/error.log", + "var/log/apache/access_log", + "var/log/apache/access.log", + "var/log/apache/error_log", + "var/log/apache/error.log", + "var/log/apache2/access_log", + "var/log/apache2/access.log", + "var/log/apache2/error_log", + "var/log/apache2/error.log", + "var/log/access_log", + "var/log/access.log", + "var/log/error_log", + "var/log/error.log", + "opt/lampp/logs/access_log", + "opt/lampp/logs/error_log", + "opt/xampp/logs/access_log", + "opt/xampp/logs/error_log", + "opt/lampp/logs/access.log", + "opt/lampp/logs/error.log", + "opt/xampp/logs/access.log", + "opt/xampp/logs/error.log", + "program files/apache group/apache/logs/access.log", + "program files/apache group/apache/logs/error.log", + "program files/apache software foundation/apache2.2/logs/error.log", + "program files/apache software foundation/apache2.2/logs/access.log", + "opt/apache/apache.conf", + "opt/apache/conf/apache.conf", + "opt/apache2/apache.conf", + "opt/apache2/conf/apache.conf", + "opt/httpd/apache.conf", + "opt/httpd/conf/apache.conf", + "etc/httpd/apache.conf", + "etc/apache2/apache.conf", + "etc/httpd/conf/apache.conf", + "usr/local/apache/apache.conf", + "usr/local/apache/conf/apache.conf", + "usr/local/apache2/apache.conf", + "usr/local/apache2/conf/apache.conf", + "usr/local/php/apache.conf.php", + "usr/local/php4/apache.conf.php", + "usr/local/php5/apache.conf.php", + "usr/local/php/apache.conf", + "usr/local/php4/apache.conf", + "usr/local/php5/apache.conf", + "private/etc/httpd/apache.conf", + "opt/apache/apache2.conf", + "opt/apache/conf/apache2.conf", + "opt/apache2/apache2.conf", + "opt/apache2/conf/apache2.conf", + "opt/httpd/apache2.conf", + "opt/httpd/conf/apache2.conf", + "etc/httpd/apache2.conf", + "etc/httpd/conf/apache2.conf", + "usr/local/apache/apache2.conf", + "usr/local/apache/conf/apache2.conf", + "usr/local/apache2/apache2.conf", + "usr/local/apache2/conf/apache2.conf", + "usr/local/php/apache2.conf.php", + "usr/local/php4/apache2.conf.php", + "usr/local/php5/apache2.conf.php", + "usr/local/php/apache2.conf", + "usr/local/php4/apache2.conf", + "usr/local/php5/apache2.conf", + "private/etc/httpd/apache2.conf", + "usr/local/apache/conf/httpd.conf", + "usr/local/apache2/conf/httpd.conf", + "etc/httpd/conf/httpd.conf", + "etc/apache/apache.conf", + "etc/apache/conf/httpd.conf", + "etc/apache2/httpd.conf", + "usr/apache2/conf/httpd.conf", + "usr/apache/conf/httpd.conf", + "usr/local/etc/apache/conf/httpd.conf", + "usr/local/apache/httpd.conf", + "usr/local/apache2/httpd.conf", + "usr/local/httpd/conf/httpd.conf", + "usr/local/etc/apache2/conf/httpd.conf", + "usr/local/etc/httpd/conf/httpd.conf", + "usr/local/apps/apache2/conf/httpd.conf", + "usr/local/apps/apache/conf/httpd.conf", + "usr/local/php/httpd.conf.php", + "usr/local/php4/httpd.conf.php", + "usr/local/php5/httpd.conf.php", + "usr/local/php/httpd.conf", + "usr/local/php4/httpd.conf", + "usr/local/php5/httpd.conf", + "etc/apache2/conf/httpd.conf", + "etc/http/conf/httpd.conf", + "etc/httpd/httpd.conf", + "etc/http/httpd.conf", + "etc/httpd.conf", + "opt/apache/conf/httpd.conf", + "opt/apache2/conf/httpd.conf", + "var/www/conf/httpd.conf", + "private/etc/httpd/httpd.conf", + "private/etc/httpd/httpd.conf.default", + "etc/apache2/vhosts.d/default_vhost.include", + "etc/apache2/conf.d/charset", + "etc/apache2/conf.d/security", + "etc/apache2/envvars", + "etc/apache2/mods-available/autoindex.conf", + "etc/apache2/mods-available/deflate.conf", + "etc/apache2/mods-available/dir.conf", + "etc/apache2/mods-available/mem_cache.conf", + "etc/apache2/mods-available/mime.conf", + "etc/apache2/mods-available/proxy.conf", + "etc/apache2/mods-available/setenvif.conf", + "etc/apache2/mods-available/ssl.conf", + "etc/apache2/mods-enabled/alias.conf", + "etc/apache2/mods-enabled/deflate.conf", + "etc/apache2/mods-enabled/dir.conf", + "etc/apache2/mods-enabled/mime.conf", + "etc/apache2/mods-enabled/negotiation.conf", + "etc/apache2/mods-enabled/php5.conf", + "etc/apache2/mods-enabled/status.conf", + "program files/apache group/apache/conf/httpd.conf", + "program files/apache group/apache2/conf/httpd.conf", + "program files/xampp/apache/conf/apache.conf", + "program files/xampp/apache/conf/apache2.conf", + "program files/xampp/apache/conf/httpd.conf", + "program files/apache group/apache/apache.conf", + "program files/apache group/apache/conf/apache.conf", + "program files/apache group/apache2/conf/apache.conf", + "program files/apache group/apache/apache2.conf", + "program files/apache group/apache/conf/apache2.conf", + "program files/apache group/apache2/conf/apache2.conf", + "program files/apache software foundation/apache2.2/conf/httpd.conf", + "volumes/macintosh_hd1/opt/httpd/conf/httpd.conf", + "volumes/macintosh_hd1/opt/apache/conf/httpd.conf", + "volumes/macintosh_hd1/opt/apache2/conf/httpd.conf", + "volumes/macintosh_hd1/usr/local/php/httpd.conf.php", + "volumes/macintosh_hd1/usr/local/php4/httpd.conf.php", + "volumes/macintosh_hd1/usr/local/php5/httpd.conf.php", + "volumes/webbackup/opt/apache2/conf/httpd.conf", + "volumes/webbackup/private/etc/httpd/httpd.conf", + "volumes/webbackup/private/etc/httpd/httpd.conf.default", + "usr/local/etc/apache/vhosts.conf", + "usr/local/jakarta/tomcat/conf/jakarta.conf", + "usr/local/jakarta/tomcat/conf/server.xml", + "usr/local/jakarta/tomcat/conf/context.xml", + "usr/local/jakarta/tomcat/conf/workers.properties", + "usr/local/jakarta/tomcat/conf/logging.properties", + "usr/local/jakarta/dist/tomcat/conf/jakarta.conf", + "usr/local/jakarta/dist/tomcat/conf/server.xml", + "usr/local/jakarta/dist/tomcat/conf/context.xml", + "usr/local/jakarta/dist/tomcat/conf/workers.properties", + "usr/local/jakarta/dist/tomcat/conf/logging.properties", + "usr/share/tomcat6/conf/server.xml", + "usr/share/tomcat6/conf/context.xml", + "usr/share/tomcat6/conf/workers.properties", + "usr/share/tomcat6/conf/logging.properties", + "var/log/tomcat6/catalina.out", + "var/cpanel/tomcat.options", + "usr/local/jakarta/tomcat/logs/catalina.out", + "usr/local/jakarta/tomcat/logs/catalina.err", + "opt/tomcat/logs/catalina.out", + "opt/tomcat/logs/catalina.err", + "usr/share/logs/catalina.out", + "usr/share/logs/catalina.err", + "usr/share/tomcat/logs/catalina.out", + "usr/share/tomcat/logs/catalina.err", + "usr/share/tomcat6/logs/catalina.out", + "usr/share/tomcat6/logs/catalina.err", + "usr/local/apache/logs/mod_jk.log", + "usr/local/jakarta/tomcat/logs/mod_jk.log", + "usr/local/jakarta/dist/tomcat/logs/mod_jk.log", + "opt/[jboss]/server/default/conf/jboss-minimal.xml", + "opt/[jboss]/server/default/conf/jboss-service.xml", + "opt/[jboss]/server/default/conf/jndi.properties", + "opt/[jboss]/server/default/conf/log4j.xml", + "opt/[jboss]/server/default/conf/login-config.xml", + "opt/[jboss]/server/default/conf/standardjaws.xml", + "opt/[jboss]/server/default/conf/standardjboss.xml", + "opt/[jboss]/server/default/conf/server.log.properties", + "opt/[jboss]/server/default/deploy/jboss-logging.xml", + "usr/local/[jboss]/server/default/conf/jboss-minimal.xml", + "usr/local/[jboss]/server/default/conf/jboss-service.xml", + "usr/local/[jboss]/server/default/conf/jndi.properties", + "usr/local/[jboss]/server/default/conf/log4j.xml", + "usr/local/[jboss]/server/default/conf/login-config.xml", + "usr/local/[jboss]/server/default/conf/standardjaws.xml", + "usr/local/[jboss]/server/default/conf/standardjboss.xml", + "usr/local/[jboss]/server/default/conf/server.log.properties", + "usr/local/[jboss]/server/default/deploy/jboss-logging.xml", + "private/tmp/[jboss]/server/default/conf/jboss-minimal.xml", + "private/tmp/[jboss]/server/default/conf/jboss-service.xml", + "private/tmp/[jboss]/server/default/conf/jndi.properties", + "private/tmp/[jboss]/server/default/conf/log4j.xml", + "private/tmp/[jboss]/server/default/conf/login-config.xml", + "private/tmp/[jboss]/server/default/conf/standardjaws.xml", + "private/tmp/[jboss]/server/default/conf/standardjboss.xml", + "private/tmp/[jboss]/server/default/conf/server.log.properties", + "private/tmp/[jboss]/server/default/deploy/jboss-logging.xml", + "tmp/[jboss]/server/default/conf/jboss-minimal.xml", + "tmp/[jboss]/server/default/conf/jboss-service.xml", + "tmp/[jboss]/server/default/conf/jndi.properties", + "tmp/[jboss]/server/default/conf/log4j.xml", + "tmp/[jboss]/server/default/conf/login-config.xml", + "tmp/[jboss]/server/default/conf/standardjaws.xml", + "tmp/[jboss]/server/default/conf/standardjboss.xml", + "tmp/[jboss]/server/default/conf/server.log.properties", + "tmp/[jboss]/server/default/deploy/jboss-logging.xml", + "program files/[jboss]/server/default/conf/jboss-minimal.xml", + "program files/[jboss]/server/default/conf/jboss-service.xml", + "program files/[jboss]/server/default/conf/jndi.properties", + "program files/[jboss]/server/default/conf/log4j.xml", + "program files/[jboss]/server/default/conf/login-config.xml", + "program files/[jboss]/server/default/conf/standardjaws.xml", + "program files/[jboss]/server/default/conf/standardjboss.xml", + "program files/[jboss]/server/default/conf/server.log.properties", + "program files/[jboss]/server/default/deploy/jboss-logging.xml", + "[jboss]/server/default/conf/jboss-minimal.xml", + "[jboss]/server/default/conf/jboss-service.xml", + "[jboss]/server/default/conf/jndi.properties", + "[jboss]/server/default/conf/log4j.xml", + "[jboss]/server/default/conf/login-config.xml", + "[jboss]/server/default/conf/standardjaws.xml", + "[jboss]/server/default/conf/standardjboss.xml", + "[jboss]/server/default/conf/server.log.properties", + "[jboss]/server/default/deploy/jboss-logging.xml", + "opt/[jboss]/server/default/log/server.log", + "opt/[jboss]/server/default/log/boot.log", + "usr/local/[jboss]/server/default/log/server.log", + "usr/local/[jboss]/server/default/log/boot.log", + "private/tmp/[jboss]/server/default/log/server.log", + "private/tmp/[jboss]/server/default/log/boot.log", + "tmp/[jboss]/server/default/log/server.log", + "tmp/[jboss]/server/default/log/boot.log", + "program files/[jboss]/server/default/log/server.log", + "program files/[jboss]/server/default/log/boot.log", + "[jboss]/server/default/log/server.log", + "[jboss]/server/default/log/boot.log", + "var/log/lighttpd.error.log", + "var/log/lighttpd.access.log", + "var/lighttpd.log", + "var/logs/access.log", + "var/log/lighttpd/", + "var/log/lighttpd/error.log", + "var/log/lighttpd/access.www.log", + "var/log/lighttpd/error.www.log", + "var/log/lighttpd/access.log", + "usr/local/apache2/logs/lighttpd.error.log", + "usr/local/apache2/logs/lighttpd.log", + "usr/local/apache/logs/lighttpd.error.log", + "usr/local/apache/logs/lighttpd.log", + "usr/local/lighttpd/log/lighttpd.error.log", + "usr/local/lighttpd/log/access.log", + "var/log/lighttpd/{domain}/access.log", + "var/log/lighttpd/{domain}/error.log", + "usr/home/user/var/log/lighttpd.error.log", + "usr/home/user/var/log/apache.log", + "home/user/lighttpd/lighttpd.conf", + "usr/home/user/lighttpd/lighttpd.conf", + "etc/lighttpd/lighthttpd.conf", + "usr/local/etc/lighttpd.conf", + "usr/local/lighttpd/conf/lighttpd.conf", + "usr/local/etc/lighttpd.conf.new", + "var/www/.lighttpdpassword", + "var/log/nginx/access_log", + "var/log/nginx/error_log", + "var/log/nginx/access.log", + "var/log/nginx/error.log", + "var/log/nginx.access_log", + "var/log/nginx.error_log", + "logs/access_log", + "logs/error_log", + "etc/nginx/nginx.conf", + "usr/local/etc/nginx/nginx.conf", + "usr/local/nginx/conf/nginx.conf", + "usr/local/zeus/web/global.cfg", + "usr/local/zeus/web/log/errors", + "opt/lsws/conf/httpd_conf.xml", + "usr/local/lsws/conf/httpd_conf.xml", + "opt/lsws/logs/error.log", + "opt/lsws/logs/access.log", + "usr/local/lsws/logs/error.log", + "usr/local/logs/access.log", + "usr/local/samba/lib/log.user", + "usr/local/logs/samba.log", + "var/log/samba/log.smbd", + "var/log/samba/log.nmbd", + "var/log/samba.log", + "var/log/samba.log1", + "var/log/samba.log2", + "var/log/log.smb", + "etc/samba/netlogon", + "etc/smbpasswd", + "etc/smb.conf", + "etc/samba/dhcp.conf", + "etc/samba/smb.conf", + "etc/samba/samba.conf", + "etc/samba/smb.conf.user", + "etc/samba/smbpasswd", + "etc/samba/smbusers", + "etc/samba/private/smbpasswd", + "usr/local/etc/smb.conf", + "usr/local/samba/lib/smb.conf.user", + "etc/dhcp3/dhclient.conf", + "etc/dhcp3/dhcpd.conf", + "etc/dhcp/dhclient.conf", + "program files/vidalia bundle/polipo/polipo.conf", + "etc/tor/tor-tsocks.conf", + "etc/stunnel/stunnel.conf", + "etc/tsocks.conf", + "etc/tinyproxy/tinyproxy.conf", + "etc/miredo-server.conf", + "etc/miredo.conf", + "etc/miredo/miredo-server.conf", + "etc/miredo/miredo.conf", + "etc/wicd/dhclient.conf.template.default", + "etc/wicd/manager-settings.conf", + "etc/wicd/wired-settings.conf", + "etc/wicd/wireless-settings.conf", + "var/log/ipfw.log", + "var/log/ipfw", + "var/log/ipfw/ipfw.log", + "var/log/ipfw.today", + "etc/ipfw.rules", + "etc/ipfw.conf", + "etc/firewall.rules", + "winnt/system32/logfiles/firewall/pfirewall.log", + "winnt/system32/logfiles/firewall/pfirewall.log.old", + "windows/system32/logfiles/firewall/pfirewall.log", + "windows/system32/logfiles/firewall/pfirewall.log.old", + "etc/clamav/clamd.conf", + "etc/clamav/freshclam.conf", + "etc/x11/xorg.conf", + "etc/x11/xorg.conf-vesa", + "etc/x11/xorg.conf-vmware", + "etc/x11/xorg.conf.beforevmwaretoolsinstall", + "etc/x11/xorg.conf.orig", + "etc/bluetooth/input.conf", + "etc/bluetooth/main.conf", + "etc/bluetooth/network.conf", + "etc/bluetooth/rfcomm.conf", + "proc/self/environ", + "proc/self/mounts", + "proc/self/stat", + "proc/self/status", + "proc/self/cmdline", + "proc/self/fd/0", + "proc/self/fd/1", + "proc/self/fd/2", + "proc/self/fd/3", + "proc/self/fd/4", + "proc/self/fd/5", + "proc/self/fd/6", + "proc/self/fd/7", + "proc/self/fd/8", + "proc/self/fd/9", + "proc/self/fd/10", + "proc/self/fd/11", + "proc/self/fd/12", + "proc/self/fd/13", + "proc/self/fd/14", + "proc/self/fd/15", + "proc/version", + "proc/devices", + "proc/cpuinfo", + "proc/meminfo", + "proc/net/tcp", + "proc/net/udp", + "etc/bash_completion.d/debconf", + "root/.bash_logout", + "root/.bash_history", + "root/.bash_config", + "root/.bashrc", + "etc/bash.bashrc", + "var/adm/syslog", + "var/adm/sulog", + "var/adm/utmp", + "var/adm/utmpx", + "var/adm/wtmp", + "var/adm/wtmpx", + "var/adm/lastlog/username", + "usr/spool/lp/log", + "var/adm/lp/lpd-errs", + "usr/lib/cron/log", + "var/adm/loginlog", + "var/adm/pacct", + "var/adm/dtmp", + "var/adm/acct/sum/loginlog", + "var/adm/x0msgs", + "var/adm/crash/vmcore", + "var/adm/crash/unix", + "etc/newsyslog.conf", + "var/adm/qacct", + "var/adm/ras/errlog", + "var/adm/ras/bootlog", + "var/adm/cron/log", + "etc/utmp", + "etc/security/lastlog", + "etc/security/failedlogin", + "usr/spool/mqueue/syslog", + "var/adm/messages", + "var/adm/aculogs", + "var/adm/aculog", + "var/adm/vold.log", + "var/adm/log/asppp.log", + "var/log/poplog", + "var/log/authlog", + "var/lp/logs/lpsched", + "var/lp/logs/lpnet", + "var/lp/logs/requests", + "var/cron/log", + "var/saf/_log", + "var/saf/port/log", + "var/log/news.all", + "var/log/news/news.all", + "var/log/news/news.crit", + "var/log/news/news.err", + "var/log/news/news.notice", + "var/log/news/suck.err", + "var/log/news/suck.notice", + "var/log/messages", + "var/log/messages.1", + "var/log/user.log", + "var/log/user.log.1", + "var/log/auth.log", + "var/log/pm-powersave.log", + "var/log/xorg.0.log", + "var/log/daemon.log", + "var/log/daemon.log.1", + "var/log/kern.log", + "var/log/kern.log.1", + "var/log/mail.err", + "var/log/mail.info", + "var/log/mail.warn", + "var/log/ufw.log", + "var/log/boot.log", + "var/log/syslog", + "var/log/syslog.1", + "tmp/access.log", + "etc/sensors.conf", + "etc/sensors3.conf", + "etc/host.conf", + "etc/pam.conf", + "etc/resolv.conf", + "etc/apt/apt.conf", + "etc/inetd.conf", + "etc/syslog.conf", + "etc/sysctl.conf", + "etc/sysctl.d/10-console-messages.conf", + "etc/sysctl.d/10-network-security.conf", + "etc/sysctl.d/10-process-security.conf", + "etc/sysctl.d/wine.sysctl.conf", + "etc/security/access.conf", + "etc/security/group.conf", + "etc/security/limits.conf", + "etc/security/namespace.conf", + "etc/security/pam_env.conf", + "etc/security/sepermit.conf", + "etc/security/time.conf", + "etc/ssh/sshd_config", + "etc/adduser.conf", + "etc/deluser.conf", + "etc/avahi/avahi-daemon.conf", + "etc/ca-certificates.conf", + "etc/ca-certificates.conf.dpkg-old", + "etc/casper.conf", + "etc/chkrootkit.conf", + "etc/debconf.conf", + "etc/dns2tcpd.conf", + "etc/e2fsck.conf", + "etc/esound/esd.conf", + "etc/etter.conf", + "etc/fuse.conf", + "etc/foremost.conf", + "etc/hdparm.conf", + "etc/kernel-img.conf", + "etc/kernel-pkg.conf", + "etc/ld.so.conf", + "etc/ltrace.conf", + "etc/mail/sendmail.conf", + "etc/manpath.config", + "etc/kbd/config", + "etc/ldap/ldap.conf", + "etc/logrotate.conf", + "etc/mtools.conf", + "etc/smi.conf", + "etc/updatedb.conf", + "etc/pulse/client.conf", + "usr/share/adduser/adduser.conf", + "etc/hostname", + "etc/networks", + "etc/timezone", + "etc/modules", + "etc/passwd", + "etc/passwd~", + "etc/passwd-", + "etc/shadow", + "etc/shadow~", + "etc/shadow-", + "etc/fstab", + "etc/motd", + "etc/hosts", + "etc/group", + "etc/group-", + "etc/alias", + "etc/crontab", + "etc/crypttab", + "etc/exports", + "etc/mtab", + "etc/hosts.allow", + "etc/hosts.deny", + "etc/os-release", + "etc/password.master", + "etc/profile", + "etc/default/grub", + "etc/resolvconf/update-libc.d/sendmail", + "etc/inittab", + "etc/issue", + "etc/issue.net", + "etc/login.defs", + "etc/sudoers", + "etc/sysconfig/network-scripts/ifcfg-eth0", + "etc/redhat-release", + "etc/debian_version", + "etc/fedora-release", + "etc/mandrake-release", + "etc/slackware-release", + "etc/suse-release", + "etc/security/group", + "etc/security/passwd", + "etc/security/user", + "etc/security/environ", + "etc/security/limits", + "etc/security/opasswd", + "boot/grub/grub.cfg", + "boot/grub/menu.lst", + "root/.ksh_history", + "root/.xauthority", + "usr/lib/security/mkuser.default", + "var/log/squirrelmail.log", + "var/log/apache2/squirrelmail.log", + "var/log/apache2/squirrelmail.err.log", + "var/lib/squirrelmail/prefs/squirrelmail.log", + "var/log/mail.log", + "etc/squirrelmail/apache.conf", + "etc/squirrelmail/config_local.php", + "etc/squirrelmail/default_pref", + "etc/squirrelmail/index.php", + "etc/squirrelmail/config_default.php", + "etc/squirrelmail/config.php", + "etc/squirrelmail/filters_setup.php", + "etc/squirrelmail/sqspell_config.php", + "etc/squirrelmail/config/config.php", + "etc/httpd/conf.d/squirrelmail.conf", + "usr/share/squirrelmail/config/config.php", + "private/etc/squirrelmail/config/config.php", + "srv/www/htdos/squirrelmail/config/config.php", + "var/www/squirrelmail/config/config.php", + "var/www/html/squirrelmail/config/config.php", + "var/www/html/squirrelmail-1.2.9/config/config.php", + "usr/share/squirrelmail/plugins/squirrel_logger/setup.php", + "usr/local/squirrelmail/www/readme", + "windows/system32/drivers/etc/hosts", + "windows/system32/drivers/etc/lmhosts.sam", + "windows/system32/drivers/etc/networks", + "windows/system32/drivers/etc/protocol", + "windows/system32/drivers/etc/services", + "/boot.ini", + "windows/debug/netsetup.log", + "windows/comsetup.log", + "windows/repair/setup.log", + "windows/setupact.log", + "windows/setupapi.log", + "windows/setuperr.log", + "windows/updspapi.log", + "windows/wmsetup.log", + "windows/windowsupdate.log", + "windows/odbc.ini", + "usr/local/psa/admin/htdocs/domains/databases/phpmyadmin/libraries/config.default.php", + "etc/apache2/conf.d/phpmyadmin.conf", + "etc/phpmyadmin/config.inc.php", + "etc/openldap/ldap.conf", + "etc/cups/acroread.conf", + "etc/cups/cupsd.conf", + "etc/cups/cupsd.conf.default", + "etc/cups/pdftops.conf", + "etc/cups/printers.conf", + "windows/system32/macromed/flash/flashinstall.log", + "windows/system32/macromed/flash/install.log", + "etc/cvs-cron.conf", + "etc/cvs-pserver.conf", + "etc/subversion/config", + "etc/modprobe.d/vmware-tools.conf", + "etc/updatedb.conf.beforevmwaretoolsinstall", + "etc/vmware-tools/config", + "etc/vmware-tools/tpvmlp.conf", + "etc/vmware-tools/vmware-tools-libraries.conf", + "var/log/vmware/hostd.log", + "var/log/vmware/hostd-1.log", + "wp-config.php", + "wp-config.bak", + "wp-config.old", + "wp-config.temp", + "wp-config.tmp", + "wp-config.txt", + "config.yml", + "config_dev.yml", + "config_prod.yml", + "config_test.yml", + "parameters.yml", + "routing.yml", + "security.yml", + "services.yml", + "sites/default/default.settings.php", + "sites/default/settings.php", + "sites/default/settings.local.php", + "app/etc/local.xml", + "sftp-config.json", + "web.config", + "includes/config.php", + "includes/configure.php", + "config.inc.php", + "localsettings.php", + "inc/config.php", + "typo3conf/localconf.php", + "config/app.php", + "config/custom.php", + "config/database.php", + "/configuration.php", + "/config.php", + "var/mail/www-data", + "etc/network/", + "etc/init/", + "inetpub/wwwroot/global.asa", + "system32/inetsrv/config/applicationhost.config", + "system32/inetsrv/config/administration.config", + "system32/inetsrv/config/redirection.config", + "system32/config/default", + "system32/config/sam", + "system32/config/system", + "system32/config/software", + "winnt/repair/sam._", + "package.json", + "package-lock.json", + "gruntfile.js", + "npm-debug.log", + "ormconfig.json", + "tsconfig.json", + "webpack.config.js", + "yarn.lock" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-931-110", + "name": "Possible Remote File Inclusion (RFI) Attack: Common RFI Vulnerable Parameter Name used w/URL Payload", + "tags": { + "type": "rfi", + "crs_id": "931110", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + } + ], + "regex": "(?:\\binclude\\s*\\([^)]*|mosConfig_absolute_path|_CONF\\[path\\]|_SERVER\\[DOCUMENT_ROOT\\]|GALLERY_BASEDIR|path\\[docroot\\]|appserv_root|config\\[root_dir\\])=(?:file|ftps?|https?)://", + "options": { + "min_length": 15 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-931-120", + "name": "Possible Remote File Inclusion (RFI) Attack: URL Payload Used w/Trailing Question Mark Character (?)", + "tags": { + "type": "rfi", + "crs_id": "931120", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "^(?i:file|ftps?|https?).*?\\?+$", + "options": { + "case_sensitive": true, + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-932-160", + "name": "Remote Command Execution: Unix Shell Code Found", + "tags": { + "type": "command_injection", + "crs_id": "932160", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "list": [ + "${cdpath}", + "${dirstack}", + "${home}", + "${hostname}", + "${ifs}", + "${oldpwd}", + "${ostype}", + "${path}", + "${pwd}", + "$cdpath", + "$dirstack", + "$home", + "$hostname", + "$ifs", + "$oldpwd", + "$ostype", + "$path", + "$pwd", + "bin/bash", + "bin/cat", + "bin/csh", + "bin/dash", + "bin/du", + "bin/echo", + "bin/grep", + "bin/less", + "bin/ls", + "bin/mknod", + "bin/more", + "bin/nc", + "bin/ps", + "bin/rbash", + "bin/sh", + "bin/sleep", + "bin/su", + "bin/tcsh", + "bin/uname", + "dev/fd/", + "dev/null", + "dev/stderr", + "dev/stdin", + "dev/stdout", + "dev/tcp/", + "dev/udp/", + "dev/zero", + "etc/group", + "etc/master.passwd", + "etc/passwd", + "etc/pwd.db", + "etc/shadow", + "etc/shells", + "etc/spwd.db", + "proc/self/", + "usr/bin/awk", + "usr/bin/base64", + "usr/bin/cat", + "usr/bin/cc", + "usr/bin/clang", + "usr/bin/clang++", + "usr/bin/curl", + "usr/bin/diff", + "usr/bin/env", + "usr/bin/fetch", + "usr/bin/file", + "usr/bin/find", + "usr/bin/ftp", + "usr/bin/gawk", + "usr/bin/gcc", + "usr/bin/head", + "usr/bin/hexdump", + "usr/bin/id", + "usr/bin/less", + "usr/bin/ln", + "usr/bin/mkfifo", + "usr/bin/more", + "usr/bin/nc", + "usr/bin/ncat", + "usr/bin/nice", + "usr/bin/nmap", + "usr/bin/perl", + "usr/bin/php", + "usr/bin/php5", + "usr/bin/php7", + "usr/bin/php-cgi", + "usr/bin/printf", + "usr/bin/psed", + "usr/bin/python", + "usr/bin/python2", + "usr/bin/python3", + "usr/bin/ruby", + "usr/bin/sed", + "usr/bin/socat", + "usr/bin/tail", + "usr/bin/tee", + "usr/bin/telnet", + "usr/bin/top", + "usr/bin/uname", + "usr/bin/wget", + "usr/bin/who", + "usr/bin/whoami", + "usr/bin/xargs", + "usr/bin/xxd", + "usr/bin/yes", + "usr/local/bin/bash", + "usr/local/bin/curl", + "usr/local/bin/ncat", + "usr/local/bin/nmap", + "usr/local/bin/perl", + "usr/local/bin/php", + "usr/local/bin/python", + "usr/local/bin/python2", + "usr/local/bin/python3", + "usr/local/bin/rbash", + "usr/local/bin/ruby", + "usr/local/bin/wget" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-932-171", + "name": "Remote Command Execution: Shellshock (CVE-2014-6271)", + "tags": { + "type": "command_injection", + "crs_id": "932171", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "^\\(\\s*\\)\\s+{", + "options": { + "case_sensitive": true, + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-932-180", + "name": "Restricted File Upload Attempt", + "tags": { + "type": "command_injection", + "crs_id": "932180", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-filename", + "x_filename", + "x-file-name" + ] + } + ], + "list": [ + ".htaccess", + ".htdigest", + ".htpasswd", + "wp-config.php", + "config.yml", + "config_dev.yml", + "config_prod.yml", + "config_test.yml", + "parameters.yml", + "routing.yml", + "security.yml", + "services.yml", + "default.settings.php", + "settings.php", + "settings.local.php", + "local.xml", + ".env" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-933-111", + "name": "PHP Injection Attack: PHP Script File Upload Found", + "tags": { + "type": "unrestricted_file_upload", + "crs_id": "933111", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-filename", + "x_filename", + "x.filename", + "x-file-name" + ] + } + ], + "regex": ".*\\.(?:php\\d*|phtml)\\..*$", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-933-130", + "name": "PHP Injection Attack: Global Variables Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933130", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "list": [ + "$globals", + "$http_cookie_vars", + "$http_env_vars", + "$http_get_vars", + "$http_post_files", + "$http_post_vars", + "$http_raw_post_data", + "$http_request_vars", + "$http_server_vars", + "$_cookie", + "$_env", + "$_files", + "$_get", + "$_post", + "$_request", + "$_server", + "$_session", + "$argc", + "$argv" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-933-131", + "name": "PHP Injection Attack: HTTP Headers Values Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933131", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?:HTTP_(?:ACCEPT(?:_(?:ENCODING|LANGUAGE|CHARSET))?|(?:X_FORWARDED_FO|REFERE)R|(?:USER_AGEN|HOS)T|CONNECTION|KEEP_ALIVE)|PATH_(?:TRANSLATED|INFO)|ORIG_PATH_INFO|QUERY_STRING|REQUEST_URI|AUTH_TYPE)", + "options": { + "case_sensitive": true, + "min_length": 9 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-933-140", + "name": "PHP Injection Attack: I/O Stream Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933140", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "php://(?:std(?:in|out|err)|(?:in|out)put|fd|memory|temp|filter)", + "options": { + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-933-150", + "name": "PHP Injection Attack: High-Risk PHP Function Name Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933150", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "list": [ + "__halt_compiler", + "apache_child_terminate", + "base64_decode", + "bzdecompress", + "call_user_func", + "call_user_func_array", + "call_user_method", + "call_user_method_array", + "convert_uudecode", + "file_get_contents", + "file_put_contents", + "fsockopen", + "get_class_methods", + "get_class_vars", + "get_defined_constants", + "get_defined_functions", + "get_defined_vars", + "gzdecode", + "gzinflate", + "gzuncompress", + "include_once", + "invokeargs", + "pcntl_exec", + "pcntl_fork", + "pfsockopen", + "posix_getcwd", + "posix_getpwuid", + "posix_getuid", + "posix_uname", + "reflectionfunction", + "require_once", + "shell_exec", + "str_rot13", + "sys_get_temp_dir", + "wp_remote_fopen", + "wp_remote_get", + "wp_remote_head", + "wp_remote_post", + "wp_remote_request", + "wp_safe_remote_get", + "wp_safe_remote_head", + "wp_safe_remote_post", + "wp_safe_remote_request", + "zlib_decode" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-933-160", + "name": "PHP Injection Attack: High-Risk PHP Function Call Found", + "tags": { + "type": "php_code_injection", + "crs_id": "933160", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "\\b(?:s(?:e(?:t(?:_(?:e(?:xception|rror)_handler|magic_quotes_runtime|include_path)|defaultstub)|ssion_s(?:et_save_handler|tart))|qlite_(?:(?:(?:unbuffered|single|array)_)?query|create_(?:aggregate|function)|p?open|exec)|tr(?:eam_(?:context_create|socket_client)|ipc?slashes|rev)|implexml_load_(?:string|file)|ocket_c(?:onnect|reate)|h(?:ow_sourc|a1_fil)e|pl_autoload_register|ystem)|p(?:r(?:eg_(?:replace(?:_callback(?:_array)?)?|match(?:_all)?|split)|oc_(?:(?:terminat|clos|nic)e|get_status|open)|int_r)|o(?:six_(?:get(?:(?:e[gu]|g)id|login|pwnam)|mk(?:fifo|nod)|ttyname|kill)|pen)|hp(?:_(?:strip_whitespac|unam)e|version|info)|g_(?:(?:execut|prepar)e|connect|query)|a(?:rse_(?:ini_file|str)|ssthru)|utenv)|r(?:unkit_(?:function_(?:re(?:defin|nam)e|copy|add)|method_(?:re(?:defin|nam)e|copy|add)|constant_(?:redefine|add))|e(?:(?:gister_(?:shutdown|tick)|name)_function|ad(?:(?:gz)?file|_exif_data|dir))|awurl(?:de|en)code)|i(?:mage(?:createfrom(?:(?:jpe|pn)g|x[bp]m|wbmp|gif)|(?:jpe|pn)g|g(?:d2?|if)|2?wbmp|xbm)|s_(?:(?:(?:execut|write?|read)ab|fi)le|dir)|ni_(?:get(?:_all)?|set)|terator_apply|ptcembed)|g(?:et(?:_(?:c(?:urrent_use|fg_va)r|meta_tags)|my(?:[gpu]id|inode)|(?:lastmo|cw)d|imagesize|env)|z(?:(?:(?:defla|wri)t|encod|fil)e|compress|open|read)|lob)|a(?:rray_(?:u(?:intersect(?:_u?assoc)?|diff(?:_u?assoc)?)|intersect_u(?:assoc|key)|diff_u(?:assoc|key)|filter|reduce|map)|ssert(?:_options)?)|h(?:tml(?:specialchars(?:_decode)?|_entity_decode|entities)|(?:ash(?:_(?:update|hmac))?|ighlight)_file|e(?:ader_register_callback|x2bin))|f(?:i(?:le(?:(?:[acm]tim|inod)e|(?:_exist|perm)s|group)?|nfo_open)|tp_(?:nb_(?:ge|pu)|connec|ge|pu)t|(?:unction_exis|pu)ts|write|open)|o(?:b_(?:get_(?:c(?:ontents|lean)|flush)|end_(?:clean|flush)|clean|flush|start)|dbc_(?:result(?:_all)?|exec(?:ute)?|connect)|pendir)|m(?:b_(?:ereg(?:_(?:replace(?:_callback)?|match)|i(?:_replace)?)?|parse_str)|(?:ove_uploaded|d5)_file|ethod_exists|ysql_query|kdir)|e(?:x(?:if_(?:t(?:humbnail|agname)|imagetype|read_data)|ec)|scapeshell(?:arg|cmd)|rror_reporting|val)|c(?:url_(?:file_create|exec|init)|onvert_uuencode|reate_function|hr)|u(?:n(?:serialize|pack)|rl(?:de|en)code|[ak]?sort)|(?:json_(?:de|en)cod|debug_backtrac|tmpfil)e|b(?:(?:son_(?:de|en)|ase64_en)code|zopen)|var_dump)(?:\\s|/\\*.*\\*/|//.*|#.*)*\\(.*\\)", + "options": { + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-933-170", + "name": "PHP Injection Attack: Serialized Object Injection", + "tags": { + "type": "php_code_injection", + "crs_id": "933170", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "[oOcC]:\\d+:\\\".+?\\\":\\d+:{[\\W\\w]*}", + "options": { + "case_sensitive": true, + "min_length": 12 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-933-200", + "name": "PHP Injection Attack: Wrapper scheme detected", + "tags": { + "type": "php_code_injection", + "crs_id": "933200", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?i:zlib|glob|phar|ssh2|rar|ogg|expect|zip)://", + "options": { + "case_sensitive": true, + "min_length": 6 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-934-100", + "name": "Node.js Injection Attack", + "tags": { + "type": "js_code_injection", + "crs_id": "934100", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?:(?:_(?:\\$\\$ND_FUNC\\$\\$_|_js_function)|(?:new\\s+Function|\\beval)\\s*\\(|String\\s*\\.\\s*fromCharCode|function\\s*\\(\\s*\\)\\s*{|this\\.constructor)|module\\.exports\\s*=)", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-941-100", + "name": "XSS Attack Detected via libinjection", + "tags": { + "type": "xss", + "crs_id": "941100", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent", + "referer" + ] + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ] + }, + "operator": "is_xss" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-941-110", + "name": "XSS Filter - Category 1: Script Tag Vector", + "tags": { + "type": "xss", + "crs_id": "941110", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent", + "referer" + ] + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "]*>[\\s\\S]*?", + "options": { + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-941-120", + "name": "XSS Filter - Category 2: Event Handler Vector", + "tags": { + "type": "xss", + "crs_id": "941120", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent", + "referer" + ] + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "[\\s\\\"'`;\\/0-9=\\x0B\\x09\\x0C\\x3B\\x2C\\x28\\x3B]on[a-zA-Z]{3,25}[\\s\\x0B\\x09\\x0C\\x3B\\x2C\\x28\\x3B]*?=[^=]", + "options": { + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-941-140", + "name": "XSS Filter - Category 4: Javascript URI Vector", + "tags": { + "type": "xss", + "crs_id": "941140", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent", + "referer" + ] + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "[a-z]+=(?:[^:=]+:.+;)*?[^:=]+:url\\(javascript", + "options": { + "min_length": 18 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-941-180", + "name": "Node-Validator Deny List Keywords", + "tags": { + "type": "xss", + "crs_id": "941180", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "list": [ + "document.cookie", + "document.write", + ".parentnode", + ".innerhtml", + "window.location", + "-moz-binding", + "]", + "options": { + "min_length": 8 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-941-300", + "name": "IE XSS Filters - Attack Detected via object tag", + "tags": { + "type": "xss", + "crs_id": "941300", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": ")|<.*\\+AD4-", + "options": { + "case_sensitive": true, + "min_length": 6 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-941-360", + "name": "JSFuck / Hieroglyphy obfuscation detected", + "tags": { + "type": "xss", + "crs_id": "941360", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "![!+ ]\\[\\]", + "options": { + "case_sensitive": true, + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-100", + "name": "SQL Injection Attack Detected via libinjection", + "tags": { + "type": "sql_injection", + "crs_id": "942100", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ] + }, + "operator": "is_sqli" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "crs-942-140", + "name": "SQL Injection Attack: Common DB Names Detected", + "tags": { + "type": "sql_injection", + "crs_id": "942140", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "\\b(?:(?:m(?:s(?:ys(?:ac(?:cess(?:objects|storage|xml)|es)|(?:relationship|object|querie)s|modules2?)|db)|aster\\.\\.sysdatabases|ysql\\.db)|pg_(?:catalog|toast)|information_schema|northwind|tempdb)\\b|s(?:(?:ys(?:\\.database_name|aux)|qlite(?:_temp)?_master)\\b|chema(?:_name\\b|\\W*\\())|d(?:atabas|b_nam)e\\W*\\()", + "options": { + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-160", + "name": "Detects blind sqli tests using sleep() or benchmark()", + "tags": { + "type": "sql_injection", + "crs_id": "942160", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?i:sleep\\(\\s*?\\d*?\\s*?\\)|benchmark\\(.*?\\,.*?\\))", + "options": { + "case_sensitive": true, + "min_length": 7 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-190", + "name": "Detects MSSQL code execution and information gathering attempts", + "tags": { + "type": "sql_injection", + "crs_id": "942190", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?:\\b(?:(?:c(?:onnection_id|urrent_user)|database)\\s*?\\([^\\)]*?|u(?:nion(?:[\\w(?:\\s]*?select| select @)|ser\\s*?\\([^\\)]*?)|s(?:chema\\s*?\\([^\\)]*?|elect.*?\\w?user\\()|into[\\s+]+(?:dump|out)file\\s*?[\\\"'`]|from\\W+information_schema\\W|exec(?:ute)?\\s+master\\.)|[\\\"'`](?:;?\\s*?(?:union\\b\\s*?(?:(?:distin|sele)ct|all)|having|select)\\b\\s*?[^\\s]|\\s*?!\\s*?[\\\"'`\\w])|\\s*?exec(?:ute)?.*?\\Wxp_cmdshell|\\Wiif\\s*?\\()", + "options": { + "min_length": 3 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-220", + "name": "Looking for integer overflow attacks, these are taken from skipfish, except 2.2.2250738585072011e-308 is the \\\"magic number\\\" crash", + "tags": { + "type": "sql_injection", + "crs_id": "942220", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "^(?i:-0000023456|4294967295|4294967296|2147483648|2147483647|0000012345|-2147483648|-2147483649|0000023456|2.2250738585072007e-308|2.2250738585072011e-308|1e309)$", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-240", + "name": "Detects MySQL charset switch and MSSQL DoS attempts", + "tags": { + "type": "sql_injection", + "crs_id": "942240", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?:[\\\"'`](?:;*?\\s*?waitfor\\s+(?:delay|time)\\s+[\\\"'`]|;.*?:\\s*?goto)|alter\\s*?\\w+.*?cha(?:racte)?r\\s+set\\s+\\w+)", + "options": { + "min_length": 7 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-250", + "name": "Detects MATCH AGAINST, MERGE and EXECUTE IMMEDIATE injections", + "tags": { + "type": "sql_injection", + "crs_id": "942250", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?i:merge.*?using\\s*?\\(|execute\\s*?immediate\\s*?[\\\"'`]|match\\s*?[\\w(?:),+-]+\\s*?against\\s*?\\()", + "options": { + "case_sensitive": true, + "min_length": 11 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-270", + "name": "Looking for basic sql injection. Common attack string for mysql, oracle and others", + "tags": { + "type": "sql_injection", + "crs_id": "942270", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "union.*?select.*?from", + "options": { + "min_length": 15 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-280", + "name": "Detects Postgres pg_sleep injection, waitfor delay attacks and database shutdown attempts", + "tags": { + "type": "sql_injection", + "crs_id": "942280", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?:;\\s*?shutdown\\s*?(?:[#;{]|\\/\\*|--)|waitfor\\s*?delay\\s?[\\\"'`]+\\s?\\d|select\\s*?pg_sleep)", + "options": { + "min_length": 10 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-290", + "name": "Finds basic MongoDB SQL injection attempts", + "tags": { + "type": "nosql_injection", + "crs_id": "942290", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?i:(?:\\[\\$(?:ne|eq|lte?|gte?|n?in|mod|all|size|exists|type|slice|x?or|div|like|between|and)\\]))", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-360", + "name": "Detects concatenated basic SQL injection and SQLLFI attempts", + "tags": { + "type": "sql_injection", + "crs_id": "942360", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?:^[\\W\\d]+\\s*?(?:alter\\s*(?:a(?:(?:pplication\\s*rol|ggregat)e|s(?:ymmetric\\s*ke|sembl)y|u(?:thorization|dit)|vailability\\s*group)|c(?:r(?:yptographic\\s*provider|edential)|o(?:l(?:latio|um)|nversio)n|ertificate|luster)|s(?:e(?:rv(?:ice|er)|curity|quence|ssion|arch)|y(?:mmetric\\s*key|nonym)|togroup|chema)|m(?:a(?:s(?:ter\\s*key|k)|terialized)|e(?:ssage\\s*type|thod)|odule)|l(?:o(?:g(?:file\\s*group|in)|ckdown)|a(?:ngua|r)ge|ibrary)|t(?:(?:abl(?:espac)?|yp)e|r(?:igger|usted)|hreshold|ext)|p(?:a(?:rtition|ckage)|ro(?:cedur|fil)e|ermission)|d(?:i(?:mension|skgroup)|atabase|efault|omain)|r(?:o(?:l(?:lback|e)|ute)|e(?:sourc|mot)e)|f(?:u(?:lltext|nction)|lashback|oreign)|e(?:xte(?:nsion|rnal)|(?:ndpoi|ve)nt)|in(?:dex(?:type)?|memory|stance)|b(?:roker\\s*priority|ufferpool)|x(?:ml\\s*schema|srobject)|w(?:ork(?:load)?|rapper)|hi(?:erarchy|stogram)|o(?:perator|utline)|(?:nicknam|queu)e|us(?:age|er)|group|java|view)\\b|(?:(?:(?:trunc|cre)at|renam)e|d(?:e(?:lete|sc)|rop)|(?:inser|selec)t|load)\\s+\\w+|u(?:nion\\s*(?:(?:distin|sele)ct|all)\\b|pdate\\s+\\w+))|\\b(?:(?:(?:(?:trunc|cre|upd)at|renam)e|(?:inser|selec)t|de(?:lete|sc)|alter|load)\\s+(?:group_concat|load_file|char)\\b\\s*\\(?|end\\s*?\\);)|[\\\"'`\\w]\\s+as\\b\\s*[\\\"'`\\w]+\\s*\\bfrom|[\\s(?:]load_file\\s*?\\(|[\\\"'`]\\s+regexp\\W)", + "options": { + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-942-500", + "name": "MySQL in-line comment detected", + "tags": { + "type": "sql_injection", + "crs_id": "942500", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?i:/\\*[!+](?:[\\w\\s=_\\-(?:)]+)?\\*/)", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-943-100", + "name": "Possible Session Fixation Attack: Setting Cookie Values in HTML", + "tags": { + "type": "http_protocol_violation", + "crs_id": "943100", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.cookies" + }, + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?i:\\.cookie\\b.*?;\\W*?(?:expires|domain)\\W*?=|\\bhttp-equiv\\W+set-cookie\\b)", + "options": { + "case_sensitive": true, + "min_length": 15 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "crs-944-100", + "name": "Remote Command Execution: Suspicious Java class detected", + "tags": { + "type": "java_code_injection", + "crs_id": "944100", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "java\\.lang\\.(?:runtime|processbuilder)", + "options": { + "case_sensitive": true, + "min_length": 17 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-944-110", + "name": "Remote Command Execution: Java process spawn (CVE-2017-9805)", + "tags": { + "type": "java_code_injection", + "crs_id": "944110", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "(?:runtime|processbuilder)", + "options": { + "case_sensitive": true, + "min_length": 7 + } + }, + "operator": "match_regex" + }, + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "(?:unmarshaller|base64data|java\\.)", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "crs-944-130", + "name": "Suspicious Java class detected", + "tags": { + "type": "java_code_injection", + "crs_id": "944130", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.cookies" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "list": [ + "com.opensymphony.xwork2", + "com.sun.org.apache", + "java.io.bufferedinputstream", + "java.io.bufferedreader", + "java.io.bytearrayinputstream", + "java.io.bytearrayoutputstream", + "java.io.chararrayreader", + "java.io.datainputstream", + "java.io.file", + "java.io.fileoutputstream", + "java.io.filepermission", + "java.io.filewriter", + "java.io.filterinputstream", + "java.io.filteroutputstream", + "java.io.filterreader", + "java.io.inputstream", + "java.io.inputstreamreader", + "java.io.linenumberreader", + "java.io.objectoutputstream", + "java.io.outputstream", + "java.io.pipedoutputstream", + "java.io.pipedreader", + "java.io.printstream", + "java.io.pushbackinputstream", + "java.io.reader", + "java.io.stringreader", + "java.lang.class", + "java.lang.integer", + "java.lang.number", + "java.lang.object", + "java.lang.process", + "java.lang.processbuilder", + "java.lang.reflect", + "java.lang.runtime", + "java.lang.string", + "java.lang.stringbuilder", + "java.lang.system", + "javax.script.scriptenginemanager", + "org.apache.commons", + "org.apache.struts", + "org.apache.struts2", + "org.omg.corba", + "java.beans.xmldecode" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "sqr-000-001", + "name": "SSRF: Try to access the credential manager of the main cloud services", + "tags": { + "type": "ssrf", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "(?i)^\\W*((http|ftp)s?://)?\\W*((::f{4}:)?(169|(0x)?0*a9|0+251)\\.?(254|(0x)?0*fe|0+376)[0-9a-fx\\.:]+|metadata\\.google\\.internal|metadata\\.goog)\\W*/", + "options": { + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "sqr-000-002", + "name": "Server-side Javascript injection: Try to detect obvious JS injection", + "tags": { + "type": "js_code_injection", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "require\\(['\"][\\w\\.]+['\"]\\)|process\\.\\w+\\([\\w\\.]*\\)|\\.toString\\(\\)", + "options": { + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [ + "removeNulls" + ] + }, + { + "id": "sqr-000-007", + "name": "NoSQL: Detect common exploitation strategy", + "tags": { + "type": "nosql_injection", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "\\$(eq|ne|lte?|gte?|n?in)\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "sqr-000-008", + "name": "Windows: Detect attempts to exfiltrate .ini files", + "tags": { + "type": "command_injection", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "(?i)[&|]\\s*type\\s+%\\w+%\\\\+\\w+\\.ini\\s*[&|]" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "sqr-000-009", + "name": "Linux: Detect attempts to exfiltrate passwd files", + "tags": { + "type": "command_injection", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "(?i)[&|]\\s*cat\\s+\\/etc\\/[\\w\\.\\/]*passwd\\s*[&|]" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "sqr-000-010", + "name": "Windows: Detect attempts to timeout a shell", + "tags": { + "type": "command_injection", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "(?i)[&|]\\s*timeout\\s+/t\\s+\\d+\\s*[&|]" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "sqr-000-011", + "name": "SSRF: Try to access internal OMI service (CVE-2021-38647)", + "tags": { + "type": "ssrf", + "category": "attack_attempt" + }, + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "http(s?):\\/\\/([A-Za-z0-9\\.\\-\\_]+|\\[[A-Fa-f0-9\\:]+\\]|):5986\\/wsman", + "options": { + "min_length": 4 + } + } + } + ], + "transformers": [] + }, + { + "id": "sqr-000-012", + "name": "SSRF: Detect SSRF attempt on internal service", + "tags": { + "type": "ssrf", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "^(jar:)?(http|https):\\/\\/([0-9oq]{1,5}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}|[0-9]{1,10}|localhost)(:[0-9]{1,5})?(\\/.*|)$" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "sqr-000-013", + "name": "SSRF: Detect SSRF attempts using IPv6 or octal/hexdecimal obfuscation", + "tags": { + "type": "ssrf", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + } + ], + "regex": "^(jar:)?(http|https):\\/\\/((\\[)?[:0-9a-f\\.x]{2,}(\\])?)(:[0-9]{1,5})?(\\/.*)?$" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "sqr-000-014", + "name": "SSRF: Detect SSRF domain redirection bypass", + "tags": { + "type": "ssrf", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "^(http|https):\\/\\/(.*burpcollaborator\\.net|localtest\\.me|mail\\.ebc\\.apple\\.com|bugbounty\\.dod\\.network|.*\\.[nx]ip\\.io)" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "sqr-000-015", + "name": "SSRF: Detect SSRF attempt using non HTTP protocol", + "tags": { + "type": "ssrf", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "server.request.headers.no_cookies" + } + ], + "regex": "^(jar:)?((file|netdoc):\\/\\/[\\\\\\/]+|(dict|gopher|ldap|sftp|tftp):\\/\\/.*:[0-9]{1,5})" + }, + "operator": "match_regex" + } + ], + "transformers": [ + "lowercase" + ] + }, + { + "id": "ua0-600-0xx", + "name": "Joomla exploitation tool", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "JDatabaseDriverMysqli" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-10x", + "name": "Nessus", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)^Nessus(/|([ :]+SOAP))" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-12x", + "name": "Arachni", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^Arachni\\/v" + }, + "operator": "match_regex" + } + ], + "transformers": [], + "on_match": ["block"] + }, + { + "id": "generate-stacktrace-on-scanner", + "name": "Arachni", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "^Arachni\\/generate-stacktrace" + }, + "operator": "match_regex" + } + ], + "transformers": [], + "on_match": ["stack_trace"] + }, + { + "id": "ua0-600-13x", + "name": "Jorgee", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bJorgee\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-14x", + "name": "Probely", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bProbely\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-15x", + "name": "Metis", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bmetis\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-16x", + "name": "SQL power injector", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "sql power injector" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-18x", + "name": "N-Stealth", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bn-stealth\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-19x", + "name": "Brutus", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)\\bbrutus\\b" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-1xx", + "name": "Shellshock exploitation tool", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "\\(\\) \\{ :; *\\}" + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, + { + "id": "ua0-600-20x", + "name": "Netsparker", + "tags": { + "type": "security_scanner", + "category": "attack_attempt" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "user-agent" + ] + } + ], + "regex": "(?i)(