use serde::Deserialize;
use std::sync::OnceLock;
const WAF_PRESETS_TOML: &str = include_str!("../rules/waf_presets.toml");
#[derive(Debug, Clone, Deserialize)]
pub struct WafPreset {
pub name: String,
pub techniques: Vec<String>,
#[serde(default)]
pub sql_tricks: Vec<String>,
#[serde(default)]
pub xss_tricks: Vec<String>,
#[serde(default)]
pub notes: String,
}
#[derive(Debug, Clone, Deserialize)]
struct WafPresetsFile {
#[serde(default)]
waf_preset: Vec<WafPreset>,
}
fn all_presets() -> &'static [WafPreset] {
static PRESETS: OnceLock<Vec<WafPreset>> = OnceLock::new();
PRESETS.get_or_init(|| {
let file: WafPresetsFile = toml::from_str(WAF_PRESETS_TOML).unwrap_or_else(|e| {
tracing::warn!(error = %e, "invalid TOML in rules/waf_presets.toml; falling back to empty preset set");
WafPresetsFile {
waf_preset: Vec::new(),
}
});
file.waf_preset
})
}
#[must_use]
pub fn preset_for(waf_name: &str) -> Option<&'static WafPreset> {
let name_lower = waf_name.to_ascii_lowercase();
all_presets()
.iter()
.find(|p| p.name.to_ascii_lowercase() == name_lower)
}
#[must_use]
pub fn known_wafs() -> Vec<&'static str> {
all_presets().iter().map(|p| p.name.as_str()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn presets_load_successfully() {
let presets = all_presets();
assert!(!presets.is_empty(), "should load at least one preset");
}
#[test]
fn cloudflare_preset_exists() {
let preset = preset_for("Cloudflare");
assert!(preset.is_some());
let p = preset.unwrap();
assert!(!p.techniques.is_empty());
assert!(!p.sql_tricks.is_empty());
}
#[test]
fn modsecurity_preset_exists() {
let preset = preset_for("ModSecurity");
assert!(preset.is_some());
}
#[test]
fn case_insensitive_lookup() {
assert!(preset_for("cloudflare").is_some());
assert!(preset_for("CLOUDFLARE").is_some());
}
#[test]
fn unknown_waf_returns_none() {
assert!(preset_for("NonExistentWAF").is_none());
}
#[test]
fn known_wafs_list() {
let wafs = known_wafs();
assert!(wafs.contains(&"Cloudflare"));
assert!(wafs.contains(&"ModSecurity"));
assert!(wafs.contains(&"AWS WAF"));
}
#[test]
fn all_presets_have_techniques() {
for preset in all_presets() {
assert!(
!preset.techniques.is_empty(),
"{} should have techniques",
preset.name
);
}
}
#[test]
fn aws_waf_preset_exists() {
assert!(preset_for("AWS WAF").is_some());
assert!(preset_for("aws waf").is_some());
}
#[test]
fn preset_has_sql_or_xss_tricks() {
for preset in all_presets() {
assert!(
!preset.sql_tricks.is_empty() || !preset.xss_tricks.is_empty(),
"{} should have at least sql_tricks or xss_tricks",
preset.name
);
}
}
#[test]
fn known_wafs_non_empty() {
let wafs = known_wafs();
assert!(!wafs.is_empty());
}
}