use wafrift_content_type::json_smuggle as json_smuggle_family;
use wafrift_content_type::multipart_smuggle::generate_smuggle_variants as multipart_variants;
use wafrift_encoding::auth_header_smuggle;
use wafrift_encoding::cookie_smuggle;
use wafrift_encoding::host_header_smuggle;
use wafrift_encoding::jwt_smuggle;
use wafrift_encoding::path_normalize_smuggle;
use wafrift_encoding::range_header_smuggle;
use wafrift_http3_evasion::capsule;
use wafrift_http3_evasion::quic_datagram;
use wafrift_smuggling::ws_compression::{CompressionBomb, ContextTakeoverSequence};
use wafrift_types::probe::SmuggleProbe;
#[derive(Debug, Clone)]
pub struct ProbeSeeds<'a> {
pub cookie_name: &'a str,
pub credential_value: &'a str,
pub form_params: Vec<(String, String)>,
pub payload: Vec<u8>,
pub protected_path: &'a str,
pub protected_host: &'a str,
}
impl<'a> Default for ProbeSeeds<'a> {
fn default() -> Self {
Self {
cookie_name: "session",
credential_value: "wafrift-test-token",
form_params: vec![
("user".to_string(), "admin".to_string()),
("token".to_string(), "wafrift-test-token".to_string()),
],
payload: b"wafrift-smuggle-payload".to_vec(),
protected_path: "/admin",
protected_host: "admin.example.com",
}
}
}
#[must_use]
pub fn all_probes(seeds: &ProbeSeeds) -> Vec<Box<dyn SmuggleProbe>> {
let mut out: Vec<Box<dyn SmuggleProbe>> = Vec::new();
for p in cookie_smuggle::all_variants(seeds.cookie_name, seeds.credential_value) {
out.push(Box::new(p));
}
for p in auth_header_smuggle::all_variants("Authorization", "Bearer", seeds.credential_value) {
out.push(Box::new(p));
}
for p in range_header_smuggle::all_variants() {
out.push(Box::new(p));
}
for p in path_normalize_smuggle::all_variants(seeds.protected_path) {
out.push(Box::new(p));
}
for p in host_header_smuggle::all_variants(seeds.protected_host) {
out.push(Box::new(p));
}
for p in jwt_smuggle::all_variants(seeds.credential_value) {
out.push(Box::new(p));
}
for v in multipart_variants(&seeds.form_params) {
out.push(Box::new(v));
}
for v in json_smuggle_family::all_variants(&seeds.form_params) {
out.push(Box::new(v));
}
for a in capsule::all_variants(&seeds.payload) {
out.push(Box::new(a));
}
for a in quic_datagram::all_variants(&seeds.payload) {
out.push(Box::new(a));
}
out.push(Box::new(CompressionBomb::build(1000)));
out.push(Box::new(ContextTakeoverSequence::build(&seeds.payload, 50)));
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn aggregator_returns_probes_from_every_family() {
let probes = all_probes(&ProbeSeeds::default());
assert!(!probes.is_empty(), "aggregator must return probes");
let families: HashSet<String> = probes
.iter()
.map(|p| p.technique().split('.').next().unwrap_or("").to_string())
.collect();
for required in [
"cookie",
"auth",
"range",
"path",
"host",
"jwt",
"content-type",
"json",
"capsule",
"quic-datagram",
"compression",
] {
assert!(
families.contains(required),
"missing family {required:?} in aggregator output; got {families:?}"
);
}
}
#[test]
fn aggregator_canaries_are_unique_across_all_probes() {
let probes = all_probes(&ProbeSeeds::default());
let tokens: HashSet<String> = probes.iter().map(|p| p.canary().token.clone()).collect();
assert_eq!(
tokens.len(),
probes.len(),
"expected {} unique canaries, got {}",
probes.len(),
tokens.len()
);
}
#[test]
fn every_aggregator_probe_has_non_empty_artifact() {
let probes = all_probes(&ProbeSeeds::default());
for p in &probes {
let n = p.artifact().wire_byte_count();
assert!(
n > 0,
"probe {} produced empty artifact (wire_byte_count = 0)",
p.technique()
);
}
}
#[test]
fn aggregator_count_grows_with_ten_families() {
let probes = all_probes(&ProbeSeeds::default());
assert!(
probes.len() >= 78,
"aggregator returned only {} probes — family dropped?",
probes.len()
);
}
#[test]
fn default_seeds_construct_without_panicking() {
let seeds = ProbeSeeds::default();
assert!(!seeds.cookie_name.is_empty());
assert!(!seeds.credential_value.is_empty());
assert!(!seeds.form_params.is_empty());
assert!(!seeds.payload.is_empty());
assert!(!seeds.protected_path.is_empty());
assert!(!seeds.protected_host.is_empty());
}
#[test]
fn custom_protected_path_propagates_to_path_family_probes() {
let seeds = ProbeSeeds {
protected_path: "/wp-admin",
..ProbeSeeds::default()
};
let probes = all_probes(&seeds);
let mut saw_target = false;
for p in &probes {
if p.technique().starts_with("path.")
&& let wafrift_types::probe::SmuggleArtifact::Headers(hs) = p.artifact()
&& hs.iter().any(|(_, v)| v.contains("wp-admin"))
{
saw_target = true;
break;
}
}
assert!(
saw_target,
"custom protected_path must propagate into path-family artifacts"
);
}
#[test]
fn custom_protected_host_propagates_to_host_family_probes() {
let seeds = ProbeSeeds {
protected_host: "secret-internal.example.io",
..ProbeSeeds::default()
};
let probes = all_probes(&seeds);
let mut saw_target = false;
for p in &probes {
if p.technique().starts_with("host.")
&& let wafrift_types::probe::SmuggleArtifact::Headers(hs) = p.artifact()
&& hs
.iter()
.any(|(_, v)| v.contains("secret-internal.example.io"))
{
saw_target = true;
break;
}
}
assert!(
saw_target,
"custom protected_host must propagate into host-family artifacts"
);
}
#[test]
fn aggregator_contains_at_least_eight_host_family_probes() {
let probes = all_probes(&ProbeSeeds::default());
let host_count = probes
.iter()
.filter(|p| p.technique().starts_with("host."))
.count();
assert!(
host_count >= 8,
"expected >=8 host-family probes, got {host_count}"
);
}
#[test]
fn aggregator_contains_at_least_ten_path_family_probes() {
let probes = all_probes(&ProbeSeeds::default());
let path_count = probes
.iter()
.filter(|p| p.technique().starts_with("path."))
.count();
assert!(
path_count >= 10,
"expected >=10 path-family probes, got {path_count}"
);
}
}