diff --git a/dd-java-agent/appsec/build.gradle b/dd-java-agent/appsec/build.gradle index dc814e84201..955e0bf881d 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: '16.0.0' + implementation group: 'io.sqreen', name: 'libsqreen', version: '17.1.0' implementation libs.moshi testImplementation libs.bytebuddy 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 d45a847e1fa..8dfa2970fad 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 @@ -40,9 +40,8 @@ import com.datadog.ddwaf.exception.InvalidRuleSetException; import com.datadog.ddwaf.exception.UnclassifiedWafException; import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.JsonReader; -import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; import datadog.remoteconfig.ConfigurationEndListener; import datadog.remoteconfig.ConfigurationPoller; import datadog.remoteconfig.PollingRateHinter; @@ -53,7 +52,6 @@ import datadog.trace.api.ConfigCollector; import datadog.trace.api.ProductActivation; import datadog.trace.api.UserIdCollectionMode; -import datadog.trace.api.telemetry.LogCollector; import datadog.trace.api.telemetry.WafMetricCollector; import java.io.ByteArrayInputStream; import java.io.FileInputStream; @@ -68,7 +66,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import javax.annotation.Nullable; import okio.Okio; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,25 +93,10 @@ public class AppSecConfigServiceImpl implements AppSecConfigService { new WAFInitializationResultReporter(); private final WAFStatsReporter statsReporter = new WAFStatsReporter(); - private static final JsonAdapter ADAPTER = + private static final JsonAdapter> ADAPTER = new Moshi.Builder() - .add( - Double.class, - new JsonAdapter() { - @Override - public Number fromJson(JsonReader reader) throws IOException { - double value = reader.nextDouble(); - long longValue = (long) value; - return value % 1 == 0 ? longValue : value; - } - - @Override - public void toJson(JsonWriter writer, @Nullable Number value) throws IOException { - throw new UnsupportedOperationException(); - } - }) .build() - .adapter(Object.class); + .adapter(Types.newParameterizedType(Map.class, String.class, Object.class)); private boolean hasUserWafConfig; private boolean defaultConfigActivated; @@ -310,7 +292,6 @@ private void handleWafUpdateResultReport(String configKey, Map r } // TODO: Send diagnostics via telemetry - final LogCollector telemetryLogger = LogCollector.get(); initReporter.setReportForPublication(wafDiagnostics); if (wafDiagnostics.rulesetVersion != null @@ -489,8 +470,7 @@ private static Map loadDefaultWafConfig() throws IOException { throw new IOException("Resource " + DEFAULT_CONFIG_LOCATION + " not found"); } - Map ret = - (Map) ADAPTER.fromJson(Okio.buffer(Okio.source(is))); + Map ret = ADAPTER.fromJson(Okio.buffer(Okio.source(is))); StandardizedLogging._initialConfigSourceAndLibddwafVersion(log, ""); if (log.isInfoEnabled()) { @@ -507,8 +487,7 @@ private static Map loadUserWafConfig(Config tracerConfig) throws return null; } try (InputStream is = new FileInputStream(filename)) { - Map ret = - (Map) ADAPTER.fromJson(Okio.buffer(Okio.source(is))); + Map ret = ADAPTER.fromJson(Okio.buffer(Okio.source(is))); StandardizedLogging._initialConfigSourceAndLibddwafVersion(log, filename); if (log.isInfoEnabled()) { 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 adbd4ffdcf7..e00ddecc9a3 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 @@ -34,7 +34,6 @@ import datadog.trace.api.ProductActivation; import datadog.trace.api.ProductTraceSource; import datadog.trace.api.gateway.Flow; -import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.telemetry.LogCollector; import datadog.trace.api.telemetry.WafMetricCollector; import datadog.trace.api.time.SystemTimeSource; @@ -53,7 +52,6 @@ import java.lang.reflect.UndeclaredThrowableException; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -81,8 +79,6 @@ public class WAFModule implements AppSecModule { private static final JsonAdapter> RES_JSON_ADAPTER; - private static final Map DEFAULT_ACTIONS; - private static final String EXPLOIT_DETECTED_MSG = "Exploit detected"; private boolean init = true; private String rulesetVersion; @@ -118,12 +114,6 @@ private CtxAndAddresses(Collection> addressesOfInterest, WafHandle ct Moshi moshi = new Moshi.Builder().build(); RES_JSON_ADAPTER = moshi.adapter(Types.newParameterizedType(List.class, WAFResultData.class)); - Map actionParams = new HashMap<>(); - actionParams.put("status_code", 403); - actionParams.put("type", "auto"); - actionParams.put("grpc_status_code", 10); - DEFAULT_ACTIONS = - Collections.singletonMap("block", new ActionInfo("block_request", actionParams)); createLimitsObject(); } @@ -425,8 +415,9 @@ public void onDataAvailable( Collection events = buildEvents(resultWithData); boolean isThrottled = reqCtx.isThrottled(rateLimiter); - if (resultWithData.keep) { - if (!isThrottled) { + if (!isThrottled) { + if (resultWithData.keep) { + reqCtx.setManuallyKept(true); AgentSpan activeSpan = AgentTracer.get().activeSpan(); if (activeSpan != null) { log.debug("Setting force-keep tag and manual keep tag on the current span"); @@ -439,19 +430,16 @@ public void onDataAvailable( activeSpan .getLocalRootSpan() .setTag(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); - } else { - // If active span is not available then we need to set manual keep in GatewayBridge - log.debug("There is no active span available"); } } else { - log.debug("Rate limited WAF events"); - if (!gwCtx.isRasp) { - reqCtx.setWafRateLimited(); - } + // If active span is not available then we need to set manual keep in GatewayBridge + log.debug("There is no active span available"); + } + } else { + log.debug("Rate limited WAF events"); + if (!gwCtx.isRasp) { + reqCtx.setWafRateLimited(); } - } - if (resultWithData.events && !events.isEmpty() && !isThrottled) { - reqCtx.reportEvents(events); } if (flow.isBlocking()) { @@ -459,11 +447,12 @@ public void onDataAvailable( reqCtx.setWafBlocked(); } } + // report is still done even without keep, in case sampler_keep is desired + if (resultWithData.events) { + reqCtx.reportEvents(events); + } } - reqCtx.setKeepType( - resultWithData.keep ? PrioritySampling.USER_KEEP : PrioritySampling.USER_DROP); - if (resultWithData.attributes != null && !resultWithData.attributes.isEmpty()) { reqCtx.reportDerivatives(resultWithData.attributes); } 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 c1748357377..6fbdb76bb25 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 @@ -13,7 +13,6 @@ import datadog.trace.api.Config; import datadog.trace.api.http.StoredBodySupplier; import datadog.trace.api.internal.TraceSegment; -import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.util.stacktrace.StackTraceEvent; import java.io.Closeable; import java.util.*; @@ -166,7 +165,6 @@ public class AppSecRequestContext implements DataBundle, Closeable { private volatile boolean keepOpenForApiSecurityPostProcessing; private volatile Long apiSecurityEndpointHash; - private volatile byte keepType = PrioritySampling.SAMPLER_KEEP; private final AtomicInteger httpClientRequestCount = new AtomicInteger(0); private final Set sampledHttpClientRequests = new HashSet<>(); @@ -175,6 +173,7 @@ public class AppSecRequestContext implements DataBundle, Closeable { AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "wafTimeouts"); private static final AtomicIntegerFieldUpdater RASP_TIMEOUTS_UPDATER = AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "raspTimeouts"); + private boolean manuallyKept = false; // to be called by the Event Dispatcher public void addAll(DataBundle newData) { @@ -421,14 +420,6 @@ public Long getApiSecurityEndpointHash() { return this.apiSecurityEndpointHash; } - public void setKeepType(byte keepType) { - this.keepType = keepType; - } - - public byte getKeepType() { - return this.keepType; - } - void addRequestHeader(String name, String value) { if (finishedRequestHeaders) { throw new IllegalStateException("Request headers were said to be finished before"); @@ -1014,4 +1005,12 @@ public boolean isRaspMatched() { public void setRaspMatched(boolean raspMatched) { this.raspMatched = raspMatched; } + + public boolean isManuallyKept() { + return manuallyKept; + } + + public void setManuallyKept(boolean manuallyKept) { + this.manuallyKept = manuallyKept; + } } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java index 71c0a23dd0c..fff447f6937 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java @@ -8,7 +8,6 @@ import static com.datadog.appsec.gateway.AppSecRequestContext.REQUEST_HEADERS_ALLOW_LIST; import static com.datadog.appsec.gateway.AppSecRequestContext.RESPONSE_HEADERS_ALLOW_LIST; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; -import static datadog.trace.bootstrap.instrumentation.api.Tags.SAMPLING_PRIORITY; import com.datadog.appsec.AppSecSystem; import com.datadog.appsec.api.security.ApiSecurityDownstreamSampler; @@ -863,10 +862,11 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) { // If detected any events - mark span at appsec.event if (!collectedEvents.isEmpty()) { - // Set asm keep in case that root span was not available when events are detected - traceSeg.setTagTop(Tags.ASM_KEEP, true); - traceSeg.setTagTop(SAMPLING_PRIORITY, ctx.getKeepType()); - traceSeg.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); + if (ctx.isManuallyKept()) { + // Set asm keep in case that root span was not available when events are detected + traceSeg.setTagTop(Tags.ASM_KEEP, true); + traceSeg.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); + } traceSeg.setTagTop("appsec.event", true); traceSeg.setTagTop("network.client.ip", ctx.getPeerAddress()); diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplJsonAdapterTest.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplJsonAdapterTest.groovy deleted file mode 100644 index 8d4556378a8..00000000000 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplJsonAdapterTest.groovy +++ /dev/null @@ -1,86 +0,0 @@ -package com.datadog.appsec.config - -import datadog.trace.test.util.DDSpecification -import okio.Buffer - -class AppSecConfigServiceImplJsonAdapterTest extends DDSpecification { - - void 'test JSON number conversion - whole numbers'() { - given: - def json = '42.0' - def buffer = new Buffer().writeUtf8(json) - - when: - def result = AppSecConfigServiceImpl.ADAPTER.fromJson(buffer) - - then: - result != null - // This should trigger the true branch: value % 1 == 0 ? longValue : value - } - - void 'test JSON number conversion - fractional numbers'() { - given: - def json = '42.5' - def buffer = new Buffer().writeUtf8(json) - - when: - def result = AppSecConfigServiceImpl.ADAPTER.fromJson(buffer) - - then: - result != null - // This should trigger the false branch: value % 1 == 0 ? longValue : value - } - - void 'test JSON number conversion - mixed object'() { - given: - def json = '{"whole": 100.0, "fractional": 3.14}' - def buffer = new Buffer().writeUtf8(json) - - when: - def result = AppSecConfigServiceImpl.ADAPTER.fromJson(buffer) - - then: - result != null - result instanceof Map - // This exercises both branches through the nested numbers - } - - void 'test JSON number conversion - zero'() { - given: - def json = '0.0' - def buffer = new Buffer().writeUtf8(json) - - when: - def result = AppSecConfigServiceImpl.ADAPTER.fromJson(buffer) - - then: - result != null - // This should trigger the true branch: 0.0 % 1 == 0 - } - - void 'test JSON number conversion - negative whole'() { - given: - def json = '-10.0' - def buffer = new Buffer().writeUtf8(json) - - when: - def result = AppSecConfigServiceImpl.ADAPTER.fromJson(buffer) - - then: - result != null - // This should trigger the true branch: -10.0 % 1 == 0 - } - - void 'test JSON number conversion - negative fractional'() { - given: - def json = '-3.5' - def buffer = new Buffer().writeUtf8(json) - - when: - def result = AppSecConfigServiceImpl.ADAPTER.fromJson(buffer) - - then: - result != null - // This should trigger the false branch: -3.5 % 1 != 0 - } -} 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 47cb2f89f8b..7589dd06a91 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy @@ -1,5 +1,6 @@ package com.datadog.appsec.ddwaf +import com.datadog.appsec.AppSecModule.AppSecModuleActivationException import com.datadog.appsec.AppSecSystem import com.datadog.appsec.config.AppSecConfigService import com.datadog.appsec.config.AppSecConfigServiceImpl @@ -150,6 +151,7 @@ class WAFModuleSpecification extends DDSpecification { listener.accept(config, json.getBytes(), null) } + void 'override on_match through reconfiguration'() { ChangeableFlow flow = Mock() @@ -206,7 +208,6 @@ class WAFModuleSpecification extends DDSpecification { rba.statusCode == 501 && rba.blockingContentType == BlockingContentType.JSON }) - 1 * ctx.setKeepType(_) 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * tracer.activeSpan() @@ -216,6 +217,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() 1 * flow.isBlocking() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 0 * _ } @@ -240,7 +242,6 @@ class WAFModuleSpecification extends DDSpecification { rba.statusCode == 403 && rba.blockingContentType == BlockingContentType.AUTO }) - 1 * ctx.setKeepType(_) 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) >> { wafContext = new WafContext(it[0]) } @@ -251,6 +252,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() 1 * flow.isBlocking() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 0 * _ when: 'merges new waf data with the one in the rules config' @@ -285,8 +287,8 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() 1 * flow.isBlocking() - 1 * ctx.setKeepType(_) 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 0 * _ when: @@ -310,7 +312,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() 1 * flow.isBlocking() 1 * ctx.isThrottled(null) - 1 * ctx.setKeepType(_) + 1 * ctx.setManuallyKept(true) 0 * _ when: 'changes the rules config' @@ -362,7 +364,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() 1 * flow.isBlocking() 1 * ctx.isThrottled(null) - 1 * ctx.setKeepType(_) + 1 * ctx.setManuallyKept(true) 0 * _ when: @@ -378,7 +380,6 @@ class WAFModuleSpecification extends DDSpecification { 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() - 1 * ctx.setKeepType(_) 0 * _ } @@ -435,7 +436,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) - 1 * ctx.setKeepType(_) + 1 * ctx.setManuallyKept(true) 0 * _ when: @@ -453,7 +454,6 @@ class WAFModuleSpecification extends DDSpecification { 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() - 1 * ctx.setKeepType(_) 0 * _ } @@ -512,8 +512,8 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() 1 * ctx.setWafBlocked() - 1 * ctx.setKeepType(_) 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 0 * _ } @@ -578,9 +578,9 @@ class WAFModuleSpecification extends DDSpecification { 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() - 1 * ctx.setKeepType(_) 1 * flow.isBlocking() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 0 * _ } @@ -602,7 +602,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.reportEvents(_) 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) - 1 * ctx.setKeepType(_) + 1 * ctx.setManuallyKept(true) 0 * ctx._(*_) flow.blocking flow.action instanceof Flow.Action.RequestBlockingAction @@ -666,7 +666,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.reportEvents(_) 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) - 1 * ctx.setKeepType(_) + 1 * ctx.setManuallyKept(true) 0 * ctx._(*_) flow.blocking flow.action.statusCode == 418 @@ -694,7 +694,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.reportEvents(_) 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) - 1 * ctx.setKeepType(_) + 1 * ctx.setManuallyKept(true) 0 * ctx._(*_) metrics == null } @@ -745,8 +745,8 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.reportEvents(*_) 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 1 * ctx.isWafContextClosed() >> false - 1 * ctx.setKeepType(_) 0 * ctx._(*_) flow.blocking } @@ -1005,10 +1005,10 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.isWafContextClosed() 2 * ctx.getWafMetrics() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 1 * ctx.closeWafContext() 2 * tracer.activeSpan() 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) - 1 * ctx.setKeepType(_) 0 * _ } @@ -1046,8 +1046,8 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() 1 * flow.isBlocking() - 1 * ctx.setKeepType(_) 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 0 * _ } @@ -1113,7 +1113,6 @@ class WAFModuleSpecification extends DDSpecification { 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() - 1 * ctx.setKeepType(_) _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ @@ -1132,7 +1131,6 @@ class WAFModuleSpecification extends DDSpecification { 1 * reconf.reloadSubscriptions() 1 * ctx.closeWafContext() 2 * ctx.getWafMetrics() - 1 * ctx.setKeepType(_) _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ @@ -1154,8 +1152,8 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() 1 * flow.isBlocking() - 1 * ctx.setKeepType(_) 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ @@ -1173,7 +1171,6 @@ class WAFModuleSpecification extends DDSpecification { 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() - 1 * ctx.setKeepType(_) _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ @@ -1202,7 +1199,6 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() >> { wafContext.close() } - 1 * ctx.setKeepType(_) _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ @@ -1225,7 +1221,6 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() >> { wafContext.close() } - 1 * ctx.setKeepType(_) _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ @@ -1253,10 +1248,10 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() >> { wafContext.close() } - 1 * ctx.setKeepType(_) _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 0 * _ when: 'removing c and a removes c and a, allows earlier toggle to take effect' @@ -1279,7 +1274,6 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() >> { wafContext.close() } - 1 * ctx.setKeepType(_) _ * ctx.increaseWafTimeouts() _ * ctx.increaseRaspTimeouts() 0 * _ @@ -1332,8 +1326,9 @@ class WAFModuleSpecification extends DDSpecification { initialRuleAddWithMap(waf) then: - thrown RuntimeException + thrown AppSecModuleActivationException wafModule.dataSubscriptions.empty + 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, false) 0 * _ } @@ -1344,8 +1339,10 @@ class WAFModuleSpecification extends DDSpecification { initialRuleAddWithMap(waf) then: - thrown RuntimeException + thrown AppSecModuleActivationException wafModule.ctxAndAddresses.get() == null + // WAF initialization is attempted but fails, so wafInit is called with success=false + 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, false) 0 * _ } @@ -1394,8 +1391,8 @@ class WAFModuleSpecification extends DDSpecification { 2 * ctx.getWafMetrics() 1 * flow.isBlocking() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 1 * ctx.isWafContextClosed() >> false - 1 * ctx.setKeepType(_) 0 * _ when: @@ -1409,11 +1406,11 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.reportEvents(_ as Collection) >> { it[0].iterator().next().ruleMatches[0].parameters[0].value == 'user-to-block-1' } - 1 * ctx.setKeepType(_) 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 1 * flow.isBlocking() 0 * _ } @@ -1486,9 +1483,9 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * ctx.getWafMetrics() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 1 * ctx.reportEvents(_ as Collection) 1 * ctx.closeWafContext() - 1 * ctx.setKeepType(_) 1 * ctx.isWafContextClosed() 2 * tracer.activeSpan() 1 * flow.isBlocking() @@ -1519,10 +1516,10 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.getOrCreateWafContext(_ as WafHandle, true, false) 2 * ctx.getWafMetrics() 1 * ctx.isThrottled(null) + 1 * ctx.setManuallyKept(true) 1 * ctx.reportEvents(_ as Collection) 1 * ctx.closeWafContext() 2 * tracer.activeSpan() - 1 * ctx.setKeepType(_) 0 * _ } @@ -1904,7 +1901,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() 1 * ctx.reportDerivatives(['_dd.appsec.trace.agent':'RulesCompat/v1', '_dd.appsec.trace.integer': 123456789]) 1 * ctx.isThrottled(null) - 1 * ctx.setKeepType(_) + 1 * ctx.reportEvents([]) 0 * ctx._(*_) !flow1.blocking @@ -1922,7 +1919,8 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() 1 * ctx.reportDerivatives(['_dd.appsec.trace.agent':'RulesCompat/v2', '_dd.appsec.trace.integer': 987654321]) 1 * ctx.isThrottled(null) - 1 * ctx.setKeepType(_) + 1 * ctx.setManuallyKept(true) + 1 * ctx.reportEvents([]) 0 * ctx._(*_) !flow2.blocking @@ -1941,11 +1939,114 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.reportDerivatives(['_dd.appsec.trace.agent':'RulesCompat/v3', '_dd.appsec.trace.integer': 555666777]) 1 * ctx.reportEvents(_ as Collection) 1 * ctx.isThrottled(null) - 1 * ctx.setKeepType(_) + 1 * ctx.setManuallyKept(true) 0 * ctx._(*_) !flow3.blocking } + void 'test trace tagging rule with attributes, no keep and event (dynamic value extraction)'() { + setup: + def rulesConfig = [ + version: '2.1', + metadata: [ + rules_version: '1.2.7' + ], + rules: [ + [ + id: 'arachni_rule', + 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'] + ] + ], + rules_compat: [ + [ + id: 'ttr-000-004', + name: 'Trace Tagging Rule: Attributes, No Keep, Event', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^TraceTagging\\/v4' + ], + operator: 'match_regex' + ] + ], + output: [ + event: true, + keep: false, + attributes: [ + '_dd.appsec.trace.integer': [ + value: 1729 + ], + '_dd.appsec.trace.agent': [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ] + ], + on_match: [] + ] + ] + ] + + when: + initialRuleAddWithMap(rulesConfig) + wafModule.applyConfig(reconf) + + then: + 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) + 1 * wafMetricCollector.wafUpdates(_, true) + 1 * reconf.reloadSubscriptions() + 0 * _ + + when: 'test trace tagging rule with attributes, no keep and event (dynamic value extraction)' + def bundle = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, + new CaseInsensitiveMap>(['user-agent': 'TraceTagging/v4'])) + def flow = new ChangeableFlow() + dataListener.onDataAvailable(flow, ctx, bundle, gwCtx) + ctx.closeWafContext() + + then: + 1 * ctx.getOrCreateWafContext(_, true, false) + 2 * ctx.getWafMetrics() >> metrics + 1 * ctx.isWafContextClosed() >> false + 1 * ctx.closeWafContext() + // Should report derivatives with dynamic value extraction - the user-agent value should be extracted + 1 * ctx.reportDerivatives(['_dd.appsec.trace.agent':'TraceTagging/v4', '_dd.appsec.trace.integer': 1729]) + 1 * ctx.reportEvents(_ as Collection) + 1 * ctx.isThrottled(null) + 0 * ctx._(*_) + !flow.blocking // Should not block since keep: false + } + private static class BadConfig implements Map { @Delegate private Map delegate diff --git a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/TraceTaggingSmokeTest.groovy b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/TraceTaggingSmokeTest.groovy index 5a10eb7a197..cebd1fb6028 100644 --- a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/TraceTaggingSmokeTest.groovy +++ b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/TraceTaggingSmokeTest.groovy @@ -154,6 +154,42 @@ class TraceTaggingSmokeTest extends AbstractAppSecServerSmokeTest { ] ], on_match: [] + ], + [ + id: 'ttr-000-004', + name: 'Trace Tagging Rule: Attributes, No Keep, Event', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^TraceTagging\\/v4' + ], + operator: 'match_regex' + ] + ], + output: [ + event: true, + keep: false, + attributes: [ + '_dd.appsec.trace.integer': [ + value: 1729 + ], + '_dd.appsec.trace.agent': [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ] + ], + on_match: [] ] ] ] @@ -265,4 +301,38 @@ class TraceTaggingSmokeTest extends AbstractAppSecServerSmokeTest { assert appsecJson.triggers != null, "Missing triggers in WAF attack event" } + def "test trace tagging rule with attributes, no keep and event"() { + when: + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "TraceTagging/v4") + .build() + def response = client.newCall(request).execute() + def responseBodyStr = response.body().string() + waitForTraceCount(1) + + then: + responseBodyStr == "Sup AppSec Dawg" + response.code() == 200 + rootSpans.size() == 1 + + def rootSpan = rootSpans[0] + assert rootSpan.meta['_dd.appsec.trace.agent'] != null, "Missing _dd.appsec.trace.agent from span's meta" + assert rootSpan.metrics['_dd.appsec.trace.integer'] != null, "Missing _dd.appsec.trace.integer from span's metrics" + + assert rootSpan.meta['_dd.appsec.trace.agent'].startsWith("TraceTagging/v4") + assert rootSpan.metrics['_dd.appsec.trace.integer'] == 1729 + + // Check for WAF attack event (should exist since event: true) + assert rootSpan.meta['_dd.appsec.json'] != null, "Missing WAF attack event" + def appsecJson = new JsonSlurper().parseText(rootSpan.meta['_dd.appsec.json']) + assert appsecJson.triggers != null, "Missing triggers in WAF attack event" + + // Should NOT have USER_KEEP sampling priority since keep: false + assert rootSpan.metrics.get('_sampling_priority_v1') < 2, + "Should not have USER_KEEP sampling priority when keep: false" + + } + }