Skip to content

Commit ebc3f03

Browse files
committed
appsec: implement rasp.rule.duration
1 parent 787a3d0 commit ebc3f03

3 files changed

Lines changed: 53 additions & 1 deletion

File tree

appsec/helper-rust/src/client/metrics.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ pub struct RaspRuleMetrics {
101101

102102
/// Total number of RASP rule timeouts
103103
pub timeouts: u32,
104+
105+
/// Duration of each individual libddwaf call, for the rasp.rule.duration
106+
/// distribution. Unlike rasp.duration, which is the per-request cumulative
107+
/// sum, this metric records one observation per call.
108+
pub durations: Vec<Duration>,
104109
}
105110

106111
impl WafMetrics {
@@ -180,6 +185,7 @@ impl WafMetrics {
180185
.entry((rule_type.to_string(), rule_variant.to_string()))
181186
.or_default();
182187
entry.evals += 1;
188+
entry.durations.push(run_output.duration());
183189
if run_output.has_events() {
184190
if run_output.is_blocking() {
185191
entry.matches_blocked += 1;
@@ -309,6 +315,15 @@ impl telemetry::TelemetryMetricsGenerator for WafMetrics {
309315
);
310316
}
311317

318+
// rasp.rule.duration distribution: one observation per libddwaf call, in microseconds
319+
for duration in &metrics.durations {
320+
submitter.submit_metric(
321+
telemetry::RASP_RULE_DURATION_DIST,
322+
duration.as_micros() as f64,
323+
tags.clone(),
324+
);
325+
}
326+
312327
// tests expect this to always be sent, even if 0
313328
submitter.submit_metric(telemetry::RASP_TIMEOUT, metrics.timeouts as f64, tags);
314329
}

appsec/helper-rust/src/telemetry.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pub const WAF_ERROR: MetricName = MetricName("waf.error");
9797
pub const WAF_DURATION_DIST: MetricName = MetricName("waf.duration");
9898
pub const RASP_DURATION_DIST: MetricName = MetricName("rasp.duration");
9999
pub const RASP_RULE_EVAL: MetricName = MetricName("rasp.rule.eval");
100+
pub const RASP_RULE_DURATION_DIST: MetricName = MetricName("rasp.rule.duration");
100101
pub const RASP_RULE_MATCH: MetricName = MetricName("rasp.rule.match");
101102
pub const RASP_TIMEOUT: MetricName = MetricName("rasp.timeout");
102103
pub const RASP_ERROR: MetricName = MetricName("rasp.error");
@@ -136,6 +137,10 @@ pub const KNOWN_METRICS: &[KnownMetric] = &[
136137
name: RASP_DURATION_DIST,
137138
metric_type: ddog_MetricType_DDOG_METRIC_TYPE_DISTRIBUTION,
138139
},
140+
KnownMetric {
141+
name: RASP_RULE_DURATION_DIST,
142+
metric_type: ddog_MetricType_DDOG_METRIC_TYPE_DISTRIBUTION,
143+
},
139144
KnownMetric {
140145
name: RASP_TIMEOUT,
141146
metric_type: ddog_MetricType_DDOG_METRIC_TYPE_COUNT,

appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/TelemetryTests.groovy

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)