use crate::coverage_feedback::PayloadClass;
use crate::edge_pop_coverage::EdgePopCoverage;
use crate::h1_dedup::{BypassFingerprint, fingerprint};
use crate::rule_corpus::RuleBypassCorpus;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProbeOutcome {
Bypass,
Block,
Challenge,
Ambiguous,
}
pub fn record_outcome(
corpus: &mut RuleBypassCorpus,
outcome: ProbeOutcome,
rule_id: Option<&str>,
payload: &str,
payload_class: PayloadClass,
encoding_chain: Vec<String>,
response_hash: u64,
) {
let key = rule_id.unwrap_or(UNATTRIBUTED_BUCKET);
match outcome {
ProbeOutcome::Bypass => {
corpus.record_bypass(key, payload, payload_class, encoding_chain, response_hash);
}
ProbeOutcome::Block => {
corpus.record_block(key, payload, payload_class, encoding_chain, response_hash);
}
ProbeOutcome::Challenge | ProbeOutcome::Ambiguous => {
}
}
}
pub const UNATTRIBUTED_BUCKET: &str = "unattributed";
pub fn record_pop_observation(
coverage: &mut EdgePopCoverage,
egress: &str,
target: &str,
pop_raw: Option<&str>,
) {
match pop_raw {
Some(pop) => {
coverage.record(egress, target, pop);
}
None => coverage.record_no_pop(egress, target),
}
}
pub struct ProbeRecord<'a> {
pub corpus: &'a mut RuleBypassCorpus,
pub coverage: &'a mut EdgePopCoverage,
pub outcome: ProbeOutcome,
pub rule_id: Option<&'a str>,
pub payload: &'a str,
pub payload_class: PayloadClass,
pub encoding_chain: Vec<String>,
pub response_hash: u64,
pub egress_label: &'a str,
pub target_host: &'a str,
pub pop_raw: Option<&'a str>,
}
pub fn record_probe(probe: ProbeRecord<'_>) -> BypassFingerprint {
let ProbeRecord {
corpus,
coverage,
outcome,
rule_id,
payload,
payload_class,
encoding_chain,
response_hash,
egress_label,
target_host,
pop_raw,
} = probe;
let key = rule_id.unwrap_or(UNATTRIBUTED_BUCKET);
let fp = fingerprint(key, &encoding_chain, payload);
record_outcome(
corpus,
outcome,
rule_id,
payload,
payload_class,
encoding_chain,
response_hash,
);
record_pop_observation(coverage, egress_label, target_host, pop_raw);
fp
}
pub fn record_global_drift(corpus: &mut RuleBypassCorpus) {
let rule_ids: Vec<String> = corpus
.buckets
.keys()
.filter(|k| !corpus.blocked_for_rule(k).is_empty())
.cloned()
.collect();
for r in rule_ids {
corpus.mark_drift(&r);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cls(s: &str) -> PayloadClass {
PayloadClass::new(s)
}
#[test]
fn bypass_outcome_records_to_bypassed() {
let mut c = RuleBypassCorpus::new("t");
record_outcome(
&mut c,
ProbeOutcome::Bypass,
Some("942100"),
"' OR 1=1",
cls("sql"),
vec!["url".into()],
0xC0FFEE,
);
assert_eq!(c.bypasses_for_rule("942100").len(), 1);
assert_eq!(c.blocked_for_rule("942100").len(), 0);
}
#[test]
fn block_outcome_records_to_blocked() {
let mut c = RuleBypassCorpus::new("t");
record_outcome(
&mut c,
ProbeOutcome::Block,
Some("942100"),
"evil",
cls("sql"),
vec![],
1,
);
assert_eq!(c.blocked_for_rule("942100").len(), 1);
assert_eq!(c.bypasses_for_rule("942100").len(), 0);
}
#[test]
fn challenge_outcome_not_recorded() {
let mut c = RuleBypassCorpus::new("t");
record_outcome(
&mut c,
ProbeOutcome::Challenge,
Some("942100"),
"x",
cls("sql"),
vec![],
1,
);
assert_eq!(c.rules_seen(), 0);
}
#[test]
fn ambiguous_outcome_not_recorded() {
let mut c = RuleBypassCorpus::new("t");
record_outcome(
&mut c,
ProbeOutcome::Ambiguous,
Some("942100"),
"x",
cls("sql"),
vec![],
1,
);
assert_eq!(c.rules_seen(), 0);
}
#[test]
fn missing_rule_id_lands_under_unattributed_bucket() {
let mut c = RuleBypassCorpus::new("t");
record_outcome(
&mut c,
ProbeOutcome::Block,
None,
"x",
cls("sql"),
vec![],
1,
);
assert_eq!(c.blocked_for_rule(UNATTRIBUTED_BUCKET).len(), 1);
assert_eq!(c.rules_seen(), 1);
}
#[test]
fn cf_corpus_key_form_passes_through() {
let mut c = RuleBypassCorpus::new("cf:cumulus");
let cf_key = "cf:sjc:waf-managed-rule";
record_outcome(
&mut c,
ProbeOutcome::Block,
Some(cf_key),
"p",
cls("sql"),
vec![],
1,
);
assert_eq!(c.blocked_for_rule(cf_key).len(), 1);
}
#[test]
fn record_global_drift_marks_only_blocked_rules() {
let mut c = RuleBypassCorpus::new("t");
record_outcome(
&mut c,
ProbeOutcome::Block,
Some("R1"),
"p1",
cls("sql"),
vec![],
1,
);
record_outcome(
&mut c,
ProbeOutcome::Bypass,
Some("R2"),
"p2",
cls("sql"),
vec![],
2,
);
record_global_drift(&mut c);
assert!(c.buckets["R1"].last_drift_at_secs.is_some());
assert!(c.buckets["R2"].last_drift_at_secs.is_none());
}
#[test]
fn record_global_drift_idempotent_when_no_blocks() {
let mut c = RuleBypassCorpus::new("t");
record_outcome(
&mut c,
ProbeOutcome::Bypass,
Some("R1"),
"p",
cls("sql"),
vec![],
1,
);
record_global_drift(&mut c);
assert!(c.buckets["R1"].last_drift_at_secs.is_none());
}
#[test]
fn dedup_carries_through_bridge() {
let mut c = RuleBypassCorpus::new("t");
for _ in 0..5 {
record_outcome(
&mut c,
ProbeOutcome::Block,
Some("R1"),
"same-payload",
cls("sql"),
vec![],
1, );
}
assert_eq!(c.blocked_for_rule("R1").len(), 1);
}
#[test]
fn unattributed_constant_is_stable() {
assert_eq!(UNATTRIBUTED_BUCKET, "unattributed");
}
#[test]
fn record_probe_routes_bypass_to_corpus_and_pop_to_coverage() {
let mut corpus = RuleBypassCorpus::new("cf:cumulus");
let mut coverage = EdgePopCoverage::new();
let fp = record_probe(ProbeRecord {
corpus: &mut corpus,
coverage: &mut coverage,
outcome: ProbeOutcome::Bypass,
rule_id: Some("cf:sjc:waf-managed-rule"),
payload: "' OR 1=1--",
payload_class: cls("sql"),
encoding_chain: vec!["url".into(), "lower".into()],
response_hash: 0xC0FFEE,
egress_label: "egress-a",
target_host: "cumulus.example",
pop_raw: Some("SJC"),
});
assert_eq!(corpus.bypasses_for_rule("cf:sjc:waf-managed-rule").len(), 1);
let pops = coverage.pops_for("egress-a", "cumulus.example");
assert!(pops.contains("SJC"));
assert_ne!(fp.hash, 0);
}
#[test]
fn record_probe_with_none_pop_only_increments_probe_count() {
let mut corpus = RuleBypassCorpus::new("t");
let mut coverage = EdgePopCoverage::new();
let _ = record_probe(ProbeRecord {
corpus: &mut corpus,
coverage: &mut coverage,
outcome: ProbeOutcome::Block,
rule_id: Some("942100"),
payload: "x",
payload_class: cls("sql"),
encoding_chain: vec![],
response_hash: 1,
egress_label: "egress-a",
target_host: "no-cf.example",
pop_raw: None,
});
assert!(coverage.pops_for("egress-a", "no-cf.example").is_empty());
assert_eq!(coverage.probes_for("egress-a", "no-cf.example"), 1);
}
#[test]
fn record_probe_unattributed_rule_lands_in_unattributed_bucket() {
let mut corpus = RuleBypassCorpus::new("t");
let mut coverage = EdgePopCoverage::new();
let _ = record_probe(ProbeRecord {
corpus: &mut corpus,
coverage: &mut coverage,
outcome: ProbeOutcome::Block,
rule_id: None,
payload: "x",
payload_class: cls("sql"),
encoding_chain: vec![],
response_hash: 1,
egress_label: "egress-a",
target_host: "t",
pop_raw: None,
});
assert_eq!(corpus.blocked_for_rule(UNATTRIBUTED_BUCKET).len(), 1);
}
#[test]
fn pop_observation_valid_pop_recorded() {
let mut cov = EdgePopCoverage::new();
record_pop_observation(&mut cov, "egress-a", "target.example", Some("SJC"));
assert_eq!(cov.pops_for("egress-a", "target.example").len(), 1);
assert_eq!(cov.probes_for("egress-a", "target.example"), 1);
}
#[test]
fn pop_observation_none_increments_probe_counter_only() {
let mut cov = EdgePopCoverage::new();
record_pop_observation(&mut cov, "egress-a", "target.example", None);
assert!(cov.pops_for("egress-a", "target.example").is_empty());
assert_eq!(cov.probes_for("egress-a", "target.example"), 1);
}
#[test]
fn pop_observation_invalid_pop_still_counts() {
let mut cov = EdgePopCoverage::new();
record_pop_observation(&mut cov, "egress-a", "target.example", Some("not-a-pop"));
assert!(cov.pops_for("egress-a", "target.example").is_empty());
assert_eq!(cov.probes_for("egress-a", "target.example"), 1);
}
#[test]
fn each_outcome_variant_distinct_pattern() {
let variants = [
ProbeOutcome::Bypass,
ProbeOutcome::Block,
ProbeOutcome::Challenge,
ProbeOutcome::Ambiguous,
];
let unique: std::collections::HashSet<_> = variants.iter().collect();
assert_eq!(unique.len(), 4);
}
}