use crate::learn::Alphabet;
use crate::mine::attack_grammar;
use crate::normalize::{self, Transform};
use crate::oracle::{ChannelSet, Rule, SimRegexWaf};
use crate::outcome::Outcome;
use crate::sfa::Sfa;
use regex::bytes::Regex;
use wafrift_types::Request;
#[derive(Debug)]
pub struct ClosureReport {
pub added_rules: Vec<Rule>,
pub holes_before: usize,
pub holes_after: usize,
pub benign_false_positives: usize,
pub proven_closed: bool,
}
fn body(bytes: &[u8]) -> Request {
Request::post("https://h/p", bytes.to_vec()).header("Content-Type", "application/json")
}
const HOLES_ENUM_CAP: usize = 4096;
fn holes(waf: &SimRegexWaf, attack: &Sfa, max_len: usize) -> (Vec<Vec<u8>>, bool) {
const ATTACK_STRINGS_BUDGET: usize = HOLES_ENUM_CAP * 16;
let enumerated = attack.enumerate_accepted(ATTACK_STRINGS_BUDGET, max_len);
let mut holes = Vec::new();
for bytes in enumerated {
if waf.classify_uncounted(&body(&bytes)) == Outcome::Pass {
holes.push(bytes);
if holes.len() >= HOLES_ENUM_CAP {
return (holes, true); }
}
}
(holes, false)
}
#[must_use]
pub fn synthesize_closure(
waf: &SimRegexWaf,
needles: &[&[u8]],
channels: ChannelSet,
benign: &[&[u8]],
alpha: &Alphabet,
max_len: usize,
) -> ClosureReport {
let grammar = attack_grammar(alpha, needles);
let (before, _before_truncated) = holes(waf, &grammar, max_len);
let tf = vec![
Transform::UrlDecodeUni,
Transform::HtmlEntityDecode,
Transform::Lowercase,
];
let mut added = Vec::new();
let mut seen_decoded = std::collections::HashSet::<Vec<u8>>::new();
for n in needles {
let decoded = normalize::apply_chain(&tf, n);
if !seen_decoded.insert(decoded.clone()) {
continue;
}
let pat = regex::escape(&String::from_utf8_lossy(&decoded));
if let Ok(re) = Regex::new(&pat) {
added.push(Rule {
id: format!("synth-close-{}", String::from_utf8_lossy(&decoded)),
channels,
transforms: tf.clone(),
pattern: re,
score: waf.threshold(),
});
}
}
let hardened = waf.with_rules_added(added.clone());
let benign_fp = benign
.iter()
.filter(|b| {
waf.classify_uncounted(&body(b)) == Outcome::Pass
&& hardened.classify_uncounted(&body(b)) == Outcome::Block
})
.count();
let (after, after_truncated) = holes(&hardened, &grammar, max_len);
ClosureReport {
added_rules: added,
holes_before: before.len(),
holes_after: after.len(),
benign_false_positives: benign_fp,
proven_closed: after.is_empty() && benign_fp == 0 && !after_truncated,
}
}