1use std::net::IpAddr;
8
9use chrono::{Datelike, Timelike};
10use ipnet::IpNet;
11use regex::Regex;
12
13use crate::event::{Event, EventValue};
14
15#[derive(Debug, Clone)]
21pub enum CompiledMatcher {
22 Exact {
25 value: String,
26 case_insensitive: bool,
27 },
28 Contains {
30 value: String,
31 case_insensitive: bool,
32 },
33 StartsWith {
35 value: String,
36 case_insensitive: bool,
37 },
38 EndsWith {
40 value: String,
41 case_insensitive: bool,
42 },
43 Regex(Regex),
45
46 Cidr(IpNet),
49
50 NumericEq(f64),
53 NumericGt(f64),
55 NumericGte(f64),
57 NumericLt(f64),
59 NumericLte(f64),
61
62 Exists(bool),
65 FieldRef {
67 field: String,
68 case_insensitive: bool,
69 },
70 Null,
72 BoolEq(bool),
74
75 Expand {
78 template: Vec<ExpandPart>,
79 case_insensitive: bool,
80 },
81
82 TimestampPart {
85 part: TimePart,
86 inner: Box<CompiledMatcher>,
87 },
88
89 Not(Box<CompiledMatcher>),
92
93 AnyOf(Vec<CompiledMatcher>),
96 AllOf(Vec<CompiledMatcher>),
98}
99
100#[derive(Debug, Clone)]
102pub enum ExpandPart {
103 Literal(String),
105 Placeholder(String),
107}
108
109#[derive(Debug, Clone, Copy)]
111pub enum TimePart {
112 Minute,
113 Hour,
114 Day,
115 Week,
116 Month,
117 Year,
118}
119
120impl CompiledMatcher {
121 #[inline]
125 pub fn matches(&self, value: &EventValue, event: &impl Event) -> bool {
126 match self {
127 CompiledMatcher::Exact {
129 value: expected,
130 case_insensitive,
131 } => match_str_value(value, |s| {
132 if *case_insensitive {
133 s.to_lowercase() == *expected
134 } else {
135 s == expected
136 }
137 }),
138
139 CompiledMatcher::Contains {
140 value: needle,
141 case_insensitive,
142 } => match_str_value(value, |s| {
143 if *case_insensitive {
144 s.to_lowercase().contains(needle.as_str())
145 } else {
146 s.contains(needle.as_str())
147 }
148 }),
149
150 CompiledMatcher::StartsWith {
151 value: prefix,
152 case_insensitive,
153 } => match_str_value(value, |s| {
154 if *case_insensitive {
155 s.to_lowercase().starts_with(prefix.as_str())
156 } else {
157 s.starts_with(prefix.as_str())
158 }
159 }),
160
161 CompiledMatcher::EndsWith {
162 value: suffix,
163 case_insensitive,
164 } => match_str_value(value, |s| {
165 if *case_insensitive {
166 s.to_lowercase().ends_with(suffix.as_str())
167 } else {
168 s.ends_with(suffix.as_str())
169 }
170 }),
171
172 CompiledMatcher::Regex(re) => match_str_value(value, |s| re.is_match(s)),
173
174 CompiledMatcher::Cidr(net) => match_str_value(value, |s| {
176 s.parse::<IpAddr>().is_ok_and(|ip| net.contains(&ip))
177 }),
178
179 CompiledMatcher::NumericEq(n) => {
181 match_numeric_value(value, |v| (v - n).abs() < f64::EPSILON)
182 }
183 CompiledMatcher::NumericGt(n) => match_numeric_value(value, |v| v > *n),
184 CompiledMatcher::NumericGte(n) => match_numeric_value(value, |v| v >= *n),
185 CompiledMatcher::NumericLt(n) => match_numeric_value(value, |v| v < *n),
186 CompiledMatcher::NumericLte(n) => match_numeric_value(value, |v| v <= *n),
187
188 CompiledMatcher::Exists(expect) => {
190 let exists = !value.is_null();
191 exists == *expect
192 }
193
194 CompiledMatcher::FieldRef {
195 field: ref_field,
196 case_insensitive,
197 } => {
198 if let Some(ref_value) = event.get_field(ref_field) {
199 if *case_insensitive {
200 match (value.as_str(), ref_value.as_str()) {
201 (Some(a), Some(b)) => a.to_lowercase() == b.to_lowercase(),
202 _ => value == &ref_value,
203 }
204 } else {
205 value == &ref_value
206 }
207 } else {
208 false
209 }
210 }
211
212 CompiledMatcher::Null => value.is_null(),
213
214 CompiledMatcher::BoolEq(expected) => match value {
215 EventValue::Bool(b) => b == expected,
216 EventValue::Str(s) => match s.to_lowercase().as_str() {
217 "true" | "1" | "yes" => *expected,
218 "false" | "0" | "no" => !*expected,
219 _ => false,
220 },
221 _ => false,
222 },
223
224 CompiledMatcher::Expand {
226 template,
227 case_insensitive,
228 } => {
229 let expanded = expand_template(template, event);
230 match_str_value(value, |s| {
231 if *case_insensitive {
232 s.to_lowercase() == expanded.to_lowercase()
233 } else {
234 s == expanded
235 }
236 })
237 }
238
239 CompiledMatcher::TimestampPart { part, inner } => {
241 match extract_timestamp_part(value, *part) {
242 Some(n) => {
243 let num_val = EventValue::Int(n);
244 inner.matches(&num_val, event)
245 }
246 None => false,
247 }
248 }
249
250 CompiledMatcher::Not(inner) => !inner.matches(value, event),
252
253 CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches(value, event)),
255 CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches(value, event)),
256 }
257 }
258
259 #[inline]
265 pub fn matches_keyword(&self, event: &impl Event) -> bool {
266 event.any_string_value(&|s| self.matches_str(s))
267 }
268
269 fn matches_str(&self, s: &str) -> bool {
275 match self {
276 CompiledMatcher::Exact {
277 value: expected,
278 case_insensitive,
279 } => {
280 if *case_insensitive {
281 s.to_lowercase() == *expected
282 } else {
283 s == expected
284 }
285 }
286 CompiledMatcher::Contains {
287 value: needle,
288 case_insensitive,
289 } => {
290 if *case_insensitive {
291 s.to_lowercase().contains(needle.as_str())
292 } else {
293 s.contains(needle.as_str())
294 }
295 }
296 CompiledMatcher::StartsWith {
297 value: prefix,
298 case_insensitive,
299 } => {
300 if *case_insensitive {
301 s.to_lowercase().starts_with(prefix.as_str())
302 } else {
303 s.starts_with(prefix.as_str())
304 }
305 }
306 CompiledMatcher::EndsWith {
307 value: suffix,
308 case_insensitive,
309 } => {
310 if *case_insensitive {
311 s.to_lowercase().ends_with(suffix.as_str())
312 } else {
313 s.ends_with(suffix.as_str())
314 }
315 }
316 CompiledMatcher::Regex(re) => re.is_match(s),
317 CompiledMatcher::Not(inner) => !inner.matches_str(s),
318 CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches_str(s)),
319 CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches_str(s)),
320 _ => false,
321 }
322 }
323}
324
325fn match_str_value(value: &EventValue, pred: impl Fn(&str) -> bool) -> bool {
333 match_str_value_ref(value, &pred)
334}
335
336fn match_str_value_ref(value: &EventValue, pred: &dyn Fn(&str) -> bool) -> bool {
337 match value {
338 EventValue::Str(s) => pred(s),
339 EventValue::Int(n) => pred(&n.to_string()),
340 EventValue::Float(f) => pred(&f.to_string()),
341 EventValue::Bool(b) => pred(if *b { "true" } else { "false" }),
342 EventValue::Array(arr) => arr.iter().any(|v| match_str_value_ref(v, pred)),
343 _ => false,
344 }
345}
346
347fn match_numeric_value(value: &EventValue, pred: impl Fn(f64) -> bool) -> bool {
351 match_numeric_value_ref(value, &pred)
352}
353
354fn match_numeric_value_ref(value: &EventValue, pred: &dyn Fn(f64) -> bool) -> bool {
355 match value {
356 EventValue::Int(n) => pred(*n as f64),
357 EventValue::Float(f) => pred(*f),
358 EventValue::Str(s) => s.parse::<f64>().is_ok_and(pred),
359 EventValue::Array(arr) => arr.iter().any(|v| match_numeric_value_ref(v, pred)),
360 _ => false,
361 }
362}
363
364pub fn sigma_string_to_regex(
369 parts: &[rsigma_parser::value::StringPart],
370 case_insensitive: bool,
371) -> String {
372 use rsigma_parser::value::{SpecialChar, StringPart};
373
374 let mut pattern = String::new();
375 if case_insensitive {
376 pattern.push_str("(?i)");
377 }
378 pattern.push('^');
379 for part in parts {
380 match part {
381 StringPart::Plain(text) => {
382 pattern.push_str(®ex::escape(text));
383 }
384 StringPart::Special(SpecialChar::WildcardMulti) => {
385 pattern.push_str(".*");
386 }
387 StringPart::Special(SpecialChar::WildcardSingle) => {
388 pattern.push('.');
389 }
390 }
391 }
392 pattern.push('$');
393 pattern
394}
395
396fn expand_template(template: &[ExpandPart], event: &impl Event) -> String {
402 let mut result = String::new();
403 for part in template {
404 match part {
405 ExpandPart::Literal(s) => result.push_str(s),
406 ExpandPart::Placeholder(field) => {
407 if let Some(val) = event.get_field(field)
408 && let Some(s) = val.as_str()
409 {
410 result.push_str(&s);
411 }
412 }
413 }
414 }
415 result
416}
417
418pub fn parse_expand_template(s: &str) -> Vec<ExpandPart> {
420 let mut parts = Vec::new();
421 let mut current = String::new();
422 let mut in_placeholder = false;
423 let mut placeholder = String::new();
424
425 for ch in s.chars() {
426 if ch == '%' {
427 if in_placeholder {
428 if !placeholder.is_empty() {
429 parts.push(ExpandPart::Placeholder(placeholder.clone()));
430 placeholder.clear();
431 }
432 in_placeholder = false;
433 } else {
434 if !current.is_empty() {
435 parts.push(ExpandPart::Literal(current.clone()));
436 current.clear();
437 }
438 in_placeholder = true;
439 }
440 } else if in_placeholder {
441 placeholder.push(ch);
442 } else {
443 current.push(ch);
444 }
445 }
446
447 if in_placeholder && !placeholder.is_empty() {
448 current.push('%');
449 current.push_str(&placeholder);
450 }
451 if !current.is_empty() {
452 parts.push(ExpandPart::Literal(current));
453 }
454
455 parts
456}
457
458fn extract_timestamp_part(value: &EventValue, part: TimePart) -> Option<i64> {
464 match value {
465 EventValue::Str(s) => parse_timestamp_str(s, part),
466 EventValue::Int(n) => {
467 let secs = if *n > 1_000_000_000_000 { n / 1000 } else { *n };
468 let dt = chrono::DateTime::from_timestamp(secs, 0)?;
469 Some(extract_part_from_datetime(&dt, part))
470 }
471 EventValue::Float(f) => {
472 let secs = *f as i64;
473 let secs = if secs > 1_000_000_000_000 {
474 secs / 1000
475 } else {
476 secs
477 };
478 let dt = chrono::DateTime::from_timestamp(secs, 0)?;
479 Some(extract_part_from_datetime(&dt, part))
480 }
481 _ => None,
482 }
483}
484
485fn parse_timestamp_str(ts_str: &str, part: TimePart) -> Option<i64> {
486 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(ts_str) {
487 return Some(extract_part_from_datetime(&dt.to_utc(), part));
488 }
489 if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%dT%H:%M:%S") {
490 let dt = naive.and_utc();
491 return Some(extract_part_from_datetime(&dt, part));
492 }
493 if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%d %H:%M:%S") {
494 let dt = naive.and_utc();
495 return Some(extract_part_from_datetime(&dt, part));
496 }
497 if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%dT%H:%M:%S%.f") {
498 let dt = naive.and_utc();
499 return Some(extract_part_from_datetime(&dt, part));
500 }
501 None
502}
503
504fn extract_part_from_datetime(dt: &chrono::DateTime<chrono::Utc>, part: TimePart) -> i64 {
506 match part {
507 TimePart::Minute => dt.minute() as i64,
508 TimePart::Hour => dt.hour() as i64,
509 TimePart::Day => dt.day() as i64,
510 TimePart::Week => dt.iso_week().week() as i64,
511 TimePart::Month => dt.month() as i64,
512 TimePart::Year => dt.year() as i64,
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use crate::event::JsonEvent;
520 use serde_json::json;
521
522 fn empty_event() -> serde_json::Value {
523 json!({})
524 }
525
526 #[test]
527 fn test_exact_case_insensitive() {
528 let m = CompiledMatcher::Exact {
529 value: "whoami".into(),
530 case_insensitive: true,
531 };
532 let e = empty_event();
533 let event = JsonEvent::borrow(&e);
534 assert!(m.matches(&EventValue::Str("whoami".into()), &event));
535 assert!(m.matches(&EventValue::Str("WHOAMI".into()), &event));
536 assert!(m.matches(&EventValue::Str("Whoami".into()), &event));
537 assert!(!m.matches(&EventValue::Str("other".into()), &event));
538 }
539
540 #[test]
541 fn test_exact_case_sensitive() {
542 let m = CompiledMatcher::Exact {
543 value: "whoami".into(),
544 case_insensitive: false,
545 };
546 let e = empty_event();
547 let event = JsonEvent::borrow(&e);
548 assert!(m.matches(&EventValue::Str("whoami".into()), &event));
549 assert!(!m.matches(&EventValue::Str("WHOAMI".into()), &event));
550 }
551
552 #[test]
553 fn test_contains() {
554 let m = CompiledMatcher::Contains {
555 value: "admin".to_lowercase(),
556 case_insensitive: true,
557 };
558 let e = empty_event();
559 let event = JsonEvent::borrow(&e);
560 assert!(m.matches(&EventValue::Str("superadminuser".into()), &event));
561 assert!(m.matches(&EventValue::Str("ADMIN".into()), &event));
562 assert!(!m.matches(&EventValue::Str("user".into()), &event));
563 }
564
565 #[test]
566 fn test_starts_with() {
567 let m = CompiledMatcher::StartsWith {
568 value: "cmd".into(),
569 case_insensitive: true,
570 };
571 let e = empty_event();
572 let event = JsonEvent::borrow(&e);
573 assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
574 assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
575 assert!(!m.matches(&EventValue::Str("xcmd".into()), &event));
576 }
577
578 #[test]
579 fn test_ends_with() {
580 let m = CompiledMatcher::EndsWith {
581 value: ".exe".into(),
582 case_insensitive: true,
583 };
584 let e = empty_event();
585 let event = JsonEvent::borrow(&e);
586 assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
587 assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
588 assert!(!m.matches(&EventValue::Str("cmd.bat".into()), &event));
589 }
590
591 #[test]
592 fn test_regex() {
593 let re = Regex::new("(?i)^test.*value$").unwrap();
594 let m = CompiledMatcher::Regex(re);
595 let e = empty_event();
596 let event = JsonEvent::borrow(&e);
597 assert!(m.matches(&EventValue::Str("testXYZvalue".into()), &event));
598 assert!(m.matches(&EventValue::Str("TESTvalue".into()), &event));
599 assert!(!m.matches(&EventValue::Str("notamatch".into()), &event));
600 }
601
602 #[test]
603 fn test_cidr() {
604 let net: IpNet = "10.0.0.0/8".parse().unwrap();
605 let m = CompiledMatcher::Cidr(net);
606 let e = empty_event();
607 let event = JsonEvent::borrow(&e);
608 assert!(m.matches(&EventValue::Str("10.1.2.3".into()), &event));
609 assert!(!m.matches(&EventValue::Str("192.168.1.1".into()), &event));
610 }
611
612 #[test]
613 fn test_numeric() {
614 let m = CompiledMatcher::NumericGte(100.0);
615 let e = empty_event();
616 let event = JsonEvent::borrow(&e);
617 assert!(m.matches(&EventValue::Int(100), &event));
618 assert!(m.matches(&EventValue::Int(200), &event));
619 assert!(!m.matches(&EventValue::Int(50), &event));
620 assert!(m.matches(&EventValue::Str("150".into()), &event));
621 }
622
623 #[test]
624 fn test_null() {
625 let m = CompiledMatcher::Null;
626 let e = empty_event();
627 let event = JsonEvent::borrow(&e);
628 assert!(m.matches(&EventValue::Null, &event));
629 assert!(!m.matches(&EventValue::Str("".into()), &event));
630 }
631
632 #[test]
633 fn test_bool() {
634 let m = CompiledMatcher::BoolEq(true);
635 let e = empty_event();
636 let event = JsonEvent::borrow(&e);
637 assert!(m.matches(&EventValue::Bool(true), &event));
638 assert!(!m.matches(&EventValue::Bool(false), &event));
639 assert!(m.matches(&EventValue::Str("true".into()), &event));
640 }
641
642 #[test]
643 fn test_field_ref() {
644 let e = json!({"src": "10.0.0.1", "dst": "10.0.0.1"});
645 let event = JsonEvent::borrow(&e);
646 let m = CompiledMatcher::FieldRef {
647 field: "dst".into(),
648 case_insensitive: true,
649 };
650 assert!(m.matches(&EventValue::Str("10.0.0.1".into()), &event));
651 }
652
653 #[test]
654 fn test_any_of() {
655 let m = CompiledMatcher::AnyOf(vec![
656 CompiledMatcher::Exact {
657 value: "a".into(),
658 case_insensitive: false,
659 },
660 CompiledMatcher::Exact {
661 value: "b".into(),
662 case_insensitive: false,
663 },
664 ]);
665 let e = empty_event();
666 let event = JsonEvent::borrow(&e);
667 assert!(m.matches(&EventValue::Str("a".into()), &event));
668 assert!(m.matches(&EventValue::Str("b".into()), &event));
669 assert!(!m.matches(&EventValue::Str("c".into()), &event));
670 }
671
672 #[test]
673 fn test_all_of() {
674 let m = CompiledMatcher::AllOf(vec![
675 CompiledMatcher::Contains {
676 value: "admin".into(),
677 case_insensitive: false,
678 },
679 CompiledMatcher::Contains {
680 value: "user".into(),
681 case_insensitive: false,
682 },
683 ]);
684 let e = empty_event();
685 let event = JsonEvent::borrow(&e);
686 assert!(m.matches(&EventValue::Str("adminuser".into()), &event));
687 assert!(!m.matches(&EventValue::Str("admin".into()), &event));
688 }
689
690 #[test]
691 fn test_array_value_matching() {
692 let m = CompiledMatcher::Exact {
693 value: "target".into(),
694 case_insensitive: true,
695 };
696 let e = empty_event();
697 let event = JsonEvent::borrow(&e);
698 let arr = EventValue::Array(vec![
699 EventValue::Str("other".into()),
700 EventValue::Str("target".into()),
701 EventValue::Str("more".into()),
702 ]);
703 assert!(m.matches(&arr, &event));
704 let arr2 = EventValue::Array(vec![
705 EventValue::Str("other".into()),
706 EventValue::Str("nope".into()),
707 ]);
708 assert!(!m.matches(&arr2, &event));
709 }
710
711 #[test]
712 fn test_number_coercion_to_string() {
713 let m = CompiledMatcher::Exact {
714 value: "42".into(),
715 case_insensitive: false,
716 };
717 let e = empty_event();
718 let event = JsonEvent::borrow(&e);
719 assert!(m.matches(&EventValue::Int(42), &event));
720 }
721
722 #[test]
727 fn test_exact_unicode_case_insensitive() {
728 let m = CompiledMatcher::Exact {
729 value: "ärzte".to_lowercase(),
730 case_insensitive: true,
731 };
732 let e = empty_event();
733 let event = JsonEvent::borrow(&e);
734 assert!(m.matches(&EventValue::Str("ÄRZTE".into()), &event));
735 assert!(m.matches(&EventValue::Str("Ärzte".into()), &event));
736 assert!(m.matches(&EventValue::Str("ärzte".into()), &event));
737 }
738
739 #[test]
740 fn test_contains_unicode_case_insensitive() {
741 let m = CompiledMatcher::Contains {
742 value: "ñ".to_lowercase(),
743 case_insensitive: true,
744 };
745 let e = empty_event();
746 let event = JsonEvent::borrow(&e);
747 assert!(m.matches(&EventValue::Str("España".into()), &event));
748 assert!(m.matches(&EventValue::Str("ESPAÑA".into()), &event));
749 }
750
751 #[test]
752 fn test_startswith_unicode_case_insensitive() {
753 let m = CompiledMatcher::StartsWith {
754 value: "über".to_lowercase(),
755 case_insensitive: true,
756 };
757 let e = empty_event();
758 let event = JsonEvent::borrow(&e);
759 assert!(m.matches(&EventValue::Str("Übersicht".into()), &event));
760 assert!(m.matches(&EventValue::Str("ÜBERSICHT".into()), &event));
761 assert!(!m.matches(&EventValue::Str("not-uber".into()), &event));
762 }
763
764 #[test]
765 fn test_endswith_unicode_case_insensitive() {
766 let m = CompiledMatcher::EndsWith {
767 value: "ção".to_lowercase(),
768 case_insensitive: true,
769 };
770 let e = empty_event();
771 let event = JsonEvent::borrow(&e);
772 assert!(m.matches(&EventValue::Str("Aplicação".into()), &event));
773 assert!(m.matches(&EventValue::Str("APLICAÇÃO".into()), &event));
774 assert!(!m.matches(&EventValue::Str("Aplicacao".into()), &event));
775 }
776
777 #[test]
778 fn test_greek_case_insensitive() {
779 let m = CompiledMatcher::Exact {
780 value: "σίγμα".to_lowercase(),
781 case_insensitive: true,
782 };
783 let e = empty_event();
784 let event = JsonEvent::borrow(&e);
785 assert!(m.matches(&EventValue::Str("ΣΊΓΜΑ".into()), &event));
786 assert!(m.matches(&EventValue::Str("σίγμα".into()), &event));
787 }
788
789 #[test]
794 fn test_parse_expand_template() {
795 let parts = parse_expand_template("C:\\Users\\%user%\\AppData");
796 assert_eq!(parts.len(), 3);
797 assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "C:\\Users\\"));
798 assert!(matches!(&parts[1], ExpandPart::Placeholder(s) if s == "user"));
799 assert!(matches!(&parts[2], ExpandPart::Literal(s) if s == "\\AppData"));
800 }
801
802 #[test]
803 fn test_parse_expand_template_no_placeholders() {
804 let parts = parse_expand_template("just a literal");
805 assert_eq!(parts.len(), 1);
806 assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "just a literal"));
807 }
808
809 #[test]
810 fn test_parse_expand_template_multiple_placeholders() {
811 let parts = parse_expand_template("%a%:%b%");
812 assert_eq!(parts.len(), 3);
813 assert!(matches!(&parts[0], ExpandPart::Placeholder(s) if s == "a"));
814 assert!(matches!(&parts[1], ExpandPart::Literal(s) if s == ":"));
815 assert!(matches!(&parts[2], ExpandPart::Placeholder(s) if s == "b"));
816 }
817
818 #[test]
819 fn test_expand_matcher() {
820 let template = parse_expand_template("C:\\Users\\%user%\\Downloads");
821 let m = CompiledMatcher::Expand {
822 template,
823 case_insensitive: true,
824 };
825 let e = json!({"user": "admin", "path": "C:\\Users\\admin\\Downloads"});
826 let event = JsonEvent::borrow(&e);
827 assert!(m.matches(
828 &EventValue::Str("C:\\Users\\admin\\Downloads".into()),
829 &event
830 ));
831 assert!(!m.matches(
832 &EventValue::Str("C:\\Users\\other\\Downloads".into()),
833 &event
834 ));
835 }
836
837 #[test]
838 fn test_expand_matcher_missing_field() {
839 let template = parse_expand_template("%user%@%domain%");
840 let m = CompiledMatcher::Expand {
841 template,
842 case_insensitive: false,
843 };
844 let e = json!({"user": "admin"});
845 let event = JsonEvent::borrow(&e);
846 assert!(m.matches(&EventValue::Str("admin@".into()), &event));
847 }
848
849 #[test]
854 fn test_timestamp_part_hour() {
855 let m = CompiledMatcher::TimestampPart {
856 part: TimePart::Hour,
857 inner: Box::new(CompiledMatcher::NumericEq(12.0)),
858 };
859 let e = json!({});
860 let event = JsonEvent::borrow(&e);
861 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
862 assert!(!m.matches(&EventValue::Str("2024-07-10T15:30:00Z".into()), &event));
863 }
864
865 #[test]
866 fn test_timestamp_part_month() {
867 let m = CompiledMatcher::TimestampPart {
868 part: TimePart::Month,
869 inner: Box::new(CompiledMatcher::NumericEq(7.0)),
870 };
871 let e = json!({});
872 let event = JsonEvent::borrow(&e);
873 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
874 assert!(!m.matches(&EventValue::Str("2024-08-10T12:30:00Z".into()), &event));
875 }
876
877 #[test]
878 fn test_timestamp_part_day() {
879 let m = CompiledMatcher::TimestampPart {
880 part: TimePart::Day,
881 inner: Box::new(CompiledMatcher::NumericEq(10.0)),
882 };
883 let e = json!({});
884 let event = JsonEvent::borrow(&e);
885 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
886 assert!(!m.matches(&EventValue::Str("2024-07-15T12:30:00Z".into()), &event));
887 }
888
889 #[test]
890 fn test_timestamp_part_year() {
891 let m = CompiledMatcher::TimestampPart {
892 part: TimePart::Year,
893 inner: Box::new(CompiledMatcher::NumericEq(2024.0)),
894 };
895 let e = json!({});
896 let event = JsonEvent::borrow(&e);
897 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
898 assert!(!m.matches(&EventValue::Str("2023-07-10T12:30:00Z".into()), &event));
899 }
900
901 #[test]
902 fn test_timestamp_part_from_epoch() {
903 let m = CompiledMatcher::TimestampPart {
904 part: TimePart::Hour,
905 inner: Box::new(CompiledMatcher::NumericEq(12.0)),
906 };
907 let e = json!({});
908 let event = JsonEvent::borrow(&e);
909 assert!(m.matches(&EventValue::Int(1720614600), &event));
911 }
912}
913
914#[cfg(test)]
919mod proptests {
920 use super::*;
921 use crate::event::JsonEvent;
922 use proptest::prelude::*;
923 use rsigma_parser::value::{SpecialChar, StringPart};
924 use serde_json::json;
925
926 fn arb_string_parts() -> impl Strategy<Value = Vec<StringPart>> {
927 prop::collection::vec(
928 prop_oneof![
929 "[[:print:]]{0,20}".prop_map(StringPart::Plain),
930 Just(StringPart::Special(SpecialChar::WildcardMulti)),
931 Just(StringPart::Special(SpecialChar::WildcardSingle)),
932 ],
933 0..8,
934 )
935 }
936
937 proptest! {
938 #[test]
939 fn wildcard_regex_always_valid(parts in arb_string_parts(), ci in any::<bool>()) {
940 let pattern = sigma_string_to_regex(&parts, ci);
941 prop_assert!(regex::Regex::new(&pattern).is_ok(),
942 "sigma_string_to_regex produced invalid regex: {}", pattern);
943 }
944 }
945
946 proptest! {
947 #[test]
948 fn plain_text_matches_itself(text in "[[:print:]]{1,30}") {
949 let parts = vec![StringPart::Plain(text.clone())];
950 let pattern = sigma_string_to_regex(&parts, false);
951 let re = regex::Regex::new(&pattern).unwrap();
952 prop_assert!(re.is_match(&text),
953 "plain text should match itself: text={:?}, pattern={}", text, pattern);
954 }
955 }
956
957 proptest! {
958 #[test]
959 fn plain_text_rejects_different_string(
960 text in "[a-zA-Z0-9]{1,10}",
961 other in "[a-zA-Z0-9]{1,10}",
962 ) {
963 prop_assume!(text != other);
964 let parts = vec![StringPart::Plain(text.clone())];
965 let pattern = sigma_string_to_regex(&parts, false);
966 let re = regex::Regex::new(&pattern).unwrap();
967 prop_assert!(!re.is_match(&other),
968 "plain {:?} should not match {:?}", text, other);
969 }
970 }
971
972 proptest! {
973 #[test]
974 fn exact_ci_symmetric(s in "[[:alpha:]]{1,20}") {
975 let m = CompiledMatcher::Exact {
976 value: s.to_lowercase(),
977 case_insensitive: true,
978 };
979 let e = json!({});
980 let event = JsonEvent::borrow(&e);
981 let upper = EventValue::Str(s.to_uppercase().into());
982 let lower = EventValue::Str(s.to_lowercase().into());
983 prop_assert!(m.matches(&upper, &event),
984 "CI exact should match uppercase: {:?}", s.to_uppercase());
985 prop_assert!(m.matches(&lower, &event),
986 "CI exact should match lowercase: {:?}", s.to_lowercase());
987 }
988 }
989
990 proptest! {
991 #[test]
992 fn contains_agrees_with_stdlib(
993 haystack in "[[:print:]]{0,30}",
994 needle in "[[:print:]]{1,10}",
995 ) {
996 let expected = haystack.contains(&needle);
997 let m = CompiledMatcher::Contains {
998 value: needle.clone(),
999 case_insensitive: false,
1000 };
1001 let e = json!({});
1002 let event = JsonEvent::borrow(&e);
1003 let val = EventValue::Str(haystack.clone().into());
1004 prop_assert_eq!(m.matches(&val, &event), expected,
1005 "Contains({:?}) on {:?}", needle, haystack);
1006 }
1007 }
1008
1009 proptest! {
1010 #[test]
1011 fn startswith_agrees_with_stdlib(
1012 haystack in "[[:print:]]{0,30}",
1013 prefix in "[[:print:]]{1,10}",
1014 ) {
1015 let expected = haystack.starts_with(&prefix);
1016 let m = CompiledMatcher::StartsWith {
1017 value: prefix.clone(),
1018 case_insensitive: false,
1019 };
1020 let e = json!({});
1021 let event = JsonEvent::borrow(&e);
1022 let val = EventValue::Str(haystack.clone().into());
1023 prop_assert_eq!(m.matches(&val, &event), expected,
1024 "StartsWith({:?}) on {:?}", prefix, haystack);
1025 }
1026 }
1027
1028 proptest! {
1029 #[test]
1030 fn endswith_agrees_with_stdlib(
1031 haystack in "[[:print:]]{0,30}",
1032 suffix in "[[:print:]]{1,10}",
1033 ) {
1034 let expected = haystack.ends_with(&suffix);
1035 let m = CompiledMatcher::EndsWith {
1036 value: suffix.clone(),
1037 case_insensitive: false,
1038 };
1039 let e = json!({});
1040 let event = JsonEvent::borrow(&e);
1041 let val = EventValue::Str(haystack.clone().into());
1042 prop_assert_eq!(m.matches(&val, &event), expected,
1043 "EndsWith({:?}) on {:?}", suffix, haystack);
1044 }
1045 }
1046
1047 proptest! {
1048 #[test]
1049 fn ci_contains_agrees_with_lowercased(
1050 haystack in "[[:alpha:]]{0,20}",
1051 needle in "[[:alpha:]]{1,8}",
1052 ) {
1053 let expected = haystack.to_lowercase().contains(&needle.to_lowercase());
1054 let m = CompiledMatcher::Contains {
1055 value: needle.to_lowercase(),
1056 case_insensitive: true,
1057 };
1058 let e = json!({});
1059 let event = JsonEvent::borrow(&e);
1060 let val = EventValue::Str(haystack.clone().into());
1061 prop_assert_eq!(m.matches(&val, &event), expected,
1062 "CI Contains({:?}) on {:?}", needle, haystack);
1063 }
1064
1065 #[test]
1066 fn ci_startswith_agrees_with_lowercased(
1067 haystack in "[[:alpha:]]{0,20}",
1068 prefix in "[[:alpha:]]{1,8}",
1069 ) {
1070 let expected = haystack.to_lowercase().starts_with(&prefix.to_lowercase());
1071 let m = CompiledMatcher::StartsWith {
1072 value: prefix.to_lowercase(),
1073 case_insensitive: true,
1074 };
1075 let e = json!({});
1076 let event = JsonEvent::borrow(&e);
1077 let val = EventValue::Str(haystack.clone().into());
1078 prop_assert_eq!(m.matches(&val, &event), expected,
1079 "CI StartsWith({:?}) on {:?}", prefix, haystack);
1080 }
1081
1082 #[test]
1083 fn ci_endswith_agrees_with_lowercased(
1084 haystack in "[[:alpha:]]{0,20}",
1085 suffix in "[[:alpha:]]{1,8}",
1086 ) {
1087 let expected = haystack.to_lowercase().ends_with(&suffix.to_lowercase());
1088 let m = CompiledMatcher::EndsWith {
1089 value: suffix.to_lowercase(),
1090 case_insensitive: true,
1091 };
1092 let e = json!({});
1093 let event = JsonEvent::borrow(&e);
1094 let val = EventValue::Str(haystack.clone().into());
1095 prop_assert_eq!(m.matches(&val, &event), expected,
1096 "CI EndsWith({:?}) on {:?}", suffix, haystack);
1097 }
1098 }
1099
1100 proptest! {
1101 #[test]
1102 fn wildcard_star_matches_anything(s in "[[:print:]]{0,30}") {
1103 let parts = vec![StringPart::Special(SpecialChar::WildcardMulti)];
1104 let pattern = sigma_string_to_regex(&parts, false);
1105 let re = regex::Regex::new(&pattern).unwrap();
1106 prop_assert!(re.is_match(&s), "* should match any string: {:?}", s);
1107 }
1108
1109 #[test]
1110 fn wildcard_question_matches_single_char(c in proptest::char::range('!', '~')) {
1111 let parts = vec![StringPart::Special(SpecialChar::WildcardSingle)];
1112 let pattern = sigma_string_to_regex(&parts, false);
1113 let re = regex::Regex::new(&pattern).unwrap();
1114 let s = c.to_string();
1115 prop_assert!(re.is_match(&s), "? should match single char: {:?}", s);
1116 }
1117 }
1118}