bext-waf 0.2.0

Web Application Firewall for bext — rate limiting, IP filtering, GeoIP, rule engine
Documentation
//! Vulnerability scanner detection — identifies automated scanning tools
//! (Nikto, sqlmap, Nmap, Burp Suite, Acunetix, etc.) by User-Agent signature.

use std::sync::OnceLock;

use regex::RegexSet;

static SCANNER_PATTERNS: OnceLock<RegexSet> = OnceLock::new();

static SCANNER_DESCRIPTIONS: &[&str] = &[
    "Scanner: Nikto",
    "Scanner: sqlmap",
    "Scanner: Nmap",
    "Scanner: Acunetix",
    "Scanner: Burp Suite",
    "Scanner: DirBuster",
    "Scanner: GoBuster",
    "Scanner: wfuzz",
    "Scanner: Hydra",
    "Scanner: Metasploit",
    "Scanner: Masscan",
    "Scanner: ZGrab",
    "Scanner: Nuclei",
    "Scanner: HTTPie",
    "Scanner: Havij",
    "Scanner: w3af",
    "Scanner: Arachni",
    "Scanner: Skipfish",
    "Scanner: WPScan",
    "Scripting library: Python-urllib/requests",
    "Scripting library: Go-http-client",
    "Scripting library: Java runtime",
    "Scripting library: libwww-perl",
    "Scripting library: Scrapy",
    "Scripting library: CensysInspect",
    "Shellshock attack pattern",
];

fn patterns() -> &'static RegexSet {
    SCANNER_PATTERNS.get_or_init(|| {
        RegexSet::new([
            r"(?i)\bnikto\b",
            r"(?i)\bsqlmap\b",
            r"(?i)\bnmap\b",
            r"(?i)\bacunetix\b",
            r"(?i)\bburp\s*suite\b",
            r"(?i)\bdirbuster\b",
            r"(?i)\bgobuster\b",
            r"(?i)\bwfuzz\b",
            r"(?i)\bhydra\b",
            r"(?i)\bmetasploit\b",
            r"(?i)\bmasscan\b",
            r"(?i)\bzgrab\b",
            r"(?i)\bnuclei\b",
            r"(?i)\bhttpie\b",
            r"(?i)\bhavij\b",
            r"(?i)\bw3af\b",
            r"(?i)\barachni\b",
            r"(?i)\bskipfish\b",
            r"(?i)\bwpscan\b",
            r"(?i)^(Python-urllib|python-requests|Python/\d)",
            r"(?i)^Go-http-client/",
            r"(?i)^Java/\d",
            r"(?i)\blibwww-perl\b",
            r"(?i)^Scrapy/",
            r"(?i)\bCensysInspect\b",
            r"\(\)\s*\{",
        ])
        .expect("scanner regex patterns must compile")
    })
}

/// Check a User-Agent string for known security scanner patterns.
/// Returns `Some(description)` if a pattern matches.
pub fn check_scanner(user_agent: &str) -> Option<String> {
    let set = patterns();
    let matches: Vec<_> = set.matches(user_agent).into_iter().collect();
    if matches.is_empty() {
        None
    } else {
        let idx = matches[0];
        Some(
            SCANNER_DESCRIPTIONS
                .get(idx)
                .unwrap_or(&"security scanner")
                .to_string(),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn detects_nikto() {
        assert!(check_scanner("Mozilla/5.0 Nikto/2.1.6").is_some());
    }

    #[test]
    fn detects_sqlmap() {
        assert!(check_scanner("sqlmap/1.5.3#stable").is_some());
    }

    #[test]
    fn detects_nmap() {
        assert!(check_scanner("Mozilla/5.0 (compatible; Nmap Scripting Engine)").is_some());
    }

    #[test]
    fn detects_acunetix() {
        assert!(check_scanner("Acunetix Web Vulnerability Scanner").is_some());
    }

    #[test]
    fn detects_burpsuite() {
        assert!(check_scanner("Mozilla/5.0 Burp Suite Professional").is_some());
    }

    #[test]
    fn detects_dirbuster() {
        assert!(check_scanner("DirBuster-1.0-RC1").is_some());
    }

    #[test]
    fn detects_gobuster() {
        assert!(check_scanner("gobuster/3.1.0").is_some());
    }

    #[test]
    fn detects_wfuzz() {
        assert!(check_scanner("Wfuzz/3.1.0").is_some());
    }

    #[test]
    fn detects_hydra() {
        assert!(check_scanner("Hydra/9.3").is_some());
    }

    #[test]
    fn detects_nuclei() {
        assert!(check_scanner("Nuclei - Open-source project").is_some());
    }

    #[test]
    fn detects_wpscan() {
        assert!(check_scanner("WPScan v3.8.22").is_some());
    }

    #[test]
    fn case_insensitive() {
        assert!(check_scanner("SQLMAP/1.5").is_some());
        assert!(check_scanner("nikTo/2.1").is_some());
    }

    // ---- False positives ----

    #[test]
    fn allows_chrome() {
        assert!(check_scanner(
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0"
        )
        .is_none());
    }

    #[test]
    fn allows_firefox() {
        assert!(check_scanner(
            "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0"
        )
        .is_none());
    }

    #[test]
    fn allows_curl() {
        assert!(check_scanner("curl/8.4.0").is_none());
    }

    #[test]
    fn allows_googlebot() {
        assert!(check_scanner(
            "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
        )
        .is_none());
    }
}