@@ -993,6 +993,9 @@ class TelemetryTests {
993993 * - rasp.duration: span metric ms × 1000 → µs must fall inside a populated bin
994994 * - rasp.duration_ext: span metric µs falls directly inside a populated bin (no conversion)
995995 *
996+ * Also covers appsec.rasp.rule.duration, the per-libddwaf-call counterpart of
997+ * appsec.rasp.duration, which is tagged per rule type and variant.
998+ *
996999 * This test only applies to the Rust helper (distributions not implemented elsewhere).
9971000 */
9981001 @Test
@@ -1020,12 +1023,21 @@ class TelemetryTests {
10201023
10211024 TelemetryHelpers.DistributionMetric raspDuration
10221025 TelemetryHelpers.DistributionMetric raspDurationExt
1026+ TelemetryHelpers.DistributionMetric lfiRuleDuration
1027+ TelemetryHelpers.DistributionMetric ssrfRuleDuration
10231028
10241029 TelemetryHelpers . waitForDistributions(CONTAINER , 30 ) { List<TelemetryHelpers.GenerateDistributions > messages ->
10251030 def allSeries = messages. collectMany { it. series }
10261031 raspDuration = raspDuration ?: allSeries. find { it. name == ' rasp.duration' }
10271032 raspDurationExt = raspDurationExt ?: allSeries. find { it. name == ' rasp.duration_ext' }
1028- raspDuration != null && raspDurationExt != null
1033+ lfiRuleDuration = lfiRuleDuration ?: allSeries. find {
1034+ it. name == ' rasp.rule.duration' && ' rule_type:lfi' in it. tags
1035+ }
1036+ ssrfRuleDuration = ssrfRuleDuration ?: allSeries. find {
1037+ it. name == ' rasp.rule.duration' && ' rule_type:ssrf' in it. tags
1038+ }
1039+ raspDuration != null && raspDurationExt != null &&
1040+ lfiRuleDuration != null && ssrfRuleDuration != null
10291041 }
10301042
10311043 assert raspDuration != null : ' rasp.duration distribution metric not found'
@@ -1060,6 +1072,26 @@ class TelemetryTests {
10601072 assert raspDurationExt. countForBinContaining(raspDurationExtUs) != null :
10611073 " span metric value ${ raspDurationExtUs} µs not found in any rasp.duration_ext " +
10621074 " distribution bin; distribution: ${ raspDurationExt} "
1075+
1076+ // RFC-1012 appsec.rasp.rule.duration: unlike rasp.duration, which contributes one
1077+ // cumulative observation per request, this records one observation per libddwaf call.
1078+ // /multiple_rasp.php evaluates lfi 3× and ssrf 2×, so those are the per-request minima.
1079+ assert lfiRuleDuration != null : ' rasp.rule.duration for lfi not found'
1080+ assert lfiRuleDuration. namespace == ' appsec'
1081+ assert lfiRuleDuration. tags. find { it. startsWith(' waf_version:' ) } != null
1082+ assert lfiRuleDuration. tags. find { it. startsWith(' event_rules_version:' ) } != null
1083+ assert lfiRuleDuration. count >= 3.0 :
1084+ " expected >= 3 lfi observations, got ${ lfiRuleDuration} "
1085+ // LFI has no variant — tag must be absent (sidecar rejects empty tag values)
1086+ assert ! lfiRuleDuration. tags. any { it. startsWith(' rule_variant:' ) }
1087+
1088+ assert ssrfRuleDuration != null : ' rasp.rule.duration for ssrf not found'
1089+ assert ssrfRuleDuration. namespace == ' appsec'
1090+ assert ssrfRuleDuration. tags. find { it. startsWith(' waf_version:' ) } != null
1091+ assert ssrfRuleDuration. tags. find { it. startsWith(' event_rules_version:' ) } != null
1092+ assert ssrfRuleDuration. count >= 2.0 :
1093+ " expected >= 2 ssrf observations, got ${ ssrfRuleDuration} "
1094+ assert ' rule_variant:request' in ssrfRuleDuration. tags
10631095 }
10641096
10651097 /**
0 commit comments