use crate::waf_detect::DetectedWaf;
use regex::Regex;
use serde::Deserialize;
const CNAME_REGEX_SIZE_LIMIT: usize = wafrift_types::REGEX_NFA_SIZE_LIMIT;
use super::types::DnsProbe;
#[derive(Debug, Deserialize)]
struct RawCnameSignature {
pub host_regex: String,
pub weight: f64,
}
#[derive(Debug, Deserialize)]
struct RawCnameRule {
pub name: String,
pub vendor: String,
pub confidence_threshold: f64,
#[serde(default)]
pub evasions: Vec<String>,
#[serde(default, rename = "source")]
pub _source: Option<String>,
pub signature: Vec<RawCnameSignature>,
}
#[derive(Debug, Deserialize)]
struct RawCnameDb {
#[serde(default)]
pub cname: Vec<RawCnameRule>,
}
#[derive(Debug, Clone)]
struct CompiledCnameSignature {
host_regex: Regex,
weight: f64,
}
#[derive(Debug, Clone)]
struct CompiledCnameRule {
name: String,
vendor: String,
confidence_threshold: f64,
evasions: Vec<String>,
signatures: Vec<CompiledCnameSignature>,
}
#[derive(Debug, Default, Clone)]
pub struct CnameRuleEngine {
rules: Vec<CompiledCnameRule>,
}
const EMBEDDED_CNAME_TOML: &str = include_str!("../../rules/detect/cname/cname.toml");
impl CnameRuleEngine {
pub fn load_embedded() -> Result<Self, String> {
Self::from_toml(EMBEDDED_CNAME_TOML)
}
pub fn from_toml(toml_str: &str) -> Result<Self, String> {
let raw: RawCnameDb =
toml::from_str(toml_str).map_err(|e| format!("parse CNAME rules TOML: {e}"))?;
let mut rules = Vec::with_capacity(raw.cname.len());
for r in raw.cname {
let mut signatures = Vec::with_capacity(r.signature.len());
for s in r.signature {
let full = if s.host_regex.starts_with("(?i)") || s.host_regex.starts_with("(?-i)")
{
s.host_regex.clone()
} else {
format!("(?i){}", s.host_regex)
};
let re = regex::RegexBuilder::new(&full)
.size_limit(CNAME_REGEX_SIZE_LIMIT)
.build()
.map_err(|e| format!("bad CNAME regex '{}': {e}", s.host_regex))?;
signatures.push(CompiledCnameSignature {
host_regex: re,
weight: s.weight,
});
}
rules.push(CompiledCnameRule {
name: r.name,
vendor: r.vendor,
confidence_threshold: r.confidence_threshold,
evasions: r.evasions,
signatures,
});
}
Ok(Self { rules })
}
pub fn detect(&self, probe: &DnsProbe) -> Vec<DetectedWaf> {
let tagged = probe.tagged_hosts();
let mut out: Vec<DetectedWaf> = Vec::new();
for rule in &self.rules {
let mut score = 0.0;
let mut indicators: Vec<String> = Vec::new();
for sig in &rule.signatures {
for (label, host) in &tagged {
if sig.host_regex.is_match(host) {
score += sig.weight;
let ind = format!("{label}: {host}");
if !indicators.contains(&ind) {
indicators.push(ind);
}
break;
}
}
}
if score >= rule.confidence_threshold {
out.push(DetectedWaf {
name: rule.name.clone(),
confidence: score.min(1.0),
indicators,
});
}
}
out.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.name.cmp(&b.name))
});
out
}
pub fn len(&self) -> usize {
self.rules.len()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn vendor_for(&self, name: &str) -> Option<&str> {
self.rules
.iter()
.find(|r| r.name == name)
.map(|r| r.vendor.as_str())
}
pub fn evasions_for(&self, name: &str) -> Vec<&str> {
self.rules
.iter()
.find(|r| r.name == name)
.map(|r| r.evasions.iter().map(String::as_str).collect())
.unwrap_or_default()
}
}