Skip to content

Commit 0480f7f

Browse files
authored
Merge pull request #10 from kryesh/event_trait_nofmt
Add event trait and remove wasted allocations when evaluating item matches
2 parents c078473 + a343400 commit 0480f7f

1 file changed

Lines changed: 111 additions & 35 deletions

File tree

src/matcher.rs

Lines changed: 111 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
//! `SigmaRuleMatcher` is thread-safe and uses `Arc` internally for efficient sharing
5050
//! across threads.
5151
52-
use std::collections::HashMap;
52+
use std::borrow::Borrow;
53+
use std::collections::{BTreeMap, HashMap};
54+
use std::hash::Hash;
5355
use std::sync::{Arc, Mutex};
5456

5557
use once_cell::sync::Lazy;
@@ -62,18 +64,6 @@ use crate::types::*;
6264
static REGEX_CACHE: Lazy<Mutex<HashMap<String, std::result::Result<Regex, String>>>> =
6365
Lazy::new(|| Mutex::new(HashMap::new()));
6466

65-
/// A compiled matcher for a Sigma detection rule.
66-
///
67-
/// This struct represents a compiled version of a SigmaRule that can efficiently
68-
/// match against events. It is thread-safe and can be used concurrently.
69-
#[derive(Debug, Clone)]
70-
pub struct SigmaRuleMatcher {
71-
/// The original rule this matcher was compiled from
72-
pub rule: Arc<SigmaRule>,
73-
/// Compiled search identifiers for efficient matching
74-
compiled_searches: HashMap<String, CompiledSearch>,
75-
}
76-
7767
/// A compiled search identifier that can be evaluated against an event.
7868
#[derive(Debug, Clone)]
7969
enum CompiledSearch {
@@ -110,6 +100,85 @@ enum CompiledPattern {
110100
Null,
111101
}
112102

103+
/// A generic event that allows matching on references
104+
pub trait Event {
105+
/// Check if the event contains a given field
106+
fn contains_key(&self, key: &str) -> bool;
107+
108+
/// Retrieve the value in a given field
109+
fn get(&self, key: &str) -> Option<&str>;
110+
111+
/// Iterate over all values in the event
112+
fn values(&self) -> impl Iterator<Item = &str>;
113+
114+
/// Get the raw value of an event
115+
///
116+
/// Default implementation joins all field values
117+
fn raw<'a>(&'a self) -> Option<impl AsRef<str> + 'a> {
118+
let mut buf = String::new();
119+
let mut iter = self.values().peekable();
120+
121+
while let Some(val) = iter.next() {
122+
buf.push_str(val.as_ref());
123+
if iter.peek().is_some() {
124+
buf.push(' ');
125+
}
126+
}
127+
128+
Some(buf)
129+
}
130+
}
131+
132+
// Implement the event trait for hashmaps of string-like keys/values
133+
impl<K, V> Event for HashMap<K, V>
134+
where
135+
K: Eq + Hash + Borrow<str>,
136+
V: AsRef<str>,
137+
{
138+
fn contains_key(&self, key: &str) -> bool {
139+
HashMap::contains_key(self, key)
140+
}
141+
142+
fn get(&self, key: &str) -> Option<&str> {
143+
HashMap::get(self, key).map(|v| v.as_ref())
144+
}
145+
146+
fn values(&self) -> impl Iterator<Item = &str> {
147+
HashMap::values(self).map(|v| v.as_ref())
148+
}
149+
}
150+
151+
// Implement the event trait for btree maps of string-like keys/values
152+
impl<K, V> Event for BTreeMap<K, V>
153+
where
154+
K: Eq + Ord + Borrow<str>,
155+
V: AsRef<str>,
156+
{
157+
fn contains_key(&self, key: &str) -> bool {
158+
BTreeMap::contains_key(self, key)
159+
}
160+
161+
fn get(&self, key: &str) -> Option<&str> {
162+
BTreeMap::get(self, key).map(|v| v.as_ref())
163+
}
164+
165+
fn values(&self) -> impl Iterator<Item = &str> {
166+
BTreeMap::values(self).map(|v| v.as_ref())
167+
}
168+
}
169+
170+
/// A compiled matcher for a Sigma detection rule.
171+
///
172+
/// This struct represents a compiled version of a SigmaRule that can efficiently
173+
/// match against events. It is thread-safe and can be used concurrently.
174+
#[derive(Debug, Clone)]
175+
pub struct SigmaRuleMatcher {
176+
/// The original rule this matcher was compiled from
177+
pub rule: Arc<SigmaRule>,
178+
/// Compiled search identifiers for efficient matching
179+
compiled_searches: HashMap<String, CompiledSearch>,
180+
}
181+
113182
impl SigmaRuleMatcher {
114183
/// Compile a Sigma rule into a matcher.
115184
///
@@ -426,7 +495,7 @@ impl SigmaRuleMatcher {
426495
///
427496
/// # Returns
428497
/// `true` if the event matches any of the rule's conditions, `false` otherwise
429-
pub fn matches(&self, event: &HashMap<String, String>) -> bool {
498+
pub fn matches<E: Event>(&self, event: &E) -> bool {
430499
// Evaluate all conditions (they are implicitly OR-connected)
431500
for condition in &self.rule.detection.conditions {
432501
if self.eval_condition(condition, event) {
@@ -437,7 +506,7 @@ impl SigmaRuleMatcher {
437506
}
438507

439508
/// Evaluate a condition expression against an event.
440-
fn eval_condition(&self, expr: &ConditionExpression, event: &HashMap<String, String>) -> bool {
509+
fn eval_condition<E: Event>(&self, expr: &ConditionExpression, event: &E) -> bool {
441510
match expr {
442511
ConditionExpression::Identifier(name) => {
443512
self.eval_search_identifier(name, event)
@@ -467,7 +536,7 @@ impl SigmaRuleMatcher {
467536
}
468537

469538
/// Evaluate a search identifier against an event.
470-
fn eval_search_identifier(&self, name: &str, event: &HashMap<String, String>) -> bool {
539+
fn eval_search_identifier<E: Event>(&self, name: &str, event: &E) -> bool {
471540
if let Some(search) = self.compiled_searches.get(name) {
472541
match search {
473542
CompiledSearch::Map(items) => {
@@ -489,7 +558,7 @@ impl SigmaRuleMatcher {
489558
}
490559

491560
/// Evaluate a list of detection items with AND logic.
492-
fn eval_detection_items_and(&self, items: &[CompiledDetectionItem], event: &HashMap<String, String>) -> bool {
561+
fn eval_detection_items_and<E: Event>(&self, items: &[CompiledDetectionItem], event: &E) -> bool {
493562
for item in items {
494563
if !self.eval_detection_item(item, event) {
495564
return false;
@@ -499,7 +568,7 @@ impl SigmaRuleMatcher {
499568
}
500569

501570
/// Evaluate a single detection item against an event.
502-
fn eval_detection_item(&self, item: &CompiledDetectionItem, event: &HashMap<String, String>) -> bool {
571+
fn eval_detection_item<E: Event>(&self, item: &CompiledDetectionItem, event: &E) -> bool {
503572
// Handle exists modifier
504573
if item.modifiers.contains(&Modifier::Exists) {
505574
if let Some(field) = &item.field {
@@ -512,35 +581,42 @@ impl SigmaRuleMatcher {
512581
return false;
513582
}
514583

515-
// Get the value to match against
516-
let value_to_match = if let Some(field) = &item.field {
584+
if let Some(field) = &item.field {
517585
// Field matching
518586
if let Some(val) = event.get(field) {
519-
val.clone()
587+
self.match_patterns(&item.patterns, &item.modifiers, val)
520588
} else {
521589
// Field doesn't exist in event
522-
return false;
590+
false
523591
}
524592
} else {
525593
// Keyword search - match against all field values or entire event
526-
event.values().cloned().collect::<Vec<_>>().join(" ")
527-
};
594+
let Some(raw) = event.raw() else {
595+
return false;
596+
};
528597

529-
// Check if ALL modifier is present
530-
let use_all_logic = item.modifiers.contains(&Modifier::All);
598+
self.match_patterns(&item.patterns, &item.modifiers, raw.as_ref())
599+
}
600+
}
531601

532-
if use_all_logic {
602+
fn match_patterns(
603+
&self,
604+
patterns: &[CompiledPattern],
605+
modifiers: &[Modifier],
606+
value: &str,
607+
) -> bool {
608+
if modifiers.contains(&Modifier::All) {
533609
// ALL: all patterns must match
534-
for pattern in &item.patterns {
535-
if !self.match_pattern(pattern, &value_to_match, &item.modifiers) {
610+
for pattern in patterns {
611+
if !self.match_pattern(pattern, value, modifiers) {
536612
return false;
537613
}
538614
}
539615
true
540616
} else {
541617
// Default OR: any pattern can match
542-
for pattern in &item.patterns {
543-
if self.match_pattern(pattern, &value_to_match, &item.modifiers) {
618+
for pattern in patterns {
619+
if self.match_pattern(pattern, value, modifiers) {
544620
return true;
545621
}
546622
}
@@ -738,7 +814,7 @@ impl SigmaRuleMatcher {
738814
}
739815

740816
/// Evaluate "1 of them" - any non-underscore-prefixed search identifier matches.
741-
fn eval_one_of_them(&self, event: &HashMap<String, String>) -> bool {
817+
fn eval_one_of_them<E: Event>(&self, event: &E) -> bool {
742818
for name in self.compiled_searches.keys() {
743819
if !name.starts_with('_') && self.eval_search_identifier(name, event) {
744820
return true;
@@ -748,7 +824,7 @@ impl SigmaRuleMatcher {
748824
}
749825

750826
/// Evaluate "all of them" - all non-underscore-prefixed search identifiers match.
751-
fn eval_all_of_them(&self, event: &HashMap<String, String>) -> bool {
827+
fn eval_all_of_them<E: Event>(&self, event: &E) -> bool {
752828
let mut found_any = false;
753829
for name in self.compiled_searches.keys() {
754830
if !name.starts_with('_') {
@@ -762,7 +838,7 @@ impl SigmaRuleMatcher {
762838
}
763839

764840
/// Evaluate "1 of pattern" - any matching search identifier matches.
765-
fn eval_one_of_pattern(&self, pattern: &str, event: &HashMap<String, String>) -> bool {
841+
fn eval_one_of_pattern<E: Event>(&self, pattern: &str, event: &E) -> bool {
766842
for name in self.compiled_searches.keys() {
767843
if self.match_identifier_pattern(name, pattern) && self.eval_search_identifier(name, event) {
768844
return true;
@@ -772,7 +848,7 @@ impl SigmaRuleMatcher {
772848
}
773849

774850
/// Evaluate "all of pattern" - all matching search identifiers match.
775-
fn eval_all_of_pattern(&self, pattern: &str, event: &HashMap<String, String>) -> bool {
851+
fn eval_all_of_pattern<E: Event>(&self, pattern: &str, event: &E) -> bool {
776852
let mut found_any = false;
777853
for name in self.compiled_searches.keys() {
778854
if self.match_identifier_pattern(name, pattern) {

0 commit comments

Comments
 (0)