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 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 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 #[inline]
67 pub fn matches(&self, value: &EventValue, event: &impl Event) -> bool {
68 match self {
69 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 CompiledMatcher::Cidr(net) => match_cidr(value, net),
134
135 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 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 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 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 CompiledMatcher::Not(inner) => !inner.matches(value, event),
219
220 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 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 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 #[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 #[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 #[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 assert!(m.matches(&EventValue::Int(1720614600), &event));
831 }
832}