sentinel_modsec/operators/
mod.rs

1//! Operator implementations for ModSecurity.
2
3mod traits;
4mod pattern;
5mod comparison;
6mod detection;
7mod validation;
8mod network;
9
10pub use traits::{Operator, OperatorResult};
11pub use pattern::{RxOperator, PmOperator};
12pub use comparison::{ContainsOperator, BeginsWithOperator, EndsWithOperator, StreqOperator};
13pub use comparison::{EqOperator, GtOperator, LtOperator, GeOperator, LeOperator};
14pub use detection::{DetectSqliOperator, DetectXssOperator};
15pub use validation::{ValidateUrlEncodingOperator, ValidateUtf8EncodingOperator};
16pub use network::IpMatchOperator;
17
18use crate::parser::{OperatorName, OperatorSpec};
19use crate::error::{Error, Result};
20use std::sync::Arc;
21
22/// Type alias for a compiled operator.
23pub type CompiledOperator = dyn Operator;
24
25/// Create an operator from a name and argument string.
26/// This is a convenience function for testing and benchmarking.
27pub fn create_operator(name: OperatorName, argument: &str) -> Result<Arc<dyn Operator>> {
28    let spec = OperatorSpec {
29        negated: false,
30        name,
31        argument: argument.to_string(),
32    };
33    compile_operator(&spec)
34}
35
36/// Create a compiled operator from a specification.
37pub fn compile_operator(spec: &OperatorSpec) -> Result<Arc<dyn Operator>> {
38    let name = &spec.name;
39    let argument = &spec.argument;
40    match name {
41        OperatorName::Rx => Ok(Arc::new(RxOperator::new(argument)?)),
42        OperatorName::Pm | OperatorName::Pmf => Ok(Arc::new(PmOperator::new(argument)?)),
43        OperatorName::PmFromFile => Ok(Arc::new(PmOperator::from_file(argument)?)),
44        OperatorName::Contains => Ok(Arc::new(ContainsOperator::new(argument))),
45        OperatorName::BeginsWith => Ok(Arc::new(BeginsWithOperator::new(argument))),
46        OperatorName::EndsWith => Ok(Arc::new(EndsWithOperator::new(argument))),
47        OperatorName::StreQ => Ok(Arc::new(StreqOperator::new(argument))),
48        OperatorName::Eq => Ok(Arc::new(EqOperator::new(argument))),
49        OperatorName::Gt => Ok(Arc::new(GtOperator::new(argument))),
50        OperatorName::Lt => Ok(Arc::new(LtOperator::new(argument))),
51        OperatorName::Ge => Ok(Arc::new(GeOperator::new(argument))),
52        OperatorName::Le => Ok(Arc::new(LeOperator::new(argument))),
53        OperatorName::DetectSqli => Ok(Arc::new(DetectSqliOperator)),
54        OperatorName::DetectXss => Ok(Arc::new(DetectXssOperator)),
55        OperatorName::ValidateUrlEncoding => Ok(Arc::new(ValidateUrlEncodingOperator)),
56        OperatorName::ValidateUtf8Encoding => Ok(Arc::new(ValidateUtf8EncodingOperator)),
57        OperatorName::IpMatch | OperatorName::IpMatchF => Ok(Arc::new(IpMatchOperator::new(argument)?)),
58        OperatorName::IpMatchFromFile => Ok(Arc::new(IpMatchOperator::from_file(argument)?)),
59        OperatorName::NoMatch => Ok(Arc::new(NoMatchOperator)),
60        OperatorName::UnconditionalMatch => Ok(Arc::new(UnconditionalMatchOperator)),
61        OperatorName::ValidateByteRange => Ok(Arc::new(ValidateByteRangeOperator::new(argument))),
62        // Placeholder operators - acknowledge but don't match
63        OperatorName::VerifyCc | OperatorName::VerifySsn | OperatorName::VerifyCpf => {
64            Ok(Arc::new(NoMatchOperator))
65        }
66        OperatorName::ValidateHash | OperatorName::ValidateDtd | OperatorName::ValidateSchema => {
67            Ok(Arc::new(NoMatchOperator))
68        }
69        OperatorName::Rbl | OperatorName::GeoLookup | OperatorName::GsbLookup => {
70            Ok(Arc::new(NoMatchOperator))
71        }
72        OperatorName::InspectFile | OperatorName::FuzzyHash | OperatorName::Rsub => {
73            Ok(Arc::new(NoMatchOperator))
74        }
75        OperatorName::ContainsWord => Ok(Arc::new(ContainsOperator::new(argument))),
76        OperatorName::Within => Ok(Arc::new(WithinOperator::new(argument))),
77        OperatorName::StrMatch => Ok(Arc::new(ContainsOperator::new(argument))),
78        OperatorName::Ne => Ok(Arc::new(NeOperator::new(argument))),
79        _ => Err(Error::UnknownOperator { name: format!("{:?}", name) }),
80    }
81}
82
83/// Operator that never matches.
84pub struct NoMatchOperator;
85
86impl Operator for NoMatchOperator {
87    fn execute(&self, _value: &str) -> OperatorResult {
88        OperatorResult::no_match()
89    }
90
91    fn name(&self) -> &'static str {
92        "noMatch"
93    }
94}
95
96/// Operator that always matches.
97pub struct UnconditionalMatchOperator;
98
99impl Operator for UnconditionalMatchOperator {
100    fn execute(&self, value: &str) -> OperatorResult {
101        OperatorResult::matched(value.to_string())
102    }
103
104    fn name(&self) -> &'static str {
105        "unconditionalMatch"
106    }
107}
108
109/// Validates that all bytes are within specified ranges.
110pub struct ValidateByteRangeOperator {
111    #[allow(dead_code)]
112    ranges: Vec<(u8, u8)>,
113}
114
115impl ValidateByteRangeOperator {
116    /// Create a new byte range validator.
117    pub fn new(spec: &str) -> Self {
118        // Parse ranges like "9,10,13,32-126"
119        let mut ranges = Vec::new();
120        for part in spec.split(',') {
121            let part = part.trim();
122            if part.contains('-') {
123                let parts: Vec<&str> = part.split('-').collect();
124                if parts.len() == 2 {
125                    if let (Ok(start), Ok(end)) = (parts[0].parse(), parts[1].parse()) {
126                        ranges.push((start, end));
127                    }
128                }
129            } else if let Ok(byte) = part.parse() {
130                ranges.push((byte, byte));
131            }
132        }
133        Self { ranges }
134    }
135}
136
137impl Operator for ValidateByteRangeOperator {
138    fn execute(&self, value: &str) -> OperatorResult {
139        // Check if all bytes are within the allowed ranges
140        for byte in value.bytes() {
141            let valid = self.ranges.iter().any(|(start, end)| byte >= *start && byte <= *end);
142            if !valid {
143                // Invalid byte found - this is a match (rule should trigger)
144                return OperatorResult::matched(format!("invalid byte: {}", byte));
145            }
146        }
147        OperatorResult::no_match()
148    }
149
150    fn name(&self) -> &'static str {
151        "validateByteRange"
152    }
153}
154
155/// Within operator - checks if value is within a list of values.
156pub struct WithinOperator {
157    values: Vec<String>,
158}
159
160impl WithinOperator {
161    /// Create a new within operator.
162    pub fn new(values: &str) -> Self {
163        Self {
164            values: values.split_whitespace().map(|s| s.to_string()).collect(),
165        }
166    }
167}
168
169impl Operator for WithinOperator {
170    fn execute(&self, value: &str) -> OperatorResult {
171        if self.values.iter().any(|v| v == value) {
172            OperatorResult::matched(value.to_string())
173        } else {
174            OperatorResult::no_match()
175        }
176    }
177
178    fn name(&self) -> &'static str {
179        "within"
180    }
181}
182
183/// Not equal operator.
184pub struct NeOperator {
185    expected: String,
186}
187
188impl NeOperator {
189    /// Create a new not-equal operator.
190    pub fn new(expected: &str) -> Self {
191        Self {
192            expected: expected.to_string(),
193        }
194    }
195}
196
197impl Operator for NeOperator {
198    fn execute(&self, value: &str) -> OperatorResult {
199        // Try numeric comparison first
200        if let (Ok(a), Ok(b)) = (value.parse::<i64>(), self.expected.parse::<i64>()) {
201            if a != b {
202                return OperatorResult::matched(value.to_string());
203            }
204        } else if value != self.expected {
205            // String comparison
206            return OperatorResult::matched(value.to_string());
207        }
208        OperatorResult::no_match()
209    }
210
211    fn name(&self) -> &'static str {
212        "ne"
213    }
214}