From 78d28d69cf9890ecfe00bbfe04e58b72a9e3b1b8 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Tue, 25 Mar 2025 12:20:32 +0100 Subject: [PATCH 1/2] Add appsec.rasp.rule.skipped RASP Metric --- .../appsec/gateway/AppSecRequestContext.java | 11 +++++++ .../appsec/powerwaf/PowerWAFModule.java | 3 ++ .../AppSecRequestContextSpecification.groovy | 11 ++++++- .../PowerWAFModuleSpecification.groovy | 24 ++++++++++++++ .../api/telemetry/WafMetricCollector.java | 32 +++++++++++++++++++ .../telemetry/WafMetricCollectorTest.groovy | 20 ++++++++++++ 6 files changed, 100 insertions(+), 1 deletion(-) 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 34536773602..4abd136954e 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 @@ -129,6 +129,7 @@ public class AppSecRequestContext implements DataBundle, Closeable { private volatile int raspInternalErrors; private volatile int raspInvalidObjectErrors; private volatile int raspInvalidArgumentErrors; + private volatile int raspRuleSkipped; private volatile int wafInternalErrors; private volatile int wafInvalidObjectErrors; private volatile int wafInvalidArgumentErrors; @@ -159,6 +160,8 @@ public class AppSecRequestContext implements DataBundle, Closeable { RASP_INVALID_ARGUMENT_ERRORS_UPDATER = AtomicIntegerFieldUpdater.newUpdater( AppSecRequestContext.class, "raspInvalidArgumentErrors"); + private static final AtomicIntegerFieldUpdater RASP_RULE_SKIPPED_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "raspRuleSkipped"); private static final AtomicIntegerFieldUpdater WAF_INTERNAL_ERRORS_UPDATER = AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "wafInternalErrors"); @@ -220,6 +223,10 @@ public void increaseRaspTimeouts() { RASP_TIMEOUTS_UPDATER.incrementAndGet(this); } + public void increaseRuleSkipped() { + RASP_RULE_SKIPPED_UPDATER.incrementAndGet(this); + } + public void increaseRaspErrorCode(int code) { switch (code) { case DD_WAF_RUN_INTERNAL_ERROR: @@ -260,6 +267,10 @@ public int getRaspTimeouts() { return raspTimeouts; } + public int getRaspRuleSkipped() { + return raspRuleSkipped; + } + public int getRaspError(int code) { switch (code) { case DD_WAF_RUN_INTERNAL_ERROR: diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java index e0f75569274..965c6c5c00b 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java @@ -417,6 +417,9 @@ public void onDataAvailable( if (reqCtx.isAdditiveClosed()) { log.debug("Skipped; the WAF context is closed"); + if (gwCtx.isRasp) { + WafMetricCollector.get().raspRuleSkipped(gwCtx.raspRuleType); + } return; } 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 92d22cb6113..ec97ba9d239 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 @@ -15,7 +15,7 @@ import io.sqreen.powerwaf.Additive import io.sqreen.powerwaf.Powerwaf import io.sqreen.powerwaf.PowerwafContext -class AppSecRequestContextSpecification extends DDSpecification { +class AppSecRequestContextSpecification extends DDSpecification { AppSecRequestContext ctx = new AppSecRequestContext() @@ -319,6 +319,15 @@ class AppSecRequestContextSpecification extends DDSpecification { ctx.getWafError(0) == 0 } + def "test increase and get raspRuleSkipped"() { + when: + ctx.increaseRuleSkipped() + ctx.increaseRuleSkipped() + + then: + ctx.getRaspRuleSkipped() == 2 + } + void 'close logs if request end was not called'() { given: TestLogCollector.enable() diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/powerwaf/PowerWAFModuleSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/powerwaf/PowerWAFModuleSpecification.groovy index d6f98339511..939f4194d5f 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/powerwaf/PowerWAFModuleSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/powerwaf/PowerWAFModuleSpecification.groovy @@ -1731,6 +1731,30 @@ class PowerWAFModuleSpecification extends DDSpecification { 0 * _ } + void 'raspRuleSkipped if rasp available and WAF context is closed'() { + setup: + ChangeableFlow flow = Mock() + GatewayContext gwCtxMock = new GatewayContext(false, RuleType.SQL_INJECTION) + + when: + setupWithStubConfigService('rules_with_data_config.json') + dataListener = pwafModule.dataSubscriptions.first() + + def bundle = MapDataBundle.of( + KnownAddresses.USER_ID, + 'legit-user' + ) + ctx.closeAdditive() + dataListener.onDataAvailable(flow, ctx, bundle, gwCtxMock) + + then: + 1 * ctx.closeAdditive() + 1 * ctx.isAdditiveClosed() >> true + 1 * wafMetricCollector.wafInit(Powerwaf.LIB_VERSION, _, true) + 1 * wafMetricCollector.raspRuleSkipped(RuleType.SQL_INJECTION) + 0 * _ + } + private Map getDefaultConfig() { def service = new StubAppSecConfigService() service.init() 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 cfabd3d49e5..91505599200 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 @@ -47,6 +47,8 @@ private WafMetricCollector() { new AtomicLongArray(WafTruncatedType.values().length); private static final AtomicLongArray raspRuleEvalCounter = new AtomicLongArray(RuleType.getNumValues()); + private static final AtomicLongArray raspRuleSkippedCounter = + new AtomicLongArray(RuleType.getNumValues()); private static final AtomicLongArray raspRuleMatchCounter = new AtomicLongArray(RuleType.getNumValues()); private static final AtomicLongArray raspTimeoutCounter = @@ -135,6 +137,10 @@ public void raspRuleEval(final RuleType ruleType) { raspRuleEvalCounter.incrementAndGet(ruleType.ordinal()); } + public void raspRuleSkipped(final RuleType ruleType) { + raspRuleSkippedCounter.incrementAndGet(ruleType.ordinal()); + } + public void raspRuleMatch(final RuleType ruleType) { raspRuleMatchCounter.incrementAndGet(ruleType.ordinal()); } @@ -347,6 +353,16 @@ public void prepareMetrics() { } } } + + // RASP rule skipped per rule type for after-request reason + for (RuleType ruleType : RuleType.values()) { + long counter = raspRuleSkippedCounter.getAndSet(ruleType.ordinal(), 0); + if (counter > 0) { + if (!rawMetricsQueue.offer(new AfterRequestRaspRuleSkipped(counter, ruleType))) { + return; + } + } + } } public abstract static class WafMetric extends MetricCollector.Metric { @@ -451,6 +467,22 @@ public RaspRuleEval(final long counter, final RuleType ruleType, final String wa } } + // Although rasp.rule.skipped reason could be before-request, there is no real case scenario + public static class AfterRequestRaspRuleSkipped extends WafMetric { + public AfterRequestRaspRuleSkipped(final long counter, final RuleType ruleType) { + super( + "rasp.rule.skipped", + counter, + ruleType.variant != null + ? new String[] { + "rule_type:" + ruleType.type, + "rule_variant:" + ruleType.variant, + "reason:" + "after-request" + } + : new String[] {"rule_type:" + ruleType.type, "reason:" + "after-request"}); + } + } + public static class RaspRuleMatch extends WafMetric { public RaspRuleMatch(final long counter, final RuleType ruleType, final String wafVersion) { super( 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 4eb8e7e6f03..cba2f0c63ea 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 @@ -43,6 +43,7 @@ class WafMetricCollectorTest extends DDSpecification { WafMetricCollector.get().wafErrorCode(RuleType.SHELL_INJECTION, DD_WAF_RUN_INTERNAL_ERROR) WafMetricCollector.get().raspErrorCode(RuleType.SQL_INJECTION, DD_WAF_RUN_INVALID_OBJECT_ERROR) WafMetricCollector.get().wafErrorCode(RuleType.SQL_INJECTION, DD_WAF_RUN_INVALID_OBJECT_ERROR) + WafMetricCollector.get().raspRuleSkipped(RuleType.SQL_INJECTION) WafMetricCollector.get().prepareMetrics() @@ -224,6 +225,13 @@ class WafMetricCollectorTest extends DDSpecification { 'waf_version:waf_ver1', 'waf_error:'+DD_WAF_RUN_INVALID_OBJECT_ERROR ].toSet() + + def raspRuleSkipped = (WafMetricCollector.AfterRequestRaspRuleSkipped)metrics[15] + raspRuleSkipped.type == 'count' + raspRuleSkipped.value == 1 + raspRuleSkipped.namespace == 'appsec' + raspRuleSkipped.metricName == 'rasp.rule.skipped' + raspRuleSkipped.tags.toSet() == ['rule_type:sql_injection', 'reason:after-request',].toSet() } def "overflowing WafMetricCollector does not crash"() { @@ -399,6 +407,7 @@ class WafMetricCollectorTest extends DDSpecification { WafMetricCollector.get().raspTimeout(ruleType) WafMetricCollector.get().raspErrorCode(ruleType, DD_WAF_RUN_INTERNAL_ERROR) WafMetricCollector.get().wafErrorCode(ruleType, DD_WAF_RUN_INTERNAL_ERROR) + WafMetricCollector.get().raspRuleSkipped(ruleType) WafMetricCollector.get().prepareMetrics() then: @@ -466,6 +475,17 @@ class WafMetricCollectorTest extends DDSpecification { 'waf_error:' + DD_WAF_RUN_INTERNAL_ERROR ].toSet() + def raspRuleSkipped = (WafMetricCollector.AfterRequestRaspRuleSkipped)metrics[6] + raspRuleSkipped.type == 'count' + raspRuleSkipped.value == 1 + raspRuleSkipped.namespace == 'appsec' + raspRuleSkipped.metricName == 'rasp.rule.skipped' + raspRuleSkipped.tags.toSet() == [ + 'rule_type:command_injection', + 'rule_variant:'+ruleType.variant, + 'reason:after-request', + ].toSet() + where: ruleType << [RuleType.COMMAND_INJECTION, RuleType.SHELL_INJECTION] } From 638b0b5f8c17169da4c84c4dc41af88a184581a3 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Tue, 25 Mar 2025 13:20:26 +0100 Subject: [PATCH 2/2] Not necessary --- .../datadog/appsec/gateway/AppSecRequestContext.java | 11 ----------- .../gateway/AppSecRequestContextSpecification.groovy | 11 +---------- 2 files changed, 1 insertion(+), 21 deletions(-) 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 4abd136954e..34536773602 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 @@ -129,7 +129,6 @@ public class AppSecRequestContext implements DataBundle, Closeable { private volatile int raspInternalErrors; private volatile int raspInvalidObjectErrors; private volatile int raspInvalidArgumentErrors; - private volatile int raspRuleSkipped; private volatile int wafInternalErrors; private volatile int wafInvalidObjectErrors; private volatile int wafInvalidArgumentErrors; @@ -160,8 +159,6 @@ public class AppSecRequestContext implements DataBundle, Closeable { RASP_INVALID_ARGUMENT_ERRORS_UPDATER = AtomicIntegerFieldUpdater.newUpdater( AppSecRequestContext.class, "raspInvalidArgumentErrors"); - private static final AtomicIntegerFieldUpdater RASP_RULE_SKIPPED_UPDATER = - AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "raspRuleSkipped"); private static final AtomicIntegerFieldUpdater WAF_INTERNAL_ERRORS_UPDATER = AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "wafInternalErrors"); @@ -223,10 +220,6 @@ public void increaseRaspTimeouts() { RASP_TIMEOUTS_UPDATER.incrementAndGet(this); } - public void increaseRuleSkipped() { - RASP_RULE_SKIPPED_UPDATER.incrementAndGet(this); - } - public void increaseRaspErrorCode(int code) { switch (code) { case DD_WAF_RUN_INTERNAL_ERROR: @@ -267,10 +260,6 @@ public int getRaspTimeouts() { return raspTimeouts; } - public int getRaspRuleSkipped() { - return raspRuleSkipped; - } - public int getRaspError(int code) { switch (code) { case DD_WAF_RUN_INTERNAL_ERROR: 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 ec97ba9d239..92d22cb6113 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 @@ -15,7 +15,7 @@ import io.sqreen.powerwaf.Additive import io.sqreen.powerwaf.Powerwaf import io.sqreen.powerwaf.PowerwafContext -class AppSecRequestContextSpecification extends DDSpecification { +class AppSecRequestContextSpecification extends DDSpecification { AppSecRequestContext ctx = new AppSecRequestContext() @@ -319,15 +319,6 @@ class AppSecRequestContextSpecification extends DDSpecification { ctx.getWafError(0) == 0 } - def "test increase and get raspRuleSkipped"() { - when: - ctx.increaseRuleSkipped() - ctx.increaseRuleSkipped() - - then: - ctx.getRaspRuleSkipped() == 2 - } - void 'close logs if request end was not called'() { given: TestLogCollector.enable()