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")
})
}
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());
}
#[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());
}
}