Skip to main content

rsigma_eval/matcher/
matching.rs

1use regex::RegexSet;
2use rsigma_ir::IrStrOp;
3
4use super::helpers::{
5    ascii_lowercase_cow, expand_template, extract_timestamp_part, match_cidr, match_numeric_value,
6    match_str_value,
7};
8use super::{CompiledMatcher, GroupMode};
9use crate::event::{Event, EventValue};
10
11/// Reduce a [`RegexSet`] match against `s` according to `mode`.
12///
13/// `Any` short-circuits via [`RegexSet::is_match`]; `All` requires every
14/// pattern in the set to fire, so we materialize the matched-pattern bitset
15/// and check its population against the set length.
16#[inline]
17fn regex_set_matches(set: &RegexSet, mode: GroupMode, s: &str) -> bool {
18    match mode {
19        GroupMode::Any => set.is_match(s),
20        GroupMode::All => {
21            let hits = set.matches(s);
22            hits.iter().count() == set.len()
23        }
24    }
25}
26
27/// Compare two runtime strings with `op`.
28fn runtime_str_match(op: IrStrOp, haystack: &str, needle: &str, case_insensitive: bool) -> bool {
29    if case_insensitive {
30        let haystack = haystack.to_lowercase();
31        let needle = needle.to_lowercase();
32        runtime_str_cmp(op, &haystack, &needle)
33    } else {
34        runtime_str_cmp(op, haystack, needle)
35    }
36}
37
38fn runtime_str_cmp(op: IrStrOp, haystack: &str, needle: &str) -> bool {
39    match op {
40        IrStrOp::Contains => haystack.contains(needle),
41        IrStrOp::StartsWith => haystack.starts_with(needle),
42        IrStrOp::EndsWith => haystack.ends_with(needle),
43        IrStrOp::Exact => haystack == needle,
44    }
45}
46
47impl CompiledMatcher {
48    /// Whether this matcher holds for a field that is absent from the event.
49    ///
50    /// Only `null` matches an absent field; negation inverts that, so a
51    /// negated item such as `|neq` matches when the field is missing.
52    pub fn matches_absent(&self) -> bool {
53        match self {
54            CompiledMatcher::Null => true,
55            CompiledMatcher::Exists(expect) => !expect,
56            CompiledMatcher::Not(inner) => !inner.matches_absent(),
57            CompiledMatcher::AnyOf(matchers) => matchers.iter().any(Self::matches_absent),
58            CompiledMatcher::AllOf(matchers) => matchers.iter().all(Self::matches_absent),
59            _ => false,
60        }
61    }
62
63    /// Check if this matcher matches an [`EventValue`] from an event.
64    ///
65    /// The `event` parameter is needed for `FieldRef` to access other fields.
66    #[inline]
67    pub fn matches(&self, value: &EventValue, event: &impl Event) -> bool {
68        match self {
69            // -- String matchers --
70            CompiledMatcher::Exact {
71                value: expected,
72                case_insensitive,
73            } => match_str_value(value, |s| {
74                if *case_insensitive {
75                    s.to_lowercase() == *expected
76                } else {
77                    s == expected
78                }
79            }),
80
81            CompiledMatcher::Contains {
82                value: needle,
83                case_insensitive,
84            } => match_str_value(value, |s| {
85                if *case_insensitive {
86                    s.to_lowercase().contains(needle.as_str())
87                } else {
88                    s.contains(needle.as_str())
89                }
90            }),
91
92            CompiledMatcher::StartsWith {
93                value: prefix,
94                case_insensitive,
95            } => match_str_value(value, |s| {
96                if *case_insensitive {
97                    s.to_lowercase().starts_with(prefix.as_str())
98                } else {
99                    s.starts_with(prefix.as_str())
100                }
101            }),
102
103            CompiledMatcher::EndsWith {
104                value: suffix,
105                case_insensitive,
106            } => match_str_value(value, |s| {
107                if *case_insensitive {
108                    s.to_lowercase().ends_with(suffix.as_str())
109                } else {
110                    s.ends_with(suffix.as_str())
111                }
112            }),
113
114            CompiledMatcher::Regex(re) => match_str_value(value, |s| re.is_match(s)),
115
116            CompiledMatcher::AhoCorasickSet {
117                automaton,
118                case_insensitive,
119                ..
120            } => match_str_value(value, |s| {
121                if *case_insensitive {
122                    automaton.is_match(ascii_lowercase_cow(s).as_ref())
123                } else {
124                    automaton.is_match(s)
125                }
126            }),
127
128            CompiledMatcher::RegexSetMatch { set, mode } => {
129                match_str_value(value, |s| regex_set_matches(set, *mode, s))
130            }
131
132            // -- Network --
133            CompiledMatcher::Cidr(net) => match_cidr(value, net),
134
135            // -- Numeric --
136            CompiledMatcher::NumericEq(n) => {
137                match_numeric_value(value, |v| (v - n).abs() < f64::EPSILON)
138            }
139            CompiledMatcher::NumericGt(n) => match_numeric_value(value, |v| v > *n),
140            CompiledMatcher::NumericGte(n) => match_numeric_value(value, |v| v >= *n),
141            CompiledMatcher::NumericLt(n) => match_numeric_value(value, |v| v < *n),
142            CompiledMatcher::NumericLte(n) => match_numeric_value(value, |v| v <= *n),
143
144            // -- Special --
145            // A value is present even when it is null.
146            CompiledMatcher::Exists(expect) => *expect,
147
148            CompiledMatcher::FieldRef {
149                field: ref_field,
150                op,
151                case_insensitive,
152            } => {
153                let Some(ref_value) = event.get_field(ref_field) else {
154                    return false;
155                };
156                match op {
157                    IrStrOp::Exact => {
158                        if *case_insensitive {
159                            match (value.as_str(), ref_value.as_str()) {
160                                (Some(a), Some(b)) => a.to_lowercase() == b.to_lowercase(),
161                                _ => value == &ref_value,
162                            }
163                        } else {
164                            value == &ref_value
165                        }
166                    }
167                    IrStrOp::Contains | IrStrOp::StartsWith | IrStrOp::EndsWith => {
168                        let Some(needle) = ref_value.as_str() else {
169                            return false;
170                        };
171                        match_str_value(value, |s| {
172                            runtime_str_match(*op, s, needle.as_ref(), *case_insensitive)
173                        })
174                    }
175                }
176            }
177
178            CompiledMatcher::Null => value.is_null(),
179
180            CompiledMatcher::BoolEq(expected) => match value {
181                EventValue::Bool(b) => b == expected,
182                EventValue::Str(s) => match s.to_lowercase().as_str() {
183                    "true" | "1" | "yes" => *expected,
184                    "false" | "0" | "no" => !*expected,
185                    _ => false,
186                },
187                EventValue::Array(arr) => arr.iter().any(|v| self.matches(v, event)),
188                _ => false,
189            },
190
191            // -- Expand --
192            CompiledMatcher::Expand {
193                template,
194                op,
195                case_insensitive,
196            } => {
197                let expanded = expand_template(template, event);
198                match_str_value(value, |s| {
199                    runtime_str_match(*op, s, &expanded, *case_insensitive)
200                })
201            }
202
203            // -- Timestamp --
204            CompiledMatcher::TimestampPart { part, inner } => {
205                if let EventValue::Array(arr) = value {
206                    return arr.iter().any(|v| self.matches(v, event));
207                }
208                match extract_timestamp_part(value, *part) {
209                    Some(n) => {
210                        let num_val = EventValue::Int(n);
211                        inner.matches(&num_val, event)
212                    }
213                    None => false,
214                }
215            }
216
217            // -- Negation --
218            CompiledMatcher::Not(inner) => !inner.matches(value, event),
219
220            // -- Composite --
221            CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches(value, event)),
222            CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches(value, event)),
223
224            CompiledMatcher::CaseInsensitiveGroup { children, mode } => {
225                match_str_value(value, |s| {
226                    let lowered = ascii_lowercase_cow(s);
227                    match mode {
228                        GroupMode::Any => children
229                            .iter()
230                            .any(|c| c.matches_pre_lowered(lowered.as_ref())),
231                        GroupMode::All => children
232                            .iter()
233                            .all(|c| c.matches_pre_lowered(lowered.as_ref())),
234                    }
235                })
236            }
237        }
238    }
239
240    /// Match a haystack that has already been Unicode-lowercased.
241    ///
242    /// **Precondition**: `lowered_str` was produced by `ascii_lowercase_cow`
243    /// (or an equivalent full-Unicode lowercaser) AND every child of the
244    /// surrounding [`CompiledMatcher::CaseInsensitiveGroup`] is pre-lowerable
245    /// (see `compiler::optimizer::is_pre_lowerable`).
246    ///
247    /// This method is internal to the crate. Optimizer bugs that violate the
248    /// precondition trip a `debug_assert!`; in release the conservative
249    /// fallback (`false`) avoids producing a false positive but may miss a
250    /// match.
251    ///
252    /// No `event` parameter: pre-lowerable matchers are pure string predicates
253    /// that never reference cross-event state. If a future event-aware matcher
254    /// becomes pre-lowerable, the signature gains `event` then.
255    pub(crate) fn matches_pre_lowered(&self, lowered_str: &str) -> bool {
256        match self {
257            CompiledMatcher::Contains {
258                value,
259                case_insensitive: true,
260            } => lowered_str.contains(value.as_str()),
261            CompiledMatcher::StartsWith {
262                value,
263                case_insensitive: true,
264            } => lowered_str.starts_with(value.as_str()),
265            CompiledMatcher::EndsWith {
266                value,
267                case_insensitive: true,
268            } => lowered_str.ends_with(value.as_str()),
269            CompiledMatcher::Exact {
270                value,
271                case_insensitive: true,
272            } => lowered_str == value,
273            CompiledMatcher::Regex(re) => re.is_match(lowered_str),
274            CompiledMatcher::AhoCorasickSet {
275                automaton,
276                case_insensitive: true,
277                ..
278            } => automaton.is_match(lowered_str),
279            CompiledMatcher::RegexSetMatch { set, mode } => {
280                regex_set_matches(set, *mode, lowered_str)
281            }
282
283            CompiledMatcher::Not(inner) => !inner.matches_pre_lowered(lowered_str),
284            CompiledMatcher::AnyOf(ms) => ms.iter().any(|m| m.matches_pre_lowered(lowered_str)),
285            CompiledMatcher::AllOf(ms) => ms.iter().all(|m| m.matches_pre_lowered(lowered_str)),
286            CompiledMatcher::CaseInsensitiveGroup { children, mode } => match mode {
287                GroupMode::Any => children.iter().any(|c| c.matches_pre_lowered(lowered_str)),
288                GroupMode::All => children.iter().all(|c| c.matches_pre_lowered(lowered_str)),
289            },
290
291            other => {
292                debug_assert!(
293                    false,
294                    "matches_pre_lowered called with non-pre-lowerable matcher: {other:?}"
295                );
296                false
297            }
298        }
299    }
300
301    /// Check if this matcher matches a plain `&str` value.
302    ///
303    /// Handles the string-matching subset of `CompiledMatcher`. Matchers that
304    /// require a full `EventValue` (numeric comparisons, field refs, etc.)
305    /// return `false` — those are never used in keyword detection.
306    pub(crate) fn matches_str(&self, s: &str) -> bool {
307        match self {
308            CompiledMatcher::Exact {
309                value: expected,
310                case_insensitive,
311            } => {
312                if *case_insensitive {
313                    s.to_lowercase() == *expected
314                } else {
315                    s == expected
316                }
317            }
318            CompiledMatcher::Contains {
319                value: needle,
320                case_insensitive,
321            } => {
322                if *case_insensitive {
323                    s.to_lowercase().contains(needle.as_str())
324                } else {
325                    s.contains(needle.as_str())
326                }
327            }
328            CompiledMatcher::StartsWith {
329                value: prefix,
330                case_insensitive,
331            } => {
332                if *case_insensitive {
333                    s.to_lowercase().starts_with(prefix.as_str())
334                } else {
335                    s.starts_with(prefix.as_str())
336                }
337            }
338            CompiledMatcher::EndsWith {
339                value: suffix,
340                case_insensitive,
341            } => {
342                if *case_insensitive {
343                    s.to_lowercase().ends_with(suffix.as_str())
344                } else {
345                    s.ends_with(suffix.as_str())
346                }
347            }
348            CompiledMatcher::Regex(re) => re.is_match(s),
349            CompiledMatcher::AhoCorasickSet {
350                automaton,
351                case_insensitive,
352                ..
353            } => {
354                if *case_insensitive {
355                    automaton.is_match(ascii_lowercase_cow(s).as_ref())
356                } else {
357                    automaton.is_match(s)
358                }
359            }
360            CompiledMatcher::RegexSetMatch { set, mode } => regex_set_matches(set, *mode, s),
361            CompiledMatcher::Not(inner) => !inner.matches_str(s),
362            CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches_str(s)),
363            CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches_str(s)),
364            CompiledMatcher::CaseInsensitiveGroup { children, mode } => {
365                let lowered = ascii_lowercase_cow(s);
366                match mode {
367                    GroupMode::Any => children
368                        .iter()
369                        .any(|c| c.matches_pre_lowered(lowered.as_ref())),
370                    GroupMode::All => children
371                        .iter()
372                        .all(|c| c.matches_pre_lowered(lowered.as_ref())),
373                }
374            }
375            _ => false,
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use crate::event::JsonEvent;
384    use crate::matcher::helpers::parse_expand_template;
385    use crate::matcher::{ExpandPart, TimePart};
386    use ipnet::IpNet;
387    use regex::Regex;
388    use serde_json::json;
389
390    fn empty_event() -> serde_json::Value {
391        json!({})
392    }
393
394    #[test]
395    fn test_exact_case_insensitive() {
396        let m = CompiledMatcher::Exact {
397            value: "whoami".into(),
398            case_insensitive: true,
399        };
400        let e = empty_event();
401        let event = JsonEvent::borrow(&e);
402        assert!(m.matches(&EventValue::Str("whoami".into()), &event));
403        assert!(m.matches(&EventValue::Str("WHOAMI".into()), &event));
404        assert!(m.matches(&EventValue::Str("Whoami".into()), &event));
405        assert!(!m.matches(&EventValue::Str("other".into()), &event));
406    }
407
408    #[test]
409    fn test_exact_case_sensitive() {
410        let m = CompiledMatcher::Exact {
411            value: "whoami".into(),
412            case_insensitive: false,
413        };
414        let e = empty_event();
415        let event = JsonEvent::borrow(&e);
416        assert!(m.matches(&EventValue::Str("whoami".into()), &event));
417        assert!(!m.matches(&EventValue::Str("WHOAMI".into()), &event));
418    }
419
420    #[test]
421    fn test_contains() {
422        let m = CompiledMatcher::Contains {
423            value: "admin".to_lowercase(),
424            case_insensitive: true,
425        };
426        let e = empty_event();
427        let event = JsonEvent::borrow(&e);
428        assert!(m.matches(&EventValue::Str("superadminuser".into()), &event));
429        assert!(m.matches(&EventValue::Str("ADMIN".into()), &event));
430        assert!(!m.matches(&EventValue::Str("user".into()), &event));
431    }
432
433    #[test]
434    fn test_starts_with() {
435        let m = CompiledMatcher::StartsWith {
436            value: "cmd".into(),
437            case_insensitive: true,
438        };
439        let e = empty_event();
440        let event = JsonEvent::borrow(&e);
441        assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
442        assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
443        assert!(!m.matches(&EventValue::Str("xcmd".into()), &event));
444    }
445
446    #[test]
447    fn test_ends_with() {
448        let m = CompiledMatcher::EndsWith {
449            value: ".exe".into(),
450            case_insensitive: true,
451        };
452        let e = empty_event();
453        let event = JsonEvent::borrow(&e);
454        assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
455        assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
456        assert!(!m.matches(&EventValue::Str("cmd.bat".into()), &event));
457    }
458
459    #[test]
460    fn test_regex() {
461        let re = Regex::new("(?i)^test.*value$").unwrap();
462        let m = CompiledMatcher::Regex(re);
463        let e = empty_event();
464        let event = JsonEvent::borrow(&e);
465        assert!(m.matches(&EventValue::Str("testXYZvalue".into()), &event));
466        assert!(m.matches(&EventValue::Str("TESTvalue".into()), &event));
467        assert!(!m.matches(&EventValue::Str("notamatch".into()), &event));
468    }
469
470    #[test]
471    fn test_cidr() {
472        let net: IpNet = "10.0.0.0/8".parse().unwrap();
473        let m = CompiledMatcher::Cidr(net);
474        let e = empty_event();
475        let event = JsonEvent::borrow(&e);
476        assert!(m.matches(&EventValue::Str("10.1.2.3".into()), &event));
477        assert!(!m.matches(&EventValue::Str("192.168.1.1".into()), &event));
478    }
479
480    #[test]
481    fn test_numeric() {
482        let m = CompiledMatcher::NumericGte(100.0);
483        let e = empty_event();
484        let event = JsonEvent::borrow(&e);
485        assert!(m.matches(&EventValue::Int(100), &event));
486        assert!(m.matches(&EventValue::Int(200), &event));
487        assert!(!m.matches(&EventValue::Int(50), &event));
488        assert!(m.matches(&EventValue::Str("150".into()), &event));
489    }
490
491    #[test]
492    fn test_null() {
493        let m = CompiledMatcher::Null;
494        let e = empty_event();
495        let event = JsonEvent::borrow(&e);
496        assert!(m.matches(&EventValue::Null, &event));
497        assert!(!m.matches(&EventValue::Str("".into()), &event));
498    }
499
500    #[test]
501    fn test_bool() {
502        let m = CompiledMatcher::BoolEq(true);
503        let e = empty_event();
504        let event = JsonEvent::borrow(&e);
505        assert!(m.matches(&EventValue::Bool(true), &event));
506        assert!(!m.matches(&EventValue::Bool(false), &event));
507        assert!(m.matches(&EventValue::Str("true".into()), &event));
508    }
509
510    #[test]
511    fn test_field_ref() {
512        let e = json!({"src": "10.0.0.1", "dst": "10.0.0.1"});
513        let event = JsonEvent::borrow(&e);
514        let m = CompiledMatcher::FieldRef {
515            field: "dst".into(),
516            op: IrStrOp::Exact,
517            case_insensitive: true,
518        };
519        assert!(m.matches(&EventValue::Str("10.0.0.1".into()), &event));
520    }
521
522    #[test]
523    fn test_field_ref_contains() {
524        let e = json!({"user": "Alice", "arn": "arn:aws:iam::123:user/alice"});
525        let event = JsonEvent::borrow(&e);
526        let m = CompiledMatcher::FieldRef {
527            field: "user".into(),
528            op: IrStrOp::Contains,
529            case_insensitive: true,
530        };
531        assert!(m.matches(
532            &EventValue::Str("arn:aws:iam::123:user/alice".into()),
533            &event
534        ));
535        let cased = CompiledMatcher::FieldRef {
536            field: "user".into(),
537            op: IrStrOp::Contains,
538            case_insensitive: false,
539        };
540        assert!(!cased.matches(
541            &EventValue::Str("arn:aws:iam::123:user/alice".into()),
542            &event
543        ));
544        let missing_type = CompiledMatcher::FieldRef {
545            field: "missing".into(),
546            op: IrStrOp::StartsWith,
547            case_insensitive: true,
548        };
549        assert!(!missing_type.matches(&EventValue::Str("alice".into()), &event));
550    }
551
552    #[test]
553    fn test_any_of() {
554        let m = CompiledMatcher::AnyOf(vec![
555            CompiledMatcher::Exact {
556                value: "a".into(),
557                case_insensitive: false,
558            },
559            CompiledMatcher::Exact {
560                value: "b".into(),
561                case_insensitive: false,
562            },
563        ]);
564        let e = empty_event();
565        let event = JsonEvent::borrow(&e);
566        assert!(m.matches(&EventValue::Str("a".into()), &event));
567        assert!(m.matches(&EventValue::Str("b".into()), &event));
568        assert!(!m.matches(&EventValue::Str("c".into()), &event));
569    }
570
571    #[test]
572    fn test_all_of() {
573        let m = CompiledMatcher::AllOf(vec![
574            CompiledMatcher::Contains {
575                value: "admin".into(),
576                case_insensitive: false,
577            },
578            CompiledMatcher::Contains {
579                value: "user".into(),
580                case_insensitive: false,
581            },
582        ]);
583        let e = empty_event();
584        let event = JsonEvent::borrow(&e);
585        assert!(m.matches(&EventValue::Str("adminuser".into()), &event));
586        assert!(!m.matches(&EventValue::Str("admin".into()), &event));
587    }
588
589    #[test]
590    fn test_array_value_matching() {
591        let m = CompiledMatcher::Exact {
592            value: "target".into(),
593            case_insensitive: true,
594        };
595        let e = empty_event();
596        let event = JsonEvent::borrow(&e);
597        let arr = EventValue::Array(vec![
598            EventValue::Str("other".into()),
599            EventValue::Str("target".into()),
600            EventValue::Str("more".into()),
601        ]);
602        assert!(m.matches(&arr, &event));
603        let arr2 = EventValue::Array(vec![
604            EventValue::Str("other".into()),
605            EventValue::Str("nope".into()),
606        ]);
607        assert!(!m.matches(&arr2, &event));
608    }
609
610    #[test]
611    fn test_number_coercion_to_string() {
612        let m = CompiledMatcher::Exact {
613            value: "42".into(),
614            case_insensitive: false,
615        };
616        let e = empty_event();
617        let event = JsonEvent::borrow(&e);
618        assert!(m.matches(&EventValue::Int(42), &event));
619    }
620
621    // =========================================================================
622    // Unicode case folding tests
623    // =========================================================================
624
625    #[test]
626    fn test_exact_unicode_case_insensitive() {
627        let m = CompiledMatcher::Exact {
628            value: "ärzte".to_lowercase(),
629            case_insensitive: true,
630        };
631        let e = empty_event();
632        let event = JsonEvent::borrow(&e);
633        assert!(m.matches(&EventValue::Str("ÄRZTE".into()), &event));
634        assert!(m.matches(&EventValue::Str("Ärzte".into()), &event));
635        assert!(m.matches(&EventValue::Str("ärzte".into()), &event));
636    }
637
638    #[test]
639    fn test_contains_unicode_case_insensitive() {
640        let m = CompiledMatcher::Contains {
641            value: "ñ".to_lowercase(),
642            case_insensitive: true,
643        };
644        let e = empty_event();
645        let event = JsonEvent::borrow(&e);
646        assert!(m.matches(&EventValue::Str("España".into()), &event));
647        assert!(m.matches(&EventValue::Str("ESPAÑA".into()), &event));
648    }
649
650    #[test]
651    fn test_startswith_unicode_case_insensitive() {
652        let m = CompiledMatcher::StartsWith {
653            value: "über".to_lowercase(),
654            case_insensitive: true,
655        };
656        let e = empty_event();
657        let event = JsonEvent::borrow(&e);
658        assert!(m.matches(&EventValue::Str("Übersicht".into()), &event));
659        assert!(m.matches(&EventValue::Str("ÜBERSICHT".into()), &event));
660        assert!(!m.matches(&EventValue::Str("not-uber".into()), &event));
661    }
662
663    #[test]
664    fn test_endswith_unicode_case_insensitive() {
665        let m = CompiledMatcher::EndsWith {
666            value: "ção".to_lowercase(),
667            case_insensitive: true,
668        };
669        let e = empty_event();
670        let event = JsonEvent::borrow(&e);
671        assert!(m.matches(&EventValue::Str("Aplicação".into()), &event));
672        assert!(m.matches(&EventValue::Str("APLICAÇÃO".into()), &event));
673        assert!(!m.matches(&EventValue::Str("Aplicacao".into()), &event));
674    }
675
676    #[test]
677    fn test_greek_case_insensitive() {
678        let m = CompiledMatcher::Exact {
679            value: "σίγμα".to_lowercase(),
680            case_insensitive: true,
681        };
682        let e = empty_event();
683        let event = JsonEvent::borrow(&e);
684        assert!(m.matches(&EventValue::Str("ΣΊΓΜΑ".into()), &event));
685        assert!(m.matches(&EventValue::Str("σίγμα".into()), &event));
686    }
687
688    // =========================================================================
689    // Expand modifier tests
690    // =========================================================================
691
692    #[test]
693    fn test_parse_expand_template() {
694        let parts = parse_expand_template("C:\\Users\\\\%user%\\AppData");
695        assert_eq!(parts.len(), 3);
696        assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "C:\\Users\\"));
697        assert!(matches!(&parts[1], ExpandPart::Placeholder(s) if s == "user"));
698        assert!(matches!(&parts[2], ExpandPart::Literal(s) if s == "\\AppData"));
699    }
700
701    #[test]
702    fn test_parse_expand_template_no_placeholders() {
703        let parts = parse_expand_template("just a literal");
704        assert_eq!(parts.len(), 1);
705        assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "just a literal"));
706    }
707
708    #[test]
709    fn test_parse_expand_template_escaped_percent() {
710        let parts = parse_expand_template("100\\%: %a%\\%b%");
711        assert_eq!(parts.len(), 3);
712        assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "100%: "));
713        assert!(matches!(&parts[1], ExpandPart::Placeholder(s) if s == "a"));
714        assert!(matches!(&parts[2], ExpandPart::Literal(s) if s == "%b%"));
715
716        let parts = parse_expand_template("\\%plain%name%");
717        assert_eq!(parts.len(), 2);
718        assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "%plain"));
719        assert!(matches!(&parts[1], ExpandPart::Placeholder(s) if s == "name"));
720
721        let parts = parse_expand_template("%%x%");
722        assert_eq!(parts.len(), 2);
723        assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "%"));
724        assert!(matches!(&parts[1], ExpandPart::Placeholder(s) if s == "x"));
725    }
726
727    #[test]
728    fn test_parse_expand_template_multiple_placeholders() {
729        let parts = parse_expand_template("%a%:%b%");
730        assert_eq!(parts.len(), 3);
731        assert!(matches!(&parts[0], ExpandPart::Placeholder(s) if s == "a"));
732        assert!(matches!(&parts[1], ExpandPart::Literal(s) if s == ":"));
733        assert!(matches!(&parts[2], ExpandPart::Placeholder(s) if s == "b"));
734    }
735
736    #[test]
737    fn test_expand_matcher() {
738        let template = parse_expand_template("C:\\Users\\\\%user%\\Downloads");
739        let m = CompiledMatcher::Expand {
740            template,
741            op: IrStrOp::Exact,
742            case_insensitive: true,
743        };
744        let e = json!({"user": "admin", "path": "C:\\Users\\admin\\Downloads"});
745        let event = JsonEvent::borrow(&e);
746        assert!(m.matches(
747            &EventValue::Str("C:\\Users\\admin\\Downloads".into()),
748            &event
749        ));
750        assert!(!m.matches(
751            &EventValue::Str("C:\\Users\\other\\Downloads".into()),
752            &event
753        ));
754    }
755
756    #[test]
757    fn test_expand_matcher_missing_field() {
758        let template = parse_expand_template("%user%@%domain%");
759        let m = CompiledMatcher::Expand {
760            template,
761            op: IrStrOp::Exact,
762            case_insensitive: false,
763        };
764        let e = json!({"user": "admin"});
765        let event = JsonEvent::borrow(&e);
766        assert!(m.matches(&EventValue::Str("admin@".into()), &event));
767    }
768
769    // =========================================================================
770    // Timestamp part tests
771    // =========================================================================
772
773    #[test]
774    fn test_timestamp_part_hour() {
775        let m = CompiledMatcher::TimestampPart {
776            part: TimePart::Hour,
777            inner: Box::new(CompiledMatcher::NumericEq(12.0)),
778        };
779        let e = json!({});
780        let event = JsonEvent::borrow(&e);
781        assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
782        assert!(!m.matches(&EventValue::Str("2024-07-10T15:30:00Z".into()), &event));
783    }
784
785    #[test]
786    fn test_timestamp_part_month() {
787        let m = CompiledMatcher::TimestampPart {
788            part: TimePart::Month,
789            inner: Box::new(CompiledMatcher::NumericEq(7.0)),
790        };
791        let e = json!({});
792        let event = JsonEvent::borrow(&e);
793        assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
794        assert!(!m.matches(&EventValue::Str("2024-08-10T12:30:00Z".into()), &event));
795    }
796
797    #[test]
798    fn test_timestamp_part_day() {
799        let m = CompiledMatcher::TimestampPart {
800            part: TimePart::Day,
801            inner: Box::new(CompiledMatcher::NumericEq(10.0)),
802        };
803        let e = json!({});
804        let event = JsonEvent::borrow(&e);
805        assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
806        assert!(!m.matches(&EventValue::Str("2024-07-15T12:30:00Z".into()), &event));
807    }
808
809    #[test]
810    fn test_timestamp_part_year() {
811        let m = CompiledMatcher::TimestampPart {
812            part: TimePart::Year,
813            inner: Box::new(CompiledMatcher::NumericEq(2024.0)),
814        };
815        let e = json!({});
816        let event = JsonEvent::borrow(&e);
817        assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
818        assert!(!m.matches(&EventValue::Str("2023-07-10T12:30:00Z".into()), &event));
819    }
820
821    #[test]
822    fn test_timestamp_part_from_epoch() {
823        let m = CompiledMatcher::TimestampPart {
824            part: TimePart::Hour,
825            inner: Box::new(CompiledMatcher::NumericEq(12.0)),
826        };
827        let e = json!({});
828        let event = JsonEvent::borrow(&e);
829        // 2024-07-10T12:30:00Z = 1720614600
830        assert!(m.matches(&EventValue::Int(1720614600), &event));
831    }
832}