sentinel_modsec/engine/
chain.rs

1//! Rule chaining logic.
2
3use super::ruleset::CompiledRule;
4
5/// Track chain state during rule execution.
6#[derive(Debug, Clone)]
7pub struct ChainState {
8    /// Whether we're currently in a chain.
9    pub in_chain: bool,
10    /// Whether the current chain has matched so far.
11    pub chain_matched: bool,
12    /// Accumulated captures from chain.
13    pub captures: Vec<String>,
14    /// Starting rule index of current chain.
15    pub chain_start: Option<usize>,
16}
17
18impl ChainState {
19    /// Create a new chain state.
20    pub fn new() -> Self {
21        Self {
22            in_chain: false,
23            chain_matched: false,
24            captures: Vec::new(),
25            chain_start: None,
26        }
27    }
28
29    /// Start a new chain.
30    pub fn start_chain(&mut self, rule_idx: usize) {
31        self.in_chain = true;
32        self.chain_matched = true;
33        self.chain_start = Some(rule_idx);
34        self.captures.clear();
35    }
36
37    /// Continue chain with match result.
38    pub fn continue_chain(&mut self, matched: bool, captures: &[String]) {
39        if matched {
40            self.captures.extend(captures.iter().cloned());
41        } else {
42            self.chain_matched = false;
43        }
44    }
45
46    /// End the current chain.
47    pub fn end_chain(&mut self) -> bool {
48        let matched = self.chain_matched;
49        self.in_chain = false;
50        self.chain_matched = false;
51        self.chain_start = None;
52        self.captures.clear();
53        matched
54    }
55
56    /// Reset chain state (on match failure).
57    pub fn reset(&mut self) {
58        self.in_chain = false;
59        self.chain_matched = false;
60        self.chain_start = None;
61        self.captures.clear();
62    }
63}
64
65impl Default for ChainState {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71/// Evaluate a chain of rules.
72pub fn evaluate_chain<F>(
73    rules: &[CompiledRule],
74    start_idx: usize,
75    mut eval_rule: F,
76) -> Option<(bool, Vec<String>)>
77where
78    F: FnMut(&CompiledRule) -> Option<(bool, Vec<String>)>,
79{
80    let mut state = ChainState::new();
81    let mut idx = start_idx;
82    let mut all_captures = Vec::new();
83
84    loop {
85        if idx >= rules.len() {
86            break;
87        }
88
89        let rule = &rules[idx];
90
91        // Evaluate the rule
92        match eval_rule(rule) {
93            Some((matched, captures)) => {
94                if matched {
95                    all_captures.extend(captures);
96                    if rule.is_chain {
97                        if let Some(next_idx) = rule.chain_next {
98                            idx = next_idx;
99                            continue;
100                        }
101                    }
102                    // Chain complete and matched
103                    return Some((true, all_captures));
104                } else {
105                    // Chain broken
106                    return Some((false, Vec::new()));
107                }
108            }
109            None => {
110                // Rule evaluation error
111                return None;
112            }
113        }
114    }
115
116    Some((false, Vec::new()))
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_chain_state() {
125        let mut state = ChainState::new();
126        assert!(!state.in_chain);
127
128        state.start_chain(0);
129        assert!(state.in_chain);
130        assert!(state.chain_matched);
131
132        state.continue_chain(true, &["test".to_string()]);
133        assert!(state.chain_matched);
134        assert_eq!(state.captures.len(), 1);
135
136        state.continue_chain(false, &[]);
137        assert!(!state.chain_matched);
138
139        let matched = state.end_chain();
140        assert!(!matched);
141        assert!(!state.in_chain);
142    }
143}