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 logsource: LogSource,
66 pub detections: HashMap<String, CompiledDetection>,
68 pub conditions: Vec<ConditionExpr>,
70 pub include_event: bool,
73 pub custom_attributes: Arc<HashMap<String, serde_json::Value>>,
78}
79
80#[derive(Debug, Clone)]
82pub enum CompiledDetection {
83 AllOf(Vec<CompiledDetectionItem>),
85 AnyOf(Vec<CompiledDetection>),
87 Keywords(CompiledMatcher),
89 ArrayMatch {
93 field: String,
94 quantifier: ArrayQuantifier,
95 body: Box<CompiledDetection>,
96 },
97 And(Vec<CompiledDetection>),
100 Conditional {
105 named: HashMap<String, CompiledDetection>,
106 condition: ConditionExpr,
107 },
108}
109
110#[derive(Debug, Clone)]
112pub struct CompiledDetectionItem {
113 pub field: Option<String>,
115 pub matcher: CompiledMatcher,
117 pub exists: Option<bool>,
119 pub bloom_eligible: bool,
124}
125
126#[derive(Clone, Copy)]
132struct ModCtx {
133 contains: bool,
134 startswith: bool,
135 endswith: bool,
136 all: bool,
137 base64: bool,
138 base64offset: bool,
139 wide: bool,
140 utf16be: bool,
141 utf16: bool,
142 windash: bool,
143 re: bool,
144 cidr: bool,
145 cased: bool,
146 exists: bool,
147 fieldref: bool,
148 gt: bool,
149 gte: bool,
150 lt: bool,
151 lte: bool,
152 neq: bool,
153 ignore_case: bool,
154 multiline: bool,
155 dotall: bool,
156 expand: bool,
157 timestamp_part: Option<crate::matcher::TimePart>,
158}
159
160impl ModCtx {
161 fn from_modifiers(modifiers: &[Modifier]) -> Self {
162 let mut ctx = ModCtx {
163 contains: false,
164 startswith: false,
165 endswith: false,
166 all: false,
167 base64: false,
168 base64offset: false,
169 wide: false,
170 utf16be: false,
171 utf16: false,
172 windash: false,
173 re: false,
174 cidr: false,
175 cased: false,
176 exists: false,
177 fieldref: false,
178 gt: false,
179 gte: false,
180 lt: false,
181 lte: false,
182 neq: false,
183 ignore_case: false,
184 multiline: false,
185 dotall: false,
186 expand: false,
187 timestamp_part: None,
188 };
189 for m in modifiers {
190 match m {
191 Modifier::Contains => ctx.contains = true,
192 Modifier::StartsWith => ctx.startswith = true,
193 Modifier::EndsWith => ctx.endswith = true,
194 Modifier::All => ctx.all = true,
195 Modifier::Base64 => ctx.base64 = true,
196 Modifier::Base64Offset => ctx.base64offset = true,
197 Modifier::Wide => ctx.wide = true,
198 Modifier::Utf16be => ctx.utf16be = true,
199 Modifier::Utf16 => ctx.utf16 = true,
200 Modifier::WindAsh => ctx.windash = true,
201 Modifier::Re => ctx.re = true,
202 Modifier::Cidr => ctx.cidr = true,
203 Modifier::Cased => ctx.cased = true,
204 Modifier::Exists => ctx.exists = true,
205 Modifier::FieldRef => ctx.fieldref = true,
206 Modifier::Gt => ctx.gt = true,
207 Modifier::Gte => ctx.gte = true,
208 Modifier::Lt => ctx.lt = true,
209 Modifier::Lte => ctx.lte = true,
210 Modifier::Neq => ctx.neq = true,
211 Modifier::IgnoreCase => ctx.ignore_case = true,
212 Modifier::Multiline => ctx.multiline = true,
213 Modifier::DotAll => ctx.dotall = true,
214 Modifier::Expand => ctx.expand = true,
215 Modifier::Hour => ctx.timestamp_part = Some(crate::matcher::TimePart::Hour),
216 Modifier::Day => ctx.timestamp_part = Some(crate::matcher::TimePart::Day),
217 Modifier::Week => ctx.timestamp_part = Some(crate::matcher::TimePart::Week),
218 Modifier::Month => ctx.timestamp_part = Some(crate::matcher::TimePart::Month),
219 Modifier::Year => ctx.timestamp_part = Some(crate::matcher::TimePart::Year),
220 Modifier::Minute => ctx.timestamp_part = Some(crate::matcher::TimePart::Minute),
221 }
222 }
223 ctx
224 }
225
226 fn is_case_insensitive(&self) -> bool {
229 !self.cased
230 }
231
232 fn has_numeric_comparison(&self) -> bool {
234 self.gt || self.gte || self.lt || self.lte
235 }
236
237 fn has_neq(&self) -> bool {
239 self.neq
240 }
241}
242
243pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
251 let ir = rsigma_ir::lower_rule(rule, &rsigma_ir::LowerOptions::default())?;
252 compile_to_compiled(&ir)
253}
254
255pub fn evaluate_rule(rule: &CompiledRule, event: &impl Event) -> Option<EvaluationResult> {
263 evaluate_rule_with_bloom(
264 rule,
265 event,
266 &crate::engine::bloom_index::NoBloom,
267 MatchDetailLevel::Off,
268 )
269}
270
271pub(crate) fn evaluate_rule_with_bloom<E, B>(
279 rule: &CompiledRule,
280 event: &E,
281 bloom: &B,
282 level: MatchDetailLevel,
283) -> Option<EvaluationResult>
284where
285 E: Event,
286 B: crate::engine::bloom_index::BloomLookup,
287{
288 for condition in &rule.conditions {
289 let mut matched_selections = Vec::new();
290 if eval_condition_with_bloom(
291 condition,
292 &rule.detections,
293 event,
294 &mut matched_selections,
295 bloom,
296 ) {
297 let matched_fields =
298 collect_field_matches(&matched_selections, &rule.detections, event, level);
299
300 let event_data = if rule.include_event {
301 Some(event.to_json())
302 } else {
303 None
304 };
305
306 return Some(EvaluationResult {
307 header: RuleHeader {
308 rule_title: rule.title.clone(),
309 rule_id: rule.id.clone(),
310 level: rule.level,
311 tags: rule.tags.clone(),
312 custom_attributes: rule.custom_attributes.clone(),
313 enrichments: None,
314 },
315 body: ResultBody::Detection(DetectionBody {
316 matched_selections,
317 matched_fields,
318 event: event_data,
319 }),
320 });
321 }
322 }
323 None
324}
325
326pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
335 match detection {
336 Detection::AllOf(items) => {
337 if items.is_empty() {
338 return Err(EvalError::InvalidModifiers(
339 "AllOf detection must not be empty (vacuous truth)".into(),
340 ));
341 }
342 let compiled: Result<Vec<_>> = items.iter().map(compile_detection_item).collect();
343 Ok(CompiledDetection::AllOf(compiled?))
344 }
345 Detection::AnyOf(dets) => {
346 if dets.is_empty() {
347 return Err(EvalError::InvalidModifiers(
348 "AnyOf detection must not be empty (would never match)".into(),
349 ));
350 }
351 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
352 Ok(CompiledDetection::AnyOf(compiled?))
353 }
354 Detection::ArrayMatch {
355 field,
356 quantifier,
357 body,
358 } => {
359 let compiled_body = compile_detection(body)?;
360 Ok(CompiledDetection::ArrayMatch {
361 field: field.clone(),
362 quantifier: *quantifier,
363 body: Box::new(compiled_body),
364 })
365 }
366 Detection::And(dets) => {
367 if dets.is_empty() {
368 return Err(EvalError::InvalidModifiers(
369 "And detection must not be empty".into(),
370 ));
371 }
372 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
373 Ok(CompiledDetection::And(compiled?))
374 }
375 Detection::Conditional { named, condition } => {
376 if named.is_empty() {
377 return Err(EvalError::InvalidModifiers(
378 "Conditional detection must have at least one named sub-selection".into(),
379 ));
380 }
381 let compiled: Result<HashMap<String, CompiledDetection>> = named
382 .iter()
383 .map(|(k, d)| Ok((k.clone(), compile_detection(d)?)))
384 .collect();
385 Ok(CompiledDetection::Conditional {
386 named: compiled?,
387 condition: condition.clone(),
388 })
389 }
390 Detection::Keywords(values) => {
391 let ci = true; let matchers: Vec<CompiledMatcher> = values
393 .iter()
394 .map(|v| compile_value_default(v, ci))
395 .collect::<Result<Vec<_>>>()?;
396 let matcher = optimizer::optimize_any_of(matchers);
398 Ok(CompiledDetection::Keywords(matcher))
399 }
400 }
401}
402
403fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
404 let ctx = ModCtx::from_modifiers(&item.field.modifiers);
405
406 validate_modifiers(&ctx, &item.field.modifiers)?;
415
416 if ctx.exists {
418 let expect = match item.values.first() {
419 Some(SigmaValue::Bool(b)) => *b,
420 Some(SigmaValue::String(s)) => match s.as_plain().as_deref() {
421 Some("true") | Some("yes") => true,
422 Some("false") | Some("no") => false,
423 _ => true,
424 },
425 _ => true,
426 };
427 return Ok(CompiledDetectionItem {
428 field: item.field.name.clone(),
429 matcher: CompiledMatcher::Exists(expect),
430 exists: Some(expect),
431 bloom_eligible: false,
432 });
433 }
434
435 if ctx.all && item.values.len() <= 1 {
437 return Err(EvalError::InvalidModifiers(
438 "|all modifier requires more than one value".to_string(),
439 ));
440 }
441
442 let matchers: Result<Vec<CompiledMatcher>> =
444 item.values.iter().map(|v| compile_value(v, &ctx)).collect();
445 let matchers = matchers?;
446
447 let combined = if ctx.all {
454 if matchers.len() == 1 {
455 matchers
456 .into_iter()
457 .next()
458 .unwrap_or(CompiledMatcher::AllOf(vec![]))
459 } else {
460 CompiledMatcher::AllOf(matchers)
461 }
462 } else {
463 optimizer::optimize_any_of(matchers)
464 };
465
466 let bloom_eligible = item.field.name.is_some()
467 && crate::engine::bloom_index::is_positive_substring_matcher(&combined);
468
469 Ok(CompiledDetectionItem {
470 field: item.field.name.clone(),
471 matcher: combined,
472 exists: None,
473 bloom_eligible,
474 })
475}
476
477fn validate_modifiers(ctx: &ModCtx, modifiers: &[Modifier]) -> Result<()> {
514 let mut operators: Vec<&'static str> = Vec::new();
516 if ctx.contains {
517 operators.push("contains");
518 }
519 if ctx.startswith {
520 operators.push("startswith");
521 }
522 if ctx.endswith {
523 operators.push("endswith");
524 }
525 if ctx.re {
526 operators.push("re");
527 }
528 if ctx.cidr {
529 operators.push("cidr");
530 }
531 if ctx.exists {
532 operators.push("exists");
533 }
534 if ctx.fieldref {
535 operators.push("fieldref");
536 }
537 if ctx.gt {
538 operators.push("gt");
539 }
540 if ctx.gte {
541 operators.push("gte");
542 }
543 if ctx.lt {
544 operators.push("lt");
545 }
546 if ctx.lte {
547 operators.push("lte");
548 }
549 for m in modifiers {
550 match m {
551 Modifier::Minute => operators.push("minute"),
552 Modifier::Hour => operators.push("hour"),
553 Modifier::Day => operators.push("day"),
554 Modifier::Week => operators.push("week"),
555 Modifier::Month => operators.push("month"),
556 Modifier::Year => operators.push("year"),
557 _ => {}
558 }
559 }
560 if operators.len() > 1 {
561 return Err(EvalError::InvalidModifiers(format!(
562 "conflicting modifiers: at most one operator may be set per field; \
563 got |{}",
564 operators.join(", |")
565 )));
566 }
567
568 let mut wide_encodings: Vec<&'static str> = Vec::new();
570 if ctx.wide {
571 wide_encodings.push("wide");
572 }
573 if ctx.utf16 {
574 wide_encodings.push("utf16");
575 }
576 if ctx.utf16be {
577 wide_encodings.push("utf16be");
578 }
579 if wide_encodings.len() > 1 {
580 return Err(EvalError::InvalidModifiers(format!(
581 "conflicting modifiers: |wide, |utf16, and |utf16be are mutually \
582 exclusive UTF-16 encodings; got |{}",
583 wide_encodings.join(", |")
584 )));
585 }
586
587 if ctx.base64 && ctx.base64offset {
589 return Err(EvalError::InvalidModifiers(
590 "conflicting modifiers: |base64 and |base64offset are mutually \
591 exclusive base64 strategies; pick one"
592 .into(),
593 ));
594 }
595
596 let has_non_string_operator = ctx.re
601 || ctx.cidr
602 || ctx.exists
603 || ctx.fieldref
604 || ctx.has_numeric_comparison()
605 || ctx.timestamp_part.is_some();
606 if has_non_string_operator {
607 let mut transforms: Vec<&'static str> = Vec::new();
608 if ctx.base64 {
609 transforms.push("base64");
610 }
611 if ctx.base64offset {
612 transforms.push("base64offset");
613 }
614 if ctx.wide {
615 transforms.push("wide");
616 }
617 if ctx.utf16 {
618 transforms.push("utf16");
619 }
620 if ctx.utf16be {
621 transforms.push("utf16be");
622 }
623 if ctx.windash {
624 transforms.push("windash");
625 }
626 if ctx.expand {
627 transforms.push("expand");
628 }
629 if !transforms.is_empty() {
630 return Err(EvalError::InvalidModifiers(format!(
631 "conflicting modifiers: value transformations |{} only apply \
632 to string match operators (default eq, contains, startswith, \
633 endswith) and cannot be combined with the operator that is \
634 also set on this field",
635 transforms.join(", |")
636 )));
637 }
638 }
639
640 if !ctx.re {
642 let mut regex_flags: Vec<&'static str> = Vec::new();
643 if ctx.ignore_case {
644 regex_flags.push("i");
645 }
646 if ctx.multiline {
647 regex_flags.push("m");
648 }
649 if ctx.dotall {
650 regex_flags.push("s");
651 }
652 if !regex_flags.is_empty() {
653 return Err(EvalError::InvalidModifiers(format!(
654 "regex flag modifiers |{} have no effect without |re; \
655 case sensitivity for substring or equality matching is \
656 controlled by |cased (or its absence, which keeps the \
657 default case-insensitive behavior)",
658 regex_flags.join(", |")
659 )));
660 }
661 }
662
663 Ok(())
664}
665
666fn compile_value(value: &SigmaValue, ctx: &ModCtx) -> Result<CompiledMatcher> {
672 let ci = ctx.is_case_insensitive();
673
674 if ctx.expand {
678 let plain = value_to_plain_string(value)?;
679 let template = crate::matcher::parse_expand_template(&plain);
680 return Ok(CompiledMatcher::Expand {
681 template,
682 case_insensitive: ci,
683 });
684 }
685
686 if let Some(part) = ctx.timestamp_part {
688 let inner = match value {
691 SigmaValue::Integer(n) => CompiledMatcher::NumericEq(*n as f64),
692 SigmaValue::Float(n) => CompiledMatcher::NumericEq(*n),
693 SigmaValue::String(s) => {
694 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
695 let n: f64 = plain.parse().map_err(|_| {
696 EvalError::IncompatibleValue(format!(
697 "timestamp part modifier requires numeric value, got: {plain}"
698 ))
699 })?;
700 CompiledMatcher::NumericEq(n)
701 }
702 _ => {
703 return Err(EvalError::IncompatibleValue(
704 "timestamp part modifier requires numeric value".into(),
705 ));
706 }
707 };
708 return Ok(CompiledMatcher::TimestampPart {
709 part,
710 inner: Box::new(inner),
711 });
712 }
713
714 if ctx.fieldref {
716 let field_name = value_to_plain_string(value)?;
717 return Ok(CompiledMatcher::FieldRef {
718 field: field_name,
719 case_insensitive: ci,
720 });
721 }
722
723 if ctx.re {
727 let pattern = value_to_plain_string(value)?;
728 let regex = build_regex(&pattern, ctx.ignore_case, ctx.multiline, ctx.dotall)?;
729 return Ok(CompiledMatcher::Regex(regex));
730 }
731
732 if ctx.cidr {
734 let cidr_str = value_to_plain_string(value)?;
735 let net: ipnet::IpNet = cidr_str
736 .parse()
737 .map_err(|e: ipnet::AddrParseError| EvalError::InvalidCidr(e))?;
738 return Ok(CompiledMatcher::Cidr(net));
739 }
740
741 if ctx.has_numeric_comparison() {
743 let n = value_to_f64(value)?;
744 if ctx.gt {
745 return Ok(CompiledMatcher::NumericGt(n));
746 }
747 if ctx.gte {
748 return Ok(CompiledMatcher::NumericGte(n));
749 }
750 if ctx.lt {
751 return Ok(CompiledMatcher::NumericLt(n));
752 }
753 if ctx.lte {
754 return Ok(CompiledMatcher::NumericLte(n));
755 }
756 }
757
758 if ctx.has_neq() {
760 let mut inner_ctx = ModCtx { ..*ctx };
762 inner_ctx.neq = false;
763 let inner = compile_value(value, &inner_ctx)?;
764 return Ok(CompiledMatcher::Not(Box::new(inner)));
765 }
766
767 match value {
769 SigmaValue::Integer(n) => {
770 if ctx.contains || ctx.startswith || ctx.endswith {
771 return compile_string_value(&n.to_string(), ctx);
773 }
774 return Ok(CompiledMatcher::NumericEq(*n as f64));
775 }
776 SigmaValue::Float(n) => {
777 if ctx.contains || ctx.startswith || ctx.endswith {
778 return compile_string_value(&n.to_string(), ctx);
779 }
780 return Ok(CompiledMatcher::NumericEq(*n));
781 }
782 SigmaValue::Bool(b) => return Ok(CompiledMatcher::BoolEq(*b)),
783 SigmaValue::Null => return Ok(CompiledMatcher::Null),
784 SigmaValue::String(_) => {} }
786
787 let sigma_str = match value {
789 SigmaValue::String(s) => s,
790 _ => unreachable!(),
791 };
792
793 let mut bytes = sigma_string_to_bytes(sigma_str);
795
796 if ctx.wide {
798 bytes = to_utf16le_bytes(&bytes);
799 }
800
801 if ctx.utf16be {
803 bytes = to_utf16be_bytes(&bytes);
804 }
805
806 if ctx.utf16 {
808 bytes = to_utf16_bom_bytes(&bytes);
809 }
810
811 if ctx.base64 {
813 let encoded = BASE64_STANDARD.encode(&bytes);
814 return compile_string_value(&encoded, ctx);
815 }
816
817 if ctx.base64offset {
819 let patterns = base64_offset_patterns(&bytes);
820 let matchers: Vec<CompiledMatcher> = patterns
821 .into_iter()
822 .map(|p| {
823 CompiledMatcher::Contains {
825 value: if ci { p.to_lowercase() } else { p },
826 case_insensitive: ci,
827 }
828 })
829 .collect();
830 return Ok(CompiledMatcher::AnyOf(matchers));
831 }
832
833 if ctx.windash {
835 let plain = sigma_str
836 .as_plain()
837 .unwrap_or_else(|| sigma_str.original.clone());
838 let variants = expand_windash(&plain)?;
839 let matchers: Result<Vec<CompiledMatcher>> = variants
840 .into_iter()
841 .map(|v| compile_string_value(&v, ctx))
842 .collect();
843 return Ok(CompiledMatcher::AnyOf(matchers?));
844 }
845
846 compile_sigma_string(sigma_str, ctx)
848}
849
850fn compile_sigma_string(sigma_str: &SigmaString, ctx: &ModCtx) -> Result<CompiledMatcher> {
852 let ci = ctx.is_case_insensitive();
853
854 if sigma_str.is_plain() {
856 let plain = sigma_str.as_plain().unwrap_or_default();
857 return compile_string_value(&plain, ctx);
858 }
859
860 let mut pattern = String::new();
865 if ci {
866 pattern.push_str("(?i)");
867 }
868
869 if !ctx.contains && !ctx.startswith {
870 pattern.push('^');
871 }
872
873 for part in &sigma_str.parts {
874 match part {
875 StringPart::Plain(text) => {
876 pattern.push_str(®ex::escape(text));
877 }
878 StringPart::Special(SpecialChar::WildcardMulti) => {
879 pattern.push_str(".*");
880 }
881 StringPart::Special(SpecialChar::WildcardSingle) => {
882 pattern.push('.');
883 }
884 }
885 }
886
887 if !ctx.contains && !ctx.endswith {
888 pattern.push('$');
889 }
890
891 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
892 Ok(CompiledMatcher::Regex(regex))
893}
894
895fn compile_string_value(plain: &str, ctx: &ModCtx) -> Result<CompiledMatcher> {
897 let ci = ctx.is_case_insensitive();
898
899 if ctx.contains {
900 Ok(CompiledMatcher::Contains {
901 value: if ci {
902 plain.to_lowercase()
903 } else {
904 plain.to_string()
905 },
906 case_insensitive: ci,
907 })
908 } else if ctx.startswith {
909 Ok(CompiledMatcher::StartsWith {
910 value: if ci {
911 plain.to_lowercase()
912 } else {
913 plain.to_string()
914 },
915 case_insensitive: ci,
916 })
917 } else if ctx.endswith {
918 Ok(CompiledMatcher::EndsWith {
919 value: if ci {
920 plain.to_lowercase()
921 } else {
922 plain.to_string()
923 },
924 case_insensitive: ci,
925 })
926 } else {
927 Ok(CompiledMatcher::Exact {
928 value: if ci {
929 plain.to_lowercase()
930 } else {
931 plain.to_string()
932 },
933 case_insensitive: ci,
934 })
935 }
936}
937
938fn compile_value_default(value: &SigmaValue, case_insensitive: bool) -> Result<CompiledMatcher> {
940 match value {
941 SigmaValue::String(s) => {
942 if s.is_plain() {
943 let plain = s.as_plain().unwrap_or_default();
944 Ok(CompiledMatcher::Contains {
945 value: if case_insensitive {
946 plain.to_lowercase()
947 } else {
948 plain
949 },
950 case_insensitive,
951 })
952 } else {
953 let pattern = sigma_string_to_regex(&s.parts, case_insensitive);
955 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
956 Ok(CompiledMatcher::Regex(regex))
957 }
958 }
959 SigmaValue::Integer(n) => Ok(CompiledMatcher::NumericEq(*n as f64)),
960 SigmaValue::Float(n) => Ok(CompiledMatcher::NumericEq(*n)),
961 SigmaValue::Bool(b) => Ok(CompiledMatcher::BoolEq(*b)),
962 SigmaValue::Null => Ok(CompiledMatcher::Null),
963 }
964}
965
966pub fn eval_condition(
975 expr: &ConditionExpr,
976 detections: &HashMap<String, CompiledDetection>,
977 event: &impl Event,
978 matched_selections: &mut Vec<String>,
979) -> bool {
980 eval_condition_with_bloom(
981 expr,
982 detections,
983 event,
984 matched_selections,
985 &crate::engine::bloom_index::NoBloom,
986 )
987}
988
989pub(crate) fn eval_condition_with_bloom<E, B>(
995 expr: &ConditionExpr,
996 detections: &HashMap<String, CompiledDetection>,
997 event: &E,
998 matched_selections: &mut Vec<String>,
999 bloom: &B,
1000) -> bool
1001where
1002 E: Event,
1003 B: crate::engine::bloom_index::BloomLookup,
1004{
1005 match expr {
1006 ConditionExpr::Identifier(name) => {
1007 if let Some(det) = detections.get(name) {
1008 let result = eval_detection_with_bloom(det, event, bloom);
1009 if result {
1010 matched_selections.push(name.clone());
1011 }
1012 result
1013 } else {
1014 false
1015 }
1016 }
1017
1018 ConditionExpr::And(exprs) => exprs
1019 .iter()
1020 .all(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1021
1022 ConditionExpr::Or(exprs) => exprs
1023 .iter()
1024 .any(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1025
1026 ConditionExpr::Not(inner) => {
1027 !eval_condition_with_bloom(inner, detections, event, matched_selections, bloom)
1028 }
1029
1030 ConditionExpr::Selector {
1031 quantifier,
1032 pattern,
1033 } => {
1034 let matching_names: Vec<&String> = detections
1035 .keys()
1036 .filter(|name| pattern.matches_detection_name(name))
1037 .collect();
1038
1039 let mut match_count = 0u64;
1040 for name in &matching_names {
1041 if let Some(det) = detections.get(*name)
1042 && eval_detection_with_bloom(det, event, bloom)
1043 {
1044 match_count += 1;
1045 matched_selections.push((*name).clone());
1046 }
1047 }
1048
1049 match quantifier {
1050 Quantifier::Any => match_count >= 1,
1051 Quantifier::All => match_count == matching_names.len() as u64,
1052 Quantifier::Count(n) => match_count >= *n,
1053 }
1054 }
1055 }
1056}
1057
1058#[cfg(test)]
1063fn eval_detection_item(item: &CompiledDetectionItem, event: &impl Event) -> bool {
1064 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1065}
1066
1067pub(crate) fn eval_detection_no_bloom(detection: &CompiledDetection, event: &impl Event) -> bool {
1073 eval_detection_with_bloom(detection, event, &crate::engine::bloom_index::NoBloom)
1074}
1075
1076pub(crate) fn eval_detection_item_no_bloom(
1080 item: &CompiledDetectionItem,
1081 event: &impl Event,
1082) -> bool {
1083 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1084}
1085
1086fn eval_detection_with_bloom<E, B>(detection: &CompiledDetection, event: &E, bloom: &B) -> bool
1088where
1089 E: Event,
1090 B: crate::engine::bloom_index::BloomLookup,
1091{
1092 match detection {
1093 CompiledDetection::AllOf(items) => items
1094 .iter()
1095 .all(|item| eval_detection_item_with_bloom(item, event, bloom)),
1096 CompiledDetection::AnyOf(dets) => dets
1097 .iter()
1098 .any(|d| eval_detection_with_bloom(d, event, bloom)),
1099 CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
1100 CompiledDetection::ArrayMatch {
1101 field,
1102 quantifier,
1103 body,
1104 } => match event.get_field(field) {
1105 Some(value) => eval_array_quantified(&value, *quantifier, body, event),
1106 None => array_quantifier_matches_empty(*quantifier),
1107 },
1108 CompiledDetection::And(dets) => dets
1109 .iter()
1110 .all(|d| eval_detection_with_bloom(d, event, bloom)),
1111 CompiledDetection::Conditional { named, condition } => {
1115 eval_condition_with_bloom(condition, named, event, &mut Vec::new(), bloom)
1116 }
1117 }
1118}
1119
1120fn eval_array_quantified<E: Event>(
1126 value: &EventValue,
1127 quantifier: ArrayQuantifier,
1128 body: &CompiledDetection,
1129 outer: &E,
1130) -> bool {
1131 match value {
1132 EventValue::Array(members) => match quantifier {
1133 ArrayQuantifier::Any => members.iter().any(|m| eval_array_body(body, m, outer)),
1134 ArrayQuantifier::All => {
1135 !members.is_empty() && members.iter().all(|m| eval_array_body(body, m, outer))
1136 }
1137 ArrayQuantifier::AllOrEmpty => members.iter().all(|m| eval_array_body(body, m, outer)),
1138 ArrayQuantifier::None => !members.iter().any(|m| eval_array_body(body, m, outer)),
1139 },
1140 EventValue::Null => array_quantifier_matches_empty(quantifier),
1143 single => match quantifier {
1145 ArrayQuantifier::None => !eval_array_body(body, single, outer),
1146 _ => eval_array_body(body, single, outer),
1147 },
1148 }
1149}
1150
1151fn array_quantifier_matches_empty(quantifier: ArrayQuantifier) -> bool {
1153 matches!(
1154 quantifier,
1155 ArrayQuantifier::None | ArrayQuantifier::AllOrEmpty
1156 )
1157}
1158
1159fn eval_array_body<E: Event>(body: &CompiledDetection, member: &EventValue, outer: &E) -> bool {
1164 match body {
1165 CompiledDetection::AllOf(items) => items
1166 .iter()
1167 .all(|item| eval_array_item(item, member, outer)),
1168 CompiledDetection::AnyOf(dets) => dets.iter().any(|d| eval_array_body(d, member, outer)),
1169 CompiledDetection::And(dets) => dets.iter().all(|d| eval_array_body(d, member, outer)),
1170 CompiledDetection::ArrayMatch {
1171 field,
1172 quantifier,
1173 body: inner,
1174 } => match element_field(member, field) {
1175 Some(value) => eval_array_quantified(value, *quantifier, inner, outer),
1176 None => array_quantifier_matches_empty(*quantifier),
1177 },
1178 CompiledDetection::Keywords(matcher) => matcher.matches(member, outer),
1180 CompiledDetection::Conditional { named, condition } => {
1183 eval_array_condition(condition, named, member, outer)
1184 }
1185 }
1186}
1187
1188fn eval_array_condition<E: Event>(
1196 expr: &ConditionExpr,
1197 named: &HashMap<String, CompiledDetection>,
1198 member: &EventValue,
1199 outer: &E,
1200) -> bool {
1201 match expr {
1202 ConditionExpr::Identifier(name) => named
1203 .get(name)
1204 .is_some_and(|d| eval_array_body(d, member, outer)),
1205 ConditionExpr::And(exprs) => exprs
1206 .iter()
1207 .all(|e| eval_array_condition(e, named, member, outer)),
1208 ConditionExpr::Or(exprs) => exprs
1209 .iter()
1210 .any(|e| eval_array_condition(e, named, member, outer)),
1211 ConditionExpr::Not(inner) => !eval_array_condition(inner, named, member, outer),
1212 ConditionExpr::Selector {
1213 quantifier,
1214 pattern,
1215 } => {
1216 let names: Vec<&String> = named
1217 .keys()
1218 .filter(|n| pattern.matches_detection_name(n))
1219 .collect();
1220 let count = names
1221 .iter()
1222 .filter(|n| {
1223 named
1224 .get(**n)
1225 .is_some_and(|d| eval_array_body(d, member, outer))
1226 })
1227 .count() as u64;
1228 match quantifier {
1229 Quantifier::Any => count >= 1,
1230 Quantifier::All => count == names.len() as u64,
1231 Quantifier::Count(n) => count >= *n,
1232 }
1233 }
1234 }
1235}
1236
1237fn eval_array_item<E: Event>(item: &CompiledDetectionItem, member: &EventValue, outer: &E) -> bool {
1239 if let Some(expect_exists) = item.exists {
1240 let exists = match &item.field {
1241 Some(name) => element_field(member, name).is_some_and(|v| !v.is_null()),
1242 None => !member.is_null(),
1243 };
1244 return exists == expect_exists;
1245 }
1246
1247 match &item.field {
1248 Some(name) => match element_field(member, name) {
1249 Some(value) => item.matcher.matches(value, outer),
1250 None => matches!(item.matcher, CompiledMatcher::Null),
1251 },
1252 None => item.matcher.matches(member, outer),
1254 }
1255}
1256
1257fn element_field<'a>(member: &'a EventValue<'a>, path: &str) -> Option<&'a EventValue<'a>> {
1263 if let EventValue::Map(entries) = member
1264 && let Some((_, v)) = entries.iter().find(|(k, _)| k.as_ref() == path)
1265 {
1266 return Some(v);
1267 }
1268 let ops = parse_event_ops(path);
1269 nav_event_value(member, &ops)
1270}
1271
1272enum EventOp<'a> {
1273 Key(Cow<'a, str>),
1274 Index(i64),
1275}
1276
1277fn parse_event_ops(path: &str) -> Vec<EventOp<'_>> {
1281 let mut ops = Vec::new();
1282 for part in path.split('.') {
1283 match first_unescaped(part, b'[') {
1284 Some(bpos) if index_groups(&part[bpos..]).is_some() => {
1285 let name = &part[..bpos];
1286 if !name.is_empty() {
1287 ops.push(EventOp::Key(unescape_brackets(name)));
1288 }
1289 for idx in index_groups(&part[bpos..]).expect("checked") {
1290 ops.push(EventOp::Index(idx));
1291 }
1292 }
1293 _ => ops.push(EventOp::Key(unescape_brackets(part))),
1294 }
1295 }
1296 ops
1297}
1298
1299fn index_groups(s: &str) -> Option<Vec<i64>> {
1302 let mut out = Vec::new();
1303 let mut rem = s;
1304 while !rem.is_empty() {
1305 let rest = rem.strip_prefix('[')?;
1306 let close = rest.find(']')?;
1307 out.push(rest[..close].parse().ok()?);
1308 rem = &rest[close + 1..];
1309 }
1310 Some(out)
1311}
1312
1313fn nav_event_value<'a>(
1314 current: &'a EventValue<'a>,
1315 ops: &[EventOp<'_>],
1316) -> Option<&'a EventValue<'a>> {
1317 let Some((op, rest)) = ops.split_first() else {
1318 return Some(current);
1319 };
1320 match op {
1321 EventOp::Key(key) => match current {
1322 EventValue::Map(entries) => {
1323 let next = entries
1324 .iter()
1325 .find(|(k, _)| k.as_ref() == key.as_ref())
1326 .map(|(_, v)| v)?;
1327 nav_event_value(next, rest)
1328 }
1329 EventValue::Array(members) => members.iter().find_map(|m| nav_event_value(m, ops)),
1330 _ => None,
1331 },
1332 EventOp::Index(i) => match current {
1333 EventValue::Array(members) => {
1334 let idx = crate::event::resolve_array_index(*i, members.len())?;
1335 nav_event_value(members.get(idx)?, rest)
1336 }
1337 _ => None,
1338 },
1339 }
1340}
1341
1342fn eval_detection_item_with_bloom<E, B>(item: &CompiledDetectionItem, event: &E, bloom: &B) -> bool
1349where
1350 E: Event,
1351 B: crate::engine::bloom_index::BloomLookup,
1352{
1353 if let Some(expect_exists) = item.exists {
1354 if let Some(field) = &item.field {
1355 let exists = event.get_field(field).is_some_and(|v| !v.is_null());
1356 return exists == expect_exists;
1357 }
1358 return !expect_exists;
1359 }
1360
1361 match &item.field {
1362 Some(field_name) => {
1363 if let Some(value) = event.get_field(field_name) {
1364 if item.bloom_eligible
1365 && bloom.verdict_for_field(field_name)
1366 == crate::engine::bloom_index::BloomVerdict::DefinitelyNoMatch
1367 {
1368 return false;
1369 }
1370 item.matcher.matches(&value, event)
1371 } else {
1372 matches!(item.matcher, CompiledMatcher::Null)
1373 }
1374 }
1375 None => item.matcher.matches_keyword(event),
1376 }
1377}
1378
1379const MAX_KEYWORD_MATCHES: usize = 16;
1383
1384fn collect_field_matches(
1392 selection_names: &[String],
1393 detections: &HashMap<String, CompiledDetection>,
1394 event: &impl Event,
1395 level: MatchDetailLevel,
1396) -> Vec<FieldMatch> {
1397 let mut matches = Vec::new();
1398 for name in selection_names {
1399 if let Some(det) = detections.get(name) {
1400 collect_detection_fields(name, det, event, level, &mut matches);
1401 }
1402 }
1403 matches
1404}
1405
1406fn collect_detection_fields(
1407 selection: &str,
1408 detection: &CompiledDetection,
1409 event: &impl Event,
1410 level: MatchDetailLevel,
1411 out: &mut Vec<FieldMatch>,
1412) {
1413 match detection {
1414 CompiledDetection::AllOf(items) => {
1415 for item in items {
1416 match &item.field {
1417 Some(field_name) => {
1418 if let Some(value) = event.get_field(field_name) {
1419 if item.matcher.matches(&value, event) {
1420 out.push(make_field_match(
1421 selection,
1422 field_name,
1423 value.to_json(),
1424 &item.matcher,
1425 level,
1426 ));
1427 }
1428 } else if level != MatchDetailLevel::Off
1429 && matches!(item.matcher, CompiledMatcher::Null)
1430 {
1431 out.push(make_field_match(
1434 selection,
1435 field_name,
1436 serde_json::Value::Null,
1437 &item.matcher,
1438 level,
1439 ));
1440 }
1441 }
1442 None => {
1443 if level != MatchDetailLevel::Off {
1445 collect_keyword_matches(selection, &item.matcher, event, level, out);
1446 }
1447 }
1448 }
1449 }
1450 }
1451 CompiledDetection::AnyOf(dets) => {
1452 for d in dets {
1453 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1454 collect_detection_fields(selection, d, event, level, out);
1455 }
1456 }
1457 }
1458 CompiledDetection::ArrayMatch { field, .. } => {
1459 if let Some(value) = event.get_field(field) {
1463 out.push(FieldMatch::new(field.clone(), value.to_json()));
1464 }
1465 }
1466 CompiledDetection::And(dets) => {
1467 for d in dets {
1468 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1469 collect_detection_fields(selection, d, event, level, out);
1470 }
1471 }
1472 }
1473 CompiledDetection::Conditional { .. } => {}
1476 CompiledDetection::Keywords(matcher) => {
1477 if level != MatchDetailLevel::Off {
1480 collect_keyword_matches(selection, matcher, event, level, out);
1481 }
1482 }
1483 }
1484}
1485
1486fn make_field_match(
1490 selection: &str,
1491 field: &str,
1492 value: serde_json::Value,
1493 matcher: &CompiledMatcher,
1494 level: MatchDetailLevel,
1495) -> FieldMatch {
1496 match level {
1497 MatchDetailLevel::Off => FieldMatch::new(field, value),
1498 MatchDetailLevel::Summary | MatchDetailLevel::Full => {
1499 let d = matcher.describe();
1500 FieldMatch {
1501 field: field.to_string(),
1502 value,
1503 selection: Some(selection.to_string()),
1504 matcher: Some(d.kind),
1505 pattern: if level == MatchDetailLevel::Full {
1506 d.pattern
1507 } else {
1508 None
1509 },
1510 case_sensitive: d.case_sensitive,
1511 negated: d.negated,
1512 }
1513 }
1514 }
1515}
1516
1517fn collect_keyword_matches(
1521 selection: &str,
1522 matcher: &CompiledMatcher,
1523 event: &impl Event,
1524 level: MatchDetailLevel,
1525 out: &mut Vec<FieldMatch>,
1526) {
1527 let descriptor = matcher.describe();
1528 let mut count = 0;
1529 for s in event.all_string_values() {
1530 if count >= MAX_KEYWORD_MATCHES {
1531 break;
1532 }
1533 if matcher.matches_str(&s) {
1534 count += 1;
1535 out.push(FieldMatch {
1536 field: "keyword".to_string(),
1537 value: serde_json::Value::String(s.into_owned()),
1538 selection: Some(selection.to_string()),
1539 matcher: Some(MatcherKind::Keyword),
1540 pattern: if level == MatchDetailLevel::Full {
1541 descriptor.pattern.clone()
1542 } else {
1543 None
1544 },
1545 case_sensitive: descriptor.case_sensitive,
1546 negated: descriptor.negated,
1547 });
1548 }
1549 }
1550}