Skip to main content

rsigma_eval/compiler/
mod.rs

1//! Compile parsed Sigma rules into optimized in-memory representations.
2//!
3//! The compiler transforms the parser AST (`SigmaRule`, `Detection`,
4//! `DetectionItem`) into compiled forms (`CompiledRule`, `CompiledDetection`,
5//! `CompiledDetectionItem`) that can be evaluated efficiently against events.
6//!
7//! Modifier interpretation happens here: the compiler reads the `Vec<Modifier>`
8//! from each `FieldSpec` and produces the appropriate `CompiledMatcher` variant.
9
10mod helpers;
11#[cfg(test)]
12mod tests;
13
14use std::collections::HashMap;
15use std::sync::Arc;
16
17use base64::Engine as Base64Engine;
18use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
19use regex::Regex;
20
21use rsigma_parser::value::{SpecialChar, StringPart};
22use rsigma_parser::{
23    ConditionExpr, Detection, DetectionItem, Level, LogSource, Modifier, Quantifier,
24    SelectorPattern, SigmaRule, SigmaString, SigmaValue,
25};
26
27use crate::error::{EvalError, Result};
28use crate::event::Event;
29use crate::matcher::{CompiledMatcher, sigma_string_to_regex};
30use crate::result::{FieldMatch, MatchResult};
31
32pub(crate) use helpers::yaml_to_json_map;
33use helpers::{
34    base64_offset_patterns, build_regex, expand_windash, pattern_matches, sigma_string_to_bytes,
35    to_utf16_bom_bytes, to_utf16be_bytes, to_utf16le_bytes, value_to_f64, value_to_plain_string,
36};
37
38// =============================================================================
39// Compiled types
40// =============================================================================
41
42/// A compiled Sigma rule, ready for evaluation.
43#[derive(Debug, Clone)]
44pub struct CompiledRule {
45    pub title: String,
46    pub id: Option<String>,
47    pub level: Option<Level>,
48    pub tags: Vec<String>,
49    pub logsource: LogSource,
50    /// Compiled named detections, keyed by detection name.
51    pub detections: HashMap<String, CompiledDetection>,
52    /// Condition expression trees (usually one, but can be multiple).
53    pub conditions: Vec<ConditionExpr>,
54    /// Whether to include the full event JSON in the match result.
55    /// Controlled by the `rsigma.include_event` custom attribute.
56    pub include_event: bool,
57    /// Custom attributes from the original Sigma rule (merged view of
58    /// arbitrary top-level keys, the explicit `custom_attributes:` block,
59    /// and pipeline `SetCustomAttribute` additions). Propagated to match
60    /// results. Wrapped in `Arc` so per-match cloning is a pointer bump.
61    pub custom_attributes: Arc<HashMap<String, serde_json::Value>>,
62}
63
64/// A compiled detection definition.
65#[derive(Debug, Clone)]
66pub enum CompiledDetection {
67    /// AND-linked detection items (from a YAML mapping).
68    AllOf(Vec<CompiledDetectionItem>),
69    /// OR-linked sub-detections (from a YAML list of mappings).
70    AnyOf(Vec<CompiledDetection>),
71    /// Keyword detection: match values across all event fields.
72    Keywords(CompiledMatcher),
73}
74
75/// A compiled detection item: a field + matcher.
76#[derive(Debug, Clone)]
77pub struct CompiledDetectionItem {
78    /// The field name to check (`None` for keyword items).
79    pub field: Option<String>,
80    /// The compiled matcher combining all values with appropriate logic.
81    pub matcher: CompiledMatcher,
82    /// If `Some(true)`, field must exist; `Some(false)`, must not exist.
83    pub exists: Option<bool>,
84}
85
86// =============================================================================
87// Modifier context
88// =============================================================================
89
90/// Parsed modifier flags for a single field specification.
91#[derive(Clone, Copy)]
92struct ModCtx {
93    contains: bool,
94    startswith: bool,
95    endswith: bool,
96    all: bool,
97    base64: bool,
98    base64offset: bool,
99    wide: bool,
100    utf16be: bool,
101    utf16: bool,
102    windash: bool,
103    re: bool,
104    cidr: bool,
105    cased: bool,
106    exists: bool,
107    fieldref: bool,
108    gt: bool,
109    gte: bool,
110    lt: bool,
111    lte: bool,
112    neq: bool,
113    ignore_case: bool,
114    multiline: bool,
115    dotall: bool,
116    expand: bool,
117    timestamp_part: Option<crate::matcher::TimePart>,
118}
119
120impl ModCtx {
121    fn from_modifiers(modifiers: &[Modifier]) -> Self {
122        let mut ctx = ModCtx {
123            contains: false,
124            startswith: false,
125            endswith: false,
126            all: false,
127            base64: false,
128            base64offset: false,
129            wide: false,
130            utf16be: false,
131            utf16: false,
132            windash: false,
133            re: false,
134            cidr: false,
135            cased: false,
136            exists: false,
137            fieldref: false,
138            gt: false,
139            gte: false,
140            lt: false,
141            lte: false,
142            neq: false,
143            ignore_case: false,
144            multiline: false,
145            dotall: false,
146            expand: false,
147            timestamp_part: None,
148        };
149        for m in modifiers {
150            match m {
151                Modifier::Contains => ctx.contains = true,
152                Modifier::StartsWith => ctx.startswith = true,
153                Modifier::EndsWith => ctx.endswith = true,
154                Modifier::All => ctx.all = true,
155                Modifier::Base64 => ctx.base64 = true,
156                Modifier::Base64Offset => ctx.base64offset = true,
157                Modifier::Wide => ctx.wide = true,
158                Modifier::Utf16be => ctx.utf16be = true,
159                Modifier::Utf16 => ctx.utf16 = true,
160                Modifier::WindAsh => ctx.windash = true,
161                Modifier::Re => ctx.re = true,
162                Modifier::Cidr => ctx.cidr = true,
163                Modifier::Cased => ctx.cased = true,
164                Modifier::Exists => ctx.exists = true,
165                Modifier::FieldRef => ctx.fieldref = true,
166                Modifier::Gt => ctx.gt = true,
167                Modifier::Gte => ctx.gte = true,
168                Modifier::Lt => ctx.lt = true,
169                Modifier::Lte => ctx.lte = true,
170                Modifier::Neq => ctx.neq = true,
171                Modifier::IgnoreCase => ctx.ignore_case = true,
172                Modifier::Multiline => ctx.multiline = true,
173                Modifier::DotAll => ctx.dotall = true,
174                Modifier::Expand => ctx.expand = true,
175                Modifier::Hour => ctx.timestamp_part = Some(crate::matcher::TimePart::Hour),
176                Modifier::Day => ctx.timestamp_part = Some(crate::matcher::TimePart::Day),
177                Modifier::Week => ctx.timestamp_part = Some(crate::matcher::TimePart::Week),
178                Modifier::Month => ctx.timestamp_part = Some(crate::matcher::TimePart::Month),
179                Modifier::Year => ctx.timestamp_part = Some(crate::matcher::TimePart::Year),
180                Modifier::Minute => ctx.timestamp_part = Some(crate::matcher::TimePart::Minute),
181            }
182        }
183        ctx
184    }
185
186    /// Whether matching should be case-insensitive.
187    /// Default is case-insensitive; `|cased` makes it case-sensitive.
188    fn is_case_insensitive(&self) -> bool {
189        !self.cased
190    }
191
192    /// Whether any numeric comparison modifier is present.
193    fn has_numeric_comparison(&self) -> bool {
194        self.gt || self.gte || self.lt || self.lte
195    }
196
197    /// Whether the neq modifier is present.
198    fn has_neq(&self) -> bool {
199        self.neq
200    }
201}
202
203// =============================================================================
204// Public API
205// =============================================================================
206
207/// Compile a parsed `SigmaRule` into a `CompiledRule`.
208pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
209    let mut detections = HashMap::new();
210    for (name, detection) in &rule.detection.named {
211        detections.insert(name.clone(), compile_detection(detection)?);
212    }
213
214    for condition in &rule.detection.conditions {
215        validate_condition_refs(condition, &detections)?;
216    }
217
218    let include_event = rule
219        .custom_attributes
220        .get("rsigma.include_event")
221        .and_then(|v| v.as_str())
222        == Some("true");
223
224    let custom_attributes = Arc::new(yaml_to_json_map(&rule.custom_attributes));
225
226    Ok(CompiledRule {
227        title: rule.title.clone(),
228        id: rule.id.clone(),
229        level: rule.level,
230        tags: rule.tags.clone(),
231        logsource: rule.logsource.clone(),
232        detections,
233        conditions: rule.detection.conditions.clone(),
234        include_event,
235        custom_attributes,
236    })
237}
238
239/// Validate that all `Identifier` references in a condition expression resolve
240/// to an existing detection name. `Selector` patterns are exempt because they
241/// match by glob/wildcard and zero matches is semantically valid.
242fn validate_condition_refs(
243    expr: &ConditionExpr,
244    detections: &HashMap<String, CompiledDetection>,
245) -> Result<()> {
246    match expr {
247        ConditionExpr::Identifier(name) => {
248            if !detections.contains_key(name) {
249                return Err(EvalError::UnknownDetection(name.clone()));
250            }
251            Ok(())
252        }
253        ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
254            for e in exprs {
255                validate_condition_refs(e, detections)?;
256            }
257            Ok(())
258        }
259        ConditionExpr::Not(inner) => validate_condition_refs(inner, detections),
260        ConditionExpr::Selector { .. } => Ok(()),
261    }
262}
263
264/// Evaluate a compiled rule against an event, returning a `MatchResult` if it matches.
265pub fn evaluate_rule(rule: &CompiledRule, event: &impl Event) -> Option<MatchResult> {
266    for condition in &rule.conditions {
267        let mut matched_selections = Vec::new();
268        if eval_condition(condition, &rule.detections, event, &mut matched_selections) {
269            let matched_fields =
270                collect_field_matches(&matched_selections, &rule.detections, event);
271
272            let event_data = if rule.include_event {
273                Some(event.to_json())
274            } else {
275                None
276            };
277
278            return Some(MatchResult {
279                rule_title: rule.title.clone(),
280                rule_id: rule.id.clone(),
281                level: rule.level,
282                tags: rule.tags.clone(),
283                matched_selections,
284                matched_fields,
285                event: event_data,
286                custom_attributes: rule.custom_attributes.clone(),
287            });
288        }
289    }
290    None
291}
292
293// =============================================================================
294// Detection compilation
295// =============================================================================
296
297/// Compile a parsed detection tree into a [`CompiledDetection`].
298///
299/// Recursively compiles `AllOf`, `AnyOf`, and `Keywords` variants.
300/// Returns an error if the detection tree is empty or contains invalid items.
301pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
302    match detection {
303        Detection::AllOf(items) => {
304            if items.is_empty() {
305                return Err(EvalError::InvalidModifiers(
306                    "AllOf detection must not be empty (vacuous truth)".into(),
307                ));
308            }
309            let compiled: Result<Vec<_>> = items.iter().map(compile_detection_item).collect();
310            Ok(CompiledDetection::AllOf(compiled?))
311        }
312        Detection::AnyOf(dets) => {
313            if dets.is_empty() {
314                return Err(EvalError::InvalidModifiers(
315                    "AnyOf detection must not be empty (would never match)".into(),
316                ));
317            }
318            let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
319            Ok(CompiledDetection::AnyOf(compiled?))
320        }
321        Detection::Keywords(values) => {
322            let ci = true; // keywords are case-insensitive by default
323            let matchers: Vec<CompiledMatcher> = values
324                .iter()
325                .map(|v| compile_value_default(v, ci))
326                .collect::<Result<Vec<_>>>()?;
327            let matcher = if matchers.len() == 1 {
328                // SAFETY: length checked above
329                matchers
330                    .into_iter()
331                    .next()
332                    .unwrap_or(CompiledMatcher::AnyOf(vec![]))
333            } else {
334                CompiledMatcher::AnyOf(matchers)
335            };
336            Ok(CompiledDetection::Keywords(matcher))
337        }
338    }
339}
340
341fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
342    let ctx = ModCtx::from_modifiers(&item.field.modifiers);
343
344    // Handle |exists modifier
345    if ctx.exists {
346        let expect = match item.values.first() {
347            Some(SigmaValue::Bool(b)) => *b,
348            Some(SigmaValue::String(s)) => match s.as_plain().as_deref() {
349                Some("true") | Some("yes") => true,
350                Some("false") | Some("no") => false,
351                _ => true,
352            },
353            _ => true,
354        };
355        return Ok(CompiledDetectionItem {
356            field: item.field.name.clone(),
357            matcher: CompiledMatcher::Exists(expect),
358            exists: Some(expect),
359        });
360    }
361
362    // Sigma spec: "Single item values are not allowed to have the all modifier."
363    if ctx.all && item.values.len() <= 1 {
364        return Err(EvalError::InvalidModifiers(
365            "|all modifier requires more than one value".to_string(),
366        ));
367    }
368
369    // Compile each value into a matcher
370    let matchers: Result<Vec<CompiledMatcher>> =
371        item.values.iter().map(|v| compile_value(v, &ctx)).collect();
372    let matchers = matchers?;
373
374    // Combine multiple values: |all → AND, default → OR
375    let combined = if matchers.len() == 1 {
376        // SAFETY: length checked above
377        matchers
378            .into_iter()
379            .next()
380            .unwrap_or(CompiledMatcher::AnyOf(vec![]))
381    } else if ctx.all {
382        CompiledMatcher::AllOf(matchers)
383    } else {
384        CompiledMatcher::AnyOf(matchers)
385    };
386
387    Ok(CompiledDetectionItem {
388        field: item.field.name.clone(),
389        matcher: combined,
390        exists: None,
391    })
392}
393
394// =============================================================================
395// Value compilation (modifier interpretation)
396// =============================================================================
397
398/// Compile a single `SigmaValue` using the modifier context.
399fn compile_value(value: &SigmaValue, ctx: &ModCtx) -> Result<CompiledMatcher> {
400    let ci = ctx.is_case_insensitive();
401
402    // Handle special modifiers first
403
404    // |expand — runtime placeholder expansion
405    if ctx.expand {
406        let plain = value_to_plain_string(value)?;
407        let template = crate::matcher::parse_expand_template(&plain);
408        return Ok(CompiledMatcher::Expand {
409            template,
410            case_insensitive: ci,
411        });
412    }
413
414    // Timestamp part modifiers (|hour, |day, |month, etc.)
415    if let Some(part) = ctx.timestamp_part {
416        // The value is compared against the extracted time component.
417        // Compile the value as a numeric matcher, then wrap in TimestampPart.
418        let inner = match value {
419            SigmaValue::Integer(n) => CompiledMatcher::NumericEq(*n as f64),
420            SigmaValue::Float(n) => CompiledMatcher::NumericEq(*n),
421            SigmaValue::String(s) => {
422                let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
423                let n: f64 = plain.parse().map_err(|_| {
424                    EvalError::IncompatibleValue(format!(
425                        "timestamp part modifier requires numeric value, got: {plain}"
426                    ))
427                })?;
428                CompiledMatcher::NumericEq(n)
429            }
430            _ => {
431                return Err(EvalError::IncompatibleValue(
432                    "timestamp part modifier requires numeric value".into(),
433                ));
434            }
435        };
436        return Ok(CompiledMatcher::TimestampPart {
437            part,
438            inner: Box::new(inner),
439        });
440    }
441
442    // |fieldref — value is a field name to compare against
443    if ctx.fieldref {
444        let field_name = value_to_plain_string(value)?;
445        return Ok(CompiledMatcher::FieldRef {
446            field: field_name,
447            case_insensitive: ci,
448        });
449    }
450
451    // |re — value is a regex pattern
452    // Sigma spec: "Regex is matched case-sensitive by default."
453    // Only the explicit |i sub-modifier enables case-insensitive matching.
454    if ctx.re {
455        let pattern = value_to_plain_string(value)?;
456        let regex = build_regex(&pattern, ctx.ignore_case, ctx.multiline, ctx.dotall)?;
457        return Ok(CompiledMatcher::Regex(regex));
458    }
459
460    // |cidr — value is a CIDR notation
461    if ctx.cidr {
462        let cidr_str = value_to_plain_string(value)?;
463        let net: ipnet::IpNet = cidr_str
464            .parse()
465            .map_err(|e: ipnet::AddrParseError| EvalError::InvalidCidr(e))?;
466        return Ok(CompiledMatcher::Cidr(net));
467    }
468
469    // |gt, |gte, |lt, |lte — numeric comparison
470    if ctx.has_numeric_comparison() {
471        let n = value_to_f64(value)?;
472        if ctx.gt {
473            return Ok(CompiledMatcher::NumericGt(n));
474        }
475        if ctx.gte {
476            return Ok(CompiledMatcher::NumericGte(n));
477        }
478        if ctx.lt {
479            return Ok(CompiledMatcher::NumericLt(n));
480        }
481        if ctx.lte {
482            return Ok(CompiledMatcher::NumericLte(n));
483        }
484    }
485
486    // |neq — not-equal: negate the normal equality match
487    if ctx.has_neq() {
488        // Compile the value as a normal matcher, then wrap in Not
489        let mut inner_ctx = ModCtx { ..*ctx };
490        inner_ctx.neq = false;
491        let inner = compile_value(value, &inner_ctx)?;
492        return Ok(CompiledMatcher::Not(Box::new(inner)));
493    }
494
495    // For non-string values without string modifiers, use simple matchers
496    match value {
497        SigmaValue::Integer(n) => {
498            if ctx.contains || ctx.startswith || ctx.endswith {
499                // Treat as string for string modifiers
500                return compile_string_value(&n.to_string(), ctx);
501            }
502            return Ok(CompiledMatcher::NumericEq(*n as f64));
503        }
504        SigmaValue::Float(n) => {
505            if ctx.contains || ctx.startswith || ctx.endswith {
506                return compile_string_value(&n.to_string(), ctx);
507            }
508            return Ok(CompiledMatcher::NumericEq(*n));
509        }
510        SigmaValue::Bool(b) => return Ok(CompiledMatcher::BoolEq(*b)),
511        SigmaValue::Null => return Ok(CompiledMatcher::Null),
512        SigmaValue::String(_) => {} // handled below
513    }
514
515    // String value — apply encoding/transformation modifiers, then string matching
516    let sigma_str = match value {
517        SigmaValue::String(s) => s,
518        _ => unreachable!(),
519    };
520
521    // Apply transformation chain: wide → base64/base64offset → windash → string match
522    let mut bytes = sigma_string_to_bytes(sigma_str);
523
524    // |wide / |utf16le — UTF-16LE encoding
525    if ctx.wide {
526        bytes = to_utf16le_bytes(&bytes);
527    }
528
529    // |utf16be — UTF-16 big-endian encoding
530    if ctx.utf16be {
531        bytes = to_utf16be_bytes(&bytes);
532    }
533
534    // |utf16 — UTF-16 with BOM (little-endian)
535    if ctx.utf16 {
536        bytes = to_utf16_bom_bytes(&bytes);
537    }
538
539    // |base64 — base64 encode, then exact/contains match
540    if ctx.base64 {
541        let encoded = BASE64_STANDARD.encode(&bytes);
542        return compile_string_value(&encoded, ctx);
543    }
544
545    // |base64offset — generate 3 offset variants
546    if ctx.base64offset {
547        let patterns = base64_offset_patterns(&bytes);
548        let matchers: Vec<CompiledMatcher> = patterns
549            .into_iter()
550            .map(|p| {
551                // base64offset implies contains matching
552                CompiledMatcher::Contains {
553                    value: if ci { p.to_lowercase() } else { p },
554                    case_insensitive: ci,
555                }
556            })
557            .collect();
558        return Ok(CompiledMatcher::AnyOf(matchers));
559    }
560
561    // |windash — expand `-` to `/` variants
562    if ctx.windash {
563        let plain = sigma_str
564            .as_plain()
565            .unwrap_or_else(|| sigma_str.original.clone());
566        let variants = expand_windash(&plain)?;
567        let matchers: Result<Vec<CompiledMatcher>> = variants
568            .into_iter()
569            .map(|v| compile_string_value(&v, ctx))
570            .collect();
571        return Ok(CompiledMatcher::AnyOf(matchers?));
572    }
573
574    // Standard string matching (exact / contains / startswith / endswith / wildcard)
575    compile_sigma_string(sigma_str, ctx)
576}
577
578/// Compile a `SigmaString` (with possible wildcards) using modifiers.
579fn compile_sigma_string(sigma_str: &SigmaString, ctx: &ModCtx) -> Result<CompiledMatcher> {
580    let ci = ctx.is_case_insensitive();
581
582    // If the string is plain (no wildcards), use optimized matchers
583    if sigma_str.is_plain() {
584        let plain = sigma_str.as_plain().unwrap_or_default();
585        return compile_string_value(&plain, ctx);
586    }
587
588    // String has wildcards — need to determine matching semantics
589    // Modifiers like |contains, |startswith, |endswith adjust the pattern
590
591    // Build a regex from the sigma string, incorporating modifier semantics
592    let mut pattern = String::new();
593    if ci {
594        pattern.push_str("(?i)");
595    }
596
597    if !ctx.contains && !ctx.startswith {
598        pattern.push('^');
599    }
600
601    for part in &sigma_str.parts {
602        match part {
603            StringPart::Plain(text) => {
604                pattern.push_str(&regex::escape(text));
605            }
606            StringPart::Special(SpecialChar::WildcardMulti) => {
607                pattern.push_str(".*");
608            }
609            StringPart::Special(SpecialChar::WildcardSingle) => {
610                pattern.push('.');
611            }
612        }
613    }
614
615    if !ctx.contains && !ctx.endswith {
616        pattern.push('$');
617    }
618
619    let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
620    Ok(CompiledMatcher::Regex(regex))
621}
622
623/// Compile a plain string value (no wildcards) using modifier context.
624fn compile_string_value(plain: &str, ctx: &ModCtx) -> Result<CompiledMatcher> {
625    let ci = ctx.is_case_insensitive();
626
627    if ctx.contains {
628        Ok(CompiledMatcher::Contains {
629            value: if ci {
630                plain.to_lowercase()
631            } else {
632                plain.to_string()
633            },
634            case_insensitive: ci,
635        })
636    } else if ctx.startswith {
637        Ok(CompiledMatcher::StartsWith {
638            value: if ci {
639                plain.to_lowercase()
640            } else {
641                plain.to_string()
642            },
643            case_insensitive: ci,
644        })
645    } else if ctx.endswith {
646        Ok(CompiledMatcher::EndsWith {
647            value: if ci {
648                plain.to_lowercase()
649            } else {
650                plain.to_string()
651            },
652            case_insensitive: ci,
653        })
654    } else {
655        Ok(CompiledMatcher::Exact {
656            value: if ci {
657                plain.to_lowercase()
658            } else {
659                plain.to_string()
660            },
661            case_insensitive: ci,
662        })
663    }
664}
665
666/// Compile a value with default settings (no modifiers except case sensitivity).
667fn compile_value_default(value: &SigmaValue, case_insensitive: bool) -> Result<CompiledMatcher> {
668    match value {
669        SigmaValue::String(s) => {
670            if s.is_plain() {
671                let plain = s.as_plain().unwrap_or_default();
672                Ok(CompiledMatcher::Contains {
673                    value: if case_insensitive {
674                        plain.to_lowercase()
675                    } else {
676                        plain
677                    },
678                    case_insensitive,
679                })
680            } else {
681                // Wildcards → regex (keywords use contains semantics)
682                let pattern = sigma_string_to_regex(&s.parts, case_insensitive);
683                let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
684                Ok(CompiledMatcher::Regex(regex))
685            }
686        }
687        SigmaValue::Integer(n) => Ok(CompiledMatcher::NumericEq(*n as f64)),
688        SigmaValue::Float(n) => Ok(CompiledMatcher::NumericEq(*n)),
689        SigmaValue::Bool(b) => Ok(CompiledMatcher::BoolEq(*b)),
690        SigmaValue::Null => Ok(CompiledMatcher::Null),
691    }
692}
693
694// =============================================================================
695// Condition evaluation
696// =============================================================================
697
698/// Evaluate a condition expression against the event using compiled detections.
699///
700/// Returns `true` if the condition is satisfied. Populates `matched_selections`
701/// with the names of detections that were evaluated and returned true.
702pub fn eval_condition(
703    expr: &ConditionExpr,
704    detections: &HashMap<String, CompiledDetection>,
705    event: &impl Event,
706    matched_selections: &mut Vec<String>,
707) -> bool {
708    match expr {
709        ConditionExpr::Identifier(name) => {
710            if let Some(det) = detections.get(name) {
711                let result = eval_detection(det, event);
712                if result {
713                    matched_selections.push(name.clone());
714                }
715                result
716            } else {
717                false
718            }
719        }
720
721        ConditionExpr::And(exprs) => exprs
722            .iter()
723            .all(|e| eval_condition(e, detections, event, matched_selections)),
724
725        ConditionExpr::Or(exprs) => exprs
726            .iter()
727            .any(|e| eval_condition(e, detections, event, matched_selections)),
728
729        ConditionExpr::Not(inner) => !eval_condition(inner, detections, event, matched_selections),
730
731        ConditionExpr::Selector {
732            quantifier,
733            pattern,
734        } => {
735            let matching_names: Vec<&String> = match pattern {
736                SelectorPattern::Them => detections
737                    .keys()
738                    .filter(|name| !name.starts_with('_'))
739                    .collect(),
740                SelectorPattern::Pattern(pat) => detections
741                    .keys()
742                    .filter(|name| pattern_matches(pat, name))
743                    .collect(),
744            };
745
746            let mut match_count = 0u64;
747            for name in &matching_names {
748                if let Some(det) = detections.get(*name)
749                    && eval_detection(det, event)
750                {
751                    match_count += 1;
752                    matched_selections.push((*name).clone());
753                }
754            }
755
756            match quantifier {
757                Quantifier::Any => match_count >= 1,
758                Quantifier::All => match_count == matching_names.len() as u64,
759                Quantifier::Count(n) => match_count >= *n,
760            }
761        }
762    }
763}
764
765/// Evaluate a compiled detection against an event.
766fn eval_detection(detection: &CompiledDetection, event: &impl Event) -> bool {
767    match detection {
768        CompiledDetection::AllOf(items) => {
769            items.iter().all(|item| eval_detection_item(item, event))
770        }
771        CompiledDetection::AnyOf(dets) => dets.iter().any(|d| eval_detection(d, event)),
772        CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
773    }
774}
775
776/// Evaluate a single compiled detection item against an event.
777fn eval_detection_item(item: &CompiledDetectionItem, event: &impl Event) -> bool {
778    if let Some(expect_exists) = item.exists {
779        if let Some(field) = &item.field {
780            let exists = event.get_field(field).is_some_and(|v| !v.is_null());
781            return exists == expect_exists;
782        }
783        return !expect_exists;
784    }
785
786    match &item.field {
787        Some(field_name) => {
788            if let Some(value) = event.get_field(field_name) {
789                item.matcher.matches(&value, event)
790            } else {
791                matches!(item.matcher, CompiledMatcher::Null)
792            }
793        }
794        None => item.matcher.matches_keyword(event),
795    }
796}
797
798/// Collect field matches from matched selections for the MatchResult.
799fn collect_field_matches(
800    selection_names: &[String],
801    detections: &HashMap<String, CompiledDetection>,
802    event: &impl Event,
803) -> Vec<FieldMatch> {
804    let mut matches = Vec::new();
805    for name in selection_names {
806        if let Some(det) = detections.get(name) {
807            collect_detection_fields(det, event, &mut matches);
808        }
809    }
810    matches
811}
812
813fn collect_detection_fields(
814    detection: &CompiledDetection,
815    event: &impl Event,
816    out: &mut Vec<FieldMatch>,
817) {
818    match detection {
819        CompiledDetection::AllOf(items) => {
820            for item in items {
821                if let Some(field_name) = &item.field
822                    && let Some(value) = event.get_field(field_name)
823                    && item.matcher.matches(&value, event)
824                {
825                    out.push(FieldMatch {
826                        field: field_name.clone(),
827                        value: value.to_json(),
828                    });
829                }
830            }
831        }
832        CompiledDetection::AnyOf(dets) => {
833            for d in dets {
834                if eval_detection(d, event) {
835                    collect_detection_fields(d, event, out);
836                }
837            }
838        }
839        CompiledDetection::Keywords(_) => {}
840    }
841}