use wafrift_evolution::coverage_feedback::{
PayloadClass, RuleCoverage, RuleId, map_elites_descriptor,
};
use wafrift_evolution::types::OracleVerdict;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn modsec_block_body(rule_id: &str) -> String {
format!(
r#"<!DOCTYPE html><html><body>
<h1>403 Forbidden</h1>
<p>Your request was blocked by ModSecurity.</p>
<p>Rule ID: {rule_id}</p>
</body></html>"#
)
}
#[tokio::test]
async fn coverage_records_rule_id_from_block_response() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/post"))
.respond_with(
ResponseTemplate::new(403)
.set_body_string(modsec_block_body("942100"))
.insert_header("x-modsecurity-rule-id", "942100"),
)
.mount(&server)
.await;
let verdict = OracleVerdict {
passed: false,
triggered_rules: 1,
confidence: 1.0,
rule_id: Some("942100".into()),
..Default::default()
};
let mut cov = RuleCoverage::new();
let sql_payload = "tautology_swap"; cov.record(sql_payload, verdict.rule_id.as_deref());
assert_eq!(cov.rule_count(), 1);
let rid = RuleId::new("942100");
assert!(
cov.by_rule.contains_key(&rid),
"rule 942100 must be in by_rule"
);
let reqs = server.received_requests().await.unwrap_or_default();
let _ = reqs; }
#[tokio::test]
async fn coverage_records_sentinel_for_unblocked_verdict() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/post"))
.respond_with(ResponseTemplate::new(200).set_body_string("ok"))
.mount(&server)
.await;
let verdict = OracleVerdict {
passed: true,
triggered_rules: 0,
confidence: 1.0,
rule_id: None,
..Default::default()
};
let mut cov = RuleCoverage::new();
cov.record("' OR 1=1--", verdict.rule_id.as_deref());
assert_eq!(cov.rule_count(), 0);
let cls = PayloadClass::new("sql");
assert!(cov.by_class.contains_key(&cls));
let _ = server.received_requests().await;
}
#[tokio::test]
async fn coverage_map_accumulates_across_multiple_rule_ids() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/sql"))
.respond_with(ResponseTemplate::new(403).set_body_string(modsec_block_body("942100")))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/xss"))
.respond_with(ResponseTemplate::new(403).set_body_string(modsec_block_body("941100")))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/path"))
.respond_with(ResponseTemplate::new(403).set_body_string(modsec_block_body("930100")))
.mount(&server)
.await;
let observations: Vec<(&str, &str)> = vec![
("tautology_swap", "942100"), ("tag_event_swap", "941100"), ("path_obfuscate", "930100"), ];
let mut cov = RuleCoverage::new();
for (payload_signal, rule_id) in &observations {
cov.record(payload_signal, Some(rule_id));
}
assert_eq!(cov.rule_count(), 3, "must have 3 distinct rules");
assert!(cov.by_rule.contains_key(&RuleId::new("942100")));
assert!(cov.by_rule.contains_key(&RuleId::new("941100")));
assert!(cov.by_rule.contains_key(&RuleId::new("930100")));
let report = cov.coverage_report();
assert!(report.contains("942100"));
assert!(report.contains("941100"));
assert!(report.contains("930100"));
for (payload_signal, rule_id) in &observations {
let (_, rid) = map_elites_descriptor(payload_signal, Some(rule_id));
assert!(rid.is_some(), "descriptor must carry rule_id dimension");
}
let _ = server.received_requests().await;
}