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 d21293e1dd0..63253985a08 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 @@ -40,7 +40,6 @@ import datadog.trace.api.gateway.Flow; import datadog.trace.api.telemetry.LogCollector; import datadog.trace.api.telemetry.WafMetricCollector; -import datadog.trace.api.telemetry.WafTruncatedType; import datadog.trace.api.time.SystemTimeSource; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; @@ -440,7 +439,6 @@ public void onDataAvailable( WafMetricCollector.get().raspTimeout(gwCtx.raspRuleType); } else { reqCtx.increaseWafTimeouts(); - WafMetricCollector.get().wafRequestTimeout(); log.debug(LogCollector.EXCLUDE_TELEMETRY, "Timeout calling the WAF", tpe); } return; @@ -448,12 +446,10 @@ public void onDataAvailable( if (!reqCtx.isWafContextClosed()) { log.error("Error calling WAF", e); } - // TODO this is wrong and will be fixed in another PR - WafMetricCollector.get().wafRequestError(); - incrementErrorCodeMetric(gwCtx, e.code); + incrementErrorCodeMetric(reqCtx, gwCtx, e.code); return; } catch (AbstractWafException e) { - incrementErrorCodeMetric(gwCtx, e.code); + incrementErrorCodeMetric(reqCtx, gwCtx, e.code); return; } finally { if (log.isDebugEnabled()) { @@ -467,17 +463,8 @@ public void onDataAvailable( final long listMapTooLarge = wafMetrics.getTruncatedListMapTooLargeCount(); final long objectTooDeep = wafMetrics.getTruncatedObjectTooDeepCount(); - if (stringTooLong > 0) { - WafMetricCollector.get() - .wafInputTruncated(WafTruncatedType.STRING_TOO_LONG, stringTooLong); - } - if (listMapTooLarge > 0) { - WafMetricCollector.get() - .wafInputTruncated(WafTruncatedType.LIST_MAP_TOO_LARGE, listMapTooLarge); - } - if (objectTooDeep > 0) { - WafMetricCollector.get() - .wafInputTruncated(WafTruncatedType.OBJECT_TOO_DEEP, objectTooDeep); + if (stringTooLong > 0 || listMapTooLarge > 0 || objectTooDeep > 0) { + reqCtx.setWafTruncated(); } } } @@ -501,10 +488,12 @@ public void onDataAvailable( ActionInfo actionInfo = new ActionInfo(actionType, actionParams); if ("block_request".equals(actionInfo.type)) { - Flow.Action.RequestBlockingAction rba = createBlockRequestAction(actionInfo); + Flow.Action.RequestBlockingAction rba = + createBlockRequestAction(actionInfo, reqCtx, gwCtx.isRasp); flow.setAction(rba); } else if ("redirect_request".equals(actionInfo.type)) { - Flow.Action.RequestBlockingAction rba = createRedirectRequestAction(actionInfo); + Flow.Action.RequestBlockingAction rba = + createRedirectRequestAction(actionInfo, reqCtx, gwCtx.isRasp); flow.setAction(rba); } else if ("generate_stack".equals(actionInfo.type)) { if (Config.get().isAppSecStackTraceEnabled()) { @@ -516,7 +505,9 @@ public void onDataAvailable( } } else { log.info("Ignoring action with type {}", actionInfo.type); - WafMetricCollector.get().wafRequestBlockFailure(); + if (!gwCtx.isRasp) { + reqCtx.setWafRequestBlockFailure(); + } } } Collection events = buildEvents(resultWithData); @@ -543,12 +534,16 @@ public void onDataAvailable( reqCtx.reportEvents(events); } else { log.debug("Rate limited WAF events"); - WafMetricCollector.get().wafRequestRateLimited(); + if (!gwCtx.isRasp) { + reqCtx.setWafRateLimited(); + } } } if (flow.isBlocking()) { - reqCtx.setBlocked(); + if (!gwCtx.isRasp) { + reqCtx.setWafBlocked(); + } } } @@ -557,7 +552,8 @@ public void onDataAvailable( } } - private Flow.Action.RequestBlockingAction createBlockRequestAction(ActionInfo actionInfo) { + private Flow.Action.RequestBlockingAction createBlockRequestAction( + final ActionInfo actionInfo, final AppSecRequestContext reqCtx, final boolean isRasp) { try { int statusCode; Object statusCodeObj = actionInfo.parameters.get("status_code"); @@ -578,12 +574,15 @@ private Flow.Action.RequestBlockingAction createBlockRequestAction(ActionInfo ac return new Flow.Action.RequestBlockingAction(statusCode, blockingContentType); } catch (RuntimeException cce) { log.warn("Invalid blocking action data", cce); - WafMetricCollector.get().wafRequestBlockFailure(); + if (!isRasp) { + reqCtx.setWafRequestBlockFailure(); + } return null; } } - private Flow.Action.RequestBlockingAction createRedirectRequestAction(ActionInfo actionInfo) { + private Flow.Action.RequestBlockingAction createRedirectRequestAction( + final ActionInfo actionInfo, final AppSecRequestContext reqCtx, final boolean isRasp) { try { int statusCode; Object statusCodeObj = actionInfo.parameters.get("status_code"); @@ -604,7 +603,9 @@ private Flow.Action.RequestBlockingAction createRedirectRequestAction(ActionInfo return Flow.Action.RequestBlockingAction.forRedirect(statusCode, location); } catch (RuntimeException cce) { log.warn("Invalid blocking action data", cce); - WafMetricCollector.get().wafRequestBlockFailure(); + if (!isRasp) { + reqCtx.setWafRequestBlockFailure(); + } return null; } } @@ -649,11 +650,13 @@ private Waf.ResultWithData runWafContext( } } - private static void incrementErrorCodeMetric(GatewayContext gwCtx, int code) { + private static void incrementErrorCodeMetric( + AppSecRequestContext reqCtx, GatewayContext gwCtx, int code) { if (gwCtx.isRasp) { WafMetricCollector.get().raspErrorCode(gwCtx.raspRuleType, code); } else { WafMetricCollector.get().wafErrorCode(code); + reqCtx.setWafErrors(); } } 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 fb3138f35dd..6314b76ba44 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 @@ -119,7 +119,13 @@ public class AppSecRequestContext implements DataBundle, Closeable { private volatile WafMetrics wafMetrics; private volatile WafMetrics raspMetrics; private final AtomicInteger raspMetricsCounter = new AtomicInteger(0); - private volatile boolean blocked; + + private volatile boolean wafBlocked; + private volatile boolean wafErrors; + private volatile boolean wafTruncated; + private volatile boolean wafRequestBlockFailure; + private volatile boolean wafRateLimited; + private volatile int wafTimeouts; private volatile int raspTimeouts; @@ -174,12 +180,44 @@ public AtomicInteger getRaspMetricsCounter() { return raspMetricsCounter; } - public void setBlocked() { - this.blocked = true; + public void setWafBlocked() { + this.wafBlocked = true; + } + + public boolean isWafBlocked() { + return wafBlocked; + } + + public void setWafErrors() { + this.wafErrors = true; + } + + public boolean hasWafErrors() { + return wafErrors; + } + + public void setWafTruncated() { + this.wafTruncated = true; + } + + public boolean isWafTruncated() { + return wafTruncated; + } + + public void setWafRequestBlockFailure() { + this.wafRequestBlockFailure = true; + } + + public boolean isWafRequestBlockFailure() { + return wafRequestBlockFailure; + } + + public void setWafRateLimited() { + this.wafRateLimited = true; } - public boolean isBlocked() { - return blocked; + public boolean isWafRateLimited() { + return wafRateLimited; } public void increaseWafTimeouts() { 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 317215a0c24..7d29e6ee8b0 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 @@ -724,13 +724,16 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) { log.debug("Unable to commit, derivatives will be skipped {}", ctx.getDerivativeKeys()); } - if (ctx.isBlocked()) { - WafMetricCollector.get().wafRequestBlocked(); - } else if (!collectedEvents.isEmpty()) { - WafMetricCollector.get().wafRequestTriggered(); - } else { - WafMetricCollector.get().wafRequest(); - } + WafMetricCollector.get() + .wafRequest( + !collectedEvents.isEmpty(), // ruleTriggered + ctx.isWafBlocked(), // requestBlocked + ctx.hasWafErrors(), // wafError + ctx.getWafTimeouts() > 0, // wafTimeout, + ctx.isWafRequestBlockFailure(), // blockFailure, + ctx.isWafRateLimited(), // rateLimited, + ctx.isWafTruncated() // inputTruncated + ); } ctx.close(); 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 6962e99093b..773c624704c 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 @@ -475,7 +475,7 @@ class WAFModuleSpecification extends DDSpecification { 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() >> { wafContext.close() } - 1 * ctx.setBlocked() + 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 0 * _ @@ -560,7 +560,7 @@ class WAFModuleSpecification extends DDSpecification { 2 * ctx.getWafMetrics() 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() - 1 * ctx.setBlocked() + 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 0 * _ } @@ -665,7 +665,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() 1 * ctx.reportEvents(_) - 1 * ctx.setBlocked() + 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 0 * ctx._(*_) flow.blocking == true @@ -731,7 +731,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() 1 * ctx.reportEvents(_) - 1 * ctx.setBlocked() + 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 0 * ctx._(*_) flow.blocking == true @@ -758,7 +758,7 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.isWafContextClosed() >> false 1 * ctx.closeWafContext() 1 * ctx.reportEvents(_) - 1 * ctx.setBlocked() + 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 0 * ctx._(*_) metrics == null @@ -817,7 +817,7 @@ class WAFModuleSpecification extends DDSpecification { } 2 * ctx.getWafMetrics() >> metrics 1 * ctx.reportEvents(*_) - 1 * ctx.setBlocked() + 1 * ctx.setWafBlocked() 1 * ctx.isThrottled(null) 1 * ctx.isWafContextClosed() >> false 0 * ctx._(*_) @@ -1016,7 +1016,6 @@ class WAFModuleSpecification extends DDSpecification { wafContext = it[0].openContext() } 2 * ctx.getWafMetrics() 1 * ctx.increaseWafTimeouts() - 1 * wafMetricCollector.get().wafRequestTimeout() 0 * _ when: @@ -1787,7 +1786,6 @@ class WAFModuleSpecification extends DDSpecification { 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) 1 * ctx.getRaspMetrics() 1 * ctx.getRaspMetricsCounter() - (0..1) * WafMetricCollector.get().wafRequestError() // TODO: remove this line when WAFModule is removed 1 * wafMetricCollector.raspErrorCode(RuleType.SQL_INJECTION, wafErrorCode.code) 0 * _ @@ -1816,8 +1814,8 @@ class WAFModuleSpecification extends DDSpecification { 1 * wafContext.run(_, _, _) >> { throw createWafException(wafErrorCode) } 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) 2 * ctx.getWafMetrics() - (0..1) * WafMetricCollector.get().wafRequestError() // TODO: remove this line when WAFModule is removed 1 * wafMetricCollector.wafErrorCode(wafErrorCode.code) + 1 * ctx.setWafErrors() 0 * _ where: diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy index c2453c52bf2..df6bd6d24b3 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy @@ -20,10 +20,12 @@ import datadog.trace.api.gateway.SubscriptionService import datadog.trace.api.http.StoredBodySupplier import datadog.trace.api.internal.TraceSegment import datadog.trace.api.telemetry.LoginEvent +import datadog.trace.api.telemetry.WafMetricCollector import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter import datadog.trace.bootstrap.instrumentation.api.URIDataAdapterBase import datadog.trace.test.util.DDSpecification +import spock.lang.Shared import java.util.function.BiConsumer import java.util.function.BiFunction @@ -37,6 +39,9 @@ import static datadog.trace.api.telemetry.LoginEvent.SIGN_UP class GatewayBridgeSpecification extends DDSpecification { + @Shared + protected static final ORIGINAL_METRIC_COLLECTOR = WafMetricCollector.get() + private static final String USER_ID = 'user' SubscriptionService ig = Mock() @@ -106,12 +111,16 @@ class GatewayBridgeSpecification extends DDSpecification { BiFunction> userCB TriFunction> loginEventCB + WafMetricCollector wafMetricCollector = Mock(WafMetricCollector) + void setup() { callInitAndCaptureCBs() AppSecSystem.active = true + WafMetricCollector.INSTANCE = wafMetricCollector } void cleanup() { + WafMetricCollector.INSTANCE = ORIGINAL_METRIC_COLLECTOR bridge.stop() } @@ -169,6 +178,13 @@ class GatewayBridgeSpecification extends DDSpecification { 1 * traceSegment.setTagTop('http.request.headers.accept', 'header_value') 1 * traceSegment.setTagTop('http.response.headers.content-type', 'text/html; charset=UTF-8') 1 * traceSegment.setTagTop('network.client.ip', '2001::1') + 1 * mockAppSecCtx.isWafBlocked() + 1 * mockAppSecCtx.hasWafErrors() + 1 * mockAppSecCtx.getWafTimeouts() + 1 * mockAppSecCtx.isWafRequestBlockFailure() + 1 * mockAppSecCtx.isWafRateLimited() + 1 * mockAppSecCtx.isWafTruncated() + 1 * wafMetricCollector.wafRequest(_, _, _, _, _, _, _) // call waf request metric flow.result == null flow.action == Flow.Action.Noop.INSTANCE } diff --git a/internal-api/build.gradle b/internal-api/build.gradle index ff2d609cb15..dd5ee2ce0cc 100644 --- a/internal-api/build.gradle +++ b/internal-api/build.gradle @@ -203,6 +203,8 @@ excludedClassesCoverage += [ 'datadog.trace.api.telemetry.LogCollector.RawLogMessage', "datadog.trace.api.telemetry.MetricCollector.DistributionSeriesPoint", "datadog.trace.api.telemetry.MetricCollector", + //Enum + "datadog.trace.api.telemetry.WafTruncatedType", // stubs 'datadog.trace.api.profiling.Timing.NoOp', 'datadog.trace.api.profiling.Timer.NoOp', diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java b/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java index 0ea292c7e97..77431d7f762 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java @@ -9,7 +9,6 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongArray; public class WafMetricCollector implements MetricCollector { @@ -32,17 +31,9 @@ private WafMetricCollector() { private static final AtomicInteger wafInitCounter = new AtomicInteger(); private static final AtomicInteger wafUpdatesCounter = new AtomicInteger(); - private static final AtomicRequestCounter wafRequestCounter = new AtomicRequestCounter(); - private static final AtomicRequestCounter wafTriggeredRequestCounter = new AtomicRequestCounter(); - private static final AtomicRequestCounter wafBlockedRequestCounter = new AtomicRequestCounter(); - private static final AtomicRequestCounter wafTimeoutRequestCounter = new AtomicRequestCounter(); - private static final AtomicRequestCounter wafErrorRequestCounter = new AtomicRequestCounter(); - private static final AtomicRequestCounter wafRateLimitedRequestCounter = - new AtomicRequestCounter(); - private static final AtomicRequestCounter wafBlockFailureRequestCounter = - new AtomicRequestCounter(); - private static final AtomicLongArray wafInputTruncatedCounter = - new AtomicLongArray(WafTruncatedType.values().length); + private static final int WAF_REQUEST_COMBINATIONS = 128; // 2^7 + private final AtomicLongArray wafRequestCounter = new AtomicLongArray(WAF_REQUEST_COMBINATIONS); + private static final AtomicLongArray raspRuleEvalCounter = new AtomicLongArray(RuleType.getNumValues()); private static final AtomicLongArray raspRuleSkippedCounter = @@ -93,36 +84,43 @@ public void wafUpdates(final String rulesVersion, final boolean success) { WafMetricCollector.rulesVersion = rulesVersion; } - public void wafRequest() { - wafRequestCounter.increment(); - } - - public void wafRequestTriggered() { - wafTriggeredRequestCounter.increment(); - } - - public void wafRequestBlocked() { - wafBlockedRequestCounter.increment(); - } - - public void wafRequestTimeout() { - wafTimeoutRequestCounter.increment(); - } - - public void wafRequestError() { - wafErrorRequestCounter.increment(); - } - - public void wafRequestRateLimited() { - wafRateLimitedRequestCounter.increment(); - } - - public void wafRequestBlockFailure() { - wafBlockFailureRequestCounter.increment(); - } - - public void wafInputTruncated(final WafTruncatedType wafTruncatedType, long increment) { - wafInputTruncatedCounter.addAndGet(wafTruncatedType.ordinal(), increment); + public void wafRequest( + final boolean ruleTriggered, + final boolean requestBlocked, + final boolean wafError, + final boolean wafTimeout, + final boolean blockFailure, + final boolean rateLimited, + final boolean inputTruncated) { + int index = + computeWafRequestIndex( + ruleTriggered, + requestBlocked, + wafError, + wafTimeout, + blockFailure, + rateLimited, + inputTruncated); + wafRequestCounter.incrementAndGet(index); + } + + static int computeWafRequestIndex( + boolean ruleTriggered, + boolean requestBlocked, + boolean wafError, + boolean wafTimeout, + boolean blockFailure, + boolean rateLimited, + boolean inputTruncated) { + int index = 0; + if (ruleTriggered) index |= 1; + if (requestBlocked) index |= 1 << 1; + if (wafError) index |= 1 << 2; + if (wafTimeout) index |= 1 << 3; + if (blockFailure) index |= 1 << 4; + if (rateLimited) index |= 1 << 5; + if (inputTruncated) index |= 1 << 6; + return index; } public void raspRuleEval(final RuleType ruleType) { @@ -188,103 +186,33 @@ public Collection drain() { @Override public void prepareMetrics() { - final boolean isRateLimited = wafRateLimitedRequestCounter.getAndReset() > 0; - final boolean isBlockFailure = wafBlockFailureRequestCounter.getAndReset() > 0; - boolean isWafInputTruncated = false; - for (WafTruncatedType wafTruncatedType : WafTruncatedType.values()) { - isWafInputTruncated = wafInputTruncatedCounter.getAndSet(wafTruncatedType.ordinal(), 0) > 0; - if (isWafInputTruncated) { - break; - } - } // Requests - if (wafRequestCounter.get() > 0) { - if (!rawMetricsQueue.offer( - new WafRequestsRawMetric( - wafRequestCounter.getAndReset(), - WafMetricCollector.wafVersion, - WafMetricCollector.rulesVersion, - false, - false, - false, - false, - isBlockFailure, - isRateLimited, - isWafInputTruncated))) { - return; - } - } - - // Triggered requests - if (wafTriggeredRequestCounter.get() > 0) { - if (!rawMetricsQueue.offer( - new WafRequestsRawMetric( - wafTriggeredRequestCounter.getAndReset(), - WafMetricCollector.wafVersion, - WafMetricCollector.rulesVersion, - true, - false, - false, - false, - isBlockFailure, - isRateLimited, - isWafInputTruncated))) { - return; - } - } - - // Blocked requests - if (wafBlockedRequestCounter.get() > 0) { - if (!rawMetricsQueue.offer( - new WafRequestsRawMetric( - wafBlockedRequestCounter.getAndReset(), - WafMetricCollector.wafVersion, - WafMetricCollector.rulesVersion, - true, - true, - false, - false, - isBlockFailure, - isRateLimited, - isWafInputTruncated))) { - return; - } - } - - // Timeout requests - if (wafTimeoutRequestCounter.get() > 0) { - if (!rawMetricsQueue.offer( - new WafRequestsRawMetric( - wafTimeoutRequestCounter.getAndReset(), - WafMetricCollector.wafVersion, - WafMetricCollector.rulesVersion, - false, - false, - false, - true, - isBlockFailure, - isRateLimited, - isWafInputTruncated))) { - return; - } - } + for (int i = 0; i < WAF_REQUEST_COMBINATIONS; i++) { + long counter = wafRequestCounter.getAndSet(i, 0); + if (counter > 0) { + boolean ruleTriggered = (i & 1) != 0; + boolean requestBlocked = (i & (1 << 1)) != 0; + boolean wafError = (i & (1 << 2)) != 0; + boolean wafTimeout = (i & (1 << 3)) != 0; + boolean blockFailure = (i & (1 << 4)) != 0; + boolean rateLimited = (i & (1 << 5)) != 0; + boolean inputTruncated = (i & (1 << 6)) != 0; - // WAF error requests - if (wafErrorRequestCounter.get() > 0) { - if (!rawMetricsQueue.offer( - new WafRequestsRawMetric( - wafErrorRequestCounter.getAndReset(), - WafMetricCollector.wafVersion, - WafMetricCollector.rulesVersion, - false, - false, - true, - false, - isBlockFailure, - isRateLimited, - isWafInputTruncated))) { - return; + if (!rawMetricsQueue.offer( + new WafRequestsRawMetric( + counter, + WafMetricCollector.wafVersion, + WafMetricCollector.rulesVersion, + ruleTriggered, + requestBlocked, + wafError, + wafTimeout, + blockFailure, + rateLimited, + inputTruncated))) { + return; + } } } @@ -588,28 +516,6 @@ public WafError(final long counter, final String wafVersion, final Integer ddwaf } } - public static class AtomicRequestCounter { - - private final AtomicLong atomicLong = new AtomicLong(); - private volatile long timestamp; - - public final long get() { - return atomicLong.get(); - } - - public final long getAndReset() { - timestamp = 0; - return atomicLong.getAndSet(0); - } - - public final void increment() { - if (timestamp == 0) { - timestamp = System.currentTimeMillis(); - } - atomicLong.incrementAndGet(); - } - } - /** * Mirror of the {@code WafErrorCode} enum defined in the {@code libddwaf-java} module. * diff --git a/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy index cbf489a58e2..f0ccb2f03e7 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy @@ -29,25 +29,15 @@ class WafMetricCollectorTest extends DDSpecification { WafMetricCollector.get().wafInit('waf_ver1', 'rules.1', true) WafMetricCollector.get().wafUpdates('rules.2', true) WafMetricCollector.get().wafUpdates('rules.3', false) - WafMetricCollector.get().wafRequest() - WafMetricCollector.get().wafRequest() - WafMetricCollector.get().wafRequest() - WafMetricCollector.get().wafRequestTriggered() - WafMetricCollector.get().wafRequestBlocked() - WafMetricCollector.get().wafRequestTimeout() - WafMetricCollector.get().wafRequestError() - WafMetricCollector.get().wafRequestRateLimited() - WafMetricCollector.get().wafRequestBlockFailure() - WafMetricCollector.get().wafInputTruncated(WafTruncatedType.STRING_TOO_LONG, 5) WafMetricCollector.get().raspRuleEval(RuleType.SQL_INJECTION) WafMetricCollector.get().raspRuleEval(RuleType.SQL_INJECTION) WafMetricCollector.get().raspRuleMatch(RuleType.SQL_INJECTION) WafMetricCollector.get().raspRuleEval(RuleType.SQL_INJECTION) WafMetricCollector.get().raspTimeout(RuleType.SQL_INJECTION) WafMetricCollector.get().raspErrorCode(RuleType.SHELL_INJECTION, DD_WAF_RUN_INTERNAL_ERROR) - WafMetricCollector.get().wafErrorCode(DD_WAF_RUN_INVALID_OBJECT_ERROR) - WafMetricCollector.get().raspErrorCode(RuleType.SQL_INJECTION, DD_WAF_RUN_INVALID_OBJECT_ERROR) WafMetricCollector.get().wafErrorCode(DD_WAF_RUN_INTERNAL_ERROR) + WafMetricCollector.get().raspErrorCode(RuleType.SQL_INJECTION, DD_WAF_RUN_INVALID_OBJECT_ERROR) + WafMetricCollector.get().wafErrorCode(DD_WAF_RUN_INVALID_OBJECT_ERROR) WafMetricCollector.get().raspRuleSkipped(RuleType.SQL_INJECTION) WafMetricCollector.get().prepareMetrics() @@ -76,113 +66,29 @@ class WafMetricCollectorTest extends DDSpecification { updateMetric2.metricName == 'waf.updates' updateMetric2.tags.toSet() == ['waf_version:waf_ver1', 'event_rules_version:rules.3', 'success:false'].toSet() - def requestMetric = (WafMetricCollector.WafRequestsRawMetric) metrics[3] - requestMetric.namespace == 'appsec' - requestMetric.metricName == 'waf.requests' - requestMetric.type == 'count' - requestMetric.value == 3 - requestMetric.tags.toSet() == [ - 'waf_version:waf_ver1', - 'event_rules_version:rules.3', - 'rule_triggered:false', - 'request_blocked:false', - 'waf_error:false', - 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', - ].toSet() - - def requestTriggeredMetric = (WafMetricCollector.WafRequestsRawMetric) metrics[4] - requestTriggeredMetric.namespace == 'appsec' - requestTriggeredMetric.metricName == 'waf.requests' - requestTriggeredMetric.value == 1 - requestTriggeredMetric.tags.toSet() == [ - 'waf_version:waf_ver1', - 'event_rules_version:rules.3', - 'rule_triggered:true', - 'request_blocked:false', - 'waf_error:false', - 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', - ].toSet() - - - def requestBlockedMetric = (WafMetricCollector.WafRequestsRawMetric) metrics[5] - requestBlockedMetric.namespace == 'appsec' - requestBlockedMetric.metricName == 'waf.requests' - requestBlockedMetric.type == 'count' - requestBlockedMetric.value == 1 - requestBlockedMetric.tags.toSet() == [ - 'waf_version:waf_ver1', - 'event_rules_version:rules.3', - 'rule_triggered:true', - 'request_blocked:true', - 'waf_error:false', - 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', - ].toSet() - - def requestTimeoutMetric = (WafMetricCollector.WafRequestsRawMetric)metrics[6] - requestTimeoutMetric.namespace == 'appsec' - requestTimeoutMetric.metricName == 'waf.requests' - requestTimeoutMetric.type == 'count' - requestTimeoutMetric.value == 1 - requestTimeoutMetric.tags.toSet() == [ - 'waf_version:waf_ver1', - 'event_rules_version:rules.3', - 'rule_triggered:false', - 'request_blocked:false', - 'waf_error:false', - 'waf_timeout:true', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', - ].toSet() - - def requestWafErrorMetric = (WafMetricCollector.WafRequestsRawMetric) metrics[7] - requestWafErrorMetric.namespace == 'appsec' - requestWafErrorMetric.metricName == 'waf.requests' - requestWafErrorMetric.type == 'count' - requestWafErrorMetric.value == 1 - requestWafErrorMetric.tags.toSet() == [ - 'waf_version:waf_ver1', - 'event_rules_version:rules.3', - 'rule_triggered:false', - 'request_blocked:false', - 'waf_error:true', - 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', - ].toSet() - def raspRuleEvalSqli = (WafMetricCollector.RaspRuleEval) metrics[8] + def raspRuleEvalSqli = (WafMetricCollector.RaspRuleEval) metrics[3] raspRuleEvalSqli.type == 'count' raspRuleEvalSqli.value == 3 raspRuleEvalSqli.namespace == 'appsec' raspRuleEvalSqli.metricName == 'rasp.rule.eval' raspRuleEvalSqli.tags.toSet() == ['rule_type:sql_injection', 'waf_version:waf_ver1'].toSet() - def raspRuleMatch = (WafMetricCollector.RaspRuleMatch) metrics[9] + def raspRuleMatch = (WafMetricCollector.RaspRuleMatch) metrics[4] raspRuleMatch.type == 'count' raspRuleMatch.value == 1 raspRuleMatch.namespace == 'appsec' raspRuleMatch.metricName == 'rasp.rule.match' raspRuleMatch.tags.toSet() == ['rule_type:sql_injection', 'waf_version:waf_ver1'].toSet() - def raspTimeout = (WafMetricCollector.RaspTimeout) metrics[10] + def raspTimeout = (WafMetricCollector.RaspTimeout) metrics[5] raspTimeout.type == 'count' raspTimeout.value == 1 raspTimeout.namespace == 'appsec' raspTimeout.metricName == 'rasp.timeout' raspTimeout.tags.toSet() == ['rule_type:sql_injection', 'waf_version:waf_ver1'].toSet() - def raspInvalidObjectCode = (WafMetricCollector.RaspError)metrics[11] + def raspInvalidObjectCode = (WafMetricCollector.RaspError)metrics[6] raspInvalidObjectCode.type == 'count' raspInvalidObjectCode.value == 1 raspInvalidObjectCode.namespace == 'appsec' @@ -194,7 +100,7 @@ class WafMetricCollectorTest extends DDSpecification { ] .toSet() - def raspInvalidCode = (WafMetricCollector.RaspError)metrics[12] + def raspInvalidCode = (WafMetricCollector.RaspError)metrics[7] raspInvalidCode.type == 'count' raspInvalidCode.value == 1 raspInvalidCode.namespace == 'appsec' @@ -207,7 +113,7 @@ class WafMetricCollectorTest extends DDSpecification { 'waf_error:' + DD_WAF_RUN_INTERNAL_ERROR ].toSet() - def wafInvalidCode = (WafMetricCollector.WafError)metrics[13] + def wafInvalidCode = (WafMetricCollector.WafError)metrics[8] wafInvalidCode.type == 'count' wafInvalidCode.value == 1 wafInvalidCode.namespace == 'appsec' @@ -218,7 +124,7 @@ class WafMetricCollectorTest extends DDSpecification { 'waf_error:' +DD_WAF_RUN_INVALID_OBJECT_ERROR ].toSet() - def wafInvalidObjectCode = (WafMetricCollector.WafError)metrics[14] + def wafInvalidObjectCode = (WafMetricCollector.WafError)metrics[9] wafInvalidObjectCode.type == 'count' wafInvalidObjectCode.value == 1 wafInvalidObjectCode.namespace == 'appsec' @@ -229,7 +135,7 @@ class WafMetricCollectorTest extends DDSpecification { 'waf_error:'+DD_WAF_RUN_INTERNAL_ERROR ].toSet() - def raspRuleSkipped = (WafMetricCollector.AfterRequestRaspRuleSkipped) metrics[15] + def raspRuleSkipped = (WafMetricCollector.AfterRequestRaspRuleSkipped) metrics[10] raspRuleSkipped.type == 'count' raspRuleSkipped.value == 1 raspRuleSkipped.namespace == 'appsec' @@ -262,7 +168,7 @@ class WafMetricCollectorTest extends DDSpecification { when: (0..limit * 2).each { - collector.wafRequest() + collector.raspRuleEval(RuleType.SQL_INJECTION) collector.prepareMetrics() } @@ -272,7 +178,7 @@ class WafMetricCollectorTest extends DDSpecification { when: (0..limit * 2).each { - collector.wafRequestTriggered() + collector.raspRuleEval(RuleType.SQL_INJECTION) collector.prepareMetrics() } @@ -282,7 +188,7 @@ class WafMetricCollectorTest extends DDSpecification { when: (0..limit * 2).each { - collector.wafRequestBlocked() + collector.raspRuleEval(RuleType.SQL_INJECTION) collector.prepareMetrics() } @@ -515,4 +421,60 @@ class WafMetricCollectorTest extends DDSpecification { loginFailure.metricName == 'sdk.event' loginFailure.tags == ['event_type:login_failure', 'sdk_version:v2'] } + + void 'test waf request metrics'() { + given: + def collector = WafMetricCollector.get() + + when: + collector.wafRequest( + triggered, + blocked, + wafError, + wafTimeout, + blockFailure, + rateLimited, + inputTruncated + ) + + then: + collector.prepareMetrics() + def metrics = collector.drain() + def requestMetrics = metrics.findAll { it.metricName == 'waf.requests' } + + final metric = requestMetrics[0] + metric.type == 'count' + metric.metricName == 'waf.requests' + metric.namespace == 'appsec' + metric.tags == [ + "waf_version:waf_ver1", + "event_rules_version:rules.1", + "rule_triggered:${triggered}", + "request_blocked:${blocked}", + "waf_error:${wafError}", + "waf_timeout:${wafTimeout}", + "block_failure:${blockFailure}", + "rate_limited:${rateLimited}", + "input_truncated:${inputTruncated}" + ] + + where: + [triggered, blocked, wafError, wafTimeout, blockFailure, rateLimited, inputTruncated] << allBooleanCombinations(7) + } + + /** + * Helper method to generate all combinations of n boolean values. + */ + static List> allBooleanCombinations(int n) { + int total = 1 << n + def combinations = [] + for (int i = 0; i < total; i++) { + def combo = [] + for (int j = 0; j < n; j++) { + combo << (((i >> j) & 1) == 1) + } + combinations << combo + } + return combinations + } } diff --git a/telemetry/src/test/groovy/datadog/telemetry/metric/WafMetricPeriodicActionSpecification.groovy b/telemetry/src/test/groovy/datadog/telemetry/metric/WafMetricPeriodicActionSpecification.groovy index d4cc604fef9..4d5459edcae 100644 --- a/telemetry/src/test/groovy/datadog/telemetry/metric/WafMetricPeriodicActionSpecification.groovy +++ b/telemetry/src/test/groovy/datadog/telemetry/metric/WafMetricPeriodicActionSpecification.groovy @@ -2,7 +2,6 @@ package datadog.telemetry.metric import datadog.telemetry.TelemetryService import datadog.telemetry.api.Metric -import datadog.trace.api.telemetry.WafTruncatedType import datadog.trace.api.telemetry.WafMetricCollector import datadog.trace.test.util.DDSpecification @@ -44,16 +43,16 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { void 'push waf request metrics and push into the telemetry'() { when: WafMetricCollector.get().wafInit('0.0.0', 'rules_ver_1', true) - WafMetricCollector.get().wafRequest() - WafMetricCollector.get().wafRequestTriggered() - WafMetricCollector.get().wafRequest() - WafMetricCollector.get().wafRequestBlocked() - WafMetricCollector.get().wafRequest() - WafMetricCollector.get().wafRequestTimeout() - WafMetricCollector.get().wafRequestError() - WafMetricCollector.get().wafRequestRateLimited() - WafMetricCollector.get().wafRequestBlockFailure() - WafMetricCollector.get().wafInputTruncated(WafTruncatedType.STRING_TOO_LONG, 5) + WafMetricCollector.get().wafRequest(false, false, false, false, false, false, false) + WafMetricCollector.get().wafRequest(true, false, false, false, false, false, false) + WafMetricCollector.get().wafRequest(false, false, false, false, false, false, false) + WafMetricCollector.get().wafRequest(false, true, false, false, false, false, false) + WafMetricCollector.get().wafRequest(false, false, false, false, false, false, false) + WafMetricCollector.get().wafRequest(false, false, false, true, false, false, false) + WafMetricCollector.get().wafRequest(false, false, true, false, false, false, false) + WafMetricCollector.get().wafRequest(false, false, false, false, false, true, false) + WafMetricCollector.get().wafRequest(false, false, false, false, true, false, false) + WafMetricCollector.get().wafRequest(false, false, false, false, false, false, true) WafMetricCollector.get().prepareMetrics() periodicAction.doIteration(telemetryService) @@ -73,9 +72,9 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { 'request_blocked:false', 'waf_error:false', 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', ] } ) 1 * telemetryService.addMetric( { Metric metric -> @@ -89,9 +88,9 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { 'request_blocked:false', 'waf_error:false', 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', ] } ) 1 * telemetryService.addMetric( { Metric metric -> @@ -101,13 +100,13 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { metric.tags == [ 'waf_version:0.0.0', 'event_rules_version:rules_ver_1', - 'rule_triggered:true', + 'rule_triggered:false', 'request_blocked:true', 'waf_error:false', 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', ] } ) 1 * telemetryService.addMetric( { Metric metric -> @@ -121,9 +120,9 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { 'request_blocked:false', 'waf_error:false', 'waf_timeout:true', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', ] } ) 1 * telemetryService.addMetric( { Metric metric -> @@ -137,8 +136,56 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { 'request_blocked:false', 'waf_error:true', 'waf_timeout:false', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', + ] + } ) + 1 * telemetryService.addMetric( { Metric metric -> + metric.namespace == 'appsec' && + metric.metric == 'waf.requests' && + metric.points[0][1] == 1 && + metric.tags == [ + 'waf_version:0.0.0', + 'event_rules_version:rules_ver_1', + 'rule_triggered:false', + 'request_blocked:false', + 'waf_error:false', + 'waf_timeout:false', 'block_failure:true', + 'rate_limited:false', + 'input_truncated:false', + ] + } ) + 1 * telemetryService.addMetric( { Metric metric -> + metric.namespace == 'appsec' && + metric.metric == 'waf.requests' && + metric.points[0][1] == 1 && + metric.tags == [ + 'waf_version:0.0.0', + 'event_rules_version:rules_ver_1', + 'rule_triggered:false', + 'request_blocked:false', + 'waf_error:false', + 'waf_timeout:false', + 'block_failure:false', 'rate_limited:true', + 'input_truncated:false', + ] + } ) + 1 * telemetryService.addMetric( { Metric metric -> + metric.namespace == 'appsec' && + metric.metric == 'waf.requests' && + metric.points[0][1] == 1 && + metric.tags == [ + 'waf_version:0.0.0', + 'event_rules_version:rules_ver_1', + 'rule_triggered:false', + 'request_blocked:false', + 'waf_error:false', + 'waf_timeout:false', + 'block_failure:false', + 'rate_limited:false', 'input_truncated:true', ] } ) @@ -146,14 +193,14 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { when: 'waf.updates happens' WafMetricCollector.get().wafUpdates('rules_ver_2', true) - WafMetricCollector.get().wafRequest() - WafMetricCollector.get().wafRequestTriggered() - WafMetricCollector.get().wafRequestBlocked() - WafMetricCollector.get().wafRequestTimeout() - WafMetricCollector.get().wafRequestError() - WafMetricCollector.get().wafRequestRateLimited() - WafMetricCollector.get().wafRequestBlockFailure() - WafMetricCollector.get().wafInputTruncated(WafTruncatedType.STRING_TOO_LONG, 5) + WafMetricCollector.get().wafRequest(false, false, false, false, false, false, false) + WafMetricCollector.get().wafRequest(true, false, false, false, false, false, false) + WafMetricCollector.get().wafRequest(false, true, false, false, false, false, false) + WafMetricCollector.get().wafRequest(false, false, false, true, false, false, false) + WafMetricCollector.get().wafRequest(false, false, true, false, false, false, false) + WafMetricCollector.get().wafRequest(false, false, false, false, false, true, false) + WafMetricCollector.get().wafRequest(false, false, false, false, true, false, false) + WafMetricCollector.get().wafRequest(false, false, false, false, false, false, true) WafMetricCollector.get().prepareMetrics() periodicAction.doIteration(telemetryService) @@ -173,9 +220,9 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { 'request_blocked:false', 'waf_error:false', 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', ] } ) 1 * telemetryService.addMetric( { Metric metric -> @@ -189,9 +236,9 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { 'request_blocked:false', 'waf_error:false', 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', ] } ) 1 * telemetryService.addMetric( { Metric metric -> @@ -201,13 +248,13 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { metric.tags == [ 'waf_version:0.0.0', 'event_rules_version:rules_ver_2', - 'rule_triggered:true', + 'rule_triggered:false', 'request_blocked:true', 'waf_error:false', 'waf_timeout:false', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', ] } ) 1 * telemetryService.addMetric( { Metric metric -> @@ -221,9 +268,9 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { 'request_blocked:false', 'waf_error:false', 'waf_timeout:true', - 'block_failure:true', - 'rate_limited:true', - 'input_truncated:true', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', ] } ) 1 * telemetryService.addMetric( { Metric metric -> @@ -237,8 +284,56 @@ class WafMetricPeriodicActionSpecification extends DDSpecification { 'request_blocked:false', 'waf_error:true', 'waf_timeout:false', + 'block_failure:false', + 'rate_limited:false', + 'input_truncated:false', + ] + } ) + 1 * telemetryService.addMetric( { Metric metric -> + metric.namespace == 'appsec' && + metric.metric == 'waf.requests' && + metric.points[0][1] == 1 && + metric.tags == [ + 'waf_version:0.0.0', + 'event_rules_version:rules_ver_2', + 'rule_triggered:false', + 'request_blocked:false', + 'waf_error:false', + 'waf_timeout:false', 'block_failure:true', + 'rate_limited:false', + 'input_truncated:false', + ] + } ) + 1 * telemetryService.addMetric( { Metric metric -> + metric.namespace == 'appsec' && + metric.metric == 'waf.requests' && + metric.points[0][1] == 1 && + metric.tags == [ + 'waf_version:0.0.0', + 'event_rules_version:rules_ver_2', + 'rule_triggered:false', + 'request_blocked:false', + 'waf_error:false', + 'waf_timeout:false', + 'block_failure:false', 'rate_limited:true', + 'input_truncated:false', + ] + } ) + 1 * telemetryService.addMetric( { Metric metric -> + metric.namespace == 'appsec' && + metric.metric == 'waf.requests' && + metric.points[0][1] == 1 && + metric.tags == [ + 'waf_version:0.0.0', + 'event_rules_version:rules_ver_2', + 'rule_triggered:false', + 'request_blocked:false', + 'waf_error:false', + 'waf_timeout:false', + 'block_failure:false', + 'rate_limited:false', 'input_truncated:true', ] } )