1mod array;
12mod from_ir;
13mod helpers;
14#[doc(hidden)]
15pub mod optimizer;
16#[cfg(test)]
17mod tests;
18
19pub(crate) use array::{
20 array_quantifier_from_member_matches, array_quantifier_matches_empty, decisive_member_verdict,
21 element_field, eval_array_body, eval_array_item, eval_array_quantified,
22 select_recorded_member_indices,
23};
24
25pub use from_ir::compile_to_compiled;
26
27#[cfg(test)]
30pub(crate) use optimizer::optimize_any_of as optimize_any_of_for_test;
31
32use std::collections::HashMap;
33use std::sync::Arc;
34
35use base64::Engine as Base64Engine;
36use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
37use regex::Regex;
38
39use rsigma_parser::value::{SpecialChar, StringPart};
40use rsigma_parser::{
41 ArrayQuantifier, ConditionExpr, Detection, DetectionItem, Level, LogSource, Modifier,
42 Quantifier, SigmaRule, SigmaString, SigmaValue,
43};
44
45use crate::error::{EvalError, Result};
46use crate::event::{Event, EventValue};
47use crate::matcher::{CompiledMatcher, sigma_string_to_regex};
48use crate::result::{
49 DetectionBody, EvaluationResult, FieldMatch, MatchDetailLevel, MatcherKind, ResultBody,
50 RuleHeader,
51};
52
53use helpers::{
54 base64_offset_patterns, build_regex, expand_windash, sigma_string_to_bytes, to_utf16_bom_bytes,
55 to_utf16be_bytes, to_utf16le_bytes, value_to_f64, value_to_plain_string,
56};
57pub(crate) use helpers::{yaml_to_json, yaml_to_json_map};
58
59#[derive(Debug, Clone)]
65pub struct CompiledRule {
66 pub title: String,
67 pub id: Option<String>,
68 pub level: Option<Level>,
69 pub tags: Vec<String>,
70 pub description: Option<String>,
73 pub falsepositives: Vec<String>,
76 pub logsource: LogSource,
77 pub detections: HashMap<String, CompiledDetection>,
79 pub conditions: Vec<ConditionExpr>,
81 pub include_event: bool,
84 pub custom_attributes: Arc<HashMap<String, serde_json::Value>>,
89}
90
91#[derive(Debug, Clone)]
93pub enum CompiledDetection {
94 AllOf(Vec<CompiledDetectionItem>),
96 AnyOf(Vec<CompiledDetection>),
98 Keywords(CompiledMatcher),
100 ArrayMatch {
104 field: String,
105 quantifier: ArrayQuantifier,
106 body: Box<CompiledDetection>,
107 },
108 And(Vec<CompiledDetection>),
111 Conditional {
116 named: HashMap<String, CompiledDetection>,
117 condition: ConditionExpr,
118 },
119}
120
121#[derive(Debug, Clone)]
123pub struct CompiledDetectionItem {
124 pub field: Option<String>,
126 pub matcher: CompiledMatcher,
128 pub exists: Option<bool>,
130 pub bloom_eligible: bool,
135}
136
137#[derive(Clone, Copy)]
143struct ModCtx {
144 contains: bool,
145 startswith: bool,
146 endswith: bool,
147 all: bool,
148 base64: bool,
149 base64offset: bool,
150 wide: bool,
151 utf16be: bool,
152 utf16: bool,
153 windash: bool,
154 re: bool,
155 cidr: bool,
156 cased: bool,
157 exists: bool,
158 fieldref: bool,
159 gt: bool,
160 gte: bool,
161 lt: bool,
162 lte: bool,
163 neq: bool,
164 ignore_case: bool,
165 multiline: bool,
166 dotall: bool,
167 expand: bool,
168 timestamp_part: Option<crate::matcher::TimePart>,
169}
170
171impl ModCtx {
172 fn from_modifiers(modifiers: &[Modifier]) -> Self {
173 let mut ctx = ModCtx {
174 contains: false,
175 startswith: false,
176 endswith: false,
177 all: false,
178 base64: false,
179 base64offset: false,
180 wide: false,
181 utf16be: false,
182 utf16: false,
183 windash: false,
184 re: false,
185 cidr: false,
186 cased: false,
187 exists: false,
188 fieldref: false,
189 gt: false,
190 gte: false,
191 lt: false,
192 lte: false,
193 neq: false,
194 ignore_case: false,
195 multiline: false,
196 dotall: false,
197 expand: false,
198 timestamp_part: None,
199 };
200 for m in modifiers {
201 match m {
202 Modifier::Contains => ctx.contains = true,
203 Modifier::StartsWith => ctx.startswith = true,
204 Modifier::EndsWith => ctx.endswith = true,
205 Modifier::All => ctx.all = true,
206 Modifier::Base64 => ctx.base64 = true,
207 Modifier::Base64Offset => ctx.base64offset = true,
208 Modifier::Wide => ctx.wide = true,
209 Modifier::Utf16be => ctx.utf16be = true,
210 Modifier::Utf16 => ctx.utf16 = true,
211 Modifier::WindAsh => ctx.windash = true,
212 Modifier::Re => ctx.re = true,
213 Modifier::Cidr => ctx.cidr = true,
214 Modifier::Cased => ctx.cased = true,
215 Modifier::Exists => ctx.exists = true,
216 Modifier::FieldRef => ctx.fieldref = true,
217 Modifier::Gt => ctx.gt = true,
218 Modifier::Gte => ctx.gte = true,
219 Modifier::Lt => ctx.lt = true,
220 Modifier::Lte => ctx.lte = true,
221 Modifier::Neq => ctx.neq = true,
222 Modifier::IgnoreCase => ctx.ignore_case = true,
223 Modifier::Multiline => ctx.multiline = true,
224 Modifier::DotAll => ctx.dotall = true,
225 Modifier::Expand => ctx.expand = true,
226 Modifier::Hour => ctx.timestamp_part = Some(crate::matcher::TimePart::Hour),
227 Modifier::Day => ctx.timestamp_part = Some(crate::matcher::TimePart::Day),
228 Modifier::Week => ctx.timestamp_part = Some(crate::matcher::TimePart::Week),
229 Modifier::Month => ctx.timestamp_part = Some(crate::matcher::TimePart::Month),
230 Modifier::Year => ctx.timestamp_part = Some(crate::matcher::TimePart::Year),
231 Modifier::Minute => ctx.timestamp_part = Some(crate::matcher::TimePart::Minute),
232 }
233 }
234 ctx
235 }
236
237 fn is_case_insensitive(&self) -> bool {
240 !self.cased
241 }
242
243 fn has_numeric_comparison(&self) -> bool {
245 self.gt || self.gte || self.lt || self.lte
246 }
247
248 fn has_neq(&self) -> bool {
250 self.neq
251 }
252}
253
254pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
262 let ir = rsigma_ir::lower_rule(rule, &rsigma_ir::LowerOptions::default())?;
263 compile_to_compiled(&ir)
264}
265
266pub fn evaluate_rule(rule: &CompiledRule, event: &impl Event) -> Option<EvaluationResult> {
274 evaluate_rule_with_bloom(
275 rule,
276 event,
277 &crate::engine::bloom_index::NoBloom,
278 MatchDetailLevel::Off,
279 )
280}
281
282pub(crate) fn evaluate_rule_with_bloom<E, B>(
290 rule: &CompiledRule,
291 event: &E,
292 bloom: &B,
293 level: MatchDetailLevel,
294) -> Option<EvaluationResult>
295where
296 E: Event,
297 B: crate::engine::bloom_index::BloomLookup,
298{
299 for condition in &rule.conditions {
300 if eval_condition_matches_with_bloom(condition, &rule.detections, event, bloom) {
301 let mut matched_selections = Vec::new();
302 let matched = eval_condition_with_bloom(
303 condition,
304 &rule.detections,
305 event,
306 &mut matched_selections,
307 bloom,
308 );
309 debug_assert!(matched, "detail pass must agree with boolean pass");
310 let matched_fields =
311 collect_field_matches(&matched_selections, &rule.detections, event, level);
312
313 let event_data = if rule.include_event {
314 Some(event.to_json())
315 } else {
316 None
317 };
318
319 return Some(EvaluationResult {
320 header: RuleHeader {
321 rule_title: rule.title.clone(),
322 rule_id: rule.id.clone(),
323 level: rule.level,
324 tags: rule.tags.clone(),
325 custom_attributes: rule.custom_attributes.clone(),
326 enrichments: None,
327 },
328 body: ResultBody::Detection(DetectionBody {
329 matched_selections,
330 matched_fields,
331 event: event_data,
332 }),
333 });
334 }
335 }
336 None
337}
338
339pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
348 match detection {
349 Detection::AllOf(items) => {
350 if items.is_empty() {
351 return Err(EvalError::InvalidModifiers(
352 "AllOf detection must not be empty (vacuous truth)".into(),
353 ));
354 }
355 let compiled: Result<Vec<_>> = items.iter().map(compile_detection_item).collect();
356 Ok(CompiledDetection::AllOf(compiled?))
357 }
358 Detection::AnyOf(dets) => {
359 if dets.is_empty() {
360 return Err(EvalError::InvalidModifiers(
361 "AnyOf detection must not be empty (would never match)".into(),
362 ));
363 }
364 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
365 Ok(CompiledDetection::AnyOf(compiled?))
366 }
367 Detection::ArrayMatch {
368 field,
369 quantifier,
370 body,
371 } => {
372 let compiled_body = compile_detection(body)?;
373 Ok(CompiledDetection::ArrayMatch {
374 field: field.clone(),
375 quantifier: *quantifier,
376 body: Box::new(compiled_body),
377 })
378 }
379 Detection::And(dets) => {
380 if dets.is_empty() {
381 return Err(EvalError::InvalidModifiers(
382 "And detection must not be empty".into(),
383 ));
384 }
385 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
386 Ok(CompiledDetection::And(compiled?))
387 }
388 Detection::Conditional { named, condition } => {
389 if named.is_empty() {
390 return Err(EvalError::InvalidModifiers(
391 "Conditional detection must have at least one named sub-selection".into(),
392 ));
393 }
394 let compiled: Result<HashMap<String, CompiledDetection>> = named
395 .iter()
396 .map(|(k, d)| Ok((k.clone(), compile_detection(d)?)))
397 .collect();
398 Ok(CompiledDetection::Conditional {
399 named: compiled?,
400 condition: condition.clone(),
401 })
402 }
403 Detection::Keywords(values) => {
404 let ci = true; let matchers: Vec<CompiledMatcher> = values
406 .iter()
407 .map(|v| compile_value_default(v, ci))
408 .collect::<Result<Vec<_>>>()?;
409 let matcher = optimizer::optimize_any_of(matchers);
411 Ok(CompiledDetection::Keywords(matcher))
412 }
413 }
414}
415
416fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
417 let ctx = ModCtx::from_modifiers(&item.field.modifiers);
418
419 validate_modifiers(&ctx, &item.field.modifiers)?;
428
429 if ctx.exists {
431 let expect = match item.values.first() {
432 Some(SigmaValue::Bool(b)) => *b,
433 Some(SigmaValue::String(s)) => match s.as_plain().as_deref() {
434 Some("true") | Some("yes") => true,
435 Some("false") | Some("no") => false,
436 _ => true,
437 },
438 _ => true,
439 };
440 return Ok(CompiledDetectionItem {
441 field: item.field.name.clone(),
442 matcher: CompiledMatcher::Exists(expect),
443 exists: Some(expect),
444 bloom_eligible: false,
445 });
446 }
447
448 if ctx.all && item.values.len() <= 1 {
450 return Err(EvalError::InvalidModifiers(
451 "|all modifier requires more than one value".to_string(),
452 ));
453 }
454
455 let matchers: Result<Vec<CompiledMatcher>> =
457 item.values.iter().map(|v| compile_value(v, &ctx)).collect();
458 let matchers = matchers?;
459
460 let combined = if ctx.all {
467 if matchers.len() == 1 {
468 matchers
469 .into_iter()
470 .next()
471 .unwrap_or(CompiledMatcher::AllOf(vec![]))
472 } else {
473 CompiledMatcher::AllOf(matchers)
474 }
475 } else {
476 optimizer::optimize_any_of(matchers)
477 };
478
479 let bloom_eligible = item.field.name.is_some()
480 && crate::engine::bloom_index::is_positive_substring_matcher(&combined);
481
482 Ok(CompiledDetectionItem {
483 field: item.field.name.clone(),
484 matcher: combined,
485 exists: None,
486 bloom_eligible,
487 })
488}
489
490fn validate_modifiers(ctx: &ModCtx, modifiers: &[Modifier]) -> Result<()> {
527 let mut operators: Vec<&'static str> = Vec::new();
529 if ctx.contains {
530 operators.push("contains");
531 }
532 if ctx.startswith {
533 operators.push("startswith");
534 }
535 if ctx.endswith {
536 operators.push("endswith");
537 }
538 if ctx.re {
539 operators.push("re");
540 }
541 if ctx.cidr {
542 operators.push("cidr");
543 }
544 if ctx.exists {
545 operators.push("exists");
546 }
547 if ctx.fieldref {
548 operators.push("fieldref");
549 }
550 if ctx.gt {
551 operators.push("gt");
552 }
553 if ctx.gte {
554 operators.push("gte");
555 }
556 if ctx.lt {
557 operators.push("lt");
558 }
559 if ctx.lte {
560 operators.push("lte");
561 }
562 for m in modifiers {
563 match m {
564 Modifier::Minute => operators.push("minute"),
565 Modifier::Hour => operators.push("hour"),
566 Modifier::Day => operators.push("day"),
567 Modifier::Week => operators.push("week"),
568 Modifier::Month => operators.push("month"),
569 Modifier::Year => operators.push("year"),
570 _ => {}
571 }
572 }
573 if operators.len() > 1 {
574 return Err(EvalError::InvalidModifiers(format!(
575 "conflicting modifiers: at most one operator may be set per field; \
576 got |{}",
577 operators.join(", |")
578 )));
579 }
580
581 let mut wide_encodings: Vec<&'static str> = Vec::new();
583 if ctx.wide {
584 wide_encodings.push("wide");
585 }
586 if ctx.utf16 {
587 wide_encodings.push("utf16");
588 }
589 if ctx.utf16be {
590 wide_encodings.push("utf16be");
591 }
592 if wide_encodings.len() > 1 {
593 return Err(EvalError::InvalidModifiers(format!(
594 "conflicting modifiers: |wide, |utf16, and |utf16be are mutually \
595 exclusive UTF-16 encodings; got |{}",
596 wide_encodings.join(", |")
597 )));
598 }
599
600 if ctx.base64 && ctx.base64offset {
602 return Err(EvalError::InvalidModifiers(
603 "conflicting modifiers: |base64 and |base64offset are mutually \
604 exclusive base64 strategies; pick one"
605 .into(),
606 ));
607 }
608
609 let has_non_string_operator = ctx.re
614 || ctx.cidr
615 || ctx.exists
616 || ctx.fieldref
617 || ctx.has_numeric_comparison()
618 || ctx.timestamp_part.is_some();
619 if has_non_string_operator {
620 let mut transforms: Vec<&'static str> = Vec::new();
621 if ctx.base64 {
622 transforms.push("base64");
623 }
624 if ctx.base64offset {
625 transforms.push("base64offset");
626 }
627 if ctx.wide {
628 transforms.push("wide");
629 }
630 if ctx.utf16 {
631 transforms.push("utf16");
632 }
633 if ctx.utf16be {
634 transforms.push("utf16be");
635 }
636 if ctx.windash {
637 transforms.push("windash");
638 }
639 if ctx.expand {
640 transforms.push("expand");
641 }
642 if !transforms.is_empty() {
643 return Err(EvalError::InvalidModifiers(format!(
644 "conflicting modifiers: value transformations |{} only apply \
645 to string match operators (default eq, contains, startswith, \
646 endswith) and cannot be combined with the operator that is \
647 also set on this field",
648 transforms.join(", |")
649 )));
650 }
651 }
652
653 if !ctx.re {
655 let mut regex_flags: Vec<&'static str> = Vec::new();
656 if ctx.ignore_case {
657 regex_flags.push("i");
658 }
659 if ctx.multiline {
660 regex_flags.push("m");
661 }
662 if ctx.dotall {
663 regex_flags.push("s");
664 }
665 if !regex_flags.is_empty() {
666 return Err(EvalError::InvalidModifiers(format!(
667 "regex flag modifiers |{} have no effect without |re; \
668 case sensitivity for substring or equality matching is \
669 controlled by |cased (or its absence, which keeps the \
670 default case-insensitive behavior)",
671 regex_flags.join(", |")
672 )));
673 }
674 }
675
676 Ok(())
677}
678
679fn compile_value(value: &SigmaValue, ctx: &ModCtx) -> Result<CompiledMatcher> {
685 let ci = ctx.is_case_insensitive();
686
687 if ctx.expand {
691 let plain = value_to_plain_string(value)?;
692 let template = crate::matcher::parse_expand_template(&plain);
693 return Ok(CompiledMatcher::Expand {
694 template,
695 case_insensitive: ci,
696 });
697 }
698
699 if let Some(part) = ctx.timestamp_part {
701 let inner = match value {
704 SigmaValue::Integer(n) => CompiledMatcher::NumericEq(*n as f64),
705 SigmaValue::Float(n) => CompiledMatcher::NumericEq(*n),
706 SigmaValue::String(s) => {
707 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
708 let n: f64 = plain.parse().map_err(|_| {
709 EvalError::IncompatibleValue(format!(
710 "timestamp part modifier requires numeric value, got: {plain}"
711 ))
712 })?;
713 CompiledMatcher::NumericEq(n)
714 }
715 _ => {
716 return Err(EvalError::IncompatibleValue(
717 "timestamp part modifier requires numeric value".into(),
718 ));
719 }
720 };
721 return Ok(CompiledMatcher::TimestampPart {
722 part,
723 inner: Box::new(inner),
724 });
725 }
726
727 if ctx.fieldref {
729 let field_name = value_to_plain_string(value)?;
730 return Ok(CompiledMatcher::FieldRef {
731 field: field_name,
732 case_insensitive: ci,
733 });
734 }
735
736 if ctx.re {
740 let pattern = value_to_plain_string(value)?;
741 let regex = build_regex(&pattern, ctx.ignore_case, ctx.multiline, ctx.dotall)?;
742 return Ok(CompiledMatcher::Regex(regex));
743 }
744
745 if ctx.cidr {
747 let cidr_str = value_to_plain_string(value)?;
748 let net: ipnet::IpNet = cidr_str
749 .parse()
750 .map_err(|e: ipnet::AddrParseError| EvalError::InvalidCidr(e))?;
751 return Ok(CompiledMatcher::Cidr(net));
752 }
753
754 if ctx.has_numeric_comparison() {
756 let n = value_to_f64(value)?;
757 if ctx.gt {
758 return Ok(CompiledMatcher::NumericGt(n));
759 }
760 if ctx.gte {
761 return Ok(CompiledMatcher::NumericGte(n));
762 }
763 if ctx.lt {
764 return Ok(CompiledMatcher::NumericLt(n));
765 }
766 if ctx.lte {
767 return Ok(CompiledMatcher::NumericLte(n));
768 }
769 }
770
771 if ctx.has_neq() {
773 let mut inner_ctx = ModCtx { ..*ctx };
775 inner_ctx.neq = false;
776 let inner = compile_value(value, &inner_ctx)?;
777 return Ok(CompiledMatcher::Not(Box::new(inner)));
778 }
779
780 match value {
782 SigmaValue::Integer(n) => {
783 if ctx.contains || ctx.startswith || ctx.endswith {
784 return compile_string_value(&n.to_string(), ctx);
786 }
787 return Ok(CompiledMatcher::NumericEq(*n as f64));
788 }
789 SigmaValue::Float(n) => {
790 if ctx.contains || ctx.startswith || ctx.endswith {
791 return compile_string_value(&n.to_string(), ctx);
792 }
793 return Ok(CompiledMatcher::NumericEq(*n));
794 }
795 SigmaValue::Bool(b) => return Ok(CompiledMatcher::BoolEq(*b)),
796 SigmaValue::Null => return Ok(CompiledMatcher::Null),
797 SigmaValue::String(_) => {} }
799
800 let sigma_str = match value {
802 SigmaValue::String(s) => s,
803 _ => unreachable!(),
804 };
805
806 let mut bytes = sigma_string_to_bytes(sigma_str);
808
809 if ctx.wide {
811 bytes = to_utf16le_bytes(&bytes);
812 }
813
814 if ctx.utf16be {
816 bytes = to_utf16be_bytes(&bytes);
817 }
818
819 if ctx.utf16 {
821 bytes = to_utf16_bom_bytes(&bytes);
822 }
823
824 if ctx.base64 {
826 let encoded = BASE64_STANDARD.encode(&bytes);
827 return compile_string_value(&encoded, ctx);
828 }
829
830 if ctx.base64offset {
832 let patterns = base64_offset_patterns(&bytes);
833 let matchers: Vec<CompiledMatcher> = patterns
834 .into_iter()
835 .map(|p| {
836 CompiledMatcher::Contains {
838 value: if ci { p.to_lowercase() } else { p },
839 case_insensitive: ci,
840 }
841 })
842 .collect();
843 return Ok(CompiledMatcher::AnyOf(matchers));
844 }
845
846 if ctx.windash {
848 let plain = sigma_str
849 .as_plain()
850 .unwrap_or_else(|| sigma_str.original.clone());
851 let variants = expand_windash(&plain)?;
852 let matchers: Result<Vec<CompiledMatcher>> = variants
853 .into_iter()
854 .map(|v| compile_string_value(&v, ctx))
855 .collect();
856 return Ok(CompiledMatcher::AnyOf(matchers?));
857 }
858
859 compile_sigma_string(sigma_str, ctx)
861}
862
863fn compile_sigma_string(sigma_str: &SigmaString, ctx: &ModCtx) -> Result<CompiledMatcher> {
865 let ci = ctx.is_case_insensitive();
866
867 if sigma_str.is_plain() {
869 let plain = sigma_str.as_plain().unwrap_or_default();
870 return compile_string_value(&plain, ctx);
871 }
872
873 let mut pattern = String::new();
878 if ci {
879 pattern.push_str("(?i)");
880 }
881
882 if !ctx.contains && !ctx.startswith {
883 pattern.push('^');
884 }
885
886 for part in &sigma_str.parts {
887 match part {
888 StringPart::Plain(text) => {
889 pattern.push_str(®ex::escape(text));
890 }
891 StringPart::Special(SpecialChar::WildcardMulti) => {
892 pattern.push_str(".*");
893 }
894 StringPart::Special(SpecialChar::WildcardSingle) => {
895 pattern.push('.');
896 }
897 }
898 }
899
900 if !ctx.contains && !ctx.endswith {
901 pattern.push('$');
902 }
903
904 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
905 Ok(CompiledMatcher::Regex(regex))
906}
907
908fn compile_string_value(plain: &str, ctx: &ModCtx) -> Result<CompiledMatcher> {
910 let ci = ctx.is_case_insensitive();
911
912 if ctx.contains {
913 Ok(CompiledMatcher::Contains {
914 value: if ci {
915 plain.to_lowercase()
916 } else {
917 plain.to_string()
918 },
919 case_insensitive: ci,
920 })
921 } else if ctx.startswith {
922 Ok(CompiledMatcher::StartsWith {
923 value: if ci {
924 plain.to_lowercase()
925 } else {
926 plain.to_string()
927 },
928 case_insensitive: ci,
929 })
930 } else if ctx.endswith {
931 Ok(CompiledMatcher::EndsWith {
932 value: if ci {
933 plain.to_lowercase()
934 } else {
935 plain.to_string()
936 },
937 case_insensitive: ci,
938 })
939 } else {
940 Ok(CompiledMatcher::Exact {
941 value: if ci {
942 plain.to_lowercase()
943 } else {
944 plain.to_string()
945 },
946 case_insensitive: ci,
947 })
948 }
949}
950
951fn compile_value_default(value: &SigmaValue, case_insensitive: bool) -> Result<CompiledMatcher> {
953 match value {
954 SigmaValue::String(s) => {
955 if s.is_plain() {
956 let plain = s.as_plain().unwrap_or_default();
957 Ok(CompiledMatcher::Contains {
958 value: if case_insensitive {
959 plain.to_lowercase()
960 } else {
961 plain
962 },
963 case_insensitive,
964 })
965 } else {
966 let pattern = sigma_string_to_regex(&s.parts, case_insensitive);
968 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
969 Ok(CompiledMatcher::Regex(regex))
970 }
971 }
972 SigmaValue::Integer(n) => Ok(CompiledMatcher::NumericEq(*n as f64)),
973 SigmaValue::Float(n) => Ok(CompiledMatcher::NumericEq(*n)),
974 SigmaValue::Bool(b) => Ok(CompiledMatcher::BoolEq(*b)),
975 SigmaValue::Null => Ok(CompiledMatcher::Null),
976 }
977}
978
979pub fn eval_condition(
988 expr: &ConditionExpr,
989 detections: &HashMap<String, CompiledDetection>,
990 event: &impl Event,
991 matched_selections: &mut Vec<String>,
992) -> bool {
993 eval_condition_with_bloom(
994 expr,
995 detections,
996 event,
997 matched_selections,
998 &crate::engine::bloom_index::NoBloom,
999 )
1000}
1001
1002fn eval_condition_matches_with_bloom<E, B>(
1008 expr: &ConditionExpr,
1009 detections: &HashMap<String, CompiledDetection>,
1010 event: &E,
1011 bloom: &B,
1012) -> bool
1013where
1014 E: Event,
1015 B: crate::engine::bloom_index::BloomLookup,
1016{
1017 match expr {
1018 ConditionExpr::Identifier(name) => detections
1019 .get(name)
1020 .is_some_and(|det| eval_detection_with_bloom(det, event, bloom)),
1021 ConditionExpr::And(exprs) => exprs
1022 .iter()
1023 .all(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
1024 ConditionExpr::Or(exprs) => exprs
1025 .iter()
1026 .any(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
1027 ConditionExpr::Not(inner) => {
1028 !eval_condition_matches_with_bloom(inner, detections, event, bloom)
1029 }
1030 ConditionExpr::Selector {
1031 quantifier,
1032 pattern,
1033 } => {
1034 let mut matching = detections
1035 .iter()
1036 .filter(|(name, _)| pattern.matches_detection_name(name));
1037 match quantifier {
1038 Quantifier::Any => {
1039 matching.any(|(_, det)| eval_detection_with_bloom(det, event, bloom))
1040 }
1041 Quantifier::All => {
1042 matching.all(|(_, det)| eval_detection_with_bloom(det, event, bloom))
1043 }
1044 Quantifier::Count(required) => {
1045 if *required == 0 {
1046 return true;
1047 }
1048 let mut matched = 0u64;
1049 matching.any(|(_, det)| {
1050 if eval_detection_with_bloom(det, event, bloom) {
1051 matched += 1;
1052 }
1053 matched >= *required
1054 })
1055 }
1056 }
1057 }
1058 }
1059}
1060
1061pub(crate) fn eval_condition_with_bloom<E, B>(
1067 expr: &ConditionExpr,
1068 detections: &HashMap<String, CompiledDetection>,
1069 event: &E,
1070 matched_selections: &mut Vec<String>,
1071 bloom: &B,
1072) -> bool
1073where
1074 E: Event,
1075 B: crate::engine::bloom_index::BloomLookup,
1076{
1077 match expr {
1078 ConditionExpr::Identifier(name) => {
1079 if let Some(det) = detections.get(name) {
1080 let result = eval_detection_with_bloom(det, event, bloom);
1081 if result {
1082 matched_selections.push(name.clone());
1083 }
1084 result
1085 } else {
1086 false
1087 }
1088 }
1089
1090 ConditionExpr::And(exprs) => exprs
1091 .iter()
1092 .all(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1093
1094 ConditionExpr::Or(exprs) => exprs
1095 .iter()
1096 .any(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1097
1098 ConditionExpr::Not(inner) => {
1099 !eval_condition_with_bloom(inner, detections, event, matched_selections, bloom)
1100 }
1101
1102 ConditionExpr::Selector {
1103 quantifier,
1104 pattern,
1105 } => {
1106 let matching_names: Vec<&String> = detections
1107 .keys()
1108 .filter(|name| pattern.matches_detection_name(name))
1109 .collect();
1110
1111 let mut match_count = 0u64;
1112 for name in &matching_names {
1113 if let Some(det) = detections.get(*name)
1114 && eval_detection_with_bloom(det, event, bloom)
1115 {
1116 match_count += 1;
1117 matched_selections.push((*name).clone());
1118 }
1119 }
1120
1121 match quantifier {
1122 Quantifier::Any => match_count >= 1,
1123 Quantifier::All => match_count == matching_names.len() as u64,
1124 Quantifier::Count(n) => match_count >= *n,
1125 }
1126 }
1127 }
1128}
1129
1130#[cfg(test)]
1135fn eval_detection_item(item: &CompiledDetectionItem, event: &impl Event) -> bool {
1136 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1137}
1138
1139pub(crate) fn eval_detection_item_no_bloom(
1143 item: &CompiledDetectionItem,
1144 event: &impl Event,
1145) -> bool {
1146 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1147}
1148
1149fn eval_detection_with_bloom<E, B>(detection: &CompiledDetection, event: &E, bloom: &B) -> bool
1151where
1152 E: Event,
1153 B: crate::engine::bloom_index::BloomLookup,
1154{
1155 match detection {
1156 CompiledDetection::AllOf(items) => items
1157 .iter()
1158 .all(|item| eval_detection_item_with_bloom(item, event, bloom)),
1159 CompiledDetection::AnyOf(dets) => dets
1160 .iter()
1161 .any(|d| eval_detection_with_bloom(d, event, bloom)),
1162 CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
1163 CompiledDetection::ArrayMatch {
1164 field,
1165 quantifier,
1166 body,
1167 } => match event.get_field(field) {
1168 Some(value) => eval_array_quantified(&value, *quantifier, body, event),
1169 None => array_quantifier_matches_empty(*quantifier),
1170 },
1171 CompiledDetection::And(dets) => dets
1172 .iter()
1173 .all(|d| eval_detection_with_bloom(d, event, bloom)),
1174 CompiledDetection::Conditional { named, condition } => {
1178 eval_condition_with_bloom(condition, named, event, &mut Vec::new(), bloom)
1179 }
1180 }
1181}
1182
1183fn eval_detection_item_with_bloom<E, B>(item: &CompiledDetectionItem, event: &E, bloom: &B) -> bool
1190where
1191 E: Event,
1192 B: crate::engine::bloom_index::BloomLookup,
1193{
1194 if let Some(expect_exists) = item.exists {
1195 if let Some(field) = &item.field {
1196 let exists = event.get_field(field).is_some_and(|v| !v.is_null());
1197 return exists == expect_exists;
1198 }
1199 return !expect_exists;
1200 }
1201
1202 match &item.field {
1203 Some(field_name) => {
1204 if let Some(value) = event.get_field(field_name) {
1205 if item.bloom_eligible
1206 && bloom.verdict_for_field(field_name)
1207 == crate::engine::bloom_index::BloomVerdict::DefinitelyNoMatch
1208 {
1209 return false;
1210 }
1211 item.matcher.matches(&value, event)
1212 } else {
1213 matches!(item.matcher, CompiledMatcher::Null)
1214 }
1215 }
1216 None => item.matcher.matches_keyword(event),
1217 }
1218}
1219
1220const MAX_KEYWORD_MATCHES: usize = 16;
1224
1225fn collect_field_matches(
1233 selection_names: &[String],
1234 detections: &HashMap<String, CompiledDetection>,
1235 event: &impl Event,
1236 level: MatchDetailLevel,
1237) -> Vec<FieldMatch> {
1238 let mut matches = Vec::new();
1239 for name in selection_names {
1240 if let Some(det) = detections.get(name) {
1241 collect_detection_fields(name, det, event, level, &mut matches);
1242 }
1243 }
1244 matches
1245}
1246
1247fn collect_detection_fields(
1248 selection: &str,
1249 detection: &CompiledDetection,
1250 event: &impl Event,
1251 level: MatchDetailLevel,
1252 out: &mut Vec<FieldMatch>,
1253) {
1254 match detection {
1255 CompiledDetection::AllOf(items) => {
1256 for item in items {
1257 match &item.field {
1258 Some(field_name) => {
1259 if let Some(value) = event.get_field(field_name) {
1260 if item.matcher.matches(&value, event) {
1261 out.push(make_field_match(
1262 selection,
1263 field_name,
1264 value.to_json(),
1265 &item.matcher,
1266 level,
1267 ));
1268 }
1269 } else if level != MatchDetailLevel::Off
1270 && matches!(
1271 item.matcher,
1272 CompiledMatcher::Null | CompiledMatcher::Exists(false)
1273 )
1274 {
1275 out.push(make_field_match(
1279 selection,
1280 field_name,
1281 serde_json::Value::Null,
1282 &item.matcher,
1283 level,
1284 ));
1285 }
1286 }
1287 None => {
1288 if level != MatchDetailLevel::Off {
1290 collect_keyword_matches(selection, &item.matcher, event, level, out);
1291 }
1292 }
1293 }
1294 }
1295 }
1296 CompiledDetection::AnyOf(dets) => {
1297 for d in dets {
1298 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1299 collect_detection_fields(selection, d, event, level, out);
1300 }
1301 }
1302 }
1303 CompiledDetection::ArrayMatch { field, body, .. } => {
1304 let value = event.get_field(field);
1305 collect_array_match_fields(
1306 selection,
1307 field,
1308 body,
1309 value.as_ref(),
1310 event,
1311 level,
1312 "",
1313 out,
1314 );
1315 }
1316 CompiledDetection::And(dets) => {
1317 for d in dets {
1318 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1319 collect_detection_fields(selection, d, event, level, out);
1320 }
1321 }
1322 }
1323 CompiledDetection::Conditional { .. } => {}
1326 CompiledDetection::Keywords(matcher) => {
1327 if level != MatchDetailLevel::Off {
1330 collect_keyword_matches(selection, matcher, event, level, out);
1331 }
1332 }
1333 }
1334}
1335
1336#[allow(clippy::too_many_arguments)]
1343fn collect_array_match_fields<E: Event>(
1344 selection: &str,
1345 field: &str,
1346 body: &CompiledDetection,
1347 value: Option<&EventValue>,
1348 outer: &E,
1349 level: MatchDetailLevel,
1350 path_prefix: &str,
1351 out: &mut Vec<FieldMatch>,
1352) {
1353 let container_path = if path_prefix.is_empty() {
1354 field.to_string()
1355 } else {
1356 array::join_member_field(path_prefix, Some(field))
1357 };
1358
1359 let (scalar, members): (bool, Vec<&EventValue>) = match value {
1360 None => return,
1361 Some(EventValue::Null) => {
1362 out.push(FieldMatch::new(container_path, serde_json::Value::Null));
1363 return;
1364 }
1365 Some(EventValue::Array(items)) if items.is_empty() => {
1366 out.push(FieldMatch::new(
1367 container_path,
1368 serde_json::Value::Array(Vec::new()),
1369 ));
1370 return;
1371 }
1372 Some(EventValue::Array(items)) => (false, items.iter().collect()),
1373 Some(single) => (true, vec![single]),
1374 };
1375
1376 let matching: Vec<usize> = members
1377 .iter()
1378 .enumerate()
1379 .filter(|(_, m)| array::eval_array_body(body, m, outer))
1380 .map(|(i, _)| i)
1381 .take(array::ARRAY_MEMBER_CAP)
1382 .collect();
1383
1384 if matching.is_empty() {
1385 if let Some(v) = value {
1386 out.push(FieldMatch::new(container_path, v.to_json()));
1387 }
1388 return;
1389 }
1390
1391 for i in matching {
1392 let member_path = array::array_member_path(&container_path, i, scalar);
1393 let before = out.len();
1394 collect_array_body_fields(selection, body, members[i], outer, level, &member_path, out);
1395 if out.len() == before && level != MatchDetailLevel::Off {
1401 out.push(FieldMatch::new(member_path, members[i].to_json()));
1402 }
1403 }
1404}
1405
1406fn collect_array_body_fields<E: Event>(
1407 selection: &str,
1408 body: &CompiledDetection,
1409 member: &EventValue,
1410 outer: &E,
1411 level: MatchDetailLevel,
1412 member_path: &str,
1413 out: &mut Vec<FieldMatch>,
1414) {
1415 match body {
1416 CompiledDetection::AllOf(items) => {
1417 for item in items {
1418 if !array::eval_array_item(item, member, outer) {
1419 continue;
1420 }
1421 let relative = item.field.as_deref();
1422 let absent =
1423 relative.is_some_and(|name| array::element_field(member, name).is_none());
1424 if absent && level == MatchDetailLevel::Off {
1425 continue;
1426 }
1427 let path = array::join_member_field(member_path, relative);
1428 let value = match relative {
1429 Some(name) => array::element_field(member, name)
1430 .map(|v| v.to_json())
1431 .unwrap_or(serde_json::Value::Null),
1432 None => member.to_json(),
1433 };
1434 out.push(make_field_match(
1435 selection,
1436 &path,
1437 value,
1438 &item.matcher,
1439 level,
1440 ));
1441 }
1442 }
1443 CompiledDetection::AnyOf(dets) | CompiledDetection::And(dets) => {
1444 for d in dets {
1445 if array::eval_array_body(d, member, outer) {
1446 collect_array_body_fields(selection, d, member, outer, level, member_path, out);
1447 }
1448 }
1449 }
1450 CompiledDetection::ArrayMatch {
1451 field, body: inner, ..
1452 } => {
1453 collect_array_match_fields(
1454 selection,
1455 field,
1456 inner,
1457 array::element_field(member, field),
1458 outer,
1459 level,
1460 member_path,
1461 out,
1462 );
1463 }
1464 CompiledDetection::Keywords(matcher) => {
1465 if level != MatchDetailLevel::Off && matcher.matches(member, outer) {
1466 let d = matcher.describe();
1467 out.push(FieldMatch {
1468 field: member_path.to_string(),
1469 value: member.to_json(),
1470 selection: Some(selection.to_string()),
1471 matcher: Some(MatcherKind::Keyword),
1472 pattern: if level == MatchDetailLevel::Full {
1473 d.pattern
1474 } else {
1475 None
1476 },
1477 case_sensitive: d.case_sensitive,
1478 negated: d.negated,
1479 });
1480 }
1481 }
1482 CompiledDetection::Conditional { named, condition } => {
1483 collect_array_condition_fields(
1484 selection,
1485 condition,
1486 named,
1487 member,
1488 outer,
1489 level,
1490 member_path,
1491 out,
1492 );
1493 }
1494 }
1495}
1496
1497#[allow(clippy::too_many_arguments)]
1498fn collect_array_condition_fields<E: Event>(
1499 selection: &str,
1500 expr: &ConditionExpr,
1501 named: &HashMap<String, CompiledDetection>,
1502 member: &EventValue,
1503 outer: &E,
1504 level: MatchDetailLevel,
1505 member_path: &str,
1506 out: &mut Vec<FieldMatch>,
1507) {
1508 match expr {
1509 ConditionExpr::Identifier(name) => {
1510 if let Some(det) = named.get(name)
1511 && array::eval_array_body(det, member, outer)
1512 {
1513 collect_array_body_fields(selection, det, member, outer, level, member_path, out);
1514 }
1515 }
1516 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1517 for e in exprs {
1518 collect_array_condition_fields(
1519 selection,
1520 e,
1521 named,
1522 member,
1523 outer,
1524 level,
1525 member_path,
1526 out,
1527 );
1528 }
1529 }
1530 ConditionExpr::Not(_) => {}
1531 ConditionExpr::Selector { pattern, .. } => {
1532 let mut names: Vec<&String> = named
1533 .keys()
1534 .filter(|n| pattern.matches_detection_name(n))
1535 .collect();
1536 names.sort();
1537 for name in names {
1538 if let Some(det) = named.get(name)
1539 && array::eval_array_body(det, member, outer)
1540 {
1541 collect_array_body_fields(
1542 selection,
1543 det,
1544 member,
1545 outer,
1546 level,
1547 member_path,
1548 out,
1549 );
1550 }
1551 }
1552 }
1553 }
1554}
1555
1556fn make_field_match(
1560 selection: &str,
1561 field: &str,
1562 value: serde_json::Value,
1563 matcher: &CompiledMatcher,
1564 level: MatchDetailLevel,
1565) -> FieldMatch {
1566 match level {
1567 MatchDetailLevel::Off => FieldMatch::new(field, value),
1568 MatchDetailLevel::Summary | MatchDetailLevel::Full => {
1569 let d = matcher.describe();
1570 FieldMatch {
1571 field: field.to_string(),
1572 value,
1573 selection: Some(selection.to_string()),
1574 matcher: Some(d.kind),
1575 pattern: if level == MatchDetailLevel::Full {
1576 d.pattern
1577 } else {
1578 None
1579 },
1580 case_sensitive: d.case_sensitive,
1581 negated: d.negated,
1582 }
1583 }
1584 }
1585}
1586
1587fn collect_keyword_matches(
1591 selection: &str,
1592 matcher: &CompiledMatcher,
1593 event: &impl Event,
1594 level: MatchDetailLevel,
1595 out: &mut Vec<FieldMatch>,
1596) {
1597 let descriptor = matcher.describe();
1598 let mut count = 0;
1599 for s in event.all_string_values() {
1600 if count >= MAX_KEYWORD_MATCHES {
1601 break;
1602 }
1603 if matcher.matches_str(&s) {
1604 count += 1;
1605 out.push(FieldMatch {
1606 field: "keyword".to_string(),
1607 value: serde_json::Value::String(s.into_owned()),
1608 selection: Some(selection.to_string()),
1609 matcher: Some(MatcherKind::Keyword),
1610 pattern: if level == MatchDetailLevel::Full {
1611 descriptor.pattern.clone()
1612 } else {
1613 None
1614 },
1615 case_sensitive: descriptor.case_sensitive,
1616 negated: descriptor.negated,
1617 });
1618 }
1619 }
1620}