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#[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
27fn fieldref_substr(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 fieldref_substr_cmp(op, &haystack, &needle)
33 } else {
34 fieldref_substr_cmp(op, haystack, needle)
35 }
36}
37
38fn fieldref_substr_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 #[inline]
52 pub fn matches(&self, value: &EventValue, event: &impl Event) -> bool {
53 match self {
54 CompiledMatcher::Exact {
56 value: expected,
57 case_insensitive,
58 } => match_str_value(value, |s| {
59 if *case_insensitive {
60 s.to_lowercase() == *expected
61 } else {
62 s == expected
63 }
64 }),
65
66 CompiledMatcher::Contains {
67 value: needle,
68 case_insensitive,
69 } => match_str_value(value, |s| {
70 if *case_insensitive {
71 s.to_lowercase().contains(needle.as_str())
72 } else {
73 s.contains(needle.as_str())
74 }
75 }),
76
77 CompiledMatcher::StartsWith {
78 value: prefix,
79 case_insensitive,
80 } => match_str_value(value, |s| {
81 if *case_insensitive {
82 s.to_lowercase().starts_with(prefix.as_str())
83 } else {
84 s.starts_with(prefix.as_str())
85 }
86 }),
87
88 CompiledMatcher::EndsWith {
89 value: suffix,
90 case_insensitive,
91 } => match_str_value(value, |s| {
92 if *case_insensitive {
93 s.to_lowercase().ends_with(suffix.as_str())
94 } else {
95 s.ends_with(suffix.as_str())
96 }
97 }),
98
99 CompiledMatcher::Regex(re) => match_str_value(value, |s| re.is_match(s)),
100
101 CompiledMatcher::AhoCorasickSet {
102 automaton,
103 case_insensitive,
104 ..
105 } => match_str_value(value, |s| {
106 if *case_insensitive {
107 automaton.is_match(ascii_lowercase_cow(s).as_ref())
108 } else {
109 automaton.is_match(s)
110 }
111 }),
112
113 CompiledMatcher::RegexSetMatch { set, mode } => {
114 match_str_value(value, |s| regex_set_matches(set, *mode, s))
115 }
116
117 CompiledMatcher::Cidr(net) => match_cidr(value, net),
119
120 CompiledMatcher::NumericEq(n) => {
122 match_numeric_value(value, |v| (v - n).abs() < f64::EPSILON)
123 }
124 CompiledMatcher::NumericGt(n) => match_numeric_value(value, |v| v > *n),
125 CompiledMatcher::NumericGte(n) => match_numeric_value(value, |v| v >= *n),
126 CompiledMatcher::NumericLt(n) => match_numeric_value(value, |v| v < *n),
127 CompiledMatcher::NumericLte(n) => match_numeric_value(value, |v| v <= *n),
128
129 CompiledMatcher::Exists(expect) => {
131 let exists = !value.is_null();
132 exists == *expect
133 }
134
135 CompiledMatcher::FieldRef {
136 field: ref_field,
137 op,
138 case_insensitive,
139 } => {
140 let Some(ref_value) = event.get_field(ref_field) else {
141 return false;
142 };
143 match op {
144 IrStrOp::Exact => {
145 if *case_insensitive {
146 match (value.as_str(), ref_value.as_str()) {
147 (Some(a), Some(b)) => a.to_lowercase() == b.to_lowercase(),
148 _ => value == &ref_value,
149 }
150 } else {
151 value == &ref_value
152 }
153 }
154 IrStrOp::Contains | IrStrOp::StartsWith | IrStrOp::EndsWith => {
155 let Some(needle) = ref_value.as_str() else {
156 return false;
157 };
158 match_str_value(value, |s| {
159 fieldref_substr(*op, s, needle.as_ref(), *case_insensitive)
160 })
161 }
162 }
163 }
164
165 CompiledMatcher::Null => value.is_null(),
166
167 CompiledMatcher::BoolEq(expected) => match value {
168 EventValue::Bool(b) => b == expected,
169 EventValue::Str(s) => match s.to_lowercase().as_str() {
170 "true" | "1" | "yes" => *expected,
171 "false" | "0" | "no" => !*expected,
172 _ => false,
173 },
174 EventValue::Array(arr) => arr.iter().any(|v| self.matches(v, event)),
175 _ => false,
176 },
177
178 CompiledMatcher::Expand {
180 template,
181 case_insensitive,
182 } => {
183 let expanded = expand_template(template, event);
184 match_str_value(value, |s| {
185 if *case_insensitive {
186 s.to_lowercase() == expanded.to_lowercase()
187 } else {
188 s == expanded
189 }
190 })
191 }
192
193 CompiledMatcher::TimestampPart { part, inner } => {
195 if let EventValue::Array(arr) = value {
196 return arr.iter().any(|v| self.matches(v, event));
197 }
198 match extract_timestamp_part(value, *part) {
199 Some(n) => {
200 let num_val = EventValue::Int(n);
201 inner.matches(&num_val, event)
202 }
203 None => false,
204 }
205 }
206
207 CompiledMatcher::Not(inner) => !inner.matches(value, event),
209
210 CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches(value, event)),
212 CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches(value, event)),
213
214 CompiledMatcher::CaseInsensitiveGroup { children, mode } => {
215 match_str_value(value, |s| {
216 let lowered = ascii_lowercase_cow(s);
217 match mode {
218 GroupMode::Any => children
219 .iter()
220 .any(|c| c.matches_pre_lowered(lowered.as_ref())),
221 GroupMode::All => children
222 .iter()
223 .all(|c| c.matches_pre_lowered(lowered.as_ref())),
224 }
225 })
226 }
227 }
228 }
229
230 pub(crate) fn matches_pre_lowered(&self, lowered_str: &str) -> bool {
246 match self {
247 CompiledMatcher::Contains {
248 value,
249 case_insensitive: true,
250 } => lowered_str.contains(value.as_str()),
251 CompiledMatcher::StartsWith {
252 value,
253 case_insensitive: true,
254 } => lowered_str.starts_with(value.as_str()),
255 CompiledMatcher::EndsWith {
256 value,
257 case_insensitive: true,
258 } => lowered_str.ends_with(value.as_str()),
259 CompiledMatcher::Exact {
260 value,
261 case_insensitive: true,
262 } => lowered_str == value,
263 CompiledMatcher::Regex(re) => re.is_match(lowered_str),
264 CompiledMatcher::AhoCorasickSet {
265 automaton,
266 case_insensitive: true,
267 ..
268 } => automaton.is_match(lowered_str),
269 CompiledMatcher::RegexSetMatch { set, mode } => {
270 regex_set_matches(set, *mode, lowered_str)
271 }
272
273 CompiledMatcher::Not(inner) => !inner.matches_pre_lowered(lowered_str),
274 CompiledMatcher::AnyOf(ms) => ms.iter().any(|m| m.matches_pre_lowered(lowered_str)),
275 CompiledMatcher::AllOf(ms) => ms.iter().all(|m| m.matches_pre_lowered(lowered_str)),
276 CompiledMatcher::CaseInsensitiveGroup { children, mode } => match mode {
277 GroupMode::Any => children.iter().any(|c| c.matches_pre_lowered(lowered_str)),
278 GroupMode::All => children.iter().all(|c| c.matches_pre_lowered(lowered_str)),
279 },
280
281 other => {
282 debug_assert!(
283 false,
284 "matches_pre_lowered called with non-pre-lowerable matcher: {other:?}"
285 );
286 false
287 }
288 }
289 }
290
291 pub(crate) fn matches_str(&self, s: &str) -> bool {
297 match self {
298 CompiledMatcher::Exact {
299 value: expected,
300 case_insensitive,
301 } => {
302 if *case_insensitive {
303 s.to_lowercase() == *expected
304 } else {
305 s == expected
306 }
307 }
308 CompiledMatcher::Contains {
309 value: needle,
310 case_insensitive,
311 } => {
312 if *case_insensitive {
313 s.to_lowercase().contains(needle.as_str())
314 } else {
315 s.contains(needle.as_str())
316 }
317 }
318 CompiledMatcher::StartsWith {
319 value: prefix,
320 case_insensitive,
321 } => {
322 if *case_insensitive {
323 s.to_lowercase().starts_with(prefix.as_str())
324 } else {
325 s.starts_with(prefix.as_str())
326 }
327 }
328 CompiledMatcher::EndsWith {
329 value: suffix,
330 case_insensitive,
331 } => {
332 if *case_insensitive {
333 s.to_lowercase().ends_with(suffix.as_str())
334 } else {
335 s.ends_with(suffix.as_str())
336 }
337 }
338 CompiledMatcher::Regex(re) => re.is_match(s),
339 CompiledMatcher::AhoCorasickSet {
340 automaton,
341 case_insensitive,
342 ..
343 } => {
344 if *case_insensitive {
345 automaton.is_match(ascii_lowercase_cow(s).as_ref())
346 } else {
347 automaton.is_match(s)
348 }
349 }
350 CompiledMatcher::RegexSetMatch { set, mode } => regex_set_matches(set, *mode, s),
351 CompiledMatcher::Not(inner) => !inner.matches_str(s),
352 CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches_str(s)),
353 CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches_str(s)),
354 CompiledMatcher::CaseInsensitiveGroup { children, mode } => {
355 let lowered = ascii_lowercase_cow(s);
356 match mode {
357 GroupMode::Any => children
358 .iter()
359 .any(|c| c.matches_pre_lowered(lowered.as_ref())),
360 GroupMode::All => children
361 .iter()
362 .all(|c| c.matches_pre_lowered(lowered.as_ref())),
363 }
364 }
365 _ => false,
366 }
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use crate::event::JsonEvent;
374 use crate::matcher::helpers::parse_expand_template;
375 use crate::matcher::{ExpandPart, TimePart};
376 use ipnet::IpNet;
377 use regex::Regex;
378 use serde_json::json;
379
380 fn empty_event() -> serde_json::Value {
381 json!({})
382 }
383
384 #[test]
385 fn test_exact_case_insensitive() {
386 let m = CompiledMatcher::Exact {
387 value: "whoami".into(),
388 case_insensitive: true,
389 };
390 let e = empty_event();
391 let event = JsonEvent::borrow(&e);
392 assert!(m.matches(&EventValue::Str("whoami".into()), &event));
393 assert!(m.matches(&EventValue::Str("WHOAMI".into()), &event));
394 assert!(m.matches(&EventValue::Str("Whoami".into()), &event));
395 assert!(!m.matches(&EventValue::Str("other".into()), &event));
396 }
397
398 #[test]
399 fn test_exact_case_sensitive() {
400 let m = CompiledMatcher::Exact {
401 value: "whoami".into(),
402 case_insensitive: false,
403 };
404 let e = empty_event();
405 let event = JsonEvent::borrow(&e);
406 assert!(m.matches(&EventValue::Str("whoami".into()), &event));
407 assert!(!m.matches(&EventValue::Str("WHOAMI".into()), &event));
408 }
409
410 #[test]
411 fn test_contains() {
412 let m = CompiledMatcher::Contains {
413 value: "admin".to_lowercase(),
414 case_insensitive: true,
415 };
416 let e = empty_event();
417 let event = JsonEvent::borrow(&e);
418 assert!(m.matches(&EventValue::Str("superadminuser".into()), &event));
419 assert!(m.matches(&EventValue::Str("ADMIN".into()), &event));
420 assert!(!m.matches(&EventValue::Str("user".into()), &event));
421 }
422
423 #[test]
424 fn test_starts_with() {
425 let m = CompiledMatcher::StartsWith {
426 value: "cmd".into(),
427 case_insensitive: true,
428 };
429 let e = empty_event();
430 let event = JsonEvent::borrow(&e);
431 assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
432 assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
433 assert!(!m.matches(&EventValue::Str("xcmd".into()), &event));
434 }
435
436 #[test]
437 fn test_ends_with() {
438 let m = CompiledMatcher::EndsWith {
439 value: ".exe".into(),
440 case_insensitive: true,
441 };
442 let e = empty_event();
443 let event = JsonEvent::borrow(&e);
444 assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
445 assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
446 assert!(!m.matches(&EventValue::Str("cmd.bat".into()), &event));
447 }
448
449 #[test]
450 fn test_regex() {
451 let re = Regex::new("(?i)^test.*value$").unwrap();
452 let m = CompiledMatcher::Regex(re);
453 let e = empty_event();
454 let event = JsonEvent::borrow(&e);
455 assert!(m.matches(&EventValue::Str("testXYZvalue".into()), &event));
456 assert!(m.matches(&EventValue::Str("TESTvalue".into()), &event));
457 assert!(!m.matches(&EventValue::Str("notamatch".into()), &event));
458 }
459
460 #[test]
461 fn test_cidr() {
462 let net: IpNet = "10.0.0.0/8".parse().unwrap();
463 let m = CompiledMatcher::Cidr(net);
464 let e = empty_event();
465 let event = JsonEvent::borrow(&e);
466 assert!(m.matches(&EventValue::Str("10.1.2.3".into()), &event));
467 assert!(!m.matches(&EventValue::Str("192.168.1.1".into()), &event));
468 }
469
470 #[test]
471 fn test_numeric() {
472 let m = CompiledMatcher::NumericGte(100.0);
473 let e = empty_event();
474 let event = JsonEvent::borrow(&e);
475 assert!(m.matches(&EventValue::Int(100), &event));
476 assert!(m.matches(&EventValue::Int(200), &event));
477 assert!(!m.matches(&EventValue::Int(50), &event));
478 assert!(m.matches(&EventValue::Str("150".into()), &event));
479 }
480
481 #[test]
482 fn test_null() {
483 let m = CompiledMatcher::Null;
484 let e = empty_event();
485 let event = JsonEvent::borrow(&e);
486 assert!(m.matches(&EventValue::Null, &event));
487 assert!(!m.matches(&EventValue::Str("".into()), &event));
488 }
489
490 #[test]
491 fn test_bool() {
492 let m = CompiledMatcher::BoolEq(true);
493 let e = empty_event();
494 let event = JsonEvent::borrow(&e);
495 assert!(m.matches(&EventValue::Bool(true), &event));
496 assert!(!m.matches(&EventValue::Bool(false), &event));
497 assert!(m.matches(&EventValue::Str("true".into()), &event));
498 }
499
500 #[test]
501 fn test_field_ref() {
502 let e = json!({"src": "10.0.0.1", "dst": "10.0.0.1"});
503 let event = JsonEvent::borrow(&e);
504 let m = CompiledMatcher::FieldRef {
505 field: "dst".into(),
506 op: IrStrOp::Exact,
507 case_insensitive: true,
508 };
509 assert!(m.matches(&EventValue::Str("10.0.0.1".into()), &event));
510 }
511
512 #[test]
513 fn test_field_ref_contains() {
514 let e = json!({"user": "Alice", "arn": "arn:aws:iam::123:user/alice"});
515 let event = JsonEvent::borrow(&e);
516 let m = CompiledMatcher::FieldRef {
517 field: "user".into(),
518 op: IrStrOp::Contains,
519 case_insensitive: true,
520 };
521 assert!(m.matches(
522 &EventValue::Str("arn:aws:iam::123:user/alice".into()),
523 &event
524 ));
525 let cased = CompiledMatcher::FieldRef {
526 field: "user".into(),
527 op: IrStrOp::Contains,
528 case_insensitive: false,
529 };
530 assert!(!cased.matches(
531 &EventValue::Str("arn:aws:iam::123:user/alice".into()),
532 &event
533 ));
534 let missing_type = CompiledMatcher::FieldRef {
535 field: "missing".into(),
536 op: IrStrOp::StartsWith,
537 case_insensitive: true,
538 };
539 assert!(!missing_type.matches(&EventValue::Str("alice".into()), &event));
540 }
541
542 #[test]
543 fn test_any_of() {
544 let m = CompiledMatcher::AnyOf(vec![
545 CompiledMatcher::Exact {
546 value: "a".into(),
547 case_insensitive: false,
548 },
549 CompiledMatcher::Exact {
550 value: "b".into(),
551 case_insensitive: false,
552 },
553 ]);
554 let e = empty_event();
555 let event = JsonEvent::borrow(&e);
556 assert!(m.matches(&EventValue::Str("a".into()), &event));
557 assert!(m.matches(&EventValue::Str("b".into()), &event));
558 assert!(!m.matches(&EventValue::Str("c".into()), &event));
559 }
560
561 #[test]
562 fn test_all_of() {
563 let m = CompiledMatcher::AllOf(vec![
564 CompiledMatcher::Contains {
565 value: "admin".into(),
566 case_insensitive: false,
567 },
568 CompiledMatcher::Contains {
569 value: "user".into(),
570 case_insensitive: false,
571 },
572 ]);
573 let e = empty_event();
574 let event = JsonEvent::borrow(&e);
575 assert!(m.matches(&EventValue::Str("adminuser".into()), &event));
576 assert!(!m.matches(&EventValue::Str("admin".into()), &event));
577 }
578
579 #[test]
580 fn test_array_value_matching() {
581 let m = CompiledMatcher::Exact {
582 value: "target".into(),
583 case_insensitive: true,
584 };
585 let e = empty_event();
586 let event = JsonEvent::borrow(&e);
587 let arr = EventValue::Array(vec![
588 EventValue::Str("other".into()),
589 EventValue::Str("target".into()),
590 EventValue::Str("more".into()),
591 ]);
592 assert!(m.matches(&arr, &event));
593 let arr2 = EventValue::Array(vec![
594 EventValue::Str("other".into()),
595 EventValue::Str("nope".into()),
596 ]);
597 assert!(!m.matches(&arr2, &event));
598 }
599
600 #[test]
601 fn test_number_coercion_to_string() {
602 let m = CompiledMatcher::Exact {
603 value: "42".into(),
604 case_insensitive: false,
605 };
606 let e = empty_event();
607 let event = JsonEvent::borrow(&e);
608 assert!(m.matches(&EventValue::Int(42), &event));
609 }
610
611 #[test]
616 fn test_exact_unicode_case_insensitive() {
617 let m = CompiledMatcher::Exact {
618 value: "ärzte".to_lowercase(),
619 case_insensitive: true,
620 };
621 let e = empty_event();
622 let event = JsonEvent::borrow(&e);
623 assert!(m.matches(&EventValue::Str("ÄRZTE".into()), &event));
624 assert!(m.matches(&EventValue::Str("Ärzte".into()), &event));
625 assert!(m.matches(&EventValue::Str("ärzte".into()), &event));
626 }
627
628 #[test]
629 fn test_contains_unicode_case_insensitive() {
630 let m = CompiledMatcher::Contains {
631 value: "ñ".to_lowercase(),
632 case_insensitive: true,
633 };
634 let e = empty_event();
635 let event = JsonEvent::borrow(&e);
636 assert!(m.matches(&EventValue::Str("España".into()), &event));
637 assert!(m.matches(&EventValue::Str("ESPAÑA".into()), &event));
638 }
639
640 #[test]
641 fn test_startswith_unicode_case_insensitive() {
642 let m = CompiledMatcher::StartsWith {
643 value: "über".to_lowercase(),
644 case_insensitive: true,
645 };
646 let e = empty_event();
647 let event = JsonEvent::borrow(&e);
648 assert!(m.matches(&EventValue::Str("Übersicht".into()), &event));
649 assert!(m.matches(&EventValue::Str("ÜBERSICHT".into()), &event));
650 assert!(!m.matches(&EventValue::Str("not-uber".into()), &event));
651 }
652
653 #[test]
654 fn test_endswith_unicode_case_insensitive() {
655 let m = CompiledMatcher::EndsWith {
656 value: "ção".to_lowercase(),
657 case_insensitive: true,
658 };
659 let e = empty_event();
660 let event = JsonEvent::borrow(&e);
661 assert!(m.matches(&EventValue::Str("Aplicação".into()), &event));
662 assert!(m.matches(&EventValue::Str("APLICAÇÃO".into()), &event));
663 assert!(!m.matches(&EventValue::Str("Aplicacao".into()), &event));
664 }
665
666 #[test]
667 fn test_greek_case_insensitive() {
668 let m = CompiledMatcher::Exact {
669 value: "σίγμα".to_lowercase(),
670 case_insensitive: true,
671 };
672 let e = empty_event();
673 let event = JsonEvent::borrow(&e);
674 assert!(m.matches(&EventValue::Str("ΣΊΓΜΑ".into()), &event));
675 assert!(m.matches(&EventValue::Str("σίγμα".into()), &event));
676 }
677
678 #[test]
683 fn test_parse_expand_template() {
684 let parts = parse_expand_template("C:\\Users\\%user%\\AppData");
685 assert_eq!(parts.len(), 3);
686 assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "C:\\Users\\"));
687 assert!(matches!(&parts[1], ExpandPart::Placeholder(s) if s == "user"));
688 assert!(matches!(&parts[2], ExpandPart::Literal(s) if s == "\\AppData"));
689 }
690
691 #[test]
692 fn test_parse_expand_template_no_placeholders() {
693 let parts = parse_expand_template("just a literal");
694 assert_eq!(parts.len(), 1);
695 assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "just a literal"));
696 }
697
698 #[test]
699 fn test_parse_expand_template_multiple_placeholders() {
700 let parts = parse_expand_template("%a%:%b%");
701 assert_eq!(parts.len(), 3);
702 assert!(matches!(&parts[0], ExpandPart::Placeholder(s) if s == "a"));
703 assert!(matches!(&parts[1], ExpandPart::Literal(s) if s == ":"));
704 assert!(matches!(&parts[2], ExpandPart::Placeholder(s) if s == "b"));
705 }
706
707 #[test]
708 fn test_expand_matcher() {
709 let template = parse_expand_template("C:\\Users\\%user%\\Downloads");
710 let m = CompiledMatcher::Expand {
711 template,
712 case_insensitive: true,
713 };
714 let e = json!({"user": "admin", "path": "C:\\Users\\admin\\Downloads"});
715 let event = JsonEvent::borrow(&e);
716 assert!(m.matches(
717 &EventValue::Str("C:\\Users\\admin\\Downloads".into()),
718 &event
719 ));
720 assert!(!m.matches(
721 &EventValue::Str("C:\\Users\\other\\Downloads".into()),
722 &event
723 ));
724 }
725
726 #[test]
727 fn test_expand_matcher_missing_field() {
728 let template = parse_expand_template("%user%@%domain%");
729 let m = CompiledMatcher::Expand {
730 template,
731 case_insensitive: false,
732 };
733 let e = json!({"user": "admin"});
734 let event = JsonEvent::borrow(&e);
735 assert!(m.matches(&EventValue::Str("admin@".into()), &event));
736 }
737
738 #[test]
743 fn test_timestamp_part_hour() {
744 let m = CompiledMatcher::TimestampPart {
745 part: TimePart::Hour,
746 inner: Box::new(CompiledMatcher::NumericEq(12.0)),
747 };
748 let e = json!({});
749 let event = JsonEvent::borrow(&e);
750 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
751 assert!(!m.matches(&EventValue::Str("2024-07-10T15:30:00Z".into()), &event));
752 }
753
754 #[test]
755 fn test_timestamp_part_month() {
756 let m = CompiledMatcher::TimestampPart {
757 part: TimePart::Month,
758 inner: Box::new(CompiledMatcher::NumericEq(7.0)),
759 };
760 let e = json!({});
761 let event = JsonEvent::borrow(&e);
762 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
763 assert!(!m.matches(&EventValue::Str("2024-08-10T12:30:00Z".into()), &event));
764 }
765
766 #[test]
767 fn test_timestamp_part_day() {
768 let m = CompiledMatcher::TimestampPart {
769 part: TimePart::Day,
770 inner: Box::new(CompiledMatcher::NumericEq(10.0)),
771 };
772 let e = json!({});
773 let event = JsonEvent::borrow(&e);
774 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
775 assert!(!m.matches(&EventValue::Str("2024-07-15T12:30:00Z".into()), &event));
776 }
777
778 #[test]
779 fn test_timestamp_part_year() {
780 let m = CompiledMatcher::TimestampPart {
781 part: TimePart::Year,
782 inner: Box::new(CompiledMatcher::NumericEq(2024.0)),
783 };
784 let e = json!({});
785 let event = JsonEvent::borrow(&e);
786 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
787 assert!(!m.matches(&EventValue::Str("2023-07-10T12:30:00Z".into()), &event));
788 }
789
790 #[test]
791 fn test_timestamp_part_from_epoch() {
792 let m = CompiledMatcher::TimestampPart {
793 part: TimePart::Hour,
794 inner: Box::new(CompiledMatcher::NumericEq(12.0)),
795 };
796 let e = json!({});
797 let event = JsonEvent::borrow(&e);
798 assert!(m.matches(&EventValue::Int(1720614600), &event));
800 }
801}