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