From 7ae11c0cb7d7f294c52c71c5d28dc22b6845e0f8 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 24 Jun 2026 10:38:03 +0200 Subject: [PATCH] fix: report correct block outcome tag in rasp.rule.match metric rasp.rule.match was always reporting block:N/A because raspRuleMatch() was called before the action loop; flow.isBlocking() is only valid after setAction() runs inside the loop. - Move raspRuleMatch() call to after the action loop (setRaspMatched stays in place) - Add boolean blocked param; double counter array for per-outcome tracking - Update prepareMetrics() to drain both blocked/non-blocked slots per RuleType - Add block: tag to both variant and non-variant RaspRuleMatch branches - Guard flow.setAction(rba) against null return from createBlockRequestAction/createRedirectRequestAction --- .../com/datadog/appsec/ddwaf/WAFModule.java | 12 ++- .../api/telemetry/WafMetricCollector.java | 35 ++++++--- .../telemetry/WafMetricCollectorTest.groovy | 73 ++++++++++++++++++- 3 files changed, 103 insertions(+), 17 deletions(-) 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 00295e0a64c..a7aac0a6f98 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 @@ -360,7 +360,6 @@ public void onDataAvailable( if (gwCtx.isRasp) { reqCtx.setRaspMatched(true); - WafMetricCollector.get().raspRuleMatch(gwCtx.raspRuleType); } String securityResponseId = null; @@ -375,13 +374,17 @@ public void onDataAvailable( securityResponseId = (String) actionInfo.parameters.get("security_response_id"); Flow.Action.RequestBlockingAction rba = createBlockRequestAction(actionInfo, reqCtx, gwCtx.isRasp, securityResponseId); - flow.setAction(rba); + if (rba != null) { + flow.setAction(rba); + } } else if ("redirect_request".equals(actionInfo.type)) { // Extract security_response_id from action parameters for use in triggers securityResponseId = (String) actionInfo.parameters.get("security_response_id"); Flow.Action.RequestBlockingAction rba = createRedirectRequestAction(actionInfo, reqCtx, gwCtx.isRasp, securityResponseId); - flow.setAction(rba); + if (rba != null) { + flow.setAction(rba); + } } else if ("generate_stack".equals(actionInfo.type)) { if (Config.get().isAppSecStackTraceEnabled()) { String stackId = (String) actionInfo.parameters.get("stack_id"); @@ -417,6 +420,9 @@ public void onDataAvailable( } } } + if (gwCtx.isRasp) { + WafMetricCollector.get().raspRuleMatch(gwCtx.raspRuleType, flow.isBlocking()); + } Collection events = buildEvents(resultWithData, securityResponseId); boolean isThrottled = reqCtx.isThrottled(rateLimiter); 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 032212a7f1a..480d5f845f9 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,7 +47,7 @@ private WafMetricCollector() { private static final AtomicLongArray raspRuleSkippedCounter = new AtomicLongArray(RuleType.getNumValues()); private static final AtomicLongArray raspRuleMatchCounter = - new AtomicLongArray(RuleType.getNumValues()); + new AtomicLongArray(RuleType.getNumValues() * 2); private static final AtomicLongArray raspTimeoutCounter = new AtomicLongArray(RuleType.getNumValues()); private static final AtomicLongArray raspErrorCodeCounter = @@ -160,8 +160,8 @@ public void raspRuleSkipped(final RuleType ruleType) { raspRuleSkippedCounter.incrementAndGet(ruleType.ordinal()); } - public void raspRuleMatch(final RuleType ruleType) { - raspRuleMatchCounter.incrementAndGet(ruleType.ordinal()); + public void raspRuleMatch(final RuleType ruleType, final boolean blocked) { + raspRuleMatchCounter.incrementAndGet(ruleType.ordinal() * 2 + (blocked ? 1 : 0)); } public void raspTimeout(final RuleType ruleType) { @@ -278,12 +278,20 @@ public void prepareMetrics() { } } - // RASP rule match per rule type + // RASP rule match per rule type: two slots per RuleType: ordinal*2 (non-blocked), + // ordinal*2+1 (blocked) for (RuleType ruleType : RuleType.values()) { - long counter = raspRuleMatchCounter.getAndSet(ruleType.ordinal(), 0); - if (counter > 0) { + long blockedCount = raspRuleMatchCounter.getAndSet(ruleType.ordinal() * 2 + 1, 0); + if (blockedCount > 0) { if (!rawMetricsQueue.offer( - new RaspRuleMatch(counter, ruleType, WafMetricCollector.wafVersion))) { + new RaspRuleMatch(blockedCount, ruleType, WafMetricCollector.wafVersion, true))) { + return; + } + } + long nonBlockedCount = raspRuleMatchCounter.getAndSet(ruleType.ordinal() * 2, 0); + if (nonBlockedCount > 0) { + if (!rawMetricsQueue.offer( + new RaspRuleMatch(nonBlockedCount, ruleType, WafMetricCollector.wafVersion, false))) { return; } } @@ -558,7 +566,11 @@ public AfterRequestRaspRuleSkipped(final long counter, final RuleType ruleType) } public static class RaspRuleMatch extends WafMetric { - public RaspRuleMatch(final long counter, final RuleType ruleType, final String wafVersion) { + public RaspRuleMatch( + final long counter, + final RuleType ruleType, + final String wafVersion, + final boolean blocked) { super( "rasp.rule.match", counter, @@ -567,9 +579,12 @@ public RaspRuleMatch(final long counter, final RuleType ruleType, final String w "rule_type:" + ruleType.type, "rule_variant:" + ruleType.variant, "waf_version:" + wafVersion, - "event_rules_version:" + rulesVersion + "event_rules_version:" + rulesVersion, + "block:" + blocked } - : new String[] {"rule_type:" + ruleType.type, "waf_version:" + wafVersion}); + : new String[] { + "rule_type:" + ruleType.type, "waf_version:" + wafVersion, "block:" + blocked + }); } } 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 251bdb4baef..eac58683f95 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 @@ -37,7 +37,7 @@ class WafMetricCollectorTest extends DDSpecification { WafMetricCollector.get().wafUpdates('rules.3', false) WafMetricCollector.get().raspRuleEval(RuleType.SQL_INJECTION) WafMetricCollector.get().raspRuleEval(RuleType.SQL_INJECTION) - WafMetricCollector.get().raspRuleMatch(RuleType.SQL_INJECTION) + WafMetricCollector.get().raspRuleMatch(RuleType.SQL_INJECTION, false) WafMetricCollector.get().raspRuleEval(RuleType.SQL_INJECTION) WafMetricCollector.get().raspTimeout(RuleType.SQL_INJECTION) WafMetricCollector.get().raspErrorCode(RuleType.SHELL_INJECTION, DD_WAF_RUN_INTERNAL_ERROR) @@ -85,7 +85,7 @@ class WafMetricCollectorTest extends DDSpecification { raspRuleMatch.value == 1 raspRuleMatch.namespace == 'appsec' raspRuleMatch.metricName == 'rasp.rule.match' - raspRuleMatch.tags.toSet() == ['rule_type:sql_injection', 'waf_version:waf_ver1'].toSet() + raspRuleMatch.tags.toSet() == ['rule_type:sql_injection', 'waf_version:waf_ver1', 'block:false'].toSet() def raspTimeout = (WafMetricCollector.RaspTimeout) metrics[5] raspTimeout.type == 'count' @@ -317,7 +317,7 @@ class WafMetricCollectorTest extends DDSpecification { WafMetricCollector.get().wafInit('waf_ver1', 'rules.1', true) WafMetricCollector.get().raspRuleEval(ruleType) WafMetricCollector.get().raspRuleEval(ruleType) - WafMetricCollector.get().raspRuleMatch(ruleType) + WafMetricCollector.get().raspRuleMatch(ruleType, false) WafMetricCollector.get().raspRuleEval(ruleType) WafMetricCollector.get().raspTimeout(ruleType) WafMetricCollector.get().raspErrorCode(ruleType, DD_WAF_RUN_INTERNAL_ERROR) @@ -349,7 +349,8 @@ class WafMetricCollectorTest extends DDSpecification { 'rule_type:command_injection', 'rule_variant:' + ruleType.variant, 'waf_version:waf_ver1', - 'event_rules_version:rules.1' + 'event_rules_version:rules.1', + 'block:false' ].toSet() def raspTimeout = (WafMetricCollector.RaspTimeout) metrics[3] @@ -585,6 +586,70 @@ class WafMetricCollectorTest extends DDSpecification { type << [MESSAGES, CONTENT] } + void 'test rasp rule match block tag'() { + given: + final collector = WafMetricCollector.get() + collector.wafInit('waf_ver1', 'rules.1', true) + + when: + collector.raspRuleMatch(ruleType, blocked) + + then: + collector.prepareMetrics() + final metrics = collector.drain() + final matchMetrics = metrics.findAll { it.metricName == 'rasp.rule.match' } + + matchMetrics.size() == 1 + final metric = matchMetrics[0] + metric.type == 'count' + metric.value == 1 + metric.namespace == 'appsec' + final expectedTags = ruleType.variant != null + ? [ + 'rule_type:' + ruleType.type, + 'rule_variant:' + ruleType.variant, + 'waf_version:waf_ver1', + 'event_rules_version:rules.1', + 'block:' + blocked + ].toSet() + : ['rule_type:' + ruleType.type, 'waf_version:waf_ver1', 'block:' + blocked].toSet() + metric.tags.toSet() == expectedTags + + where: + ruleType | blocked + RuleType.SQL_INJECTION | true + RuleType.SQL_INJECTION | false + RuleType.LFI | true + RuleType.LFI | false + RuleType.SSRF_REQUEST | true + RuleType.SSRF_REQUEST | false + RuleType.SSRF_RESPONSE | true + RuleType.SSRF_RESPONSE | false + RuleType.SHELL_INJECTION | true + RuleType.SHELL_INJECTION | false + RuleType.COMMAND_INJECTION | true + RuleType.COMMAND_INJECTION | false + } + + void 'test rasp rule match drains blocked and non-blocked as separate metrics'() { + given: + final collector = WafMetricCollector.get() + collector.wafInit('waf_ver1', 'rules.1', true) + + when: + collector.raspRuleMatch(RuleType.SQL_INJECTION, true) + collector.raspRuleMatch(RuleType.SQL_INJECTION, false) + + then: + collector.prepareMetrics() + final metrics = collector.drain() + final matchMetrics = metrics.findAll { it.metricName == 'rasp.rule.match' } + + matchMetrics.size() == 2 + matchMetrics.find { it.tags.contains('block:true') }?.value == 1 + matchMetrics.find { it.tags.contains('block:false') }?.value == 1 + } + /** * Helper method to generate all combinations of n boolean values. */