Skip to main content

rsigma_eval/matcher/
matching.rs

1use super::CompiledMatcher;
2use super::helpers::{
3    expand_template, extract_timestamp_part, match_cidr, match_numeric_value, match_str_value,
4};
5use crate::event::{Event, EventValue};
6
7impl CompiledMatcher {
8    /// Check if this matcher matches an [`EventValue`] from an event.
9    ///
10    /// The `event` parameter is needed for `FieldRef` to access other fields.
11    #[inline]
12    pub fn matches(&self, value: &EventValue, event: &impl Event) -> bool {
13        match self {
14            // -- String matchers --
15            CompiledMatcher::Exact {
16                value: expected,
17                case_insensitive,
18            } => match_str_value(value, |s| {
19                if *case_insensitive {
20                    s.to_lowercase() == *expected
21                } else {
22                    s == expected
23                }
24            }),
25
26            CompiledMatcher::Contains {
27                value: needle,
28                case_insensitive,
29            } => match_str_value(value, |s| {
30                if *case_insensitive {
31                    s.to_lowercase().contains(needle.as_str())
32                } else {
33                    s.contains(needle.as_str())
34                }
35            }),
36
37            CompiledMatcher::StartsWith {
38                value: prefix,
39                case_insensitive,
40            } => match_str_value(value, |s| {
41                if *case_insensitive {
42                    s.to_lowercase().starts_with(prefix.as_str())
43                } else {
44                    s.starts_with(prefix.as_str())
45                }
46            }),
47
48            CompiledMatcher::EndsWith {
49                value: suffix,
50                case_insensitive,
51            } => match_str_value(value, |s| {
52                if *case_insensitive {
53                    s.to_lowercase().ends_with(suffix.as_str())
54                } else {
55                    s.ends_with(suffix.as_str())
56                }
57            }),
58
59            CompiledMatcher::Regex(re) => match_str_value(value, |s| re.is_match(s)),
60
61            // -- Network --
62            CompiledMatcher::Cidr(net) => match_cidr(value, net),
63
64            // -- Numeric --
65            CompiledMatcher::NumericEq(n) => {
66                match_numeric_value(value, |v| (v - n).abs() < f64::EPSILON)
67            }
68            CompiledMatcher::NumericGt(n) => match_numeric_value(value, |v| v > *n),
69            CompiledMatcher::NumericGte(n) => match_numeric_value(value, |v| v >= *n),
70            CompiledMatcher::NumericLt(n) => match_numeric_value(value, |v| v < *n),
71            CompiledMatcher::NumericLte(n) => match_numeric_value(value, |v| v <= *n),
72
73            // -- Special --
74            CompiledMatcher::Exists(expect) => {
75                let exists = !value.is_null();
76                exists == *expect
77            }
78
79            CompiledMatcher::FieldRef {
80                field: ref_field,
81                case_insensitive,
82            } => {
83                if let Some(ref_value) = event.get_field(ref_field) {
84                    if *case_insensitive {
85                        match (value.as_str(), ref_value.as_str()) {
86                            (Some(a), Some(b)) => a.to_lowercase() == b.to_lowercase(),
87                            _ => value == &ref_value,
88                        }
89                    } else {
90                        value == &ref_value
91                    }
92                } else {
93                    false
94                }
95            }
96
97            CompiledMatcher::Null => value.is_null(),
98
99            CompiledMatcher::BoolEq(expected) => match value {
100                EventValue::Bool(b) => b == expected,
101                EventValue::Str(s) => match s.to_lowercase().as_str() {
102                    "true" | "1" | "yes" => *expected,
103                    "false" | "0" | "no" => !*expected,
104                    _ => false,
105                },
106                _ => false,
107            },
108
109            // -- Expand --
110            CompiledMatcher::Expand {
111                template,
112                case_insensitive,
113            } => {
114                let expanded = expand_template(template, event);
115                match_str_value(value, |s| {
116                    if *case_insensitive {
117                        s.to_lowercase() == expanded.to_lowercase()
118                    } else {
119                        s == expanded
120                    }
121                })
122            }
123
124            // -- Timestamp --
125            CompiledMatcher::TimestampPart { part, inner } => {
126                match extract_timestamp_part(value, *part) {
127                    Some(n) => {
128                        let num_val = EventValue::Int(n);
129                        inner.matches(&num_val, event)
130                    }
131                    None => false,
132                }
133            }
134
135            // -- Negation --
136            CompiledMatcher::Not(inner) => !inner.matches(value, event),
137
138            // -- Composite --
139            CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches(value, event)),
140            CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches(value, event)),
141        }
142    }
143
144    /// Check if this matcher matches a plain `&str` value.
145    ///
146    /// Handles the string-matching subset of `CompiledMatcher`. Matchers that
147    /// require a full `EventValue` (numeric comparisons, field refs, etc.)
148    /// return `false` — those are never used in keyword detection.
149    pub(super) fn matches_str(&self, s: &str) -> bool {
150        match self {
151            CompiledMatcher::Exact {
152                value: expected,
153                case_insensitive,
154            } => {
155                if *case_insensitive {
156                    s.to_lowercase() == *expected
157                } else {
158                    s == expected
159                }
160            }
161            CompiledMatcher::Contains {
162                value: needle,
163                case_insensitive,
164            } => {
165                if *case_insensitive {
166                    s.to_lowercase().contains(needle.as_str())
167                } else {
168                    s.contains(needle.as_str())
169                }
170            }
171            CompiledMatcher::StartsWith {
172                value: prefix,
173                case_insensitive,
174            } => {
175                if *case_insensitive {
176                    s.to_lowercase().starts_with(prefix.as_str())
177                } else {
178                    s.starts_with(prefix.as_str())
179                }
180            }
181            CompiledMatcher::EndsWith {
182                value: suffix,
183                case_insensitive,
184            } => {
185                if *case_insensitive {
186                    s.to_lowercase().ends_with(suffix.as_str())
187                } else {
188                    s.ends_with(suffix.as_str())
189                }
190            }
191            CompiledMatcher::Regex(re) => re.is_match(s),
192            CompiledMatcher::Not(inner) => !inner.matches_str(s),
193            CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches_str(s)),
194            CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches_str(s)),
195            _ => false,
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::event::JsonEvent;
204    use crate::matcher::helpers::parse_expand_template;
205    use crate::matcher::{ExpandPart, TimePart};
206    use ipnet::IpNet;
207    use regex::Regex;
208    use serde_json::json;
209
210    fn empty_event() -> serde_json::Value {
211        json!({})
212    }
213
214    #[test]
215    fn test_exact_case_insensitive() {
216        let m = CompiledMatcher::Exact {
217            value: "whoami".into(),
218            case_insensitive: true,
219        };
220        let e = empty_event();
221        let event = JsonEvent::borrow(&e);
222        assert!(m.matches(&EventValue::Str("whoami".into()), &event));
223        assert!(m.matches(&EventValue::Str("WHOAMI".into()), &event));
224        assert!(m.matches(&EventValue::Str("Whoami".into()), &event));
225        assert!(!m.matches(&EventValue::Str("other".into()), &event));
226    }
227
228    #[test]
229    fn test_exact_case_sensitive() {
230        let m = CompiledMatcher::Exact {
231            value: "whoami".into(),
232            case_insensitive: false,
233        };
234        let e = empty_event();
235        let event = JsonEvent::borrow(&e);
236        assert!(m.matches(&EventValue::Str("whoami".into()), &event));
237        assert!(!m.matches(&EventValue::Str("WHOAMI".into()), &event));
238    }
239
240    #[test]
241    fn test_contains() {
242        let m = CompiledMatcher::Contains {
243            value: "admin".to_lowercase(),
244            case_insensitive: true,
245        };
246        let e = empty_event();
247        let event = JsonEvent::borrow(&e);
248        assert!(m.matches(&EventValue::Str("superadminuser".into()), &event));
249        assert!(m.matches(&EventValue::Str("ADMIN".into()), &event));
250        assert!(!m.matches(&EventValue::Str("user".into()), &event));
251    }
252
253    #[test]
254    fn test_starts_with() {
255        let m = CompiledMatcher::StartsWith {
256            value: "cmd".into(),
257            case_insensitive: true,
258        };
259        let e = empty_event();
260        let event = JsonEvent::borrow(&e);
261        assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
262        assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
263        assert!(!m.matches(&EventValue::Str("xcmd".into()), &event));
264    }
265
266    #[test]
267    fn test_ends_with() {
268        let m = CompiledMatcher::EndsWith {
269            value: ".exe".into(),
270            case_insensitive: true,
271        };
272        let e = empty_event();
273        let event = JsonEvent::borrow(&e);
274        assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
275        assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
276        assert!(!m.matches(&EventValue::Str("cmd.bat".into()), &event));
277    }
278
279    #[test]
280    fn test_regex() {
281        let re = Regex::new("(?i)^test.*value$").unwrap();
282        let m = CompiledMatcher::Regex(re);
283        let e = empty_event();
284        let event = JsonEvent::borrow(&e);
285        assert!(m.matches(&EventValue::Str("testXYZvalue".into()), &event));
286        assert!(m.matches(&EventValue::Str("TESTvalue".into()), &event));
287        assert!(!m.matches(&EventValue::Str("notamatch".into()), &event));
288    }
289
290    #[test]
291    fn test_cidr() {
292        let net: IpNet = "10.0.0.0/8".parse().unwrap();
293        let m = CompiledMatcher::Cidr(net);
294        let e = empty_event();
295        let event = JsonEvent::borrow(&e);
296        assert!(m.matches(&EventValue::Str("10.1.2.3".into()), &event));
297        assert!(!m.matches(&EventValue::Str("192.168.1.1".into()), &event));
298    }
299
300    #[test]
301    fn test_numeric() {
302        let m = CompiledMatcher::NumericGte(100.0);
303        let e = empty_event();
304        let event = JsonEvent::borrow(&e);
305        assert!(m.matches(&EventValue::Int(100), &event));
306        assert!(m.matches(&EventValue::Int(200), &event));
307        assert!(!m.matches(&EventValue::Int(50), &event));
308        assert!(m.matches(&EventValue::Str("150".into()), &event));
309    }
310
311    #[test]
312    fn test_null() {
313        let m = CompiledMatcher::Null;
314        let e = empty_event();
315        let event = JsonEvent::borrow(&e);
316        assert!(m.matches(&EventValue::Null, &event));
317        assert!(!m.matches(&EventValue::Str("".into()), &event));
318    }
319
320    #[test]
321    fn test_bool() {
322        let m = CompiledMatcher::BoolEq(true);
323        let e = empty_event();
324        let event = JsonEvent::borrow(&e);
325        assert!(m.matches(&EventValue::Bool(true), &event));
326        assert!(!m.matches(&EventValue::Bool(false), &event));
327        assert!(m.matches(&EventValue::Str("true".into()), &event));
328    }
329
330    #[test]
331    fn test_field_ref() {
332        let e = json!({"src": "10.0.0.1", "dst": "10.0.0.1"});
333        let event = JsonEvent::borrow(&e);
334        let m = CompiledMatcher::FieldRef {
335            field: "dst".into(),
336            case_insensitive: true,
337        };
338        assert!(m.matches(&EventValue::Str("10.0.0.1".into()), &event));
339    }
340
341    #[test]
342    fn test_any_of() {
343        let m = CompiledMatcher::AnyOf(vec![
344            CompiledMatcher::Exact {
345                value: "a".into(),
346                case_insensitive: false,
347            },
348            CompiledMatcher::Exact {
349                value: "b".into(),
350                case_insensitive: false,
351            },
352        ]);
353        let e = empty_event();
354        let event = JsonEvent::borrow(&e);
355        assert!(m.matches(&EventValue::Str("a".into()), &event));
356        assert!(m.matches(&EventValue::Str("b".into()), &event));
357        assert!(!m.matches(&EventValue::Str("c".into()), &event));
358    }
359
360    #[test]
361    fn test_all_of() {
362        let m = CompiledMatcher::AllOf(vec![
363            CompiledMatcher::Contains {
364                value: "admin".into(),
365                case_insensitive: false,
366            },
367            CompiledMatcher::Contains {
368                value: "user".into(),
369                case_insensitive: false,
370            },
371        ]);
372        let e = empty_event();
373        let event = JsonEvent::borrow(&e);
374        assert!(m.matches(&EventValue::Str("adminuser".into()), &event));
375        assert!(!m.matches(&EventValue::Str("admin".into()), &event));
376    }
377
378    #[test]
379    fn test_array_value_matching() {
380        let m = CompiledMatcher::Exact {
381            value: "target".into(),
382            case_insensitive: true,
383        };
384        let e = empty_event();
385        let event = JsonEvent::borrow(&e);
386        let arr = EventValue::Array(vec![
387            EventValue::Str("other".into()),
388            EventValue::Str("target".into()),
389            EventValue::Str("more".into()),
390        ]);
391        assert!(m.matches(&arr, &event));
392        let arr2 = EventValue::Array(vec![
393            EventValue::Str("other".into()),
394            EventValue::Str("nope".into()),
395        ]);
396        assert!(!m.matches(&arr2, &event));
397    }
398
399    #[test]
400    fn test_number_coercion_to_string() {
401        let m = CompiledMatcher::Exact {
402            value: "42".into(),
403            case_insensitive: false,
404        };
405        let e = empty_event();
406        let event = JsonEvent::borrow(&e);
407        assert!(m.matches(&EventValue::Int(42), &event));
408    }
409
410    // =========================================================================
411    // Unicode case folding tests
412    // =========================================================================
413
414    #[test]
415    fn test_exact_unicode_case_insensitive() {
416        let m = CompiledMatcher::Exact {
417            value: "ärzte".to_lowercase(),
418            case_insensitive: true,
419        };
420        let e = empty_event();
421        let event = JsonEvent::borrow(&e);
422        assert!(m.matches(&EventValue::Str("ÄRZTE".into()), &event));
423        assert!(m.matches(&EventValue::Str("Ärzte".into()), &event));
424        assert!(m.matches(&EventValue::Str("ärzte".into()), &event));
425    }
426
427    #[test]
428    fn test_contains_unicode_case_insensitive() {
429        let m = CompiledMatcher::Contains {
430            value: "ñ".to_lowercase(),
431            case_insensitive: true,
432        };
433        let e = empty_event();
434        let event = JsonEvent::borrow(&e);
435        assert!(m.matches(&EventValue::Str("España".into()), &event));
436        assert!(m.matches(&EventValue::Str("ESPAÑA".into()), &event));
437    }
438
439    #[test]
440    fn test_startswith_unicode_case_insensitive() {
441        let m = CompiledMatcher::StartsWith {
442            value: "über".to_lowercase(),
443            case_insensitive: true,
444        };
445        let e = empty_event();
446        let event = JsonEvent::borrow(&e);
447        assert!(m.matches(&EventValue::Str("Übersicht".into()), &event));
448        assert!(m.matches(&EventValue::Str("ÜBERSICHT".into()), &event));
449        assert!(!m.matches(&EventValue::Str("not-uber".into()), &event));
450    }
451
452    #[test]
453    fn test_endswith_unicode_case_insensitive() {
454        let m = CompiledMatcher::EndsWith {
455            value: "ção".to_lowercase(),
456            case_insensitive: true,
457        };
458        let e = empty_event();
459        let event = JsonEvent::borrow(&e);
460        assert!(m.matches(&EventValue::Str("Aplicação".into()), &event));
461        assert!(m.matches(&EventValue::Str("APLICAÇÃO".into()), &event));
462        assert!(!m.matches(&EventValue::Str("Aplicacao".into()), &event));
463    }
464
465    #[test]
466    fn test_greek_case_insensitive() {
467        let m = CompiledMatcher::Exact {
468            value: "σίγμα".to_lowercase(),
469            case_insensitive: true,
470        };
471        let e = empty_event();
472        let event = JsonEvent::borrow(&e);
473        assert!(m.matches(&EventValue::Str("ΣΊΓΜΑ".into()), &event));
474        assert!(m.matches(&EventValue::Str("σίγμα".into()), &event));
475    }
476
477    // =========================================================================
478    // Expand modifier tests
479    // =========================================================================
480
481    #[test]
482    fn test_parse_expand_template() {
483        let parts = parse_expand_template("C:\\Users\\%user%\\AppData");
484        assert_eq!(parts.len(), 3);
485        assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "C:\\Users\\"));
486        assert!(matches!(&parts[1], ExpandPart::Placeholder(s) if s == "user"));
487        assert!(matches!(&parts[2], ExpandPart::Literal(s) if s == "\\AppData"));
488    }
489
490    #[test]
491    fn test_parse_expand_template_no_placeholders() {
492        let parts = parse_expand_template("just a literal");
493        assert_eq!(parts.len(), 1);
494        assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "just a literal"));
495    }
496
497    #[test]
498    fn test_parse_expand_template_multiple_placeholders() {
499        let parts = parse_expand_template("%a%:%b%");
500        assert_eq!(parts.len(), 3);
501        assert!(matches!(&parts[0], ExpandPart::Placeholder(s) if s == "a"));
502        assert!(matches!(&parts[1], ExpandPart::Literal(s) if s == ":"));
503        assert!(matches!(&parts[2], ExpandPart::Placeholder(s) if s == "b"));
504    }
505
506    #[test]
507    fn test_expand_matcher() {
508        let template = parse_expand_template("C:\\Users\\%user%\\Downloads");
509        let m = CompiledMatcher::Expand {
510            template,
511            case_insensitive: true,
512        };
513        let e = json!({"user": "admin", "path": "C:\\Users\\admin\\Downloads"});
514        let event = JsonEvent::borrow(&e);
515        assert!(m.matches(
516            &EventValue::Str("C:\\Users\\admin\\Downloads".into()),
517            &event
518        ));
519        assert!(!m.matches(
520            &EventValue::Str("C:\\Users\\other\\Downloads".into()),
521            &event
522        ));
523    }
524
525    #[test]
526    fn test_expand_matcher_missing_field() {
527        let template = parse_expand_template("%user%@%domain%");
528        let m = CompiledMatcher::Expand {
529            template,
530            case_insensitive: false,
531        };
532        let e = json!({"user": "admin"});
533        let event = JsonEvent::borrow(&e);
534        assert!(m.matches(&EventValue::Str("admin@".into()), &event));
535    }
536
537    // =========================================================================
538    // Timestamp part tests
539    // =========================================================================
540
541    #[test]
542    fn test_timestamp_part_hour() {
543        let m = CompiledMatcher::TimestampPart {
544            part: TimePart::Hour,
545            inner: Box::new(CompiledMatcher::NumericEq(12.0)),
546        };
547        let e = json!({});
548        let event = JsonEvent::borrow(&e);
549        assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
550        assert!(!m.matches(&EventValue::Str("2024-07-10T15:30:00Z".into()), &event));
551    }
552
553    #[test]
554    fn test_timestamp_part_month() {
555        let m = CompiledMatcher::TimestampPart {
556            part: TimePart::Month,
557            inner: Box::new(CompiledMatcher::NumericEq(7.0)),
558        };
559        let e = json!({});
560        let event = JsonEvent::borrow(&e);
561        assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
562        assert!(!m.matches(&EventValue::Str("2024-08-10T12:30:00Z".into()), &event));
563    }
564
565    #[test]
566    fn test_timestamp_part_day() {
567        let m = CompiledMatcher::TimestampPart {
568            part: TimePart::Day,
569            inner: Box::new(CompiledMatcher::NumericEq(10.0)),
570        };
571        let e = json!({});
572        let event = JsonEvent::borrow(&e);
573        assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
574        assert!(!m.matches(&EventValue::Str("2024-07-15T12:30:00Z".into()), &event));
575    }
576
577    #[test]
578    fn test_timestamp_part_year() {
579        let m = CompiledMatcher::TimestampPart {
580            part: TimePart::Year,
581            inner: Box::new(CompiledMatcher::NumericEq(2024.0)),
582        };
583        let e = json!({});
584        let event = JsonEvent::borrow(&e);
585        assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
586        assert!(!m.matches(&EventValue::Str("2023-07-10T12:30:00Z".into()), &event));
587    }
588
589    #[test]
590    fn test_timestamp_part_from_epoch() {
591        let m = CompiledMatcher::TimestampPart {
592            part: TimePart::Hour,
593            inner: Box::new(CompiledMatcher::NumericEq(12.0)),
594        };
595        let e = json!({});
596        let event = JsonEvent::borrow(&e);
597        // 2024-07-10T12:30:00Z = 1720614600
598        assert!(m.matches(&EventValue::Int(1720614600), &event));
599    }
600}