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