Skip to content

Commit

Permalink
Rework the listeners for AMS_FEATURES (#7158)
Browse files Browse the repository at this point in the history
Rework the listeners for ASM_FEATURES
  • Loading branch information
manuel-alvarez-alvarez committed Jun 17, 2024
1 parent ecd93fb commit 8f7a318
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 95 deletions.
3 changes: 3 additions & 0 deletions dd-java-agent/appsec/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ ext {
'com.datadog.appsec.config.AppSecConfig.AppSecConfigV1',
'com.datadog.appsec.config.AppSecConfig.AppSecConfigV2',
'com.datadog.appsec.config.AppSecConfig.NumberJsonAdapter',
'com.datadog.appsec.config.AppSecFeatures',
'com.datadog.appsec.config.AppSecFeatures.Asm',
'com.datadog.appsec.config.AppSecFeatures.ApiSecurity',
'com.datadog.appsec.event.ReplaceableEventProducerService',
]
excludedClassesBranchCoverage = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,21 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s
EventDispatcher eventDispatcher = new EventDispatcher();
REPLACEABLE_EVENT_PRODUCER.replaceEventProducerService(eventDispatcher);

ApiSecurityRequestSampler requestSampler = new ApiSecurityRequestSampler(config);

ConfigurationPoller configurationPoller = sco.configurationPoller(config);
// may throw and abort startup
APP_SEC_CONFIG_SERVICE =
new AppSecConfigServiceImpl(
config, configurationPoller, () -> reloadSubscriptions(REPLACEABLE_EVENT_PRODUCER));
config,
configurationPoller,
requestSampler,
() -> reloadSubscriptions(REPLACEABLE_EVENT_PRODUCER));
APP_SEC_CONFIG_SERVICE.init();

sco.createRemaining(config);

RateLimiter rateLimiter = getRateLimiter(config, sco.monitoring);
ApiSecurityRequestSampler requestSampler =
new ApiSecurityRequestSampler(config, configurationPoller);

