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#[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 #[inline]
31 pub fn matches(&self, value: &EventValue, event: &impl Event) -> bool {
32 match self {
33 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 CompiledMatcher::Cidr(net) => match_cidr(value, net),
98
99 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 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 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 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 CompiledMatcher::Not(inner) => !inner.matches(value, event),
172
173 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 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 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 #[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 #[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 #[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 assert!(m.matches(&EventValue::Int(1720614600), &event));
732 }
733}