1mod lexer;
22mod directive;
23mod variable;
24mod operator;
25mod action;
26
27pub use lexer::{Lexer, Token, TokenKind};
28pub use directive::{Directive, SecRule, SecAction, SecMarker, RuleEngineMode};
29pub use variable::{VariableSpec, VariableName, Selection};
30pub use operator::{OperatorSpec, OperatorName};
31pub use action::{Action, DisruptiveAction, FlowAction, MetadataAction, DataAction, LoggingAction, ControlAction, SetVarSpec, SetVarValue, parse_actions};
32
33use crate::error::{Error, Result, SourceLocation};
34use std::path::Path;
35
36pub struct Parser {
38 directives: Vec<Directive>,
40 location: SourceLocation,
42 default_actions: Vec<Action>,
44}
45
46impl Parser {
47 pub fn new() -> Self {
49 Self {
50 directives: Vec::new(),
51 location: SourceLocation::default(),
52 default_actions: Vec::new(),
53 }
54 }
55
56 pub fn parse(&mut self, input: &str) -> Result<()> {
58 self.parse_with_location(input, None)
59 }
60
61 pub fn parse_with_location(&mut self, input: &str, file: Option<&Path>) -> Result<()> {
63 self.location.file = file.map(|p| p.to_path_buf());
64 self.location.line = 1;
65 self.location.column = 1;
66
67 let mut lexer = Lexer::new(input);
68
69 while let Some(token) = lexer.next_token() {
70 self.location.line = token.line;
71 self.location.column = token.column;
72
73 match token.kind {
74 TokenKind::Directive(name) => {
75 let directive = self.parse_directive(&name, &mut lexer)?;
76 self.directives.push(directive);
77 }
78 TokenKind::Comment => {
79 }
81 TokenKind::Newline => {
82 }
84 _ => {
85 return Err(Error::parse(
86 format!("unexpected token: {:?}", token.kind),
87 self.location.to_string(),
88 ));
89 }
90 }
91 }
92
93 Ok(())
94 }
95
96 pub fn parse_file(&mut self, path: &Path) -> Result<()> {
98 let content = std::fs::read_to_string(path).map_err(|e| Error::RuleFileLoad {
99 path: path.to_path_buf(),
100 source: e,
101 })?;
102 self.parse_with_location(&content, Some(path))
103 }
104
105 pub fn parse_glob(&mut self, pattern: &str) -> Result<()> {
107 let paths = glob::glob(pattern)
108 .map_err(|e| Error::parse(format!("invalid glob pattern: {}", e), pattern))?;
109
110 for entry in paths {
111 match entry {
112 Ok(path) => {
113 if path.is_file() {
114 self.parse_file(&path)?;
115 }
116 }
117 Err(e) => {
118 tracing::warn!(error = %e, "error reading glob entry");
119 }
120 }
121 }
122
123 Ok(())
124 }
125
126 pub fn into_directives(self) -> Vec<Directive> {
128 self.directives
129 }
130
131 pub fn directives(&self) -> &[Directive] {
133 &self.directives
134 }
135
136 fn parse_directive(&mut self, name: &str, lexer: &mut Lexer) -> Result<Directive> {
138 match name.to_lowercase().as_str() {
139 "secrule" => self.parse_secrule(lexer),
140 "secaction" => self.parse_secaction(lexer),
141 "secmarker" => self.parse_secmarker(lexer),
142 "secruleengine" => self.parse_secruleengine(lexer),
143 "secdefaultaction" => self.parse_secdefaultaction(lexer),
144 "secruleremovebyid" => self.parse_secruleremovebyid(lexer),
145 "secrequestbodyaccess" => self.parse_boolean_directive(lexer, "SecRequestBodyAccess"),
146 "secresponsebodyaccess" => self.parse_boolean_directive(lexer, "SecResponseBodyAccess"),
147 "include" => self.parse_include(lexer),
148 _ => {
149 tracing::warn!(
151 directive = name,
152 location = %self.location,
153 "unknown directive, skipping"
154 );
155 self.skip_to_end_of_line(lexer);
156 Ok(Directive::Unknown(name.to_string()))
157 }
158 }
159 }
160
161 fn parse_secrule(&mut self, lexer: &mut Lexer) -> Result<Directive> {
163 let variables_str = self.expect_argument(lexer, "SecRule variables")?;
165 let variables = variable::parse_variables(&variables_str)?;
166
167 let operator_str = self.expect_quoted_argument(lexer, "SecRule operator")?;
169 let operator = operator::parse_operator(&operator_str)?;
170
171 let actions = if self.peek_quoted(lexer) {
173 let actions_str = self.expect_quoted_argument(lexer, "SecRule actions")?;
174 let mut actions = action::parse_actions(&actions_str)?;
175 actions = self.merge_default_actions(actions);
177 actions
178 } else {
179 self.default_actions.clone()
180 };
181
182 Ok(Directive::SecRule(SecRule {
183 variables,
184 operator,
185 actions,
186 location: self.location.clone(),
187 }))
188 }
189
190 fn parse_secaction(&mut self, lexer: &mut Lexer) -> Result<Directive> {
192 let actions_str = self.expect_quoted_argument(lexer, "SecAction")?;
193 let actions = action::parse_actions(&actions_str)?;
194
195 Ok(Directive::SecAction(SecAction {
196 actions,
197 location: self.location.clone(),
198 }))
199 }
200
201 fn parse_secmarker(&mut self, lexer: &mut Lexer) -> Result<Directive> {
203 let name = self.expect_argument(lexer, "SecMarker name")?;
204 Ok(Directive::SecMarker(SecMarker { name }))
205 }
206
207 fn parse_secruleengine(&mut self, lexer: &mut Lexer) -> Result<Directive> {
209 let mode_str = self.expect_argument(lexer, "SecRuleEngine mode")?;
210 let mode = match mode_str.to_lowercase().as_str() {
211 "on" => RuleEngineMode::On,
212 "off" => RuleEngineMode::Off,
213 "detectiononly" => RuleEngineMode::DetectionOnly,
214 _ => {
215 return Err(Error::parse(
216 format!("invalid SecRuleEngine mode: {}", mode_str),
217 self.location.to_string(),
218 ));
219 }
220 };
221 Ok(Directive::SecRuleEngine(mode))
222 }
223
224 fn parse_secdefaultaction(&mut self, lexer: &mut Lexer) -> Result<Directive> {
226 let actions_str = self.expect_quoted_argument(lexer, "SecDefaultAction")?;
227 let actions = action::parse_actions(&actions_str)?;
228 self.default_actions = actions.clone();
229 Ok(Directive::SecDefaultAction(actions))
230 }
231
232 fn parse_secruleremovebyid(&mut self, lexer: &mut Lexer) -> Result<Directive> {
234 let ids_str = self.expect_argument(lexer, "SecRuleRemoveById")?;
235 let ids: Vec<u64> = ids_str
236 .split_whitespace()
237 .filter_map(|s| s.parse().ok())
238 .collect();
239 Ok(Directive::SecRuleRemoveById(ids))
240 }
241
242 fn parse_boolean_directive(&mut self, lexer: &mut Lexer, name: &str) -> Result<Directive> {
244 let value_str = self.expect_argument(lexer, name)?;
245 let value = match value_str.to_lowercase().as_str() {
246 "on" => true,
247 "off" => false,
248 _ => {
249 return Err(Error::parse(
250 format!("invalid {} value: {} (expected On/Off)", name, value_str),
251 self.location.to_string(),
252 ));
253 }
254 };
255
256 match name {
257 "SecRequestBodyAccess" => Ok(Directive::SecRequestBodyAccess(value)),
258 "SecResponseBodyAccess" => Ok(Directive::SecResponseBodyAccess(value)),
259 _ => Ok(Directive::Unknown(name.to_string())),
260 }
261 }
262
263 fn parse_include(&mut self, lexer: &mut Lexer) -> Result<Directive> {
265 let path = self.expect_argument(lexer, "Include path")?;
266
267 let resolved_path = if let Some(ref base) = self.location.file {
269 if let Some(parent) = base.parent() {
270 let full_path = parent.join(&path);
271 if full_path.exists() {
272 full_path.to_string_lossy().to_string()
273 } else {
274 path
275 }
276 } else {
277 path
278 }
279 } else {
280 path
281 };
282
283 self.parse_glob(&resolved_path)?;
285
286 Ok(Directive::Include(resolved_path.into()))
287 }
288
289 fn expect_argument(&mut self, lexer: &mut Lexer, context: &str) -> Result<String> {
291 lexer.skip_whitespace();
292
293 match lexer.next_token() {
294 Some(token) => match token.kind {
295 TokenKind::Word(s) | TokenKind::QuotedString(s) => Ok(s),
296 _ => Err(Error::parse(
297 format!("expected {} but got {:?}", context, token.kind),
298 self.location.to_string(),
299 )),
300 },
301 None => Err(Error::parse(
302 format!("expected {} but got end of input", context),
303 self.location.to_string(),
304 )),
305 }
306 }
307
308 fn expect_quoted_argument(&mut self, lexer: &mut Lexer, context: &str) -> Result<String> {
310 lexer.skip_whitespace();
311
312 match lexer.next_token() {
313 Some(token) => match token.kind {
314 TokenKind::QuotedString(s) => Ok(s),
315 _ => Err(Error::parse(
316 format!("expected quoted {} but got {:?}", context, token.kind),
317 self.location.to_string(),
318 )),
319 },
320 None => Err(Error::parse(
321 format!("expected quoted {} but got end of input", context),
322 self.location.to_string(),
323 )),
324 }
325 }
326
327 fn peek_quoted(&self, lexer: &mut Lexer) -> bool {
329 lexer.skip_whitespace();
330 lexer.peek().map(|c| c == '"' || c == '\'').unwrap_or(false)
331 }
332
333 fn skip_to_end_of_line(&self, lexer: &mut Lexer) {
335 while let Some(token) = lexer.next_token() {
336 if matches!(token.kind, TokenKind::Newline) {
337 break;
338 }
339 }
340 }
341
342 fn merge_default_actions(&self, rule_actions: Vec<Action>) -> Vec<Action> {
344 let mut result = self.default_actions.clone();
346 for action in rule_actions {
347 result.retain(|a| !actions_same_type(a, &action));
350 result.push(action);
351 }
352 result
353 }
354}
355
356impl Default for Parser {
357 fn default() -> Self {
358 Self::new()
359 }
360}
361
362fn actions_same_type(a: &Action, b: &Action) -> bool {
364 match (a, b) {
365 (Action::Metadata(ma), Action::Metadata(mb)) => {
367 std::mem::discriminant(ma) == std::mem::discriminant(mb)
368 }
369 _ => std::mem::discriminant(a) == std::mem::discriminant(b),
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_parse_simple_rule() {
380 let mut parser = Parser::new();
381 parser
382 .parse(r#"SecRule REQUEST_URI "@contains /admin" "id:1,deny,status:403""#)
383 .unwrap();
384
385 assert_eq!(parser.directives.len(), 1);
386 match &parser.directives[0] {
387 Directive::SecRule(rule) => {
388 assert_eq!(rule.variables.len(), 1);
389 assert_eq!(rule.variables[0].name, VariableName::RequestUri);
390 }
391 _ => panic!("expected SecRule"),
392 }
393 }
394
395 #[test]
396 fn test_parse_secruleengine() {
397 let mut parser = Parser::new();
398 parser.parse("SecRuleEngine On").unwrap();
399
400 assert_eq!(parser.directives.len(), 1);
401 match &parser.directives[0] {
402 Directive::SecRuleEngine(mode) => {
403 assert_eq!(*mode, RuleEngineMode::On);
404 }
405 _ => panic!("expected SecRuleEngine"),
406 }
407 }
408}