GatewayBridge gatewayBridge =
new GatewayBridge(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,56 +1,34 @@
package com.datadog.appsec.api.security;

import static datadog.remoteconfig.tuf.RemoteConfigRequest.ClientInfo.CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE;

import com.datadog.appsec.config.AppSecFeaturesDeserializer;
import datadog.remoteconfig.ConfigurationPoller;
import datadog.remoteconfig.Product;
import datadog.trace.api.Config;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ApiSecurityRequestSampler {

private static final Logger log = LoggerFactory.getLogger(ApiSecurityRequestSampler.class);

private volatile int sampling;
private final AtomicLong cumulativeCounter = new AtomicLong();

public ApiSecurityRequestSampler(final Config config) {
sampling = computeSamplingParameter(config.getApiSecurityRequestSampleRate());
}

public ApiSecurityRequestSampler(final Config config, ConfigurationPoller configurationPoller) {
this(config);
if (configurationPoller == null) {
return;
/**
* Sets the new sampling parameter
*
* @return {@code true} if the value changed
*/
public boolean setSampling(final float newSamplingFloat) {
int newSampling = computeSamplingParameter(newSamplingFloat);
if (newSampling != sampling) {
sampling = newSampling;
cumulativeCounter.set(0); // Reset current sampling counter
return true;
}
return false;
}

configurationPoller.addListener(
Product.ASM_FEATURES,
"asm_api_security",
AppSecFeaturesDeserializer.INSTANCE,
(configKey, newConfig, pollingRateHinter) -> {
if (newConfig != null && newConfig.apiSecurity != null) {
Float newSamplingFloat = newConfig.apiSecurity.requestSampleRate;
if (newSamplingFloat != null) {
int newSampling = computeSamplingParameter(newSamplingFloat);
if (newSampling != sampling) {
sampling = newSampling;
cumulativeCounter.set(0); // Reset current sampling counter
if (sampling == 0) {
log.info("Api Security is disabled via remote-config");
} else {
log.info(
"Api Security changed via remote-config. New sampling rate is {}% of all requests.",
sampling);
}
}
}
}
});
configurationPoller.addCapabilities(CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE);
public int getSampling() {
return sampling;
}

public boolean sampleRequest() {
Expand All @@ -69,7 +47,7 @@ static int computeSamplingParameter(final float pct) {
return 100;
}
if (pct < 0) {
// We don't support disabling Api Security by setting it, so we set it to 100%.
// Api security can only be disabled by setting the sampling to zero, so we set it to 100%.
// TODO: We probably want a warning here.
return 100;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
import static datadog.remoteconfig.tuf.RemoteConfigRequest.ClientInfo.CAPABILITY_ASM_REQUEST_BLOCKING;
import static datadog.remoteconfig.tuf.RemoteConfigRequest.ClientInfo.CAPABILITY_ASM_TRUSTED_IPS;
import static datadog.remoteconfig.tuf.RemoteConfigRequest.ClientInfo.CAPABILITY_ASM_USER_BLOCKING;
import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY;

import com.datadog.appsec.AppSecSystem;
import com.datadog.appsec.api.security.ApiSecurityRequestSampler;
import com.datadog.appsec.config.AppSecModuleConfigurer.SubconfigListener;
import com.datadog.appsec.config.CurrentAppSecConfig.DirtyStatus;
import com.datadog.appsec.util.AbortStartupException;
Expand All @@ -34,7 +36,6 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -60,27 +61,34 @@ public class AppSecConfigServiceImpl implements AppSecConfigService {
private final Config tracerConfig;
private final List<TraceSegmentPostProcessor> traceSegmentPostProcessors = new ArrayList<>();
private final AppSecModuleConfigurer.Reconfiguration reconfiguration;
private final ApiSecurityRequestSampler apiSecurityRequestSampler;

private final ConfigurationEndListener applyWAFChangesAsListener = this::applyWAFChanges;

private boolean hasUserWafConfig;

public AppSecConfigServiceImpl(
Config tracerConfig,
@Nullable ConfigurationPoller configurationPoller,
ConfigurationPoller configurationPoller,
ApiSecurityRequestSampler apiSecurityRequestSampler,
AppSecModuleConfigurer.Reconfiguration reconfig) {
this.tracerConfig = tracerConfig;
this.configurationPoller = configurationPoller;
this.reconfiguration = reconfig;
this.apiSecurityRequestSampler = apiSecurityRequestSampler;
}

private void subscribeConfigurationPoller() {
// see also close() method
if (tracerConfig.getAppSecActivation() == ProductActivation.ENABLED_INACTIVE) {
subscribeActivation();
} else {
log.debug("Will not subscribe to ASM_FEATURES (AppSec explicitly enabled)");
log.debug(
"Will not subscribe to ASM_FEATURES['asm_features_activation'] (AppSec explicitly enabled)");
}

subscribeApiSecurity();

if (!hasUserWafConfig) {
subscribeRulesAndData();
} else {
Expand Down Expand Up @@ -164,8 +172,18 @@ private void subscribeActivation() {
if (!initialized) {
throw new IllegalStateException();
}
final boolean newState =
newConfig != null && newConfig.asm != null && newConfig.asm.enabled;
final boolean newState;
if (newConfig == null) {
// configuration file was removed, restore the default
newState = tracerConfig.getAppSecActivation() == ProductActivation.FULLY_ENABLED;
} else if (newConfig.asm == null || newConfig.asm.enabled == null) {
// invalid payload from the backend, restore the default
log.debug(
SEND_TELEMETRY, "Invalid 'asm_features_activation' payload : {}", newConfig.asm);
newState = tracerConfig.getAppSecActivation() == ProductActivation.FULLY_ENABLED;
} else {
newState = newConfig.asm.enabled;
}
if (AppSecSystem.isActive() != newState) {
log.info("AppSec {} (runtime)", newState ? "enabled" : "disabled");
AppSecSystem.setActive(newState);
Expand All @@ -179,6 +197,43 @@ private void subscribeActivation() {
this.configurationPoller.addCapabilities(CAPABILITY_ASM_ACTIVATION);
}

private void subscribeApiSecurity() {
this.configurationPoller.addListener(
Product.ASM_FEATURES,
"asm_api_security",
AppSecFeaturesDeserializer.INSTANCE,
(configKey, newConfig, hinter) -> {
if (!initialized) {
throw new IllegalStateException();
}
final float newSampling;
if (newConfig == null) {
// configuration file was removed on the backend, restore the default
newSampling = tracerConfig.getApiSecurityRequestSampleRate();
} else if (newConfig.apiSecurity == null
|| newConfig.apiSecurity.requestSampleRate == null) {
// invalid payload from the backend, restore the default
log.debug(
SEND_TELEMETRY, "Invalid 'asm_api_security' payload : {}", newConfig.apiSecurity);
newSampling = tracerConfig.getApiSecurityRequestSampleRate();
} else {
newSampling = newConfig.apiSecurity.requestSampleRate;
}

if (apiSecurityRequestSampler.setSampling(newSampling)) {
int pct = apiSecurityRequestSampler.getSampling();
if (pct == 0) {
log.info("Api Security is disabled via remote-config");
} else {
log.info(
"Api Security changed via remote-config. New sampling rate is {}% of all requests.",
pct);
}
}
});
this.configurationPoller.addCapabilities(CAPABILITY_ASM_API_SECURITY_SAMPLE_RATE);
}

private void distributeSubConfigurations(
Map<String, Object> newConfig, AppSecModuleConfigurer.Reconfiguration reconfiguration) {
for (Map.Entry<String, SubconfigListener> entry : subconfigListeners.entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,26 @@ public class AppSecFeatures {
public ApiSecurity apiSecurity;

public static class Asm {
public boolean enabled;
public Boolean enabled;

@Override
public String toString() {
return "Asm{" + "enabled=" + enabled + '}';
}
}

public static class ApiSecurity {
@com.squareup.moshi.Json(name = "request_sample_rate")
public Float requestSampleRate;

@Override
public String toString() {
return "ApiSecurity{" + "requestSampleRate=" + requestSampleRate + '}';
}
}

@Override
public String toString() {
return "AppSecFeatures{" + "asm=" + asm + ", apiSecurity=" + apiSecurity + '}';
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package com.datadog.appsec.api.security

import com.datadog.appsec.config.AppSecFeatures
import com.datadog.appsec.config.AppSecFeaturesDeserializer
import datadog.remoteconfig.ConfigurationChangesTypedListener
import datadog.remoteconfig.ConfigurationPoller
import datadog.remoteconfig.Product
import datadog.trace.api.Config
import datadog.trace.test.util.DDSpecification
import spock.lang.Shared
Expand Down Expand Up @@ -46,31 +41,13 @@ class ApiSecurityRequestSamplerTest extends DDSpecification {
-0.5 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // Wrong sample rate - use 100%
}

void 'update sample rate via remote-config'() {
void 'update sample rate'() {
given:
ConfigurationPoller poller = Mock()
def config = Spy(Config.get())
ConfigurationChangesTypedListener<AppSecFeatures> listener
AppSecFeatures newConfig = new AppSecFeatures().tap {
asm = new AppSecFeatures.Asm().tap {
enabled = true
}
apiSecurity = new AppSecFeatures.ApiSecurity().tap {
requestSampleRate = 0.2
}
}

when:
def sampler = new ApiSecurityRequestSampler(config, poller)

then:
1 * poller.addListener(Product.ASM_FEATURES, 'asm_api_security', AppSecFeaturesDeserializer.INSTANCE, _) >> {
listener = it[3] as ConfigurationChangesTypedListener<AppSecFeatures>
}
listener != null
def sampler = new ApiSecurityRequestSampler(config)

when:
listener.accept(null, newConfig, null)
sampler.setSampling(0.2)

then:
sampler.sampling == 20
Expand Down
Loading

0 comments on commit 8f7a318

Please sign in to comment.