sentinel_modsec/operators/
comparison.rs

1//! Comparison operators (@contains, @eq, @gt, etc.).
2
3use super::traits::{Operator, OperatorResult};
4use crate::error::{Error, Result};
5
6/// Contains operator (@contains).
7pub struct ContainsOperator {
8    needle: String,
9}
10
11impl ContainsOperator {
12    pub fn new(needle: &str) -> Self {
13        Self {
14            needle: needle.to_string(),
15        }
16    }
17}
18
19impl Operator for ContainsOperator {
20    fn execute(&self, value: &str) -> OperatorResult {
21        if value.contains(&self.needle) {
22            OperatorResult::matched(self.needle.clone())
23        } else {
24            OperatorResult::no_match()
25        }
26    }
27
28    fn name(&self) -> &'static str {
29        "contains"
30    }
31}
32
33/// BeginsWith operator (@beginsWith).
34pub struct BeginsWithOperator {
35    prefix: String,
36}
37
38impl BeginsWithOperator {
39    pub fn new(prefix: &str) -> Self {
40        Self {
41            prefix: prefix.to_string(),
42        }
43    }
44}
45
46impl Operator for BeginsWithOperator {
47    fn execute(&self, value: &str) -> OperatorResult {
48        if value.starts_with(&self.prefix) {
49            OperatorResult::matched(self.prefix.clone())
50        } else {
51            OperatorResult::no_match()
52        }
53    }
54
55    fn name(&self) -> &'static str {
56        "beginsWith"
57    }
58}
59
60/// EndsWith operator (@endsWith).
61pub struct EndsWithOperator {
62    suffix: String,
63}
64
65impl EndsWithOperator {
66    pub fn new(suffix: &str) -> Self {
67        Self {
68            suffix: suffix.to_string(),
69        }
70    }
71}
72
73impl Operator for EndsWithOperator {
74    fn execute(&self, value: &str) -> OperatorResult {
75        if value.ends_with(&self.suffix) {
76            OperatorResult::matched(self.suffix.clone())
77        } else {
78            OperatorResult::no_match()
79        }
80    }
81
82    fn name(&self) -> &'static str {
83        "endsWith"
84    }
85}
86
87/// String equals operator (@streq).
88pub struct StreqOperator {
89    expected: String,
90}
91
92impl StreqOperator {
93    pub fn new(expected: &str) -> Self {
94        Self {
95            expected: expected.to_string(),
96        }
97    }
98}
99
100impl Operator for StreqOperator {
101    fn execute(&self, value: &str) -> OperatorResult {
102        if value == self.expected {
103            OperatorResult::matched(value.to_string())
104        } else {
105            OperatorResult::no_match()
106        }
107    }
108
109    fn name(&self) -> &'static str {
110        "streq"
111    }
112}
113
114/// Numeric equals operator (@eq).
115/// Supports both numeric literals and variable references (e.g., %{tx.var}).
116pub struct EqOperator {
117    /// The argument (may be a number or variable reference).
118    arg: String,
119}
120
121impl EqOperator {
122    pub fn new(value: &str) -> Self {
123        Self {
124            arg: value.to_string(),
125        }
126    }
127
128    fn target_value(&self) -> Option<i64> {
129        // If it's a variable reference, we can't resolve it statically
130        if self.arg.contains("%{") {
131            return None;
132        }
133        self.arg.parse().ok()
134    }
135}
136
137impl Operator for EqOperator {
138    fn execute(&self, value: &str) -> OperatorResult {
139        if let Some(target) = self.target_value() {
140            if let Ok(n) = value.parse::<i64>() {
141                if n == target {
142                    return OperatorResult::matched(value.to_string());
143                }
144            }
145        }
146        // For variable references, comparison would need runtime resolution
147        // For now, we don't match if we can't resolve
148        OperatorResult::no_match()
149    }
150
151    fn name(&self) -> &'static str {
152        "eq"
153    }
154}
155
156/// Greater than operator (@gt).
157pub struct GtOperator {
158    arg: String,
159}
160
161impl GtOperator {
162    pub fn new(value: &str) -> Self {
163        Self {
164            arg: value.to_string(),
165        }
166    }
167
168    fn target_value(&self) -> Option<i64> {
169        if self.arg.contains("%{") {
170            return None;
171        }
172        self.arg.parse().ok()
173    }
174}
175
176impl Operator for GtOperator {
177    fn execute(&self, value: &str) -> OperatorResult {
178        if let Some(target) = self.target_value() {
179            if let Ok(n) = value.parse::<i64>() {
180                if n > target {
181                    return OperatorResult::matched(value.to_string());
182                }
183            }
184        }
185        OperatorResult::no_match()
186    }
187
188    fn name(&self) -> &'static str {
189        "gt"
190    }
191}
192
193/// Less than operator (@lt).
194pub struct LtOperator {
195    arg: String,
196}
197
198impl LtOperator {
199    pub fn new(value: &str) -> Self {
200        Self {
201            arg: value.to_string(),
202        }
203    }
204
205    fn target_value(&self) -> Option<i64> {
206        if self.arg.contains("%{") {
207            return None;
208        }
209        self.arg.parse().ok()
210    }
211}
212
213impl Operator for LtOperator {
214    fn execute(&self, value: &str) -> OperatorResult {
215        if let Some(target) = self.target_value() {
216            if let Ok(n) = value.parse::<i64>() {
217                if n < target {
218                    return OperatorResult::matched(value.to_string());
219                }
220            }
221        }
222        OperatorResult::no_match()
223    }
224
225    fn name(&self) -> &'static str {
226        "lt"
227    }
228}
229
230/// Greater than or equal operator (@ge).
231pub struct GeOperator {
232    arg: String,
233}
234
235impl GeOperator {
236    pub fn new(value: &str) -> Self {
237        Self {
238            arg: value.to_string(),
239        }
240    }
241
242    fn target_value(&self) -> Option<i64> {
243        if self.arg.contains("%{") {
244            return None;
245        }
246        self.arg.parse().ok()
247    }
248}
249
250impl Operator for GeOperator {
251    fn execute(&self, value: &str) -> OperatorResult {
252        if let Some(target) = self.target_value() {
253            if let Ok(n) = value.parse::<i64>() {
254                if n >= target {
255                    return OperatorResult::matched(value.to_string());
256                }
257            }
258        }
259        OperatorResult::no_match()
260    }
261
262    fn name(&self) -> &'static str {
263        "ge"
264    }
265}
266
267/// Less than or equal operator (@le).
268pub struct LeOperator {
269    arg: String,
270}
271
272impl LeOperator {
273    pub fn new(value: &str) -> Self {
274        Self {
275            arg: value.to_string(),
276        }
277    }
278
279    fn target_value(&self) -> Option<i64> {
280        if self.arg.contains("%{") {
281            return None;
282        }
283        self.arg.parse().ok()
284    }
285}
286
287impl Operator for LeOperator {
288    fn execute(&self, value: &str) -> OperatorResult {
289        if let Some(target) = self.target_value() {
290            if let Ok(n) = value.parse::<i64>() {
291                if n <= target {
292                    return OperatorResult::matched(value.to_string());
293                }
294            }
295        }
296        OperatorResult::no_match()
297    }
298
299    fn name(&self) -> &'static str {
300        "le"
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_contains() {
310        let op = ContainsOperator::new("admin");
311        assert!(op.execute("/admin/users").matched);
312        assert!(!op.execute("/users").matched);
313    }
314
315    #[test]
316    fn test_begins_with() {
317        let op = BeginsWithOperator::new("/admin");
318        assert!(op.execute("/admin/users").matched);
319        assert!(!op.execute("/users/admin").matched);
320    }
321
322    #[test]
323    fn test_ends_with() {
324        let op = EndsWithOperator::new(".php");
325        assert!(op.execute("index.php").matched);
326        assert!(!op.execute("index.html").matched);
327    }
328
329    #[test]
330    fn test_streq() {
331        let op = StreqOperator::new("admin");
332        assert!(op.execute("admin").matched);
333        assert!(!op.execute("Admin").matched);
334    }
335
336    #[test]
337    fn test_numeric_operators() {
338        let eq = EqOperator::new("10");
339        assert!(eq.execute("10").matched);
340        assert!(!eq.execute("11").matched);
341
342        let gt = GtOperator::new("10");
343        assert!(gt.execute("11").matched);
344        assert!(!gt.execute("10").matched);
345
346        let lt = LtOperator::new("10");
347        assert!(lt.execute("9").matched);
348        assert!(!lt.execute("10").matched);
349    }
350}