use std::collections::BTreeMap;
use regex::{Regex, RegexSet};
use tracing::warn;
use waf_core::{Config, Decision, Phase, RequestContext, ScoreItem, Severity, WafModule};
use super::ast::{CrsRule, MatchUnit, ParsedRuleset, SkippedRule, Transform, Variable};
use super::operator::{compile, rx_pattern, Matcher};
use super::parse::parse;
use super::{target, transform};
struct Link {
vars: Vec<Variable>,
transforms: Vec<Transform>,
matcher: Matcher,
negated: bool,
}
struct Compiled {
id: u64,
severity: Severity,
links: Vec<Link>,
}
struct FastBucket {
vars: Vec<Variable>,
transforms: Vec<Transform>,
set: RegexSet,
ids: Vec<u64>,
severities: Vec<Severity>,
}
struct BucketAcc {
vars: Vec<Variable>,
transforms: Vec<Transform>,
patterns: Vec<String>,
ids: Vec<u64>,
severities: Vec<Severity>,
}
pub struct CrsModule {
fast: Vec<FastBucket>,
slow: Vec<Compiled>,
skipped: Vec<SkippedRule>,
loaded: usize,
}
impl CrsModule {
pub fn from_source(source: &str) -> Self {
Self::from_parsed(parse(source))
}
pub fn from_parsed(parsed: ParsedRuleset) -> Self {
let mut skipped = parsed.skipped;
let mut fast_map: BTreeMap<String, BucketAcc> = BTreeMap::new();
let mut slow: Vec<Compiled> = Vec::new();
let mut loaded = 0usize;
for rule in parsed.rules {
if let Some(pattern) = fast_pattern(&rule) {
if let Err(e) = Regex::new(pattern) {
skipped.push(SkippedRule {
id: Some(rule.id),
line_no: rule.line_no,
reason: format!("invalid @rx regex: {e}"),
});
continue;
}
let key = format!("{:?}|{:?}", rule.head.variables, rule.head.transforms);
let acc = fast_map.entry(key).or_insert_with(|| BucketAcc {
vars: rule.head.variables.clone(),
transforms: rule.head.transforms.clone(),
patterns: Vec::new(),
ids: Vec::new(),
severities: Vec::new(),
});
acc.patterns.push(pattern.to_string());
acc.ids.push(rule.id);
acc.severities.push(rule.severity);
loaded += 1;
} else {
match compile_rule(&rule) {
Ok(c) => {
slow.push(c);
loaded += 1;
}
Err(reason) => skipped.push(SkippedRule {
id: Some(rule.id),
line_no: rule.line_no,
reason,
}),
}
}
}
let mut fast = Vec::with_capacity(fast_map.len());
for acc in fast_map.into_values() {
match RegexSet::new(&acc.patterns) {
Ok(set) => fast.push(FastBucket {
vars: acc.vars,
transforms: acc.transforms,
set,
ids: acc.ids,
severities: acc.severities,
}),
Err(_) => {
for (i, pattern) in acc.patterns.iter().enumerate() {
if let Ok(re) = Regex::new(pattern) {
slow.push(Compiled {
id: acc.ids[i],
severity: acc.severities[i],
links: vec![Link {
vars: acc.vars.clone(),
transforms: acc.transforms.clone(),
matcher: Matcher::Rx(re),
negated: false,
}],
});
}
}
}
}
}
Self { fast, slow, skipped, loaded }
}
pub fn loaded_count(&self) -> usize {
self.loaded
}
pub fn skipped(&self) -> &[SkippedRule] {
&self.skipped
}
pub fn report(&self) -> String {
format!("CRS import: {} rules loaded, {} skipped", self.loaded, self.skipped.len())
}
fn values_for(vars: &[Variable], transforms: &[Transform], ctx: &RequestContext) -> Vec<String> {
target::collect_values(vars, ctx)
.into_iter()
.map(|v| transform::apply(&v, transforms))
.collect()
}
}
fn fast_pattern(rule: &CrsRule) -> Option<&str> {
if rule.chain.is_empty() && !rule.head.negated {
rx_pattern(&rule.head.operator)
} else {
None
}
}
fn compile_rule(rule: &CrsRule) -> Result<Compiled, String> {
let mut links = Vec::with_capacity(1 + rule.chain.len());
links.push(compile_link(&rule.head)?);
for child in &rule.chain {
links.push(compile_link(child)?);
}
Ok(Compiled { id: rule.id, severity: rule.severity, links })
}
fn compile_link(u: &MatchUnit) -> Result<Link, String> {
Ok(Link {
vars: u.variables.clone(),
transforms: u.transforms.clone(),
matcher: compile(&u.operator)?,
negated: u.negated,
})
}
fn eval_compiled(c: &Compiled, ctx: &RequestContext) -> bool {
for link in &c.links {
let values = CrsModule::values_for(&link.vars, &link.transforms, ctx);
let any = values.iter().any(|v| link.matcher.matches(v));
let link_matches = if link.negated { !any } else { any };
if !link_matches {
return false;
}
}
true
}
fn push_item(items: &mut Vec<ScoreItem>, ctx: &RequestContext, id: u64, severity: Severity) {
let rule_id = format!("crs-{id}");
warn!(request_id = %ctx.request_id, rule_id = %rule_id, severity = ?severity, "crs detection");
items.push(ScoreItem { rule_id, severity });
}
impl WafModule for CrsModule {
fn id(&self) -> &str {
"crs"
}
fn phase(&self) -> Phase {
Phase::Body
}
fn init(&mut self, _cfg: &Config) {
}
fn inspect(&self, ctx: &RequestContext) -> Decision {
let mut items: Vec<ScoreItem> = Vec::new();
for b in &self.fast {
let values = Self::values_for(&b.vars, &b.transforms, ctx);
if values.is_empty() {
continue;
}
let mut hit = vec![false; b.ids.len()];
for v in &values {
for idx in b.set.matches(v).into_iter() {
hit[idx] = true;
}
}
for (i, &h) in hit.iter().enumerate() {
if h {
push_item(&mut items, ctx, b.ids[i], b.severities[i]);
}
}
}
for c in &self.slow {
if eval_compiled(c, ctx) {
push_item(&mut items, ctx, c.id, c.severity);
}
}
if items.is_empty() {
Decision::Allow
} else {
Decision::Scores(items)
}
}
fn structural(&self) -> bool {
true
}
}