Skip to main content

rsigma_eval/matcher/
matching.rs

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