1use std::collections::BTreeMap;
16
17use regex::{Regex, RegexSet};
18use tracing::warn;
19use waf_core::{Config, Decision, Phase, RequestContext, ScoreItem, Severity, WafModule};
20
21use super::ast::{CrsRule, MatchUnit, ParsedRuleset, SkippedRule, Transform, Variable};
22use super::operator::{compile, rx_pattern, Matcher};
23use super::parse::parse;
24use super::{target, transform};
25
26struct Link {
28 vars: Vec<Variable>,
29 transforms: Vec<Transform>,
30 matcher: Matcher,
31 negated: bool,
32}
33
34struct Compiled {
36 id: u64,
37 severity: Severity,
38 links: Vec<Link>,
39}
40
41struct FastBucket {
43 vars: Vec<Variable>,
44 transforms: Vec<Transform>,
45 set: RegexSet,
46 ids: Vec<u64>,
48 severities: Vec<Severity>,
49}
50
51struct BucketAcc {
53 vars: Vec<Variable>,
54 transforms: Vec<Transform>,
55 patterns: Vec<String>,
56 ids: Vec<u64>,
57 severities: Vec<Severity>,
58}
59
60pub struct CrsModule {
64 fast: Vec<FastBucket>,
65 slow: Vec<Compiled>,
66 skipped: Vec<SkippedRule>,
67 loaded: usize,
68}
69
70impl CrsModule {
71 pub fn from_source(source: &str) -> Self {
74 Self::from_parsed(parse(source))
75 }
76
77 pub fn from_parsed(parsed: ParsedRuleset) -> Self {
79 let mut skipped = parsed.skipped;
80 let mut fast_map: BTreeMap<String, BucketAcc> = BTreeMap::new();
81 let mut slow: Vec<Compiled> = Vec::new();
82 let mut loaded = 0usize;
83
84 for rule in parsed.rules {
85 if let Some(pattern) = fast_pattern(&rule) {
86 if let Err(e) = Regex::new(pattern) {
89 skipped.push(SkippedRule {
90 id: Some(rule.id),
91 line_no: rule.line_no,
92 reason: format!("invalid @rx regex: {e}"),
93 });
94 continue;
95 }
96 let key = format!("{:?}|{:?}", rule.head.variables, rule.head.transforms);
97 let acc = fast_map.entry(key).or_insert_with(|| BucketAcc {
98 vars: rule.head.variables.clone(),
99 transforms: rule.head.transforms.clone(),
100 patterns: Vec::new(),
101 ids: Vec::new(),
102 severities: Vec::new(),
103 });
104 acc.patterns.push(pattern.to_string());
105 acc.ids.push(rule.id);
106 acc.severities.push(rule.severity);
107 loaded += 1;
108 } else {
109 match compile_rule(&rule) {
110 Ok(c) => {
111 slow.push(c);
112 loaded += 1;
113 }
114 Err(reason) => skipped.push(SkippedRule {
115 id: Some(rule.id),
116 line_no: rule.line_no,
117 reason,
118 }),
119 }
120 }
121 }
122
123 let mut fast = Vec::with_capacity(fast_map.len());
127 for acc in fast_map.into_values() {
128 match RegexSet::new(&acc.patterns) {
129 Ok(set) => fast.push(FastBucket {
130 vars: acc.vars,
131 transforms: acc.transforms,
132 set,
133 ids: acc.ids,
134 severities: acc.severities,
135 }),
136 Err(_) => {
137 for (i, pattern) in acc.patterns.iter().enumerate() {
138 if let Ok(re) = Regex::new(pattern) {
139 slow.push(Compiled {
140 id: acc.ids[i],
141 severity: acc.severities[i],
142 links: vec![Link {
143 vars: acc.vars.clone(),
144 transforms: acc.transforms.clone(),
145 matcher: Matcher::Rx(re),
146 negated: false,
147 }],
148 });
149 }
150 }
151 }
152 }
153 }
154
155 Self { fast, slow, skipped, loaded }
156 }
157
158 pub fn loaded_count(&self) -> usize {
160 self.loaded
161 }
162
163 pub fn skipped(&self) -> &[SkippedRule] {
165 &self.skipped
166 }
167
168 pub fn report(&self) -> String {
170 format!("CRS import: {} rules loaded, {} skipped", self.loaded, self.skipped.len())
171 }
172
173 fn values_for(vars: &[Variable], transforms: &[Transform], ctx: &RequestContext) -> Vec<String> {
175 target::collect_values(vars, ctx)
176 .into_iter()
177 .map(|v| transform::apply(&v, transforms))
178 .collect()
179 }
180}
181
182fn fast_pattern(rule: &CrsRule) -> Option<&str> {
184 if rule.chain.is_empty() && !rule.head.negated {
185 rx_pattern(&rule.head.operator)
186 } else {
187 None
188 }
189}
190
191fn compile_rule(rule: &CrsRule) -> Result<Compiled, String> {
192 let mut links = Vec::with_capacity(1 + rule.chain.len());
193 links.push(compile_link(&rule.head)?);
194 for child in &rule.chain {
195 links.push(compile_link(child)?);
196 }
197 Ok(Compiled { id: rule.id, severity: rule.severity, links })
198}
199
200fn compile_link(u: &MatchUnit) -> Result<Link, String> {
201 Ok(Link {
202 vars: u.variables.clone(),
203 transforms: u.transforms.clone(),
204 matcher: compile(&u.operator)?,
205 negated: u.negated,
206 })
207}
208
209fn eval_compiled(c: &Compiled, ctx: &RequestContext) -> bool {
210 for link in &c.links {
212 let values = CrsModule::values_for(&link.vars, &link.transforms, ctx);
213 let any = values.iter().any(|v| link.matcher.matches(v));
214 let link_matches = if link.negated { !any } else { any };
217 if !link_matches {
218 return false;
219 }
220 }
221 true
222}
223
224fn push_item(items: &mut Vec<ScoreItem>, ctx: &RequestContext, id: u64, severity: Severity) {
225 let rule_id = format!("crs-{id}");
226 warn!(request_id = %ctx.request_id, rule_id = %rule_id, severity = ?severity, "crs detection");
227 items.push(ScoreItem { rule_id, severity });
228}
229
230impl WafModule for CrsModule {
231 fn id(&self) -> &str {
232 "crs"
233 }
234
235 fn phase(&self) -> Phase {
236 Phase::Body
237 }
238
239 fn init(&mut self, _cfg: &Config) {
240 }
242
243 fn inspect(&self, ctx: &RequestContext) -> Decision {
244 let mut items: Vec<ScoreItem> = Vec::new();
245
246 for b in &self.fast {
248 let values = Self::values_for(&b.vars, &b.transforms, ctx);
249 if values.is_empty() {
250 continue;
251 }
252 let mut hit = vec![false; b.ids.len()];
253 for v in &values {
254 for idx in b.set.matches(v).into_iter() {
255 hit[idx] = true;
256 }
257 }
258 for (i, &h) in hit.iter().enumerate() {
259 if h {
260 push_item(&mut items, ctx, b.ids[i], b.severities[i]);
261 }
262 }
263 }
264
265 for c in &self.slow {
267 if eval_compiled(c, ctx) {
268 push_item(&mut items, ctx, c.id, c.severity);
269 }
270 }
271
272 if items.is_empty() {
273 Decision::Allow
274 } else {
275 Decision::Scores(items)
276 }
277 }
278
279 fn structural(&self) -> bool {
282 true
283 }
284}