sentinel_modsec/actions/
mod.rs

1//! Action system for ModSecurity rule execution.
2
3mod disruptive;
4mod flow;
5mod data;
6mod metadata;
7
8pub use disruptive::*;
9pub use flow::*;
10pub use data::*;
11pub use metadata::*;
12
13use crate::parser::{Action, DisruptiveAction, FlowAction, DataAction, MetadataAction, LoggingAction, SetVarValue};
14
15/// Result of action execution.
16#[derive(Debug, Clone)]
17pub struct ActionResult {
18    /// Whether to stop processing (disruptive action taken).
19    pub disruptive: Option<DisruptiveOutcome>,
20    /// Flow control modifications.
21    pub flow: FlowOutcome,
22    /// Variables to set.
23    pub setvar_ops: Vec<SetVarOp>,
24    /// Captures from regex.
25    pub captures: Vec<String>,
26    /// Metadata collected.
27    pub metadata: RuleMetadata,
28}
29
30impl Default for ActionResult {
31    fn default() -> Self {
32        Self {
33            disruptive: None,
34            flow: FlowOutcome::Continue,
35            setvar_ops: Vec::new(),
36            captures: Vec::new(),
37            metadata: RuleMetadata::default(),
38        }
39    }
40}
41
42/// Outcome of a disruptive action.
43#[derive(Debug, Clone)]
44pub enum DisruptiveOutcome {
45    /// Deny the request with status code.
46    Deny(u16),
47    /// Block (defer to SecRuleEngine).
48    Block,
49    /// Allow the request.
50    Allow,
51    /// Redirect to URL.
52    Redirect(String),
53    /// Pass (continue but mark as matched).
54    Pass,
55    /// Drop the connection.
56    Drop,
57}
58
59/// Flow control outcome.
60#[derive(Debug, Clone, PartialEq)]
61pub enum FlowOutcome {
62    /// Continue normal processing.
63    Continue,
64    /// Chain to next rule.
65    Chain,
66    /// Skip N rules.
67    Skip(u32),
68    /// Skip to marker.
69    SkipAfter(String),
70}
71
72/// Variable set operation.
73#[derive(Debug, Clone)]
74pub struct SetVarOp {
75    /// Collection name (usually "TX").
76    pub collection: String,
77    /// Variable name.
78    pub name: String,
79    /// Operation to perform.
80    pub operation: SetVarOperation,
81}
82
83/// Type of setvar operation.
84#[derive(Debug, Clone)]
85pub enum SetVarOperation {
86    /// Set to value.
87    Set(String),
88    /// Increment by value.
89    Increment(i64),
90    /// Decrement by value.
91    Decrement(i64),
92    /// Delete the variable.
93    Delete,
94}
95
96/// Metadata from a rule.
97#[derive(Debug, Clone, Default)]
98pub struct RuleMetadata {
99    /// Rule ID.
100    pub id: Option<String>,
101    /// Rule message.
102    pub msg: Option<String>,
103    /// Log message.
104    pub logdata: Option<String>,
105    /// Severity (0-7).
106    pub severity: Option<u8>,
107    /// Tags.
108    pub tags: Vec<String>,
109    /// Maturity level.
110    pub maturity: Option<u8>,
111    /// Accuracy level.
112    pub accuracy: Option<u8>,
113    /// Revision.
114    pub rev: Option<String>,
115    /// Version.
116    pub ver: Option<String>,
117}
118
119/// Execute actions and collect results.
120pub fn execute_actions(
121    actions: &[Action],
122    matched_value: Option<&str>,
123    captures: &[String],
124) -> ActionResult {
125    let mut result = ActionResult::default();
126    result.captures = captures.to_vec();
127
128    for action in actions {
129        match action {
130            Action::Disruptive(d) => {
131                result.disruptive = Some(execute_disruptive(d));
132            }
133            Action::Flow(f) => {
134                result.flow = execute_flow(f);
135            }
136            Action::Data(d) => {
137                execute_data(d, &mut result, matched_value);
138            }
139            Action::Metadata(m) => {
140                execute_metadata(m, &mut result.metadata);
141            }
142            Action::Logging(l) => {
143                execute_logging(l, &mut result.metadata);
144            }
145            Action::Control(_) => {
146                // Control actions (ctl:) modify engine behavior, handled elsewhere
147            }
148            Action::Transformation(_) => {
149                // Transformations are applied during variable resolution, not execution
150            }
151        }
152    }
153
154    result
155}
156
157/// Execute a disruptive action.
158fn execute_disruptive(action: &DisruptiveAction) -> DisruptiveOutcome {
159    match action {
160        DisruptiveAction::Deny => DisruptiveOutcome::Deny(403),
161        DisruptiveAction::Block => DisruptiveOutcome::Block,
162        DisruptiveAction::Allow | DisruptiveAction::AllowPhase | DisruptiveAction::AllowRequest => {
163            DisruptiveOutcome::Allow
164        }
165        DisruptiveAction::Pass => DisruptiveOutcome::Pass,
166        DisruptiveAction::Drop => DisruptiveOutcome::Drop,
167        DisruptiveAction::Redirect(url) => DisruptiveOutcome::Redirect(url.clone()),
168    }
169}
170
171/// Execute a flow action.
172fn execute_flow(action: &FlowAction) -> FlowOutcome {
173    match action {
174        FlowAction::Chain => FlowOutcome::Chain,
175        FlowAction::Skip(n) => FlowOutcome::Skip(*n),
176        FlowAction::SkipAfter(marker) => FlowOutcome::SkipAfter(marker.clone()),
177        FlowAction::MultiMatch => FlowOutcome::Continue, // MultiMatch affects matching, not post-match flow
178    }
179}
180
181/// Execute a data action.
182fn execute_data(action: &DataAction, result: &mut ActionResult, _matched_value: Option<&str>) {
183    match action {
184        DataAction::Capture => {
185            // Captures are already populated from regex match
186        }
187        DataAction::SetVar(spec) => {
188            let op = match &spec.value {
189                SetVarValue::String(v) => SetVarOperation::Set(v.clone()),
190                SetVarValue::Int(v) => SetVarOperation::Set(v.to_string()),
191                SetVarValue::Increment(v) => SetVarOperation::Increment(*v),
192                SetVarValue::Decrement(v) => SetVarOperation::Decrement(*v),
193                SetVarValue::Delete => SetVarOperation::Delete,
194            };
195            result.setvar_ops.push(SetVarOp {
196                collection: spec.collection.clone(),
197                name: spec.key.clone(),
198                operation: op,
199            });
200        }
201        DataAction::InitCol { .. } => {
202            // Collection initialization not implemented yet
203        }
204        DataAction::SetUid(_) => {
205            // User ID setting not implemented yet
206        }
207        DataAction::SetSid(_) => {
208            // Session ID setting not implemented yet
209        }
210        DataAction::ExpireVar { .. } => {
211            // Variable expiration not implemented yet
212        }
213        DataAction::DeprecateVar(_) => {
214            // Variable deprecation not implemented yet
215        }
216        DataAction::Exec(_) => {
217            // Script execution not implemented
218        }
219        DataAction::Prepend(_) | DataAction::Append(_) => {
220            // Response body modification not implemented
221        }
222    }
223}
224
225/// Execute a metadata action.
226fn execute_metadata(action: &MetadataAction, metadata: &mut RuleMetadata) {
227    match action {
228        MetadataAction::Id(id) => {
229            metadata.id = Some(id.to_string());
230        }
231        MetadataAction::Phase(_) => {
232            // Phase is handled at rule level
233        }
234        MetadataAction::Msg(msg) => {
235            metadata.msg = Some(msg.clone());
236        }
237        MetadataAction::Severity(sev) => {
238            metadata.severity = Some(*sev);
239        }
240        MetadataAction::Tag(tag) => {
241            metadata.tags.push(tag.clone());
242        }
243        MetadataAction::Maturity(m) => {
244            metadata.maturity = Some(*m);
245        }
246        MetadataAction::Accuracy(a) => {
247            metadata.accuracy = Some(*a);
248        }
249        MetadataAction::Rev(rev) => {
250            metadata.rev = Some(rev.clone());
251        }
252        MetadataAction::Ver(ver) => {
253            metadata.ver = Some(ver.clone());
254        }
255        MetadataAction::LogData(data) => {
256            metadata.logdata = Some(data.clone());
257        }
258        MetadataAction::Status(_) => {
259            // Status is handled at disruptive action level
260        }
261    }
262}
263
264/// Execute a logging action.
265fn execute_logging(action: &LoggingAction, _metadata: &mut RuleMetadata) {
266    match action {
267        LoggingAction::Log | LoggingAction::NoLog | LoggingAction::AuditLog | LoggingAction::NoAuditLog => {
268            // Logging flags handled elsewhere
269        }
270        LoggingAction::SanitiseMatched | LoggingAction::SanitizeMatched => {
271            // Sanitization not implemented yet
272        }
273        LoggingAction::SanitiseArg(_)
274        | LoggingAction::SanitiseRequestHeader(_)
275        | LoggingAction::SanitiseResponseHeader(_) => {
276            // Sanitization not implemented yet
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::parser::{Action, DisruptiveAction, MetadataAction};
285
286    #[test]
287    fn test_execute_deny() {
288        let actions = vec![Action::Disruptive(DisruptiveAction::Deny)];
289        let result = execute_actions(&actions, None, &[]);
290        assert!(matches!(result.disruptive, Some(DisruptiveOutcome::Deny(403))));
291    }
292
293    #[test]
294    fn test_execute_metadata() {
295        let actions = vec![
296            Action::Metadata(MetadataAction::Id(12345)),
297            Action::Metadata(MetadataAction::Msg("Test rule".to_string())),
298            Action::Metadata(MetadataAction::Severity(2)),
299            Action::Metadata(MetadataAction::Tag("attack-sqli".to_string())),
300        ];
301        let result = execute_actions(&actions, None, &[]);
302        assert_eq!(result.metadata.id, Some("12345".to_string()));
303        assert_eq!(result.metadata.msg, Some("Test rule".to_string()));
304        assert_eq!(result.metadata.severity, Some(2));
305        assert_eq!(result.metadata.tags, vec!["attack-sqli".to_string()]);
306    }
307}