1use super::CompiledMatcher;
2use super::helpers::{
3 expand_template, extract_timestamp_part, match_cidr, match_numeric_value, match_str_value,
4};
5use crate::event::{Event, EventValue};
6
7impl CompiledMatcher {
8 #[inline]
12 pub fn matches(&self, value: &EventValue, event: &impl Event) -> bool {
13 match self {
14 CompiledMatcher::Exact {
16 value: expected,
17 case_insensitive,
18 } => match_str_value(value, |s| {
19 if *case_insensitive {
20 s.to_lowercase() == *expected
21 } else {
22 s == expected
23 }
24 }),
25
26 CompiledMatcher::Contains {
27 value: needle,
28 case_insensitive,
29 } => match_str_value(value, |s| {
30 if *case_insensitive {
31 s.to_lowercase().contains(needle.as_str())
32 } else {
33 s.contains(needle.as_str())
34 }
35 }),
36
37 CompiledMatcher::StartsWith {
38 value: prefix,
39 case_insensitive,
40 } => match_str_value(value, |s| {
41 if *case_insensitive {
42 s.to_lowercase().starts_with(prefix.as_str())
43 } else {
44 s.starts_with(prefix.as_str())
45 }
46 }),
47
48 CompiledMatcher::EndsWith {
49 value: suffix,
50 case_insensitive,
51 } => match_str_value(value, |s| {
52 if *case_insensitive {
53 s.to_lowercase().ends_with(suffix.as_str())
54 } else {
55 s.ends_with(suffix.as_str())
56 }
57 }),
58
59 CompiledMatcher::Regex(re) => match_str_value(value, |s| re.is_match(s)),
60
61 CompiledMatcher::Cidr(net) => match_cidr(value, net),
63
64 CompiledMatcher::NumericEq(n) => {
66 match_numeric_value(value, |v| (v - n).abs() < f64::EPSILON)
67 }
68 CompiledMatcher::NumericGt(n) => match_numeric_value(value, |v| v > *n),
69 CompiledMatcher::NumericGte(n) => match_numeric_value(value, |v| v >= *n),
70 CompiledMatcher::NumericLt(n) => match_numeric_value(value, |v| v < *n),
71 CompiledMatcher::NumericLte(n) => match_numeric_value(value, |v| v <= *n),
72
73 CompiledMatcher::Exists(expect) => {
75 let exists = !value.is_null();
76 exists == *expect
77 }
78
79 CompiledMatcher::FieldRef {
80 field: ref_field,
81 case_insensitive,
82 } => {
83 if let Some(ref_value) = event.get_field(ref_field) {
84 if *case_insensitive {
85 match (value.as_str(), ref_value.as_str()) {
86 (Some(a), Some(b)) => a.to_lowercase() == b.to_lowercase(),
87 _ => value == &ref_value,
88 }
89 } else {
90 value == &ref_value
91 }
92 } else {
93 false
94 }
95 }
96
97 CompiledMatcher::Null => value.is_null(),
98
99 CompiledMatcher::BoolEq(expected) => match value {
100 EventValue::Bool(b) => b == expected,
101 EventValue::Str(s) => match s.to_lowercase().as_str() {
102 "true" | "1" | "yes" => *expected,
103 "false" | "0" | "no" => !*expected,
104 _ => false,
105 },
106 _ => false,
107 },
108
109 CompiledMatcher::Expand {
111 template,
112 case_insensitive,
113 } => {
114 let expanded = expand_template(template, event);
115 match_str_value(value, |s| {
116 if *case_insensitive {
117 s.to_lowercase() == expanded.to_lowercase()
118 } else {
119 s == expanded
120 }
121 })
122 }
123
124 CompiledMatcher::TimestampPart { part, inner } => {
126 match extract_timestamp_part(value, *part) {
127 Some(n) => {
128 let num_val = EventValue::Int(n);
129 inner.matches(&num_val, event)
130 }
131 None => false,
132 }
133 }
134
135 CompiledMatcher::Not(inner) => !inner.matches(value, event),
137
138 CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches(value, event)),
140 CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches(value, event)),
141 }
142 }
143
144 pub(super) fn matches_str(&self, s: &str) -> bool {
150 match self {
151 CompiledMatcher::Exact {
152 value: expected,
153 case_insensitive,
154 } => {
155 if *case_insensitive {
156 s.to_lowercase() == *expected
157 } else {
158 s == expected
159 }
160 }
161 CompiledMatcher::Contains {
162 value: needle,
163 case_insensitive,
164 } => {
165 if *case_insensitive {
166 s.to_lowercase().contains(needle.as_str())
167 } else {
168 s.contains(needle.as_str())
169 }
170 }
171 CompiledMatcher::StartsWith {
172 value: prefix,
173 case_insensitive,
174 } => {
175 if *case_insensitive {
176 s.to_lowercase().starts_with(prefix.as_str())
177 } else {
178 s.starts_with(prefix.as_str())
179 }
180 }
181 CompiledMatcher::EndsWith {
182 value: suffix,
183 case_insensitive,
184 } => {
185 if *case_insensitive {
186 s.to_lowercase().ends_with(suffix.as_str())
187 } else {
188 s.ends_with(suffix.as_str())
189 }
190 }
191 CompiledMatcher::Regex(re) => re.is_match(s),
192 CompiledMatcher::Not(inner) => !inner.matches_str(s),
193 CompiledMatcher::AnyOf(matchers) => matchers.iter().any(|m| m.matches_str(s)),
194 CompiledMatcher::AllOf(matchers) => matchers.iter().all(|m| m.matches_str(s)),
195 _ => false,
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::event::JsonEvent;
204 use crate::matcher::helpers::parse_expand_template;
205 use crate::matcher::{ExpandPart, TimePart};
206 use ipnet::IpNet;
207 use regex::Regex;
208 use serde_json::json;
209
210 fn empty_event() -> serde_json::Value {
211 json!({})
212 }
213
214 #[test]
215 fn test_exact_case_insensitive() {
216 let m = CompiledMatcher::Exact {
217 value: "whoami".into(),
218 case_insensitive: true,
219 };
220 let e = empty_event();
221 let event = JsonEvent::borrow(&e);
222 assert!(m.matches(&EventValue::Str("whoami".into()), &event));
223 assert!(m.matches(&EventValue::Str("WHOAMI".into()), &event));
224 assert!(m.matches(&EventValue::Str("Whoami".into()), &event));
225 assert!(!m.matches(&EventValue::Str("other".into()), &event));
226 }
227
228 #[test]
229 fn test_exact_case_sensitive() {
230 let m = CompiledMatcher::Exact {
231 value: "whoami".into(),
232 case_insensitive: false,
233 };
234 let e = empty_event();
235 let event = JsonEvent::borrow(&e);
236 assert!(m.matches(&EventValue::Str("whoami".into()), &event));
237 assert!(!m.matches(&EventValue::Str("WHOAMI".into()), &event));
238 }
239
240 #[test]
241 fn test_contains() {
242 let m = CompiledMatcher::Contains {
243 value: "admin".to_lowercase(),
244 case_insensitive: true,
245 };
246 let e = empty_event();
247 let event = JsonEvent::borrow(&e);
248 assert!(m.matches(&EventValue::Str("superadminuser".into()), &event));
249 assert!(m.matches(&EventValue::Str("ADMIN".into()), &event));
250 assert!(!m.matches(&EventValue::Str("user".into()), &event));
251 }
252
253 #[test]
254 fn test_starts_with() {
255 let m = CompiledMatcher::StartsWith {
256 value: "cmd".into(),
257 case_insensitive: true,
258 };
259 let e = empty_event();
260 let event = JsonEvent::borrow(&e);
261 assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
262 assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
263 assert!(!m.matches(&EventValue::Str("xcmd".into()), &event));
264 }
265
266 #[test]
267 fn test_ends_with() {
268 let m = CompiledMatcher::EndsWith {
269 value: ".exe".into(),
270 case_insensitive: true,
271 };
272 let e = empty_event();
273 let event = JsonEvent::borrow(&e);
274 assert!(m.matches(&EventValue::Str("cmd.exe".into()), &event));
275 assert!(m.matches(&EventValue::Str("CMD.EXE".into()), &event));
276 assert!(!m.matches(&EventValue::Str("cmd.bat".into()), &event));
277 }
278
279 #[test]
280 fn test_regex() {
281 let re = Regex::new("(?i)^test.*value$").unwrap();
282 let m = CompiledMatcher::Regex(re);
283 let e = empty_event();
284 let event = JsonEvent::borrow(&e);
285 assert!(m.matches(&EventValue::Str("testXYZvalue".into()), &event));
286 assert!(m.matches(&EventValue::Str("TESTvalue".into()), &event));
287 assert!(!m.matches(&EventValue::Str("notamatch".into()), &event));
288 }
289
290 #[test]
291 fn test_cidr() {
292 let net: IpNet = "10.0.0.0/8".parse().unwrap();
293 let m = CompiledMatcher::Cidr(net);
294 let e = empty_event();
295 let event = JsonEvent::borrow(&e);
296 assert!(m.matches(&EventValue::Str("10.1.2.3".into()), &event));
297 assert!(!m.matches(&EventValue::Str("192.168.1.1".into()), &event));
298 }
299
300 #[test]
301 fn test_numeric() {
302 let m = CompiledMatcher::NumericGte(100.0);
303 let e = empty_event();
304 let event = JsonEvent::borrow(&e);
305 assert!(m.matches(&EventValue::Int(100), &event));
306 assert!(m.matches(&EventValue::Int(200), &event));
307 assert!(!m.matches(&EventValue::Int(50), &event));
308 assert!(m.matches(&EventValue::Str("150".into()), &event));
309 }
310
311 #[test]
312 fn test_null() {
313 let m = CompiledMatcher::Null;
314 let e = empty_event();
315 let event = JsonEvent::borrow(&e);
316 assert!(m.matches(&EventValue::Null, &event));
317 assert!(!m.matches(&EventValue::Str("".into()), &event));
318 }
319
320 #[test]
321 fn test_bool() {
322 let m = CompiledMatcher::BoolEq(true);
323 let e = empty_event();
324 let event = JsonEvent::borrow(&e);
325 assert!(m.matches(&EventValue::Bool(true), &event));
326 assert!(!m.matches(&EventValue::Bool(false), &event));
327 assert!(m.matches(&EventValue::Str("true".into()), &event));
328 }
329
330 #[test]
331 fn test_field_ref() {
332 let e = json!({"src": "10.0.0.1", "dst": "10.0.0.1"});
333 let event = JsonEvent::borrow(&e);
334 let m = CompiledMatcher::FieldRef {
335 field: "dst".into(),
336 case_insensitive: true,
337 };
338 assert!(m.matches(&EventValue::Str("10.0.0.1".into()), &event));
339 }
340
341 #[test]
342 fn test_any_of() {
343 let m = CompiledMatcher::AnyOf(vec![
344 CompiledMatcher::Exact {
345 value: "a".into(),
346 case_insensitive: false,
347 },
348 CompiledMatcher::Exact {
349 value: "b".into(),
350 case_insensitive: false,
351 },
352 ]);
353 let e = empty_event();
354 let event = JsonEvent::borrow(&e);
355 assert!(m.matches(&EventValue::Str("a".into()), &event));
356 assert!(m.matches(&EventValue::Str("b".into()), &event));
357 assert!(!m.matches(&EventValue::Str("c".into()), &event));
358 }
359
360 #[test]
361 fn test_all_of() {
362 let m = CompiledMatcher::AllOf(vec![
363 CompiledMatcher::Contains {
364 value: "admin".into(),
365 case_insensitive: false,
366 },
367 CompiledMatcher::Contains {
368 value: "user".into(),
369 case_insensitive: false,
370 },
371 ]);
372 let e = empty_event();
373 let event = JsonEvent::borrow(&e);
374 assert!(m.matches(&EventValue::Str("adminuser".into()), &event));
375 assert!(!m.matches(&EventValue::Str("admin".into()), &event));
376 }
377
378 #[test]
379 fn test_array_value_matching() {
380 let m = CompiledMatcher::Exact {
381 value: "target".into(),
382 case_insensitive: true,
383 };
384 let e = empty_event();
385 let event = JsonEvent::borrow(&e);
386 let arr = EventValue::Array(vec![
387 EventValue::Str("other".into()),
388 EventValue::Str("target".into()),
389 EventValue::Str("more".into()),
390 ]);
391 assert!(m.matches(&arr, &event));
392 let arr2 = EventValue::Array(vec![
393 EventValue::Str("other".into()),
394 EventValue::Str("nope".into()),
395 ]);
396 assert!(!m.matches(&arr2, &event));
397 }
398
399 #[test]
400 fn test_number_coercion_to_string() {
401 let m = CompiledMatcher::Exact {
402 value: "42".into(),
403 case_insensitive: false,
404 };
405 let e = empty_event();
406 let event = JsonEvent::borrow(&e);
407 assert!(m.matches(&EventValue::Int(42), &event));
408 }
409
410 #[test]
415 fn test_exact_unicode_case_insensitive() {
416 let m = CompiledMatcher::Exact {
417 value: "ärzte".to_lowercase(),
418 case_insensitive: true,
419 };
420 let e = empty_event();
421 let event = JsonEvent::borrow(&e);
422 assert!(m.matches(&EventValue::Str("ÄRZTE".into()), &event));
423 assert!(m.matches(&EventValue::Str("Ärzte".into()), &event));
424 assert!(m.matches(&EventValue::Str("ärzte".into()), &event));
425 }
426
427 #[test]
428 fn test_contains_unicode_case_insensitive() {
429 let m = CompiledMatcher::Contains {
430 value: "ñ".to_lowercase(),
431 case_insensitive: true,
432 };
433 let e = empty_event();
434 let event = JsonEvent::borrow(&e);
435 assert!(m.matches(&EventValue::Str("España".into()), &event));
436 assert!(m.matches(&EventValue::Str("ESPAÑA".into()), &event));
437 }
438
439 #[test]
440 fn test_startswith_unicode_case_insensitive() {
441 let m = CompiledMatcher::StartsWith {
442 value: "über".to_lowercase(),
443 case_insensitive: true,
444 };
445 let e = empty_event();
446 let event = JsonEvent::borrow(&e);
447 assert!(m.matches(&EventValue::Str("Übersicht".into()), &event));
448 assert!(m.matches(&EventValue::Str("ÜBERSICHT".into()), &event));
449 assert!(!m.matches(&EventValue::Str("not-uber".into()), &event));
450 }
451
452 #[test]
453 fn test_endswith_unicode_case_insensitive() {
454 let m = CompiledMatcher::EndsWith {
455 value: "ção".to_lowercase(),
456 case_insensitive: true,
457 };
458 let e = empty_event();
459 let event = JsonEvent::borrow(&e);
460 assert!(m.matches(&EventValue::Str("Aplicação".into()), &event));
461 assert!(m.matches(&EventValue::Str("APLICAÇÃO".into()), &event));
462 assert!(!m.matches(&EventValue::Str("Aplicacao".into()), &event));
463 }
464
465 #[test]
466 fn test_greek_case_insensitive() {
467 let m = CompiledMatcher::Exact {
468 value: "σίγμα".to_lowercase(),
469 case_insensitive: true,
470 };
471 let e = empty_event();
472 let event = JsonEvent::borrow(&e);
473 assert!(m.matches(&EventValue::Str("ΣΊΓΜΑ".into()), &event));
474 assert!(m.matches(&EventValue::Str("σίγμα".into()), &event));
475 }
476
477 #[test]
482 fn test_parse_expand_template() {
483 let parts = parse_expand_template("C:\\Users\\%user%\\AppData");
484 assert_eq!(parts.len(), 3);
485 assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "C:\\Users\\"));
486 assert!(matches!(&parts[1], ExpandPart::Placeholder(s) if s == "user"));
487 assert!(matches!(&parts[2], ExpandPart::Literal(s) if s == "\\AppData"));
488 }
489
490 #[test]
491 fn test_parse_expand_template_no_placeholders() {
492 let parts = parse_expand_template("just a literal");
493 assert_eq!(parts.len(), 1);
494 assert!(matches!(&parts[0], ExpandPart::Literal(s) if s == "just a literal"));
495 }
496
497 #[test]
498 fn test_parse_expand_template_multiple_placeholders() {
499 let parts = parse_expand_template("%a%:%b%");
500 assert_eq!(parts.len(), 3);
501 assert!(matches!(&parts[0], ExpandPart::Placeholder(s) if s == "a"));
502 assert!(matches!(&parts[1], ExpandPart::Literal(s) if s == ":"));
503 assert!(matches!(&parts[2], ExpandPart::Placeholder(s) if s == "b"));
504 }
505
506 #[test]
507 fn test_expand_matcher() {
508 let template = parse_expand_template("C:\\Users\\%user%\\Downloads");
509 let m = CompiledMatcher::Expand {
510 template,
511 case_insensitive: true,
512 };
513 let e = json!({"user": "admin", "path": "C:\\Users\\admin\\Downloads"});
514 let event = JsonEvent::borrow(&e);
515 assert!(m.matches(
516 &EventValue::Str("C:\\Users\\admin\\Downloads".into()),
517 &event
518 ));
519 assert!(!m.matches(
520 &EventValue::Str("C:\\Users\\other\\Downloads".into()),
521 &event
522 ));
523 }
524
525 #[test]
526 fn test_expand_matcher_missing_field() {
527 let template = parse_expand_template("%user%@%domain%");
528 let m = CompiledMatcher::Expand {
529 template,
530 case_insensitive: false,
531 };
532 let e = json!({"user": "admin"});
533 let event = JsonEvent::borrow(&e);
534 assert!(m.matches(&EventValue::Str("admin@".into()), &event));
535 }
536
537 #[test]
542 fn test_timestamp_part_hour() {
543 let m = CompiledMatcher::TimestampPart {
544 part: TimePart::Hour,
545 inner: Box::new(CompiledMatcher::NumericEq(12.0)),
546 };
547 let e = json!({});
548 let event = JsonEvent::borrow(&e);
549 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
550 assert!(!m.matches(&EventValue::Str("2024-07-10T15:30:00Z".into()), &event));
551 }
552
553 #[test]
554 fn test_timestamp_part_month() {
555 let m = CompiledMatcher::TimestampPart {
556 part: TimePart::Month,
557 inner: Box::new(CompiledMatcher::NumericEq(7.0)),
558 };
559 let e = json!({});
560 let event = JsonEvent::borrow(&e);
561 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
562 assert!(!m.matches(&EventValue::Str("2024-08-10T12:30:00Z".into()), &event));
563 }
564
565 #[test]
566 fn test_timestamp_part_day() {
567 let m = CompiledMatcher::TimestampPart {
568 part: TimePart::Day,
569 inner: Box::new(CompiledMatcher::NumericEq(10.0)),
570 };
571 let e = json!({});
572 let event = JsonEvent::borrow(&e);
573 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
574 assert!(!m.matches(&EventValue::Str("2024-07-15T12:30:00Z".into()), &event));
575 }
576
577 #[test]
578 fn test_timestamp_part_year() {
579 let m = CompiledMatcher::TimestampPart {
580 part: TimePart::Year,
581 inner: Box::new(CompiledMatcher::NumericEq(2024.0)),
582 };
583 let e = json!({});
584 let event = JsonEvent::borrow(&e);
585 assert!(m.matches(&EventValue::Str("2024-07-10T12:30:00Z".into()), &event));
586 assert!(!m.matches(&EventValue::Str("2023-07-10T12:30:00Z".into()), &event));
587 }
588
589 #[test]
590 fn test_timestamp_part_from_epoch() {
591 let m = CompiledMatcher::TimestampPart {
592 part: TimePart::Hour,
593 inner: Box::new(CompiledMatcher::NumericEq(12.0)),
594 };
595 let e = json!({});
596 let event = JsonEvent::borrow(&e);
597 assert!(m.matches(&EventValue::Int(1720614600), &event));
599 }
600}