wafrift-detect 0.2.0

WAF detection from response headers and body, response fingerprint drift analysis.
Documentation
use crate::waf_detect::detect;

fn first_name(status: u16, headers: &[(String, String)], body: &[u8]) -> Option<String> {
    detect(status, headers, body)
        .first()
        .map(|w| w.name.clone())
}

#[test]
fn detect_cloudflare_from_headers() {
    let headers = vec![
        ("cf-ray".into(), "abc123-IAD".into()),
        ("server".into(), "cloudflare".into()),
    ];
    let result = first_name(403, &headers, b"Access denied");
    assert_eq!(result.as_deref(), Some("Cloudflare"));
}

#[test]
fn detect_major_wafs_from_headers_alone() {
    let cases = [
        (
            "Cloudflare",
            vec![
                ("cf-ray".to_string(), "abc123-IAD".to_string()),
                ("server".to_string(), "cloudflare".to_string()),
            ],
        ),
        (
            "Kona SiteDefender",
            vec![
                ("x-akamai-transformed".to_string(), "9 12345".to_string()),
                ("server".to_string(), "akamaighost".to_string()),
            ],
        ),
        (
            "AWS Elastic Load Balancer",
            vec![("x-amz-id".to_string(), "BLOCK".to_string())],
        ),
        (
            "Incapsula",
            vec![
                ("x-iinfo".to_string(), "10-12345678-0 0NNN RT(0".to_string()),
                ("x-cdn".to_string(), "Incapsula".to_string()),
            ],
        ),
        (
            "ModSecurity",
            vec![("server".to_string(), "Mod_Security".to_string())],
        ),
    ];

    for (expected, headers) in cases {
        let name =
            first_name(403, &headers, b"").unwrap_or_else(|| panic!("should detect {expected}"));
        assert_eq!(name, expected);
    }
}

#[test]
fn detect_aws_from_body() {
    let headers = vec![];
    let result = first_name(403, &headers, b"<html>Request blocked by AWS WAF</html>");
    assert_eq!(result.as_deref(), Some("AWS Elastic Load Balancer"));
}

#[test]
fn detect_akamai_reference() {
    let headers = vec![];
    let result = first_name(403, &headers, b"Access Denied. Reference #18.abc123def.456");
    assert_eq!(result.as_deref(), Some("Kona SiteDefender"));
}

#[test]
fn detect_imperva_cookie() {
    let headers = vec![("set-cookie".into(), "visid_incap_123=abc; path=/".into())];
    let result = first_name(200, &headers, b"OK");
    assert_eq!(result.as_deref(), Some("Incapsula"));
}

#[test]
fn no_waf_on_clean_response() {
    let headers = vec![("server".into(), "nginx".into())];
    let result = detect(200, &headers, b"<html>Welcome</html>");
    assert!(result.is_empty());
}

#[test]
fn detect_f5_bigip() {
    let headers = vec![("server".into(), "bigip".into())];
    let result = first_name(200, &headers, b"OK");
    assert_eq!(result.as_deref(), Some("BIG-IP AP Manager"));
}

#[test]
fn highest_confidence_wins() {
    let headers = vec![
        ("cf-ray".into(), "abc".into()),
        ("server".into(), "cloudflare".into()),
        ("x-amz-requestid".into(), "123".into()),
    ];
    let result = first_name(403, &headers, b"blocked");
    assert_eq!(result.as_deref(), Some("Cloudflare"));
}

#[test]
fn detect_barracuda() {
    let headers = vec![("set-cookie".into(), "barra_counter_session=abc".into())];
    let result = first_name(403, &headers, b"Blocked by Barracuda");
    assert_eq!(result.as_deref(), Some("Barracuda"));
}

#[test]
fn detect_fortiweb_cookie() {
    let headers = vec![("set-cookie".into(), "fortiwafsid=abc123".into())];
    let result = first_name(403, &headers, b"Blocked");
    assert_eq!(result.as_deref(), Some("FortiWeb"));
}

#[test]
fn detect_wordfence_body() {
    let result = first_name(403, &[], b"This response was generated by Wordfence.");
    assert_eq!(result.as_deref(), Some("Wordfence"));
}

#[test]
fn modsecurity_406_pattern() {
    let result = first_name(406, &[], b"Not Acceptable. ModSecurity blocked the request");
    assert_eq!(result.as_deref(), Some("ModSecurity"));
}

#[test]
fn ambiguity_returns_multiple() {
    // Headers that could match multiple WAFs with similar confidence
    let headers = vec![
        ("server".into(), "cloudflare".into()),
        ("x-amz-id".into(), "123".into()),
    ];
    let results = detect(403, &headers, b"blocked");
    // Both Cloudflare and AWS may match; if confidence delta is < 0.15,
    // the ambiguity logic should return both.
    assert!(!results.is_empty());
}