1mod from_ir;
12mod helpers;
13#[doc(hidden)]
14pub mod optimizer;
15#[cfg(test)]
16mod tests;
17
18pub use from_ir::compile_to_compiled;
19
20#[cfg(test)]
23pub(crate) use optimizer::optimize_any_of as optimize_any_of_for_test;
24
25use std::borrow::Cow;
26use std::collections::HashMap;
27use std::sync::Arc;
28
29use base64::Engine as Base64Engine;
30use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
31use regex::Regex;
32
33use rsigma_parser::fieldpath::{first_unescaped, unescape_brackets};
34use rsigma_parser::value::{SpecialChar, StringPart};
35use rsigma_parser::{
36 ArrayQuantifier, ConditionExpr, Detection, DetectionItem, Level, LogSource, Modifier,
37 Quantifier, SigmaRule, SigmaString, SigmaValue,
38};
39
40use crate::error::{EvalError, Result};
41use crate::event::{Event, EventValue};
42use crate::matcher::{CompiledMatcher, sigma_string_to_regex};
43use crate::result::{
44 DetectionBody, EvaluationResult, FieldMatch, MatchDetailLevel, MatcherKind, ResultBody,
45 RuleHeader,
46};
47
48pub(crate) use helpers::yaml_to_json_map;
49use helpers::{
50 base64_offset_patterns, build_regex, expand_windash, sigma_string_to_bytes, to_utf16_bom_bytes,
51 to_utf16be_bytes, to_utf16le_bytes, value_to_f64, value_to_plain_string,
52};
53
54#[derive(Debug, Clone)]
60pub struct CompiledRule {
61 pub title: String,
62 pub id: Option<String>,
63 pub level: Option<Level>,
64 pub tags: Vec<String>,
65 pub description: Option<String>,
68 pub falsepositives: Vec<String>,
71 pub logsource: LogSource,
72 pub detections: HashMap<String, CompiledDetection>,
74 pub conditions: Vec<ConditionExpr>,
76 pub include_event: bool,
79 pub custom_attributes: Arc<HashMap<String, serde_json::Value>>,
84}
85
86#[derive(Debug, Clone)]
88pub enum CompiledDetection {
89 AllOf(Vec<CompiledDetectionItem>),
91 AnyOf(Vec<CompiledDetection>),
93 Keywords(CompiledMatcher),
95 ArrayMatch {
99 field: String,
100 quantifier: ArrayQuantifier,
101 body: Box<CompiledDetection>,
102 },
103 And(Vec<CompiledDetection>),
106 Conditional {
111 named: HashMap<String, CompiledDetection>,
112 condition: ConditionExpr,
113 },
114}
115
116#[derive(Debug, Clone)]
118pub struct CompiledDetectionItem {
119 pub field: Option<String>,
121 pub matcher: CompiledMatcher,
123 pub exists: Option<bool>,
125 pub bloom_eligible: bool,
130}
131
132#[derive(Clone, Copy)]
138struct ModCtx {
139 contains: bool,
140 startswith: bool,
141 endswith: bool,
142 all: bool,
143 base64: bool,
144 base64offset: bool,
145 wide: bool,
146 utf16be: bool,
147 utf16: bool,
148 windash: bool,
149 re: bool,
150 cidr: bool,
151 cased: bool,
152 exists: bool,
153 fieldref: bool,
154 gt: bool,
155 gte: bool,
156 lt: bool,
157 lte: bool,
158 neq: bool,
159 ignore_case: bool,
160 multiline: bool,
161 dotall: bool,
162 expand: bool,
163 timestamp_part: Option<crate::matcher::TimePart>,
164}
165
166impl ModCtx {
167 fn from_modifiers(modifiers: &[Modifier]) -> Self {
168 let mut ctx = ModCtx {
169 contains: false,
170 startswith: false,
171 endswith: false,
172 all: false,
173 base64: false,
174 base64offset: false,
175 wide: false,
176 utf16be: false,
177 utf16: false,
178 windash: false,
179 re: false,
180 cidr: false,
181 cased: false,
182 exists: false,
183 fieldref: false,
184 gt: false,
185 gte: false,
186 lt: false,
187 lte: false,
188 neq: false,
189 ignore_case: false,
190 multiline: false,
191 dotall: false,
192 expand: false,
193 timestamp_part: None,
194 };
195 for m in modifiers {
196 match m {
197 Modifier::Contains => ctx.contains = true,
198 Modifier::StartsWith => ctx.startswith = true,
199 Modifier::EndsWith => ctx.endswith = true,
200 Modifier::All => ctx.all = true,
201 Modifier::Base64 => ctx.base64 = true,
202 Modifier::Base64Offset => ctx.base64offset = true,
203 Modifier::Wide => ctx.wide = true,
204 Modifier::Utf16be => ctx.utf16be = true,
205 Modifier::Utf16 => ctx.utf16 = true,
206 Modifier::WindAsh => ctx.windash = true,
207 Modifier::Re => ctx.re = true,
208 Modifier::Cidr => ctx.cidr = true,
209 Modifier::Cased => ctx.cased = true,
210 Modifier::Exists => ctx.exists = true,
211 Modifier::FieldRef => ctx.fieldref = true,
212 Modifier::Gt => ctx.gt = true,
213 Modifier::Gte => ctx.gte = true,
214 Modifier::Lt => ctx.lt = true,
215 Modifier::Lte => ctx.lte = true,
216 Modifier::Neq => ctx.neq = true,
217 Modifier::IgnoreCase => ctx.ignore_case = true,
218 Modifier::Multiline => ctx.multiline = true,
219 Modifier::DotAll => ctx.dotall = true,
220 Modifier::Expand => ctx.expand = true,
221 Modifier::Hour => ctx.timestamp_part = Some(crate::matcher::TimePart::Hour),
222 Modifier::Day => ctx.timestamp_part = Some(crate::matcher::TimePart::Day),
223 Modifier::Week => ctx.timestamp_part = Some(crate::matcher::TimePart::Week),
224 Modifier::Month => ctx.timestamp_part = Some(crate::matcher::TimePart::Month),
225 Modifier::Year => ctx.timestamp_part = Some(crate::matcher::TimePart::Year),
226 Modifier::Minute => ctx.timestamp_part = Some(crate::matcher::TimePart::Minute),
227 }
228 }
229 ctx
230 }
231
232 fn is_case_insensitive(&self) -> bool {
235 !self.cased
236 }
237
238 fn has_numeric_comparison(&self) -> bool {
240 self.gt || self.gte || self.lt || self.lte
241 }
242
243 fn has_neq(&self) -> bool {
245 self.neq
246 }
247}
248
249pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
257 let ir = rsigma_ir::lower_rule(rule, &rsigma_ir::LowerOptions::default())?;
258 compile_to_compiled(&ir)
259}
260
261pub fn evaluate_rule(rule: &CompiledRule, event: &impl Event) -> Option<EvaluationResult> {
269 evaluate_rule_with_bloom(
270 rule,
271 event,
272 &crate::engine::bloom_index::NoBloom,
273 MatchDetailLevel::Off,
274 )
275}
276
277pub(crate) fn evaluate_rule_with_bloom<E, B>(
285 rule: &CompiledRule,
286 event: &E,
287 bloom: &B,
288 level: MatchDetailLevel,
289) -> Option<EvaluationResult>
290where
291 E: Event,
292 B: crate::engine::bloom_index::BloomLookup,
293{
294 for condition in &rule.conditions {
295 if eval_condition_matches_with_bloom(condition, &rule.detections, event, bloom) {
296 let mut matched_selections = Vec::new();
297 let matched = eval_condition_with_bloom(
298 condition,
299 &rule.detections,
300 event,
301 &mut matched_selections,
302 bloom,
303 );
304 debug_assert!(matched, "detail pass must agree with boolean pass");
305 let matched_fields =
306 collect_field_matches(&matched_selections, &rule.detections, event, level);
307
308 let event_data = if rule.include_event {
309 Some(event.to_json())
310 } else {
311 None
312 };
313
314 return Some(EvaluationResult {
315 header: RuleHeader {
316 rule_title: rule.title.clone(),
317 rule_id: rule.id.clone(),
318 level: rule.level,
319 tags: rule.tags.clone(),
320 custom_attributes: rule.custom_attributes.clone(),
321 enrichments: None,
322 },
323 body: ResultBody::Detection(DetectionBody {
324 matched_selections,
325 matched_fields,
326 event: event_data,
327 }),
328 });
329 }
330 }
331 None
332}
333
334pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
343 match detection {
344 Detection::AllOf(items) => {
345 if items.is_empty() {
346 return Err(EvalError::InvalidModifiers(
347 "AllOf detection must not be empty (vacuous truth)".into(),
348 ));
349 }
350 let compiled: Result<Vec<_>> = items.iter().map(compile_detection_item).collect();
351 Ok(CompiledDetection::AllOf(compiled?))
352 }
353 Detection::AnyOf(dets) => {
354 if dets.is_empty() {
355 return Err(EvalError::InvalidModifiers(
356 "AnyOf detection must not be empty (would never match)".into(),
357 ));
358 }
359 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
360 Ok(CompiledDetection::AnyOf(compiled?))
361 }
362 Detection::ArrayMatch {
363 field,
364 quantifier,
365 body,
366 } => {
367 let compiled_body = compile_detection(body)?;
368 Ok(CompiledDetection::ArrayMatch {
369 field: field.clone(),
370 quantifier: *quantifier,
371 body: Box::new(compiled_body),
372 })
373 }
374 Detection::And(dets) => {
375 if dets.is_empty() {
376 return Err(EvalError::InvalidModifiers(
377 "And detection must not be empty".into(),
378 ));
379 }
380 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
381 Ok(CompiledDetection::And(compiled?))
382 }
383 Detection::Conditional { named, condition } => {
384 if named.is_empty() {
385 return Err(EvalError::InvalidModifiers(
386 "Conditional detection must have at least one named sub-selection".into(),
387 ));
388 }
389 let compiled: Result<HashMap<String, CompiledDetection>> = named
390 .iter()
391 .map(|(k, d)| Ok((k.clone(), compile_detection(d)?)))
392 .collect();
393 Ok(CompiledDetection::Conditional {
394 named: compiled?,
395 condition: condition.clone(),
396 })
397 }
398 Detection::Keywords(values) => {
399 let ci = true; let matchers: Vec<CompiledMatcher> = values
401 .iter()
402 .map(|v| compile_value_default(v, ci))
403 .collect::<Result<Vec<_>>>()?;
404 let matcher = optimizer::optimize_any_of(matchers);
406 Ok(CompiledDetection::Keywords(matcher))
407 }
408 }
409}
410
411fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
412 let ctx = ModCtx::from_modifiers(&item.field.modifiers);
413
414 validate_modifiers(&ctx, &item.field.modifiers)?;
423
424 if ctx.exists {
426 let expect = match item.values.first() {
427 Some(SigmaValue::Bool(b)) => *b,
428 Some(SigmaValue::String(s)) => match s.as_plain().as_deref() {
429 Some("true") | Some("yes") => true,
430 Some("false") | Some("no") => false,
431 _ => true,
432 },
433 _ => true,
434 };
435 return Ok(CompiledDetectionItem {
436 field: item.field.name.clone(),
437 matcher: CompiledMatcher::Exists(expect),
438 exists: Some(expect),
439 bloom_eligible: false,
440 });
441 }
442
443 if ctx.all && item.values.len() <= 1 {
445 return Err(EvalError::InvalidModifiers(
446 "|all modifier requires more than one value".to_string(),
447 ));
448 }
449
450 let matchers: Result<Vec<CompiledMatcher>> =
452 item.values.iter().map(|v| compile_value(v, &ctx)).collect();
453 let matchers = matchers?;
454
455 let combined = if ctx.all {
462 if matchers.len() == 1 {
463 matchers
464 .into_iter()
465 .next()
466 .unwrap_or(CompiledMatcher::AllOf(vec![]))
467 } else {
468 CompiledMatcher::AllOf(matchers)
469 }
470 } else {
471 optimizer::optimize_any_of(matchers)
472 };
473
474 let bloom_eligible = item.field.name.is_some()
475 && crate::engine::bloom_index::is_positive_substring_matcher(&combined);
476
477 Ok(CompiledDetectionItem {
478 field: item.field.name.clone(),
479 matcher: combined,
480 exists: None,
481 bloom_eligible,
482 })
483}
484
485fn validate_modifiers(ctx: &ModCtx, modifiers: &[Modifier]) -> Result<()> {
522 let mut operators: Vec<&'static str> = Vec::new();
524 if ctx.contains {
525 operators.push("contains");
526 }
527 if ctx.startswith {
528 operators.push("startswith");
529 }
530 if ctx.endswith {
531 operators.push("endswith");
532 }
533 if ctx.re {
534 operators.push("re");
535 }
536 if ctx.cidr {
537 operators.push("cidr");
538 }
539 if ctx.exists {
540 operators.push("exists");
541 }
542 if ctx.fieldref {
543 operators.push("fieldref");
544 }
545 if ctx.gt {
546 operators.push("gt");
547 }
548 if ctx.gte {
549 operators.push("gte");
550 }
551 if ctx.lt {
552 operators.push("lt");
553 }
554 if ctx.lte {
555 operators.push("lte");
556 }
557 for m in modifiers {
558 match m {
559 Modifier::Minute => operators.push("minute"),
560 Modifier::Hour => operators.push("hour"),
561 Modifier::Day => operators.push("day"),
562 Modifier::Week => operators.push("week"),
563 Modifier::Month => operators.push("month"),
564 Modifier::Year => operators.push("year"),
565 _ => {}
566 }
567 }
568 if operators.len() > 1 {
569 return Err(EvalError::InvalidModifiers(format!(
570 "conflicting modifiers: at most one operator may be set per field; \
571 got |{}",
572 operators.join(", |")
573 )));
574 }
575
576 let mut wide_encodings: Vec<&'static str> = Vec::new();
578 if ctx.wide {
579 wide_encodings.push("wide");
580 }
581 if ctx.utf16 {
582 wide_encodings.push("utf16");
583 }
584 if ctx.utf16be {
585 wide_encodings.push("utf16be");
586 }
587 if wide_encodings.len() > 1 {
588 return Err(EvalError::InvalidModifiers(format!(
589 "conflicting modifiers: |wide, |utf16, and |utf16be are mutually \
590 exclusive UTF-16 encodings; got |{}",
591 wide_encodings.join(", |")
592 )));
593 }
594
595 if ctx.base64 && ctx.base64offset {
597 return Err(EvalError::InvalidModifiers(
598 "conflicting modifiers: |base64 and |base64offset are mutually \
599 exclusive base64 strategies; pick one"
600 .into(),
601 ));
602 }
603
604 let has_non_string_operator = ctx.re
609 || ctx.cidr
610 || ctx.exists
611 || ctx.fieldref
612 || ctx.has_numeric_comparison()
613 || ctx.timestamp_part.is_some();
614 if has_non_string_operator {
615 let mut transforms: Vec<&'static str> = Vec::new();
616 if ctx.base64 {
617 transforms.push("base64");
618 }
619 if ctx.base64offset {
620 transforms.push("base64offset");
621 }
622 if ctx.wide {
623 transforms.push("wide");
624 }
625 if ctx.utf16 {
626 transforms.push("utf16");
627 }
628 if ctx.utf16be {
629 transforms.push("utf16be");
630 }
631 if ctx.windash {
632 transforms.push("windash");
633 }
634 if ctx.expand {
635 transforms.push("expand");
636 }
637 if !transforms.is_empty() {
638 return Err(EvalError::InvalidModifiers(format!(
639 "conflicting modifiers: value transformations |{} only apply \
640 to string match operators (default eq, contains, startswith, \
641 endswith) and cannot be combined with the operator that is \
642 also set on this field",
643 transforms.join(", |")
644 )));
645 }
646 }
647
648 if !ctx.re {
650 let mut regex_flags: Vec<&'static str> = Vec::new();
651 if ctx.ignore_case {
652 regex_flags.push("i");
653 }
654 if ctx.multiline {
655 regex_flags.push("m");
656 }
657 if ctx.dotall {
658 regex_flags.push("s");
659 }
660 if !regex_flags.is_empty() {
661 return Err(EvalError::InvalidModifiers(format!(
662 "regex flag modifiers |{} have no effect without |re; \
663 case sensitivity for substring or equality matching is \
664 controlled by |cased (or its absence, which keeps the \
665 default case-insensitive behavior)",
666 regex_flags.join(", |")
667 )));
668 }
669 }
670
671 Ok(())
672}
673
674fn compile_value(value: &SigmaValue, ctx: &ModCtx) -> Result<CompiledMatcher> {
680 let ci = ctx.is_case_insensitive();
681
682 if ctx.expand {
686 let plain = value_to_plain_string(value)?;
687 let template = crate::matcher::parse_expand_template(&plain);
688 return Ok(CompiledMatcher::Expand {
689 template,
690 case_insensitive: ci,
691 });
692 }
693
694 if let Some(part) = ctx.timestamp_part {
696 let inner = match value {
699 SigmaValue::Integer(n) => CompiledMatcher::NumericEq(*n as f64),
700 SigmaValue::Float(n) => CompiledMatcher::NumericEq(*n),
701 SigmaValue::String(s) => {
702 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
703 let n: f64 = plain.parse().map_err(|_| {
704 EvalError::IncompatibleValue(format!(
705 "timestamp part modifier requires numeric value, got: {plain}"
706 ))
707 })?;
708 CompiledMatcher::NumericEq(n)
709 }
710 _ => {
711 return Err(EvalError::IncompatibleValue(
712 "timestamp part modifier requires numeric value".into(),
713 ));
714 }
715 };
716 return Ok(CompiledMatcher::TimestampPart {
717 part,
718 inner: Box::new(inner),
719 });
720 }
721
722 if ctx.fieldref {
724 let field_name = value_to_plain_string(value)?;
725 return Ok(CompiledMatcher::FieldRef {
726 field: field_name,
727 case_insensitive: ci,
728 });
729 }
730
731 if ctx.re {
735 let pattern = value_to_plain_string(value)?;
736 let regex = build_regex(&pattern, ctx.ignore_case, ctx.multiline, ctx.dotall)?;
737 return Ok(CompiledMatcher::Regex(regex));
738 }
739
740 if ctx.cidr {
742 let cidr_str = value_to_plain_string(value)?;
743 let net: ipnet::IpNet = cidr_str
744 .parse()
745 .map_err(|e: ipnet::AddrParseError| EvalError::InvalidCidr(e))?;
746 return Ok(CompiledMatcher::Cidr(net));
747 }
748
749 if ctx.has_numeric_comparison() {
751 let n = value_to_f64(value)?;
752 if ctx.gt {
753 return Ok(CompiledMatcher::NumericGt(n));
754 }
755 if ctx.gte {
756 return Ok(CompiledMatcher::NumericGte(n));
757 }
758 if ctx.lt {
759 return Ok(CompiledMatcher::NumericLt(n));
760 }
761 if ctx.lte {
762 return Ok(CompiledMatcher::NumericLte(n));
763 }
764 }
765
766 if ctx.has_neq() {
768 let mut inner_ctx = ModCtx { ..*ctx };
770 inner_ctx.neq = false;
771 let inner = compile_value(value, &inner_ctx)?;
772 return Ok(CompiledMatcher::Not(Box::new(inner)));
773 }
774
775 match value {
777 SigmaValue::Integer(n) => {
778 if ctx.contains || ctx.startswith || ctx.endswith {
779 return compile_string_value(&n.to_string(), ctx);
781 }
782 return Ok(CompiledMatcher::NumericEq(*n as f64));
783 }
784 SigmaValue::Float(n) => {
785 if ctx.contains || ctx.startswith || ctx.endswith {
786 return compile_string_value(&n.to_string(), ctx);
787 }
788 return Ok(CompiledMatcher::NumericEq(*n));
789 }
790 SigmaValue::Bool(b) => return Ok(CompiledMatcher::BoolEq(*b)),
791 SigmaValue::Null => return Ok(CompiledMatcher::Null),
792 SigmaValue::String(_) => {} }
794
795 let sigma_str = match value {
797 SigmaValue::String(s) => s,
798 _ => unreachable!(),
799 };
800
801 let mut bytes = sigma_string_to_bytes(sigma_str);
803
804 if ctx.wide {
806 bytes = to_utf16le_bytes(&bytes);
807 }
808
809 if ctx.utf16be {
811 bytes = to_utf16be_bytes(&bytes);
812 }
813
814 if ctx.utf16 {
816 bytes = to_utf16_bom_bytes(&bytes);
817 }
818
819 if ctx.base64 {
821 let encoded = BASE64_STANDARD.encode(&bytes);
822 return compile_string_value(&encoded, ctx);
823 }
824
825 if ctx.base64offset {
827 let patterns = base64_offset_patterns(&bytes);
828 let matchers: Vec<CompiledMatcher> = patterns
829 .into_iter()
830 .map(|p| {
831 CompiledMatcher::Contains {
833 value: if ci { p.to_lowercase() } else { p },
834 case_insensitive: ci,
835 }
836 })
837 .collect();
838 return Ok(CompiledMatcher::AnyOf(matchers));
839 }
840
841 if ctx.windash {
843 let plain = sigma_str
844 .as_plain()
845 .unwrap_or_else(|| sigma_str.original.clone());
846 let variants = expand_windash(&plain)?;
847 let matchers: Result<Vec<CompiledMatcher>> = variants
848 .into_iter()
849 .map(|v| compile_string_value(&v, ctx))
850 .collect();
851 return Ok(CompiledMatcher::AnyOf(matchers?));
852 }
853
854 compile_sigma_string(sigma_str, ctx)
856}
857
858fn compile_sigma_string(sigma_str: &SigmaString, ctx: &ModCtx) -> Result<CompiledMatcher> {
860 let ci = ctx.is_case_insensitive();
861
862 if sigma_str.is_plain() {
864 let plain = sigma_str.as_plain().unwrap_or_default();
865 return compile_string_value(&plain, ctx);
866 }
867
868 let mut pattern = String::new();
873 if ci {
874 pattern.push_str("(?i)");
875 }
876
877 if !ctx.contains && !ctx.startswith {
878 pattern.push('^');
879 }
880
881 for part in &sigma_str.parts {
882 match part {
883 StringPart::Plain(text) => {
884 pattern.push_str(®ex::escape(text));
885 }
886 StringPart::Special(SpecialChar::WildcardMulti) => {
887 pattern.push_str(".*");
888 }
889 StringPart::Special(SpecialChar::WildcardSingle) => {
890 pattern.push('.');
891 }
892 }
893 }
894
895 if !ctx.contains && !ctx.endswith {
896 pattern.push('$');
897 }
898
899 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
900 Ok(CompiledMatcher::Regex(regex))
901}
902
903fn compile_string_value(plain: &str, ctx: &ModCtx) -> Result<CompiledMatcher> {
905 let ci = ctx.is_case_insensitive();
906
907 if ctx.contains {
908 Ok(CompiledMatcher::Contains {
909 value: if ci {
910 plain.to_lowercase()
911 } else {
912 plain.to_string()
913 },
914 case_insensitive: ci,
915 })
916 } else if ctx.startswith {
917 Ok(CompiledMatcher::StartsWith {
918 value: if ci {
919 plain.to_lowercase()
920 } else {
921 plain.to_string()
922 },
923 case_insensitive: ci,
924 })
925 } else if ctx.endswith {
926 Ok(CompiledMatcher::EndsWith {
927 value: if ci {
928 plain.to_lowercase()
929 } else {
930 plain.to_string()
931 },
932 case_insensitive: ci,
933 })
934 } else {
935 Ok(CompiledMatcher::Exact {
936 value: if ci {
937 plain.to_lowercase()
938 } else {
939 plain.to_string()
940 },
941 case_insensitive: ci,
942 })
943 }
944}
945
946fn compile_value_default(value: &SigmaValue, case_insensitive: bool) -> Result<CompiledMatcher> {
948 match value {
949 SigmaValue::String(s) => {
950 if s.is_plain() {
951 let plain = s.as_plain().unwrap_or_default();
952 Ok(CompiledMatcher::Contains {
953 value: if case_insensitive {
954 plain.to_lowercase()
955 } else {
956 plain
957 },
958 case_insensitive,
959 })
960 } else {
961 let pattern = sigma_string_to_regex(&s.parts, case_insensitive);
963 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
964 Ok(CompiledMatcher::Regex(regex))
965 }
966 }
967 SigmaValue::Integer(n) => Ok(CompiledMatcher::NumericEq(*n as f64)),
968 SigmaValue::Float(n) => Ok(CompiledMatcher::NumericEq(*n)),
969 SigmaValue::Bool(b) => Ok(CompiledMatcher::BoolEq(*b)),
970 SigmaValue::Null => Ok(CompiledMatcher::Null),
971 }
972}
973
974pub fn eval_condition(
983 expr: &ConditionExpr,
984 detections: &HashMap<String, CompiledDetection>,
985 event: &impl Event,
986 matched_selections: &mut Vec<String>,
987) -> bool {
988 eval_condition_with_bloom(
989 expr,
990 detections,
991 event,
992 matched_selections,
993 &crate::engine::bloom_index::NoBloom,
994 )
995}
996
997fn eval_condition_matches_with_bloom<E, B>(
1003 expr: &ConditionExpr,
1004 detections: &HashMap<String, CompiledDetection>,
1005 event: &E,
1006 bloom: &B,
1007) -> bool
1008where
1009 E: Event,
1010 B: crate::engine::bloom_index::BloomLookup,
1011{
1012 match expr {
1013 ConditionExpr::Identifier(name) => detections
1014 .get(name)
1015 .is_some_and(|det| eval_detection_with_bloom(det, event, bloom)),
1016 ConditionExpr::And(exprs) => exprs
1017 .iter()
1018 .all(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
1019 ConditionExpr::Or(exprs) => exprs
1020 .iter()
1021 .any(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
1022 ConditionExpr::Not(inner) => {
1023 !eval_condition_matches_with_bloom(inner, detections, event, bloom)
1024 }
1025 ConditionExpr::Selector {
1026 quantifier,
1027 pattern,
1028 } => {
1029 let mut matching = detections
1030 .iter()
1031 .filter(|(name, _)| pattern.matches_detection_name(name));
1032 match quantifier {
1033 Quantifier::Any => {
1034 matching.any(|(_, det)| eval_detection_with_bloom(det, event, bloom))
1035 }
1036 Quantifier::All => {
1037 matching.all(|(_, det)| eval_detection_with_bloom(det, event, bloom))
1038 }
1039 Quantifier::Count(required) => {
1040 if *required == 0 {
1041 return true;
1042 }
1043 let mut matched = 0u64;
1044 matching.any(|(_, det)| {
1045 if eval_detection_with_bloom(det, event, bloom) {
1046 matched += 1;
1047 }
1048 matched >= *required
1049 })
1050 }
1051 }
1052 }
1053 }
1054}
1055
1056pub(crate) fn eval_condition_with_bloom<E, B>(
1062 expr: &ConditionExpr,
1063 detections: &HashMap<String, CompiledDetection>,
1064 event: &E,
1065 matched_selections: &mut Vec<String>,
1066 bloom: &B,
1067) -> bool
1068where
1069 E: Event,
1070 B: crate::engine::bloom_index::BloomLookup,
1071{
1072 match expr {
1073 ConditionExpr::Identifier(name) => {
1074 if let Some(det) = detections.get(name) {
1075 let result = eval_detection_with_bloom(det, event, bloom);
1076 if result {
1077 matched_selections.push(name.clone());
1078 }
1079 result
1080 } else {
1081 false
1082 }
1083 }
1084
1085 ConditionExpr::And(exprs) => exprs
1086 .iter()
1087 .all(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1088
1089 ConditionExpr::Or(exprs) => exprs
1090 .iter()
1091 .any(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1092
1093 ConditionExpr::Not(inner) => {
1094 !eval_condition_with_bloom(inner, detections, event, matched_selections, bloom)
1095 }
1096
1097 ConditionExpr::Selector {
1098 quantifier,
1099 pattern,
1100 } => {
1101 let matching_names: Vec<&String> = detections
1102 .keys()
1103 .filter(|name| pattern.matches_detection_name(name))
1104 .collect();
1105
1106 let mut match_count = 0u64;
1107 for name in &matching_names {
1108 if let Some(det) = detections.get(*name)
1109 && eval_detection_with_bloom(det, event, bloom)
1110 {
1111 match_count += 1;
1112 matched_selections.push((*name).clone());
1113 }
1114 }
1115
1116 match quantifier {
1117 Quantifier::Any => match_count >= 1,
1118 Quantifier::All => match_count == matching_names.len() as u64,
1119 Quantifier::Count(n) => match_count >= *n,
1120 }
1121 }
1122 }
1123}
1124
1125#[cfg(test)]
1130fn eval_detection_item(item: &CompiledDetectionItem, event: &impl Event) -> bool {
1131 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1132}
1133
1134pub(crate) fn eval_detection_no_bloom(detection: &CompiledDetection, event: &impl Event) -> bool {
1140 eval_detection_with_bloom(detection, event, &crate::engine::bloom_index::NoBloom)
1141}
1142
1143pub(crate) fn eval_detection_item_no_bloom(
1147 item: &CompiledDetectionItem,
1148 event: &impl Event,
1149) -> bool {
1150 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1151}
1152
1153fn eval_detection_with_bloom<E, B>(detection: &CompiledDetection, event: &E, bloom: &B) -> bool
1155where
1156 E: Event,
1157 B: crate::engine::bloom_index::BloomLookup,
1158{
1159 match detection {
1160 CompiledDetection::AllOf(items) => items
1161 .iter()
1162 .all(|item| eval_detection_item_with_bloom(item, event, bloom)),
1163 CompiledDetection::AnyOf(dets) => dets
1164 .iter()
1165 .any(|d| eval_detection_with_bloom(d, event, bloom)),
1166 CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
1167 CompiledDetection::ArrayMatch {
1168 field,
1169 quantifier,
1170 body,
1171 } => match event.get_field(field) {
1172 Some(value) => eval_array_quantified(&value, *quantifier, body, event),
1173 None => array_quantifier_matches_empty(*quantifier),
1174 },
1175 CompiledDetection::And(dets) => dets
1176 .iter()
1177 .all(|d| eval_detection_with_bloom(d, event, bloom)),
1178 CompiledDetection::Conditional { named, condition } => {
1182 eval_condition_with_bloom(condition, named, event, &mut Vec::new(), bloom)
1183 }
1184 }
1185}
1186
1187fn eval_array_quantified<E: Event>(
1193 value: &EventValue,
1194 quantifier: ArrayQuantifier,
1195 body: &CompiledDetection,
1196 outer: &E,
1197) -> bool {
1198 match value {
1199 EventValue::Array(members) => match quantifier {
1200 ArrayQuantifier::Any => members.iter().any(|m| eval_array_body(body, m, outer)),
1201 ArrayQuantifier::All => {
1202 !members.is_empty() && members.iter().all(|m| eval_array_body(body, m, outer))
1203 }
1204 ArrayQuantifier::AllOrEmpty => members.iter().all(|m| eval_array_body(body, m, outer)),
1205 ArrayQuantifier::None => !members.iter().any(|m| eval_array_body(body, m, outer)),
1206 },
1207 EventValue::Null => array_quantifier_matches_empty(quantifier),
1210 single => match quantifier {
1212 ArrayQuantifier::None => !eval_array_body(body, single, outer),
1213 _ => eval_array_body(body, single, outer),
1214 },
1215 }
1216}
1217
1218fn array_quantifier_matches_empty(quantifier: ArrayQuantifier) -> bool {
1220 matches!(
1221 quantifier,
1222 ArrayQuantifier::None | ArrayQuantifier::AllOrEmpty
1223 )
1224}
1225
1226fn eval_array_body<E: Event>(body: &CompiledDetection, member: &EventValue, outer: &E) -> bool {
1231 match body {
1232 CompiledDetection::AllOf(items) => items
1233 .iter()
1234 .all(|item| eval_array_item(item, member, outer)),
1235 CompiledDetection::AnyOf(dets) => dets.iter().any(|d| eval_array_body(d, member, outer)),
1236 CompiledDetection::And(dets) => dets.iter().all(|d| eval_array_body(d, member, outer)),
1237 CompiledDetection::ArrayMatch {
1238 field,
1239 quantifier,
1240 body: inner,
1241 } => match element_field(member, field) {
1242 Some(value) => eval_array_quantified(value, *quantifier, inner, outer),
1243 None => array_quantifier_matches_empty(*quantifier),
1244 },
1245 CompiledDetection::Keywords(matcher) => matcher.matches(member, outer),
1247 CompiledDetection::Conditional { named, condition } => {
1250 eval_array_condition(condition, named, member, outer)
1251 }
1252 }
1253}
1254
1255fn eval_array_condition<E: Event>(
1263 expr: &ConditionExpr,
1264 named: &HashMap<String, CompiledDetection>,
1265 member: &EventValue,
1266 outer: &E,
1267) -> bool {
1268 match expr {
1269 ConditionExpr::Identifier(name) => named
1270 .get(name)
1271 .is_some_and(|d| eval_array_body(d, member, outer)),
1272 ConditionExpr::And(exprs) => exprs
1273 .iter()
1274 .all(|e| eval_array_condition(e, named, member, outer)),
1275 ConditionExpr::Or(exprs) => exprs
1276 .iter()
1277 .any(|e| eval_array_condition(e, named, member, outer)),
1278 ConditionExpr::Not(inner) => !eval_array_condition(inner, named, member, outer),
1279 ConditionExpr::Selector {
1280 quantifier,
1281 pattern,
1282 } => {
1283 let names: Vec<&String> = named
1284 .keys()
1285 .filter(|n| pattern.matches_detection_name(n))
1286 .collect();
1287 let count = names
1288 .iter()
1289 .filter(|n| {
1290 named
1291 .get(**n)
1292 .is_some_and(|d| eval_array_body(d, member, outer))
1293 })
1294 .count() as u64;
1295 match quantifier {
1296 Quantifier::Any => count >= 1,
1297 Quantifier::All => count == names.len() as u64,
1298 Quantifier::Count(n) => count >= *n,
1299 }
1300 }
1301 }
1302}
1303
1304fn eval_array_item<E: Event>(item: &CompiledDetectionItem, member: &EventValue, outer: &E) -> bool {
1306 if let Some(expect_exists) = item.exists {
1307 let exists = match &item.field {
1308 Some(name) => element_field(member, name).is_some_and(|v| !v.is_null()),
1309 None => !member.is_null(),
1310 };
1311 return exists == expect_exists;
1312 }
1313
1314 match &item.field {
1315 Some(name) => match element_field(member, name) {
1316 Some(value) => item.matcher.matches(value, outer),
1317 None => matches!(item.matcher, CompiledMatcher::Null),
1318 },
1319 None => item.matcher.matches(member, outer),
1321 }
1322}
1323
1324fn element_field<'a>(member: &'a EventValue<'a>, path: &str) -> Option<&'a EventValue<'a>> {
1330 if let EventValue::Map(entries) = member
1331 && let Some((_, v)) = entries.iter().find(|(k, _)| k.as_ref() == path)
1332 {
1333 return Some(v);
1334 }
1335 let ops = parse_event_ops(path);
1336 nav_event_value(member, &ops)
1337}
1338
1339enum EventOp<'a> {
1340 Key(Cow<'a, str>),
1341 Index(i64),
1342}
1343
1344fn parse_event_ops(path: &str) -> Vec<EventOp<'_>> {
1348 let mut ops = Vec::new();
1349 for part in path.split('.') {
1350 match first_unescaped(part, b'[') {
1351 Some(bpos) if index_groups(&part[bpos..]).is_some() => {
1352 let name = &part[..bpos];
1353 if !name.is_empty() {
1354 ops.push(EventOp::Key(unescape_brackets(name)));
1355 }
1356 for idx in index_groups(&part[bpos..]).expect("checked") {
1357 ops.push(EventOp::Index(idx));
1358 }
1359 }
1360 _ => ops.push(EventOp::Key(unescape_brackets(part))),
1361 }
1362 }
1363 ops
1364}
1365
1366fn index_groups(s: &str) -> Option<Vec<i64>> {
1369 let mut out = Vec::new();
1370 let mut rem = s;
1371 while !rem.is_empty() {
1372 let rest = rem.strip_prefix('[')?;
1373 let close = rest.find(']')?;
1374 out.push(rest[..close].parse().ok()?);
1375 rem = &rest[close + 1..];
1376 }
1377 Some(out)
1378}
1379
1380fn nav_event_value<'a>(
1381 current: &'a EventValue<'a>,
1382 ops: &[EventOp<'_>],
1383) -> Option<&'a EventValue<'a>> {
1384 let Some((op, rest)) = ops.split_first() else {
1385 return Some(current);
1386 };
1387 match op {
1388 EventOp::Key(key) => match current {
1389 EventValue::Map(entries) => {
1390 let next = entries
1391 .iter()
1392 .find(|(k, _)| k.as_ref() == key.as_ref())
1393 .map(|(_, v)| v)?;
1394 nav_event_value(next, rest)
1395 }
1396 EventValue::Array(members) => members.iter().find_map(|m| nav_event_value(m, ops)),
1397 _ => None,
1398 },
1399 EventOp::Index(i) => match current {
1400 EventValue::Array(members) => {
1401 let idx = crate::event::resolve_array_index(*i, members.len())?;
1402 nav_event_value(members.get(idx)?, rest)
1403 }
1404 _ => None,
1405 },
1406 }
1407}
1408
1409fn eval_detection_item_with_bloom<E, B>(item: &CompiledDetectionItem, event: &E, bloom: &B) -> bool
1416where
1417 E: Event,
1418 B: crate::engine::bloom_index::BloomLookup,
1419{
1420 if let Some(expect_exists) = item.exists {
1421 if let Some(field) = &item.field {
1422 let exists = event.get_field(field).is_some_and(|v| !v.is_null());
1423 return exists == expect_exists;
1424 }
1425 return !expect_exists;
1426 }
1427
1428 match &item.field {
1429 Some(field_name) => {
1430 if let Some(value) = event.get_field(field_name) {
1431 if item.bloom_eligible
1432 && bloom.verdict_for_field(field_name)
1433 == crate::engine::bloom_index::BloomVerdict::DefinitelyNoMatch
1434 {
1435 return false;
1436 }
1437 item.matcher.matches(&value, event)
1438 } else {
1439 matches!(item.matcher, CompiledMatcher::Null)
1440 }
1441 }
1442 None => item.matcher.matches_keyword(event),
1443 }
1444}
1445
1446const MAX_KEYWORD_MATCHES: usize = 16;
1450
1451fn collect_field_matches(
1459 selection_names: &[String],
1460 detections: &HashMap<String, CompiledDetection>,
1461 event: &impl Event,
1462 level: MatchDetailLevel,
1463) -> Vec<FieldMatch> {
1464 let mut matches = Vec::new();
1465 for name in selection_names {
1466 if let Some(det) = detections.get(name) {
1467 collect_detection_fields(name, det, event, level, &mut matches);
1468 }
1469 }
1470 matches
1471}
1472
1473fn collect_detection_fields(
1474 selection: &str,
1475 detection: &CompiledDetection,
1476 event: &impl Event,
1477 level: MatchDetailLevel,
1478 out: &mut Vec<FieldMatch>,
1479) {
1480 match detection {
1481 CompiledDetection::AllOf(items) => {
1482 for item in items {
1483 match &item.field {
1484 Some(field_name) => {
1485 if let Some(value) = event.get_field(field_name) {
1486 if item.matcher.matches(&value, event) {
1487 out.push(make_field_match(
1488 selection,
1489 field_name,
1490 value.to_json(),
1491 &item.matcher,
1492 level,
1493 ));
1494 }
1495 } else if level != MatchDetailLevel::Off
1496 && matches!(item.matcher, CompiledMatcher::Null)
1497 {
1498 out.push(make_field_match(
1501 selection,
1502 field_name,
1503 serde_json::Value::Null,
1504 &item.matcher,
1505 level,
1506 ));
1507 }
1508 }
1509 None => {
1510 if level != MatchDetailLevel::Off {
1512 collect_keyword_matches(selection, &item.matcher, event, level, out);
1513 }
1514 }
1515 }
1516 }
1517 }
1518 CompiledDetection::AnyOf(dets) => {
1519 for d in dets {
1520 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1521 collect_detection_fields(selection, d, event, level, out);
1522 }
1523 }
1524 }
1525 CompiledDetection::ArrayMatch { field, .. } => {
1526 if let Some(value) = event.get_field(field) {
1530 out.push(FieldMatch::new(field.clone(), value.to_json()));
1531 }
1532 }
1533 CompiledDetection::And(dets) => {
1534 for d in dets {
1535 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1536 collect_detection_fields(selection, d, event, level, out);
1537 }
1538 }
1539 }
1540 CompiledDetection::Conditional { .. } => {}
1543 CompiledDetection::Keywords(matcher) => {
1544 if level != MatchDetailLevel::Off {
1547 collect_keyword_matches(selection, matcher, event, level, out);
1548 }
1549 }
1550 }
1551}
1552
1553fn make_field_match(
1557 selection: &str,
1558 field: &str,
1559 value: serde_json::Value,
1560 matcher: &CompiledMatcher,
1561 level: MatchDetailLevel,
1562) -> FieldMatch {
1563 match level {
1564 MatchDetailLevel::Off => FieldMatch::new(field, value),
1565 MatchDetailLevel::Summary | MatchDetailLevel::Full => {
1566 let d = matcher.describe();
1567 FieldMatch {
1568 field: field.to_string(),
1569 value,
1570 selection: Some(selection.to_string()),
1571 matcher: Some(d.kind),
1572 pattern: if level == MatchDetailLevel::Full {
1573 d.pattern
1574 } else {
1575 None
1576 },
1577 case_sensitive: d.case_sensitive,
1578 negated: d.negated,
1579 }
1580 }
1581 }
1582}
1583
1584fn collect_keyword_matches(
1588 selection: &str,
1589 matcher: &CompiledMatcher,
1590 event: &impl Event,
1591 level: MatchDetailLevel,
1592 out: &mut Vec<FieldMatch>,
1593) {
1594 let descriptor = matcher.describe();
1595 let mut count = 0;
1596 for s in event.all_string_values() {
1597 if count >= MAX_KEYWORD_MATCHES {
1598 break;
1599 }
1600 if matcher.matches_str(&s) {
1601 count += 1;
1602 out.push(FieldMatch {
1603 field: "keyword".to_string(),
1604 value: serde_json::Value::String(s.into_owned()),
1605 selection: Some(selection.to_string()),
1606 matcher: Some(MatcherKind::Keyword),
1607 pattern: if level == MatchDetailLevel::Full {
1608 descriptor.pattern.clone()
1609 } else {
1610 None
1611 },
1612 case_sensitive: descriptor.case_sensitive,
1613 negated: descriptor.negated,
1614 });
1615 }
1616 }
1617}