use wafrift_types::{Request, Technique, WafClass};
use wafrift_wafmodel::mlwaf::{MlEvasion, is_attack_payload, propose_mutation};
pub const DEFAULT_ML_BUDGET: u64 = 512;
#[must_use]
pub fn ml_evasion_candidates(payload: &[u8], budget: u64, seed: u64) -> Option<MlEvasion> {
if !is_attack_payload(payload) {
return None;
}
let mut s = seed;
let cap = budget.clamp(1, 4096);
for off in 0..cap {
let cand = propose_mutation(payload, s);
s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
if cand != payload && is_attack_payload(&cand) {
return Some(MlEvasion {
input: cand,
queries: 0,
off_manifold_rejected: off,
});
}
}
None
}
#[must_use]
pub fn apply_ml_evasion_if_applicable(
request: &Request,
waf_name: &str,
budget: u64,
seed: u64,
) -> (Request, Vec<Technique>) {
let waf_class = WafClass::from_waf_name(waf_name);
if !waf_class.is_ml_backed() {
return (request.clone(), Vec::new());
}
let Some(ref body) = request.body else {
return (request.clone(), Vec::new());
};
let Some(evasion) = ml_evasion_candidates(body, budget, seed) else {
return (request.clone(), Vec::new());
};
let mut req = request.clone();
req.body = Some(evasion.input);
let techniques = vec![Technique::MlEvasion {
waf_class: format!("{waf_class:?}"),
queries: evasion.queries,
off_manifold_rejected: evasion.off_manifold_rejected,
}];
(req, techniques)
}
#[must_use]
pub fn ml_evasion_probe_payload(
raw_payload: &str,
waf_name: &str,
budget: u64,
seed: u64,
) -> Option<(String, Vec<Technique>)> {
let probe = Request::post("https://probe.internal/", raw_payload.as_bytes().to_vec());
let (mutated, techniques) = apply_ml_evasion_if_applicable(&probe, waf_name, budget, seed);
if techniques.is_empty() {
return None;
}
let payload = mutated
.body
.as_deref()
.map(|b| String::from_utf8_lossy(b).into_owned())?;
Some((payload, techniques))
}
#[cfg(test)]
mod tests {
use super::*;
use wafrift_types::Request;
#[test]
fn ml_evasion_produces_a_real_on_manifold_mutation() {
let payload = b"' UNION SELECT 1,2--";
let evasion = ml_evasion_candidates(payload, 256, 99)
.expect("an attack payload must yield an on-manifold structural mutation");
assert_ne!(
evasion.input, payload,
"the returned candidate must be an actual mutation, not the input"
);
let mutated = String::from_utf8_lossy(&evasion.input).to_ascii_lowercase();
assert!(
mutated.contains("union") || mutated.contains("select"),
"manifold projection must preserve attack tokens; got: {mutated:?}"
);
}
#[test]
fn ml_evasion_rejects_non_attack_input() {
assert!(
ml_evasion_candidates(b"hello world", 256, 1).is_none(),
"a non-attack payload must yield no ML-evasion candidate"
);
}
#[test]
fn ml_evasion_is_deterministic_for_a_seed() {
let payload = b"<script>alert(1)</script>";
let a = ml_evasion_candidates(payload, 256, 7);
let b = ml_evasion_candidates(payload, 256, 7);
assert_eq!(
a.map(|e| e.input),
b.map(|e| e.input),
"ml_evasion_candidates must be deterministic for a fixed seed"
);
}
#[test]
fn apply_ml_evasion_no_body_returns_original() {
let req = Request::get("https://example.com/");
let (result_req, techniques) =
apply_ml_evasion_if_applicable(&req, "AWS Bot Control", 64, 0);
assert_eq!(result_req.url, req.url);
assert!(techniques.is_empty(), "no body → no techniques applied");
}
#[test]
fn apply_ml_evasion_cloudflare_bot_mgmt_mutates_body() {
let original_body = b"q=<script>alert(1)</script>".to_vec();
let req = Request::post("https://target.internal/", original_body.clone())
.header("Content-Type", "application/x-www-form-urlencoded");
let waf = "Cloudflare Bot Management";
assert!(
WafClass::from_waf_name(waf).is_ml_backed(),
"Cloudflare Bot Management must be identified as ML-backed"
);
let (mutated, techs) = apply_ml_evasion_if_applicable(&req, waf, 256, 7);
assert!(
!techs.is_empty(),
"an ML-backed target with a body must mutate"
);
assert_ne!(
mutated.body.as_deref(),
Some(original_body.as_slice()),
"the body must actually change (regression guard for the no-op fake)"
);
}
#[test]
fn ml_evasion_probe_payload_mutates_for_ml_backed() {
let (payload, techs) = ml_evasion_probe_payload("' OR 1=1--", "AWS Bot Control", 256, 7)
.expect("ML-backed + attack payload must yield a mutated probe payload");
assert_ne!(
payload, "' OR 1=1--",
"probe payload must be MUTATED, not the original (no-op regression guard)"
);
assert!(
payload.to_ascii_lowercase().contains("or 1"),
"manifold projection must preserve the attack token: {payload:?}"
);
assert!(
techs
.iter()
.any(|t| matches!(t, Technique::MlEvasion { .. })),
"must carry the MlEvasion technique"
);
}
#[test]
fn ml_evasion_probe_payload_none_for_rule_waf() {
assert!(
ml_evasion_probe_payload("' OR 1=1--", "ModSecurity", 256, 0).is_none(),
"a rule-based WAF must not route through ML evasion"
);
}
}