zentinel_modsec/parser/directive.rs
1//! Directive types for ModSecurity configuration.
2
3use super::{Action, OperatorSpec, VariableSpec};
4use crate::error::SourceLocation;
5use std::path::PathBuf;
6
7/// A parsed ModSecurity directive.
8#[derive(Debug, Clone)]
9pub enum Directive {
10 /// SecRule directive - the main rule type.
11 SecRule(SecRule),
12 /// SecAction directive - actions without matching.
13 SecAction(SecAction),
14 /// SecMarker directive - named marker for skipAfter.
15 SecMarker(SecMarker),
16 /// SecRuleEngine directive - enable/disable rules.
17 SecRuleEngine(RuleEngineMode),
18 /// SecDefaultAction directive - default actions for rules.
19 SecDefaultAction(Vec<Action>),
20 /// SecRuleRemoveById directive - remove rules by ID or ID range.
21 SecRuleRemoveById(Vec<RuleIdSelector>),
22 /// SecRuleUpdateTargetById directive - update rule targets (CRS exclusions).
23 SecRuleUpdateTargetById(UpdateTargetById),
24 /// SecRuleUpdateActionById directive - update rule actions.
25 SecRuleUpdateActionById { id: u64, actions: Vec<Action> },
26 /// SecRequestBodyAccess directive.
27 SecRequestBodyAccess(bool),
28 /// SecResponseBodyAccess directive.
29 SecResponseBodyAccess(bool),
30 /// SecRequestBodyLimit directive.
31 SecRequestBodyLimit(usize),
32 /// SecResponseBodyLimit directive.
33 SecResponseBodyLimit(usize),
34 /// Include directive - include another file.
35 Include(PathBuf),
36 /// Unknown directive (logged and skipped).
37 Unknown(String),
38}
39
40/// A rule ID selector: a single ID or an inclusive ID range.
41///
42/// ModSecurity accepts both forms in `SecRuleRemoveById` and
43/// `SecRuleUpdateTargetById` (e.g. `942100` or `942100-942199`).
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum RuleIdSelector {
46 /// A single rule ID.
47 Single(u64),
48 /// An inclusive range of rule IDs.
49 Range(u64, u64),
50}
51
52impl RuleIdSelector {
53 /// Check whether a rule ID matches this selector.
54 pub fn matches(&self, id: u64) -> bool {
55 match *self {
56 Self::Single(s) => id == s,
57 Self::Range(start, end) => (start..=end).contains(&id),
58 }
59 }
60}
61
62/// A parsed `SecRuleUpdateTargetById` directive.
63///
64/// Per ModSecurity semantics:
65/// - positive targets (`ARGS:foo`) are appended to the rule's variable list
66/// (or replace `replaced` when a third argument is given);
67/// - `!`-prefixed targets (`!ARGS:password`) add a target exclusion so that
68/// the named variable is no longer inspected by the rule.
69#[derive(Debug, Clone)]
70pub struct UpdateTargetById {
71 /// The rule IDs (or ranges) to update.
72 pub ids: Vec<RuleIdSelector>,
73 /// Positive targets to append (or to substitute for `replaced`).
74 pub additions: Vec<VariableSpec>,
75 /// Target exclusions (the text after `!`, e.g. `ARGS:password`).
76 pub exclusions: Vec<String>,
77 /// Optional target to replace (third directive argument).
78 pub replaced: Option<String>,
79 /// Source location for diagnostics.
80 pub location: SourceLocation,
81}
82
83/// A SecRule directive.
84#[derive(Debug, Clone)]
85pub struct SecRule {
86 /// Variables to inspect.
87 pub variables: Vec<VariableSpec>,
88 /// Operator to apply.
89 pub operator: OperatorSpec,
90 /// Actions to execute on match.
91 pub actions: Vec<Action>,
92 /// Source location for error reporting.
93 pub location: SourceLocation,
94}
95
96/// A SecAction directive.
97#[derive(Debug, Clone)]
98pub struct SecAction {
99 /// Actions to execute.
100 pub actions: Vec<Action>,
101 /// Source location for error reporting.
102 pub location: SourceLocation,
103}
104
105/// A SecMarker directive.
106#[derive(Debug, Clone)]
107pub struct SecMarker {
108 /// Marker name.
109 pub name: String,
110}
111
112/// Rule engine mode.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum RuleEngineMode {
115 /// Rules are enabled and will block.
116 On,
117 /// Rules are disabled.
118 Off,
119 /// Rules are enabled but will only log, not block.
120 DetectionOnly,
121}
122
123impl Default for RuleEngineMode {
124 fn default() -> Self {
125 Self::Off
126 }
127}
128
129impl SecRule {
130 /// Check if this rule has the chain action.
131 pub fn is_chained(&self) -> bool {
132 self.actions.iter().any(|a| matches!(a, Action::Flow(super::FlowAction::Chain)))
133 }
134
135 /// Get the rule ID if present.
136 pub fn id(&self) -> Option<u64> {
137 for action in &self.actions {
138 if let Action::Metadata(super::MetadataAction::Id(id)) = action {
139 return Some(*id);
140 }
141 }
142 None
143 }
144
145 /// Get the phase for this rule (defaults to 2).
146 pub fn phase(&self) -> u8 {
147 for action in &self.actions {
148 if let Action::Metadata(super::MetadataAction::Phase(phase)) = action {
149 return *phase;
150 }
151 }
152 2 // Default phase is 2 (request body)
153 }
154}