pub mod bogon;
pub mod calibration;
pub mod canary;
pub mod config;
pub mod discovery;
pub mod entropy;
pub mod escalation;
pub mod explanation;
pub mod format;
pub mod gene_bank_io;
pub mod hash;
pub mod injection_context;
pub mod loaders;
pub mod oob;
pub mod pick;
pub mod probe;
pub mod request;
pub mod result;
pub mod session;
pub mod technique;
pub mod utf7;
pub mod verdict;
pub mod waf_class;
pub const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
pub const DEFAULT_MAX_REDIRECTS: usize = 5;
pub const DEFAULT_EGRESS_CHALLENGE_THRESHOLD: u32 = 3;
pub const DEFAULT_EGRESS_COOLDOWN_SECS: u64 = 300;
pub const DEFAULT_SMUGGLE_COMPOSED_CAP: usize = 64;
pub const DEFAULT_SMUGGLE_FIRE_DELAY_MS: u64 = 200;
pub const DEFAULT_SMUGGLE_FIRE_TIMEOUT_SECS: u64 = 10;
pub const DEFAULT_SMUGGLE_BODY_DIVERGENCE_THRESHOLD: f64 = 0.05;
pub const DEFAULT_SMUGGLE_FIRE_PARALLEL: usize = 1;
pub const REGEX_NFA_SIZE_LIMIT: usize = 4 * 1024 * 1024;
pub const MAX_RESPONSE_BODY_BYTES: usize = 64 * 1024 * 1024;
pub const HOST_STATES_CAP: usize = 10_000;
pub const HOST_TECHNIQUE_HINTS_CAP: usize = 200;
pub const BLOCK_SCAN_BODY_WINDOW: usize = 4096;
#[must_use]
pub fn glob_match(pattern: &str, subject: &str) -> bool {
glob_match_bytes(pattern.as_bytes(), subject.as_bytes())
}
#[must_use]
pub fn glob_match_bytes(p: &[u8], s: &[u8]) -> bool {
let (mut pi, mut si) = (0usize, 0usize);
let (mut star_pi, mut star_si) = (usize::MAX, 0usize);
while si < s.len() {
if pi < p.len() && (p[pi] == b'?' || p[pi].eq_ignore_ascii_case(&s[si])) {
pi += 1;
si += 1;
} else if pi < p.len() && p[pi] == b'*' {
star_pi = pi;
star_si = si;
pi += 1;
} else if star_pi != usize::MAX {
star_si += 1;
si = star_si;
pi = star_pi + 1;
} else {
return false;
}
}
while pi < p.len() && p[pi] == b'*' {
pi += 1;
}
pi == p.len()
}
pub use bogon::ip_addr_is_bogon;
pub use calibration::CalibrationResult;
pub use config::EvasionConfig;
pub use entropy::{binary_shannon, shannon};
pub use escalation::EscalationLevel;
pub use hash::{FNV_OFFSET_64, FNV_PRIME_64, fnv1a_64, fnv1a_64_extend, fnv1a_64_step};
pub use request::{Method, Request};
pub use result::EvasionResult;
pub use technique::Technique;
pub use verdict::{BlockReason, ConnectionBehavior, Signal, Verdict};
pub use waf_class::WafClass;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glob_empty_pattern_matches_only_empty_subject() {
assert!(glob_match("", ""));
assert!(!glob_match("", "a"));
assert!(!glob_match("", "abc"));
}
#[test]
fn glob_star_matches_any_string_including_empty() {
assert!(glob_match("*", ""));
assert!(glob_match("*", "anything"));
assert!(glob_match("*", "a.b.c.d.e"));
}
#[test]
fn glob_question_matches_exactly_one_byte() {
assert!(!glob_match("?", ""));
assert!(glob_match("?", "x"));
assert!(!glob_match("?", "xy"));
}
#[test]
fn glob_star_mid_pattern() {
assert!(glob_match("*.example.com", "api.example.com"));
assert!(glob_match("*.example.com", "deep.api.example.com"));
assert!(!glob_match("*.example.com", "example.com"));
assert!(glob_match("/api/*", "/api/v1/users"));
assert!(!glob_match("/api/*", "/web/v1"));
}
#[test]
fn glob_case_insensitive_literal() {
assert!(glob_match("Example.com", "example.COM"));
assert!(glob_match("example.com", "EXAMPLE.COM"));
assert!(!glob_match("example.com", "example.net"));
assert!(!glob_match("example.com", "example.comm"));
}
#[test]
fn glob_anchored_both_ends() {
assert!(!glob_match("example.com", "api.example.com"));
assert!(!glob_match("example.com", "example.com.evil"));
}
#[test]
fn glob_star_at_end_matches_any_suffix() {
assert!(glob_match("/api/*", "/api/"));
assert!(glob_match("/api/*", "/api/v2/users/me"));
}
#[test]
fn glob_star_at_start_matches_any_prefix() {
assert!(glob_match("*.js", "bundle.js"));
assert!(glob_match("*.js", "a/b/c.js"));
assert!(!glob_match("*.js", "bundle.ts"));
}
#[test]
fn glob_double_star_acts_as_single_star() {
assert!(glob_match("**", "anything"));
assert!(glob_match("a**b", "ab"));
assert!(glob_match("a**b", "aXXb"));
}
#[test]
fn glob_no_wildcards_is_exact_case_insensitive_match() {
assert!(glob_match("example.com", "EXAMPLE.COM"));
assert!(!glob_match("example.com", "example.net"));
assert!(!glob_match("example.com", "example.comm"));
}
#[test]
fn glob_worst_case_does_not_hang() {
let start = std::time::Instant::now();
let pattern = "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a";
let subject = "b".repeat(128);
let result = glob_match(pattern, &subject);
let elapsed = start.elapsed();
assert!(!result, "expected no match");
assert!(
elapsed.as_millis() < 100,
"glob_match took {elapsed:?} on adversarial input — iterative impl required"
);
}
#[test]
fn default_request_timeout_secs_is_30() {
assert_eq!(DEFAULT_REQUEST_TIMEOUT_SECS, 30u64);
}
#[test]
fn default_egress_constants_are_stable() {
assert_eq!(DEFAULT_EGRESS_CHALLENGE_THRESHOLD, 3u32);
assert_eq!(DEFAULT_EGRESS_COOLDOWN_SECS, 300u64);
}
#[test]
fn default_smuggle_constants_are_stable() {
assert_eq!(DEFAULT_SMUGGLE_COMPOSED_CAP, 64);
assert_eq!(DEFAULT_SMUGGLE_FIRE_DELAY_MS, 200);
assert_eq!(DEFAULT_SMUGGLE_FIRE_TIMEOUT_SECS, 10);
assert!((DEFAULT_SMUGGLE_BODY_DIVERGENCE_THRESHOLD - 0.05).abs() < f64::EPSILON);
assert_eq!(DEFAULT_SMUGGLE_FIRE_PARALLEL, 1);
}
#[test]
fn regex_nfa_size_limit_is_4_mib() {
assert_eq!(REGEX_NFA_SIZE_LIMIT, 4 * 1024 * 1024);
}
#[test]
fn host_states_cap_is_10k() {
assert_eq!(HOST_STATES_CAP, 10_000);
}
#[test]
fn host_technique_hints_cap_is_200() {
assert_eq!(HOST_TECHNIQUE_HINTS_CAP, 200);
}
#[test]
fn block_scan_body_window_is_4096() {
assert_eq!(BLOCK_SCAN_BODY_WINDOW, 4096);
}
#[test]
fn max_response_body_bytes_is_64_mib() {
assert_eq!(MAX_RESPONSE_BODY_BYTES, 64 * 1024 * 1024);
}
}