1use crate::error::{Error, Result};
4
5#[derive(Debug, Clone)]
7pub enum Action {
8 Disruptive(DisruptiveAction),
10 Flow(FlowAction),
12 Metadata(MetadataAction),
14 Data(DataAction),
16 Logging(LoggingAction),
18 Control(ControlAction),
20 Transformation(String),
22}
23
24#[derive(Debug, Clone)]
26pub enum DisruptiveAction {
27 Deny,
29 Block,
31 Pass,
33 Allow,
35 AllowPhase,
37 AllowRequest,
39 Redirect(String),
41 Drop,
43}
44
45#[derive(Debug, Clone)]
47pub enum FlowAction {
48 Chain,
50 Skip(u32),
52 SkipAfter(String),
54 MultiMatch,
56}
57
58#[derive(Debug, Clone)]
60pub enum MetadataAction {
61 Id(u64),
63 Phase(u8),
65 Severity(u8),
67 Msg(String),
69 Tag(String),
71 Rev(String),
73 Ver(String),
75 Maturity(u8),
77 Accuracy(u8),
79 LogData(String),
81 Status(u16),
83}
84
85#[derive(Debug, Clone)]
87pub enum DataAction {
88 SetVar(SetVarSpec),
90 Capture,
92 InitCol { collection: String, key: String },
94 SetUid(String),
96 SetSid(String),
98 ExpireVar { var: String, seconds: u64 },
100 DeprecateVar(String),
102 Exec(String),
104 Prepend(String),
106 Append(String),
108}
109
110#[derive(Debug, Clone)]
112pub struct SetVarSpec {
113 pub collection: String,
115 pub key: String,
117 pub value: SetVarValue,
119}
120
121#[derive(Debug, Clone)]
123pub enum SetVarValue {
124 String(String),
126 Int(i64),
128 Increment(i64),
130 Decrement(i64),
132 Delete,
134 Macro(String),
136}
137
138#[derive(Debug, Clone)]
140pub enum LoggingAction {
141 Log,
143 NoLog,
145 AuditLog,
147 NoAuditLog,
149 SanitiseMatched,
151 SanitizeMatched,
153 SanitiseArg(String),
155 SanitiseRequestHeader(String),
157 SanitiseResponseHeader(String),
159}
160
161#[derive(Debug, Clone)]
163pub struct ControlAction {
164 pub directive: String,
166 pub value: String,
168}
169
170fn normalize_line_continuations(input: &str) -> String {
173 let mut result = String::with_capacity(input.len());
174 let mut chars = input.chars().peekable();
175
176 while let Some(c) = chars.next() {
177 if c == '\\' {
178 if chars.peek() == Some(&'\n') {
180 chars.next();
182 while chars.peek().map(|c| c.is_whitespace() && *c != '\n').unwrap_or(false) {
184 chars.next();
185 }
186 continue;
187 } else if chars.peek() == Some(&'\r') {
188 chars.next();
190 if chars.peek() == Some(&'\n') {
191 chars.next();
192 }
193 while chars.peek().map(|c| c.is_whitespace() && *c != '\n').unwrap_or(false) {
195 chars.next();
196 }
197 continue;
198 }
199 }
200 result.push(c);
201 }
202
203 result
204}
205
206pub fn parse_actions(input: &str) -> Result<Vec<Action>> {
208 let normalized = normalize_line_continuations(input);
210
211 let mut actions = Vec::new();
212 let mut chars = normalized.chars().peekable();
213 let mut current = String::new();
214 let mut in_quotes = false;
215 let mut quote_char = '"';
216 let mut paren_depth: u32 = 0;
217
218 while let Some(c) = chars.next() {
219 match c {
220 '"' | '\'' if !in_quotes => {
221 in_quotes = true;
222 quote_char = c;
223 current.push(c);
224 }
225 c if in_quotes && c == quote_char => {
226 in_quotes = false;
227 current.push(c);
228 }
229 '(' if !in_quotes => {
230 paren_depth += 1;
231 current.push(c);
232 }
233 ')' if !in_quotes => {
234 paren_depth = paren_depth.saturating_sub(1);
235 current.push(c);
236 }
237 ',' if !in_quotes && paren_depth == 0 => {
238 if !current.trim().is_empty() {
239 actions.push(parse_single_action(current.trim())?);
240 }
241 current.clear();
242 }
243 _ => {
244 current.push(c);
245 }
246 }
247 }
248
249 if !current.trim().is_empty() {
251 actions.push(parse_single_action(current.trim())?);
252 }
253
254 Ok(actions)
255}
256
257fn parse_single_action(input: &str) -> Result<Action> {
259 let input = input.trim();
260
261 if input.starts_with("t:") {
263 return Ok(Action::Transformation(input[2..].to_string()));
264 }
265
266 let (name, argument) = if let Some(pos) = input.find(':') {
268 let name = &input[..pos];
269 let arg = &input[pos + 1..];
270 (name.to_lowercase(), Some(arg.to_string()))
271 } else {
272 (input.to_lowercase(), None)
273 };
274
275 match name.as_str() {
276 "deny" => Ok(Action::Disruptive(DisruptiveAction::Deny)),
278 "block" => Ok(Action::Disruptive(DisruptiveAction::Block)),
279 "pass" => Ok(Action::Disruptive(DisruptiveAction::Pass)),
280 "allow" => Ok(Action::Disruptive(DisruptiveAction::Allow)),
281 "drop" => Ok(Action::Disruptive(DisruptiveAction::Drop)),
282 "redirect" => {
283 let url = argument.ok_or_else(|| Error::InvalidActionArgument {
284 action: "redirect".to_string(),
285 message: "missing URL".to_string(),
286 })?;
287 Ok(Action::Disruptive(DisruptiveAction::Redirect(url)))
288 }
289
290 "chain" => Ok(Action::Flow(FlowAction::Chain)),
292 "skip" => {
293 let count: u32 = argument
294 .as_ref()
295 .and_then(|s| s.parse().ok())
296 .ok_or_else(|| Error::InvalidActionArgument {
297 action: "skip".to_string(),
298 message: "invalid count".to_string(),
299 })?;
300 Ok(Action::Flow(FlowAction::Skip(count)))
301 }
302 "skipafter" => {
303 let marker = argument.ok_or_else(|| Error::InvalidActionArgument {
304 action: "skipAfter".to_string(),
305 message: "missing marker name".to_string(),
306 })?;
307 Ok(Action::Flow(FlowAction::SkipAfter(marker)))
308 }
309
310 "id" => {
312 let id: u64 = argument
313 .as_ref()
314 .and_then(|s| s.parse().ok())
315 .ok_or_else(|| Error::InvalidActionArgument {
316 action: "id".to_string(),
317 message: "invalid ID".to_string(),
318 })?;
319 Ok(Action::Metadata(MetadataAction::Id(id)))
320 }
321 "phase" => {
322 let phase: u8 = argument
323 .as_ref()
324 .and_then(|s| s.parse().ok())
325 .ok_or_else(|| Error::InvalidActionArgument {
326 action: "phase".to_string(),
327 message: "invalid phase".to_string(),
328 })?;
329 Ok(Action::Metadata(MetadataAction::Phase(phase)))
330 }
331 "severity" => {
332 let sev: u8 = argument
333 .as_ref()
334 .map(|s| s.trim_matches(|c| c == '\'' || c == '"'))
335 .and_then(|s| parse_severity(s))
336 .ok_or_else(|| Error::InvalidActionArgument {
337 action: "severity".to_string(),
338 message: "invalid severity".to_string(),
339 })?;
340 Ok(Action::Metadata(MetadataAction::Severity(sev)))
341 }
342 "msg" => {
343 let msg = argument.unwrap_or_default();
344 let msg = msg.trim_matches(|c| c == '\'' || c == '"');
346 Ok(Action::Metadata(MetadataAction::Msg(msg.to_string())))
347 }
348 "tag" => {
349 let tag = argument.unwrap_or_default();
350 let tag = tag.trim_matches(|c| c == '\'' || c == '"');
351 Ok(Action::Metadata(MetadataAction::Tag(tag.to_string())))
352 }
353 "rev" => {
354 let rev = argument.unwrap_or_default();
355 let rev = rev.trim_matches(|c| c == '\'' || c == '"');
356 Ok(Action::Metadata(MetadataAction::Rev(rev.to_string())))
357 }
358 "ver" => {
359 let ver = argument.unwrap_or_default();
360 let ver = ver.trim_matches(|c| c == '\'' || c == '"');
361 Ok(Action::Metadata(MetadataAction::Ver(ver.to_string())))
362 }
363 "maturity" => {
364 let mat: u8 = argument
365 .as_ref()
366 .and_then(|s| s.parse().ok())
367 .ok_or_else(|| Error::InvalidActionArgument {
368 action: "maturity".to_string(),
369 message: "invalid maturity".to_string(),
370 })?;
371 Ok(Action::Metadata(MetadataAction::Maturity(mat)))
372 }
373 "accuracy" => {
374 let acc: u8 = argument
375 .as_ref()
376 .and_then(|s| s.parse().ok())
377 .ok_or_else(|| Error::InvalidActionArgument {
378 action: "accuracy".to_string(),
379 message: "invalid accuracy".to_string(),
380 })?;
381 Ok(Action::Metadata(MetadataAction::Accuracy(acc)))
382 }
383 "logdata" => {
384 let data = argument.unwrap_or_default();
385 let data = data.trim_matches(|c| c == '\'' || c == '"');
386 Ok(Action::Metadata(MetadataAction::LogData(data.to_string())))
387 }
388 "status" => {
389 let status: u16 = argument
390 .as_ref()
391 .and_then(|s| s.parse().ok())
392 .ok_or_else(|| Error::InvalidActionArgument {
393 action: "status".to_string(),
394 message: "invalid status code".to_string(),
395 })?;
396 Ok(Action::Metadata(MetadataAction::Status(status)))
397 }
398
399 "setvar" => {
401 let spec = argument.ok_or_else(|| Error::InvalidActionArgument {
402 action: "setvar".to_string(),
403 message: "missing variable specification".to_string(),
404 })?;
405 let setvar = parse_setvar(&spec)?;
406 Ok(Action::Data(DataAction::SetVar(setvar)))
407 }
408 "capture" => Ok(Action::Data(DataAction::Capture)),
409
410 "log" => Ok(Action::Logging(LoggingAction::Log)),
412 "nolog" => Ok(Action::Logging(LoggingAction::NoLog)),
413 "auditlog" => Ok(Action::Logging(LoggingAction::AuditLog)),
414 "noauditlog" => Ok(Action::Logging(LoggingAction::NoAuditLog)),
415 "sanitisematched" | "sanitizematched" => Ok(Action::Logging(LoggingAction::SanitiseMatched)),
416
417 "ctl" => {
419 let spec = argument.ok_or_else(|| Error::InvalidActionArgument {
420 action: "ctl".to_string(),
421 message: "missing control specification".to_string(),
422 })?;
423 let (directive, value) = if let Some(pos) = spec.find('=') {
424 (spec[..pos].to_string(), spec[pos + 1..].to_string())
425 } else {
426 (spec, String::new())
427 };
428 Ok(Action::Control(ControlAction { directive, value }))
429 }
430
431 "initcol" => {
433 let spec = argument.ok_or_else(|| Error::InvalidActionArgument {
434 action: "initcol".to_string(),
435 message: "missing collection specification".to_string(),
436 })?;
437 let (collection, key) = if let Some(pos) = spec.find('=') {
438 (spec[..pos].to_string(), spec[pos + 1..].to_string())
439 } else {
440 (spec, String::new())
441 };
442 Ok(Action::Data(DataAction::InitCol { collection, key }))
443 }
444
445 "setsid" | "setuid" => {
447 Ok(Action::Logging(LoggingAction::NoAuditLog)) }
450
451 "deprecatevar" => {
453 Ok(Action::Logging(LoggingAction::NoAuditLog)) }
455
456 "expirevar" => {
458 let spec = argument.unwrap_or_default();
459 let (var, seconds) = if let Some(pos) = spec.find('=') {
460 let var = spec[..pos].to_string();
461 let secs: u64 = spec[pos + 1..].parse().unwrap_or(0);
462 (var, secs)
463 } else {
464 (spec, 0)
465 };
466 Ok(Action::Data(DataAction::ExpireVar { var, seconds }))
467 }
468
469 "multimatch" => Ok(Action::Flow(FlowAction::MultiMatch)),
471
472 "exec" => Ok(Action::Logging(LoggingAction::NoAuditLog)), "append" | "prepend" => Ok(Action::Logging(LoggingAction::NoAuditLog)), "proxy" => Ok(Action::Logging(LoggingAction::NoAuditLog)), "pause" => Ok(Action::Logging(LoggingAction::NoAuditLog)), "xmlns" => Ok(Action::Logging(LoggingAction::NoAuditLog)), _ => Err(Error::UnknownAction {
488 name: name.to_string(),
489 }),
490 }
491}
492
493fn parse_setvar(input: &str) -> Result<SetVarSpec> {
495 let input = input.trim();
496 let input = if input.len() >= 2
499 && ((input.starts_with('\'') && input.ends_with('\''))
500 || (input.starts_with('"') && input.ends_with('"')))
501 {
502 &input[1..input.len() - 1]
503 } else {
504 input
505 };
506
507 if input.starts_with('!') {
509 let var = &input[1..];
510 let (collection, key) = parse_var_name(var)?;
511 return Ok(SetVarSpec {
512 collection,
513 key,
514 value: SetVarValue::Delete,
515 });
516 }
517
518 let (var, value_str) = if let Some(pos) = input.find('=') {
520 (&input[..pos], Some(&input[pos + 1..]))
521 } else {
522 (input, None)
523 };
524
525 let (collection, key) = parse_var_name(var)?;
526
527 let value = if let Some(val) = value_str {
528 if val.contains("%{") {
529 SetVarValue::Macro(val.to_string())
532 } else if val.starts_with('+') {
533 let amount: i64 = val[1..].parse().unwrap_or(1);
535 SetVarValue::Increment(amount)
536 } else if val.starts_with('-') {
537 let amount: i64 = val[1..].parse().unwrap_or(1);
539 SetVarValue::Decrement(amount)
540 } else if let Ok(n) = val.parse::<i64>() {
541 SetVarValue::Int(n)
542 } else {
543 SetVarValue::String(val.to_string())
544 }
545 } else {
546 SetVarValue::String("1".to_string())
547 };
548
549 Ok(SetVarSpec {
550 collection,
551 key,
552 value,
553 })
554}
555
556fn parse_var_name(input: &str) -> Result<(String, String)> {
558 if let Some(pos) = input.find('.') {
559 Ok((input[..pos].to_lowercase(), input[pos + 1..].to_string()))
560 } else {
561 Ok(("tx".to_string(), input.to_string()))
563 }
564}
565
566fn parse_severity(s: &str) -> Option<u8> {
568 if let Ok(n) = s.parse::<u8>() {
570 return Some(n);
571 }
572
573 match s.to_lowercase().as_str() {
575 "emergency" => Some(0),
576 "alert" => Some(1),
577 "critical" => Some(2),
578 "error" => Some(3),
579 "warning" => Some(4),
580 "notice" => Some(5),
581 "info" => Some(6),
582 "debug" => Some(7),
583 _ => None,
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590
591 #[test]
592 fn test_parse_simple_actions() {
593 let actions = parse_actions("id:1,deny,status:403").unwrap();
594 assert_eq!(actions.len(), 3);
595 }
596
597 #[test]
598 fn test_parse_action_with_msg() {
599 let actions = parse_actions("id:1,msg:'Hello world',deny").unwrap();
600 assert_eq!(actions.len(), 3);
601 }
602
603 #[test]
604 fn test_parse_setvar() {
605 let actions = parse_actions("setvar:tx.score=+5").unwrap();
606 assert_eq!(actions.len(), 1);
607 match &actions[0] {
608 Action::Data(DataAction::SetVar(spec)) => {
609 assert_eq!(spec.collection, "tx");
610 assert_eq!(spec.key, "score");
611 assert!(matches!(spec.value, SetVarValue::Increment(5)));
612 }
613 _ => panic!("expected SetVar"),
614 }
615 }
616
617 #[test]
618 fn test_parse_setvar_quoted_increment() {
619 let actions = parse_actions("setvar:'tx.anomaly_score=+5'").unwrap();
621 match &actions[0] {
622 Action::Data(DataAction::SetVar(spec)) => {
623 assert_eq!(spec.key, "anomaly_score");
624 assert!(matches!(spec.value, SetVarValue::Increment(5)),
625 "expected Increment(5), got {:?}", spec.value);
626 }
627 _ => panic!("expected SetVar"),
628 }
629 }
630
631 #[test]
632 fn test_parse_setvar_quoted_set() {
633 let actions = parse_actions("setvar:'tx.anomaly_score=7'").unwrap();
634 match &actions[0] {
635 Action::Data(DataAction::SetVar(spec)) => {
636 assert_eq!(spec.key, "anomaly_score");
637 assert!(matches!(spec.value, SetVarValue::Int(7)),
638 "expected Int(7), got {:?}", spec.value);
639 }
640 _ => panic!("expected SetVar"),
641 }
642 }
643
644 #[test]
645 fn test_parse_chain() {
646 let actions = parse_actions("id:1,phase:2,chain").unwrap();
647 assert!(actions.iter().any(|a| matches!(a, Action::Flow(FlowAction::Chain))));
648 }
649
650 #[test]
651 fn test_parse_transformation() {
652 let actions = parse_actions("id:1,t:lowercase,t:urlDecode").unwrap();
653 let transforms: Vec<_> = actions
654 .iter()
655 .filter(|a| matches!(a, Action::Transformation(_)))
656 .collect();
657 assert_eq!(transforms.len(), 2);
658 }
659}