1mod helpers;
9mod matching;
10
11pub use helpers::{ascii_lowercase_cow, parse_expand_template, sigma_string_to_regex};
12
13use aho_corasick::AhoCorasick;
14use regex::{Regex, RegexSet};
15
16use crate::event::Event;
17use crate::result::MatcherKind;
18use ipnet::IpNet;
19
20const MAX_PATTERN_LEN: usize = 256;
24
25#[derive(Debug, Clone)]
32pub struct MatchDescriptor {
33 pub kind: MatcherKind,
35 pub pattern: Option<String>,
37 pub case_sensitive: Option<bool>,
39 pub negated: bool,
41}
42
43fn truncate_pattern(s: String) -> String {
44 if s.len() <= MAX_PATTERN_LEN {
45 return s;
46 }
47 let mut end = MAX_PATTERN_LEN.saturating_sub(3);
48 while end > 0 && !s.is_char_boundary(end) {
49 end -= 1;
50 }
51 let mut out = s[..end].to_string();
52 out.push_str("...");
53 out
54}
55
56fn numeric_descriptor(op: &str, n: f64) -> MatchDescriptor {
57 MatchDescriptor {
58 kind: MatcherKind::Numeric,
59 pattern: Some(format!("{op} {n}")),
60 case_sensitive: None,
61 negated: false,
62 }
63}
64
65fn join_child_patterns(children: &[CompiledMatcher]) -> String {
66 children
67 .iter()
68 .filter_map(|c| c.describe().pattern)
69 .collect::<Vec<_>>()
70 .join(", ")
71}
72
73fn expand_template_to_string(parts: &[ExpandPart]) -> String {
74 let mut s = String::new();
75 for part in parts {
76 match part {
77 ExpandPart::Literal(t) => s.push_str(t),
78 ExpandPart::Placeholder(name) => {
79 s.push('%');
80 s.push_str(name);
81 s.push('%');
82 }
83 }
84 }
85 s
86}
87
88#[derive(Debug, Clone)]
94pub enum CompiledMatcher {
95 Exact {
98 value: String,
99 case_insensitive: bool,
100 },
101 Contains {
103 value: String,
104 case_insensitive: bool,
105 },
106 StartsWith {
108 value: String,
109 case_insensitive: bool,
110 },
111 EndsWith {
113 value: String,
114 case_insensitive: bool,
115 },
116 Regex(Regex),
118
119 AhoCorasickSet {
135 automaton: AhoCorasick,
136 case_insensitive: bool,
137 needles: Vec<String>,
142 },
143
144 RegexSetMatch { set: RegexSet, mode: GroupMode },
158
159 Cidr(IpNet),
162
163 NumericEq(f64),
166 NumericGt(f64),
168 NumericGte(f64),
170 NumericLt(f64),
172 NumericLte(f64),
174
175 Exists(bool),
178 FieldRef {
180 field: String,
181 case_insensitive: bool,
182 },
183 Null,
185 BoolEq(bool),
187
188 Expand {
191 template: Vec<ExpandPart>,
192 case_insensitive: bool,
193 },
194
195 TimestampPart {
198 part: TimePart,
199 inner: Box<CompiledMatcher>,
200 },
201
202 Not(Box<CompiledMatcher>),
205
206 AnyOf(Vec<CompiledMatcher>),
209 AllOf(Vec<CompiledMatcher>),
211
212 CaseInsensitiveGroup {
230 children: Vec<CompiledMatcher>,
231 mode: GroupMode,
232 },
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum GroupMode {
242 Any,
244 All,
246}
247
248#[derive(Debug, Clone)]
250pub enum ExpandPart {
251 Literal(String),
253 Placeholder(String),
255}
256
257#[derive(Debug, Clone, Copy)]
259pub enum TimePart {
260 Minute,
261 Hour,
262 Day,
263 Week,
264 Month,
265 Year,
266}
267
268impl CompiledMatcher {
269 #[inline]
275 pub fn matches_keyword(&self, event: &impl Event) -> bool {
276 event.any_string_value(&|s| self.matches_str(s))
277 }
278
279 pub fn describe(&self) -> MatchDescriptor {
285 match self {
286 CompiledMatcher::Exact {
287 value,
288 case_insensitive,
289 } => MatchDescriptor {
290 kind: MatcherKind::Exact,
291 pattern: Some(truncate_pattern(value.clone())),
292 case_sensitive: Some(!case_insensitive),
293 negated: false,
294 },
295 CompiledMatcher::Contains {
296 value,
297 case_insensitive,
298 } => MatchDescriptor {
299 kind: MatcherKind::Contains,
300 pattern: Some(truncate_pattern(value.clone())),
301 case_sensitive: Some(!case_insensitive),
302 negated: false,
303 },
304 CompiledMatcher::StartsWith {
305 value,
306 case_insensitive,
307 } => MatchDescriptor {
308 kind: MatcherKind::StartsWith,
309 pattern: Some(truncate_pattern(value.clone())),
310 case_sensitive: Some(!case_insensitive),
311 negated: false,
312 },
313 CompiledMatcher::EndsWith {
314 value,
315 case_insensitive,
316 } => MatchDescriptor {
317 kind: MatcherKind::EndsWith,
318 pattern: Some(truncate_pattern(value.clone())),
319 case_sensitive: Some(!case_insensitive),
320 negated: false,
321 },
322 CompiledMatcher::Regex(re) => MatchDescriptor {
323 kind: MatcherKind::Regex,
324 pattern: Some(truncate_pattern(re.as_str().to_string())),
325 case_sensitive: None,
326 negated: false,
327 },
328 CompiledMatcher::AhoCorasickSet {
329 needles,
330 case_insensitive,
331 ..
332 } => MatchDescriptor {
333 kind: MatcherKind::OneOf,
334 pattern: Some(truncate_pattern(needles.join(", "))),
335 case_sensitive: Some(!case_insensitive),
336 negated: false,
337 },
338 CompiledMatcher::RegexSetMatch { set, .. } => MatchDescriptor {
339 kind: MatcherKind::OneOf,
340 pattern: Some(truncate_pattern(set.patterns().join(", "))),
341 case_sensitive: None,
342 negated: false,
343 },
344 CompiledMatcher::Cidr(net) => MatchDescriptor {
345 kind: MatcherKind::Cidr,
346 pattern: Some(net.to_string()),
347 case_sensitive: None,
348 negated: false,
349 },
350 CompiledMatcher::NumericEq(n) => numeric_descriptor("=", *n),
351 CompiledMatcher::NumericGt(n) => numeric_descriptor(">", *n),
352 CompiledMatcher::NumericGte(n) => numeric_descriptor(">=", *n),
353 CompiledMatcher::NumericLt(n) => numeric_descriptor("<", *n),
354 CompiledMatcher::NumericLte(n) => numeric_descriptor("<=", *n),
355 CompiledMatcher::Exists(expect) => MatchDescriptor {
356 kind: MatcherKind::Exists,
357 pattern: Some(expect.to_string()),
358 case_sensitive: None,
359 negated: false,
360 },
361 CompiledMatcher::FieldRef {
362 field,
363 case_insensitive,
364 } => MatchDescriptor {
365 kind: MatcherKind::FieldRef,
366 pattern: Some(field.clone()),
367 case_sensitive: Some(!case_insensitive),
368 negated: false,
369 },
370 CompiledMatcher::Null => MatchDescriptor {
371 kind: MatcherKind::Null,
372 pattern: None,
373 case_sensitive: None,
374 negated: false,
375 },
376 CompiledMatcher::BoolEq(b) => MatchDescriptor {
377 kind: MatcherKind::Bool,
378 pattern: Some(b.to_string()),
379 case_sensitive: None,
380 negated: false,
381 },
382 CompiledMatcher::Expand {
383 template,
384 case_insensitive,
385 } => MatchDescriptor {
386 kind: MatcherKind::Expand,
387 pattern: Some(truncate_pattern(expand_template_to_string(template))),
388 case_sensitive: Some(!case_insensitive),
389 negated: false,
390 },
391 CompiledMatcher::TimestampPart { inner, .. } => {
392 let inner_d = inner.describe();
393 MatchDescriptor {
394 kind: MatcherKind::Timestamp,
395 pattern: inner_d.pattern,
396 case_sensitive: inner_d.case_sensitive,
397 negated: inner_d.negated,
398 }
399 }
400 CompiledMatcher::Not(inner) => {
401 let mut d = inner.describe();
402 d.negated = !d.negated;
403 d
404 }
405 CompiledMatcher::AnyOf(ms) | CompiledMatcher::AllOf(ms) => MatchDescriptor {
406 kind: MatcherKind::OneOf,
407 pattern: Some(truncate_pattern(join_child_patterns(ms))),
408 case_sensitive: None,
409 negated: false,
410 },
411 CompiledMatcher::CaseInsensitiveGroup { children, .. } => MatchDescriptor {
412 kind: MatcherKind::OneOf,
413 pattern: Some(truncate_pattern(join_child_patterns(children))),
414 case_sensitive: Some(false),
415 negated: false,
416 },
417 }
418 }
419}
420
421#[cfg(test)]
422mod describe_tests {
423 use super::*;
424
425 #[test]
426 fn string_matchers_report_kind_pattern_and_case() {
427 let d = CompiledMatcher::Contains {
428 value: "abc".to_string(),
429 case_insensitive: true,
430 }
431 .describe();
432 assert_eq!(d.kind, MatcherKind::Contains);
433 assert_eq!(d.pattern.as_deref(), Some("abc"));
434 assert_eq!(d.case_sensitive, Some(false));
435 assert!(!d.negated);
436
437 let cased = CompiledMatcher::EndsWith {
438 value: "\\powershell.exe".to_string(),
439 case_insensitive: false,
440 }
441 .describe();
442 assert_eq!(cased.kind, MatcherKind::EndsWith);
443 assert_eq!(cased.case_sensitive, Some(true));
444 }
445
446 #[test]
447 fn numeric_exists_and_null_descriptors() {
448 let gt = CompiledMatcher::NumericGt(5.0).describe();
449 assert_eq!(gt.kind, MatcherKind::Numeric);
450 assert_eq!(gt.pattern.as_deref(), Some("> 5"));
451
452 let exists = CompiledMatcher::Exists(false).describe();
453 assert_eq!(exists.kind, MatcherKind::Exists);
454 assert_eq!(exists.pattern.as_deref(), Some("false"));
455
456 let null = CompiledMatcher::Null.describe();
457 assert_eq!(null.kind, MatcherKind::Null);
458 assert!(null.pattern.is_none());
459 }
460
461 #[test]
462 fn not_inverts_negated_flag_and_keeps_inner_kind() {
463 let inner = CompiledMatcher::Contains {
464 value: "evil".to_string(),
465 case_insensitive: true,
466 };
467 let d = CompiledMatcher::Not(Box::new(inner)).describe();
468 assert_eq!(d.kind, MatcherKind::Contains);
469 assert!(d.negated);
470 assert_eq!(d.pattern.as_deref(), Some("evil"));
471 }
472
473 #[test]
474 fn composite_collapses_to_one_of_with_joined_patterns() {
475 let d = CompiledMatcher::AnyOf(vec![
476 CompiledMatcher::Contains {
477 value: "foo".to_string(),
478 case_insensitive: true,
479 },
480 CompiledMatcher::Contains {
481 value: "bar".to_string(),
482 case_insensitive: true,
483 },
484 ])
485 .describe();
486 assert_eq!(d.kind, MatcherKind::OneOf);
487 assert_eq!(d.pattern.as_deref(), Some("foo, bar"));
488 }
489
490 #[test]
491 fn long_patterns_are_truncated() {
492 let long = "x".repeat(MAX_PATTERN_LEN * 2);
493 let d = CompiledMatcher::Contains {
494 value: long,
495 case_insensitive: true,
496 }
497 .describe();
498 let pattern = d.pattern.unwrap();
499 assert!(pattern.len() <= MAX_PATTERN_LEN);
500 assert!(pattern.ends_with("..."));
501 }
502}