1use crate::error::Result;
4use crate::operators::{compile_operator, Operator};
5use crate::parser::{Action, MetadataAction, Directive, Parser, VariableSpec, OperatorSpec, OperatorName, FlowAction, RuleEngineMode as ParserRuleEngineMode};
6use crate::transformations::TransformationPipeline;
7
8use super::phase::Phase;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12#[derive(Clone)]
14pub struct CompiledRule {
15 pub id: Option<String>,
17 pub phase: Phase,
19 pub variables: Vec<VariableSpec>,
21 pub operator: Arc<dyn Operator>,
23 pub operator_negated: bool,
25 pub transformations: TransformationPipeline,
27 pub actions: Vec<Action>,
29 pub is_chain: bool,
31 pub chain_next: Option<usize>,
33}
34
35impl std::fmt::Debug for CompiledRule {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("CompiledRule")
38 .field("id", &self.id)
39 .field("phase", &self.phase)
40 .field("variables", &self.variables)
41 .field("operator_negated", &self.operator_negated)
42 .field("is_chain", &self.is_chain)
43 .finish()
44 }
45}
46
47pub struct Rules {
49 by_phase: HashMap<Phase, Vec<CompiledRule>>,
51 markers: HashMap<String, (Phase, usize)>,
53}
54
55impl Rules {
56 pub fn new() -> Self {
58 Self {
59 by_phase: HashMap::new(),
60 markers: HashMap::new(),
61 }
62 }
63
64 pub fn add(&mut self, phase: Phase, rule: CompiledRule) {
66 self.by_phase.entry(phase).or_default().push(rule);
67 }
68
69 pub fn add_marker(&mut self, name: String, phase: Phase, index: usize) {
71 self.markers.insert(name, (phase, index));
72 }
73
74 pub fn for_phase(&self, phase: Phase) -> &[CompiledRule] {
76 self.by_phase.get(&phase).map(|v| v.as_slice()).unwrap_or(&[])
77 }
78
79 pub fn marker(&self, name: &str) -> Option<(Phase, usize)> {
81 self.markers.get(name).copied()
82 }
83
84 pub fn count(&self) -> usize {
86 self.by_phase.values().map(|v| v.len()).sum()
87 }
88}
89
90impl Default for Rules {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96pub struct CompiledRuleset {
98 rules: Rules,
100 engine_mode: RuleEngineMode,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum RuleEngineMode {
107 On,
109 DetectionOnly,
111 Off,
113}
114
115impl Default for RuleEngineMode {
116 fn default() -> Self {
117 RuleEngineMode::On
118 }
119}
120
121impl CompiledRuleset {
122 pub fn new() -> Self {
124 Self {
125 rules: Rules::new(),
126 engine_mode: RuleEngineMode::default(),
127 }
128 }
129
130 pub fn from_file(path: &str) -> Result<Self> {
132 let mut parser = Parser::new();
133 parser.parse_file(std::path::Path::new(path))?;
134 Self::compile(parser.into_directives())
135 }
136
137 pub fn from_string(rules: &str) -> Result<Self> {
139 let mut parser = Parser::new();
140 parser.parse(rules)?;
141 Self::compile(parser.into_directives())
142 }
143
144 pub fn compile(directives: Vec<Directive>) -> Result<Self> {
146 let mut ruleset = Self::new();
147 let mut pending_chain: Option<(Phase, usize)> = None;
148
149 for directive in directives {
150 match directive {
151 Directive::SecRuleEngine(mode) => {
152 ruleset.engine_mode = match mode {
153 ParserRuleEngineMode::On => RuleEngineMode::On,
154 ParserRuleEngineMode::Off => RuleEngineMode::Off,
155 ParserRuleEngineMode::DetectionOnly => RuleEngineMode::DetectionOnly,
156 };
157 }
158 Directive::SecRule(rule) => {
159 let phase = extract_phase(&rule.actions);
160 let id = extract_id(&rule.actions);
161 let is_chain = has_chain(&rule.actions);
162 let transformations = extract_transformations(&rule.actions)?;
163
164 let operator = compile_operator(&rule.operator)?;
165
166 let compiled = CompiledRule {
167 id,
168 phase,
169 variables: rule.variables,
170 operator,
171 operator_negated: rule.operator.negated,
172 transformations,
173 actions: rule.actions,
174 is_chain,
175 chain_next: None,
176 };
177
178 let rules_for_phase = ruleset.rules.by_phase.entry(phase).or_default();
179 let idx = rules_for_phase.len();
180 rules_for_phase.push(compiled);
181
182 if let Some((chain_phase, chain_idx)) = pending_chain.take() {
184 if chain_phase == phase {
185 if let Some(prev_rule) = ruleset.rules.by_phase
186 .get_mut(&chain_phase)
187 .and_then(|r| r.get_mut(chain_idx))
188 {
189 prev_rule.chain_next = Some(idx);
190 }
191 }
192 }
193
194 if is_chain {
195 pending_chain = Some((phase, idx));
196 }
197 }
198 Directive::SecAction(sec_action) => {
199 let phase = extract_phase(&sec_action.actions);
201 let id = extract_id(&sec_action.actions);
202 let transformations = extract_transformations(&sec_action.actions)?;
203
204 let operator = compile_operator(&OperatorSpec {
206 negated: false,
207 name: OperatorName::UnconditionalMatch,
208 argument: String::new(),
209 })?;
210
211 let compiled = CompiledRule {
212 id,
213 phase,
214 variables: vec![],
215 operator,
216 operator_negated: false,
217 transformations,
218 actions: sec_action.actions,
219 is_chain: false,
220 chain_next: None,
221 };
222
223 ruleset.rules.add(phase, compiled);
224 }
225 Directive::SecMarker(marker) => {
226 let phase = Phase::RequestHeaders;
228 let idx = ruleset.rules.by_phase.get(&phase).map(|v| v.len()).unwrap_or(0);
229 ruleset.rules.add_marker(marker.name, phase, idx);
230 }
231 _ => {
232 }
234 }
235 }
236
237 Ok(ruleset)
238 }
239
240 pub fn rules_for_phase(&self, phase: Phase) -> &[CompiledRule] {
242 self.rules.for_phase(phase)
243 }
244
245 pub fn rule_count(&self) -> usize {
247 self.rules.count()
248 }
249
250 pub fn engine_mode(&self) -> RuleEngineMode {
252 self.engine_mode
253 }
254
255 pub fn marker(&self, name: &str) -> Option<(Phase, usize)> {
257 self.rules.marker(name)
258 }
259}
260
261impl Default for CompiledRuleset {
262 fn default() -> Self {
263 Self::new()
264 }
265}
266
267fn extract_phase(actions: &[Action]) -> Phase {
269 for action in actions {
270 if let Action::Metadata(MetadataAction::Phase(p)) = action {
271 return Phase::from_number(*p).unwrap_or(Phase::RequestBody);
272 }
273 }
274 Phase::RequestBody }
276
277fn extract_id(actions: &[Action]) -> Option<String> {
279 for action in actions {
280 if let Action::Metadata(MetadataAction::Id(id)) = action {
281 return Some(id.to_string());
282 }
283 }
284 None
285}
286
287fn has_chain(actions: &[Action]) -> bool {
289 actions.iter().any(|a| matches!(a, Action::Flow(FlowAction::Chain)))
290}
291
292fn extract_transformations(actions: &[Action]) -> Result<TransformationPipeline> {
294 let mut names = Vec::new();
295 for action in actions {
296 if let Action::Transformation(t) = action {
297 names.push(t.clone());
298 }
299 }
300 if names.is_empty() {
301 Ok(TransformationPipeline::new())
302 } else {
303 TransformationPipeline::from_names(&names)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_compile_simple_rule() {
313 let rules = r#"
314 SecRule REQUEST_URI "@contains /admin" "id:1,phase:1,deny"
315 "#;
316 let ruleset = CompiledRuleset::from_string(rules).unwrap();
317 assert_eq!(ruleset.rule_count(), 1);
318
319 let phase1_rules = ruleset.rules_for_phase(Phase::RequestHeaders);
320 assert_eq!(phase1_rules.len(), 1);
321 assert_eq!(phase1_rules[0].id, Some("1".to_string()));
322 }
323
324 #[test]
325 fn test_compile_multiple_phases() {
326 let rules = r#"
327 SecRule REQUEST_URI "@contains /admin" "id:1,phase:1,deny"
328 SecRule REQUEST_BODY "@rx attack" "id:2,phase:2,deny"
329 "#;
330 let ruleset = CompiledRuleset::from_string(rules).unwrap();
331 assert_eq!(ruleset.rule_count(), 2);
332
333 assert_eq!(ruleset.rules_for_phase(Phase::RequestHeaders).len(), 1);
334 assert_eq!(ruleset.rules_for_phase(Phase::RequestBody).len(), 1);
335 }
336
337 #[test]
338 fn test_engine_mode() {
339 let rules = r#"
340 SecRuleEngine DetectionOnly
341 SecRule REQUEST_URI "@contains /admin" "id:1,phase:1,deny"
342 "#;
343 let ruleset = CompiledRuleset::from_string(rules).unwrap();
344 assert_eq!(ruleset.engine_mode(), RuleEngineMode::DetectionOnly);
345 }
346}