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_ir::IrStrOp;
40use rsigma_parser::value::{SpecialChar, StringPart};
41use rsigma_parser::{
42 ArrayQuantifier, ConditionExpr, Detection, DetectionItem, Level, LogSource, Modifier,
43 Quantifier, SigmaRule, SigmaString, SigmaValue,
44};
45
46use crate::error::{EvalError, Result};
47use crate::event::{Event, EventValue};
48use crate::matcher::{CompiledMatcher, sigma_string_to_regex};
49use crate::result::{
50 DetectionBody, EvaluationResult, FieldMatch, MatchDetailLevel, MatcherKind, ResultBody,
51 RuleHeader,
52};
53
54use helpers::{
55 base64_offset_patterns, build_regex, expand_windash, sigma_string_to_bytes, to_utf16_bom_bytes,
56 to_utf16be_bytes, to_utf16le_bytes, value_to_f64, value_to_plain_string,
57};
58pub(crate) use helpers::{yaml_to_json, yaml_to_json_map};
59
60#[derive(Debug, Clone)]
66pub struct CompiledRule {
67 pub title: String,
68 pub id: Option<String>,
69 pub level: Option<Level>,
70 pub tags: Vec<String>,
71 pub description: Option<String>,
74 pub falsepositives: Vec<String>,
77 pub logsource: LogSource,
78 pub detections: HashMap<String, CompiledDetection>,
80 pub conditions: Vec<ConditionExpr>,
82 pub include_event: bool,
85 pub custom_attributes: Arc<HashMap<String, serde_json::Value>>,
90}
91
92#[derive(Debug, Clone)]
94pub enum CompiledDetection {
95 AllOf(Vec<CompiledDetectionItem>),
97 AnyOf(Vec<CompiledDetection>),
99 Keywords(CompiledMatcher),
101 ArrayMatch {
105 field: String,
106 quantifier: ArrayQuantifier,
107 body: Box<CompiledDetection>,
108 },
109 And(Vec<CompiledDetection>),
112 Conditional {
117 named: HashMap<String, CompiledDetection>,
118 condition: ConditionExpr,
119 },
120}
121
122#[derive(Debug, Clone)]
124pub struct CompiledDetectionItem {
125 pub field: Option<String>,
127 pub matcher: CompiledMatcher,
129 pub exists: Option<bool>,
131 pub bloom_eligible: bool,
136}
137
138#[derive(Clone, Copy)]
144struct ModCtx {
145 contains: bool,
146 startswith: bool,
147 endswith: bool,
148 all: bool,
149 base64: bool,
150 base64offset: bool,
151 wide: bool,
152 utf16be: bool,
153 utf16: bool,
154 windash: bool,
155 re: bool,
156 cidr: bool,
157 cased: bool,
158 exists: bool,
159 fieldref: bool,
160 gt: bool,
161 gte: bool,
162 lt: bool,
163 lte: bool,
164 neq: bool,
165 ignore_case: bool,
166 multiline: bool,
167 dotall: bool,
168 expand: bool,
169 timestamp_part: Option<crate::matcher::TimePart>,
170}
171
172impl ModCtx {
173 fn from_modifiers(modifiers: &[Modifier]) -> Self {
174 let mut ctx = ModCtx {
175 contains: false,
176 startswith: false,
177 endswith: false,
178 all: false,
179 base64: false,
180 base64offset: false,
181 wide: false,
182 utf16be: false,
183 utf16: false,
184 windash: false,
185 re: false,
186 cidr: false,
187 cased: false,
188 exists: false,
189 fieldref: false,
190 gt: false,
191 gte: false,
192 lt: false,
193 lte: false,
194 neq: false,
195 ignore_case: false,
196 multiline: false,
197 dotall: false,
198 expand: false,
199 timestamp_part: None,
200 };
201 for m in modifiers {
202 match m {
203 Modifier::Contains => ctx.contains = true,
204 Modifier::StartsWith => ctx.startswith = true,
205 Modifier::EndsWith => ctx.endswith = true,
206 Modifier::All => ctx.all = true,
207 Modifier::Base64 => ctx.base64 = true,
208 Modifier::Base64Offset => ctx.base64offset = true,
209 Modifier::Wide => ctx.wide = true,
210 Modifier::Utf16be => ctx.utf16be = true,
211 Modifier::Utf16 => ctx.utf16 = true,
212 Modifier::WindAsh => ctx.windash = true,
213 Modifier::Re => ctx.re = true,
214 Modifier::Cidr => ctx.cidr = true,
215 Modifier::Cased => ctx.cased = true,
216 Modifier::Exists => ctx.exists = true,
217 Modifier::FieldRef => ctx.fieldref = true,
218 Modifier::Gt => ctx.gt = true,
219 Modifier::Gte => ctx.gte = true,
220 Modifier::Lt => ctx.lt = true,
221 Modifier::Lte => ctx.lte = true,
222 Modifier::Neq => ctx.neq = true,
223 Modifier::IgnoreCase => ctx.ignore_case = true,
224 Modifier::Multiline => ctx.multiline = true,
225 Modifier::DotAll => ctx.dotall = true,
226 Modifier::Expand => ctx.expand = true,
227 Modifier::Hour => ctx.timestamp_part = Some(crate::matcher::TimePart::Hour),
228 Modifier::Day => ctx.timestamp_part = Some(crate::matcher::TimePart::Day),
229 Modifier::Week => ctx.timestamp_part = Some(crate::matcher::TimePart::Week),
230 Modifier::Month => ctx.timestamp_part = Some(crate::matcher::TimePart::Month),
231 Modifier::Year => ctx.timestamp_part = Some(crate::matcher::TimePart::Year),
232 Modifier::Minute => ctx.timestamp_part = Some(crate::matcher::TimePart::Minute),
233 }
234 }
235 ctx
236 }
237
238 fn is_case_insensitive(&self) -> bool {
241 !self.cased
242 }
243
244 fn has_numeric_comparison(&self) -> bool {
246 self.gt || self.gte || self.lt || self.lte
247 }
248
249 fn has_neq(&self) -> bool {
251 self.neq
252 }
253}
254
255pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
263 let ir = rsigma_ir::lower_rule(rule, &rsigma_ir::LowerOptions::default())?;
264 compile_to_compiled(&ir)
265}
266
267pub fn evaluate_rule(rule: &CompiledRule, event: &impl Event) -> Option<EvaluationResult> {
275 evaluate_rule_with_bloom(
276 rule,
277 event,
278 &crate::engine::bloom_index::NoBloom,
279 MatchDetailLevel::Off,
280 )
281}
282
283pub(crate) fn evaluate_rule_with_bloom<E, B>(
291 rule: &CompiledRule,
292 event: &E,
293 bloom: &B,
294 level: MatchDetailLevel,
295) -> Option<EvaluationResult>
296where
297 E: Event,
298 B: crate::engine::bloom_index::BloomLookup,
299{
300 for condition in &rule.conditions {
301 if eval_condition_matches_with_bloom(condition, &rule.detections, event, bloom) {
302 let mut matched_selections = Vec::new();
303 let matched = eval_condition_with_bloom(
304 condition,
305 &rule.detections,
306 event,
307 &mut matched_selections,
308 bloom,
309 );
310 debug_assert!(matched, "detail pass must agree with boolean pass");
311 let matched_fields =
312 collect_field_matches(&matched_selections, &rule.detections, event, level);
313
314 let event_data = if rule.include_event {
315 Some(event.to_json())
316 } else {
317 None
318 };
319
320 return Some(EvaluationResult {
321 header: RuleHeader {
322 rule_title: rule.title.clone(),
323 rule_id: rule.id.clone(),
324 level: rule.level,
325 tags: rule.tags.clone(),
326 custom_attributes: rule.custom_attributes.clone(),
327 enrichments: None,
328 },
329 body: ResultBody::Detection(DetectionBody {
330 matched_selections,
331 matched_fields,
332 event: event_data,
333 }),
334 });
335 }
336 }
337 None
338}
339
340pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
349 match detection {
350 Detection::AllOf(items) => {
351 if items.is_empty() {
352 return Err(EvalError::InvalidModifiers(
353 "AllOf detection must not be empty (vacuous truth)".into(),
354 ));
355 }
356 let compiled: Result<Vec<_>> = items.iter().map(compile_detection_item).collect();
357 Ok(CompiledDetection::AllOf(compiled?))
358 }
359 Detection::AnyOf(dets) => {
360 if dets.is_empty() {
361 return Err(EvalError::InvalidModifiers(
362 "AnyOf detection must not be empty (would never match)".into(),
363 ));
364 }
365 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
366 Ok(CompiledDetection::AnyOf(compiled?))
367 }
368 Detection::ArrayMatch {
369 field,
370 quantifier,
371 body,
372 } => {
373 let compiled_body = compile_detection(body)?;
374 Ok(CompiledDetection::ArrayMatch {
375 field: field.clone(),
376 quantifier: *quantifier,
377 body: Box::new(compiled_body),
378 })
379 }
380 Detection::And(dets) => {
381 if dets.is_empty() {
382 return Err(EvalError::InvalidModifiers(
383 "And detection must not be empty".into(),
384 ));
385 }
386 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
387 Ok(CompiledDetection::And(compiled?))
388 }
389 Detection::Conditional { named, condition } => {
390 if named.is_empty() {
391 return Err(EvalError::InvalidModifiers(
392 "Conditional detection must have at least one named sub-selection".into(),
393 ));
394 }
395 let compiled: Result<HashMap<String, CompiledDetection>> = named
396 .iter()
397 .map(|(k, d)| Ok((k.clone(), compile_detection(d)?)))
398 .collect();
399 Ok(CompiledDetection::Conditional {
400 named: compiled?,
401 condition: condition.clone(),
402 })
403 }
404 Detection::Keywords(values) => {
405 let ci = true; let matchers: Vec<CompiledMatcher> = values
407 .iter()
408 .map(|v| compile_value_default(v, ci))
409 .collect::<Result<Vec<_>>>()?;
410 let matcher = optimizer::optimize_any_of(matchers);
412 Ok(CompiledDetection::Keywords(matcher))
413 }
414 }
415}
416
417fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
418 let ctx = ModCtx::from_modifiers(&item.field.modifiers);
419
420 validate_modifiers(&ctx, &item.field.modifiers)?;
429
430 if ctx.exists {
432 let expect = match item.values.first() {
433 Some(SigmaValue::Bool(b)) => *b,
434 Some(SigmaValue::String(s)) => match s.as_plain().as_deref() {
435 Some("true") | Some("yes") => true,
436 Some("false") | Some("no") => false,
437 _ => true,
438 },
439 _ => true,
440 };
441 return Ok(CompiledDetectionItem {
442 field: item.field.name.clone(),
443 matcher: CompiledMatcher::Exists(expect),
444 exists: Some(expect),
445 bloom_eligible: false,
446 });
447 }
448
449 if ctx.all && item.values.len() <= 1 {
451 return Err(EvalError::InvalidModifiers(
452 "|all modifier requires more than one value".to_string(),
453 ));
454 }
455
456 let value_ctx = ModCtx { neq: false, ..ctx };
459 let matchers: Result<Vec<CompiledMatcher>> = item
460 .values
461 .iter()
462 .map(|v| compile_value(v, &value_ctx))
463 .collect();
464 let matchers = matchers?;
465
466 let combined = if ctx.all {
473 if matchers.len() == 1 {
474 matchers
475 .into_iter()
476 .next()
477 .unwrap_or(CompiledMatcher::AllOf(vec![]))
478 } else {
479 CompiledMatcher::AllOf(matchers)
480 }
481 } else {
482 optimizer::optimize_any_of(matchers)
483 };
484 let combined = if ctx.has_neq() {
485 CompiledMatcher::Not(Box::new(combined))
486 } else {
487 combined
488 };
489
490 let bloom_eligible = item.field.name.is_some()
491 && crate::engine::bloom_index::is_positive_substring_matcher(&combined);
492
493 Ok(CompiledDetectionItem {
494 field: item.field.name.clone(),
495 matcher: combined,
496 exists: None,
497 bloom_eligible,
498 })
499}
500
501fn validate_modifiers(ctx: &ModCtx, modifiers: &[Modifier]) -> Result<()> {
540 let mut operators: Vec<&'static str> = Vec::new();
542 if ctx.contains {
543 operators.push("contains");
544 }
545 if ctx.startswith {
546 operators.push("startswith");
547 }
548 if ctx.endswith {
549 operators.push("endswith");
550 }
551 if ctx.re {
552 operators.push("re");
553 }
554 if ctx.cidr {
555 operators.push("cidr");
556 }
557 if ctx.exists {
558 operators.push("exists");
559 }
560 if ctx.fieldref && fieldref_conflicts(ctx) {
563 operators.push("fieldref");
564 }
565 if ctx.gt {
566 operators.push("gt");
567 }
568 if ctx.gte {
569 operators.push("gte");
570 }
571 if ctx.lt {
572 operators.push("lt");
573 }
574 if ctx.lte {
575 operators.push("lte");
576 }
577 for m in modifiers {
578 match m {
579 Modifier::Minute => operators.push("minute"),
580 Modifier::Hour => operators.push("hour"),
581 Modifier::Day => operators.push("day"),
582 Modifier::Week => operators.push("week"),
583 Modifier::Month => operators.push("month"),
584 Modifier::Year => operators.push("year"),
585 _ => {}
586 }
587 }
588 if ctx.fieldref
589 && !fieldref_conflicts(ctx)
590 && let Some(name) = string_modifier_before_fieldref(modifiers)
591 {
592 return Err(EvalError::InvalidModifiers(format!(
593 "conflicting modifiers: |{name} must follow |fieldref"
594 )));
595 }
596 if operators.len() > 1 {
597 return Err(EvalError::InvalidModifiers(format!(
598 "conflicting modifiers: at most one operator may be set per field; \
599 got |{}",
600 operators.join(", |")
601 )));
602 }
603
604 let mut wide_encodings: Vec<&'static str> = Vec::new();
606 if ctx.wide {
607 wide_encodings.push("wide");
608 }
609 if ctx.utf16 {
610 wide_encodings.push("utf16");
611 }
612 if ctx.utf16be {
613 wide_encodings.push("utf16be");
614 }
615 if wide_encodings.len() > 1 {
616 return Err(EvalError::InvalidModifiers(format!(
617 "conflicting modifiers: |wide, |utf16, and |utf16be are mutually \
618 exclusive UTF-16 encodings; got |{}",
619 wide_encodings.join(", |")
620 )));
621 }
622
623 if ctx.base64 && ctx.base64offset {
625 return Err(EvalError::InvalidModifiers(
626 "conflicting modifiers: |base64 and |base64offset are mutually \
627 exclusive base64 strategies; pick one"
628 .into(),
629 ));
630 }
631
632 let has_non_string_operator = ctx.re
637 || ctx.cidr
638 || ctx.exists
639 || ctx.fieldref
640 || ctx.has_numeric_comparison()
641 || ctx.timestamp_part.is_some();
642 if has_non_string_operator {
643 let mut transforms: Vec<&'static str> = Vec::new();
644 if ctx.base64 {
645 transforms.push("base64");
646 }
647 if ctx.base64offset {
648 transforms.push("base64offset");
649 }
650 if ctx.wide {
651 transforms.push("wide");
652 }
653 if ctx.utf16 {
654 transforms.push("utf16");
655 }
656 if ctx.utf16be {
657 transforms.push("utf16be");
658 }
659 if ctx.windash {
660 transforms.push("windash");
661 }
662 if ctx.expand {
663 transforms.push("expand");
664 }
665 if !transforms.is_empty() {
666 return Err(EvalError::InvalidModifiers(format!(
667 "conflicting modifiers: value transformations |{} only apply \
668 to string match operators (default eq, contains, startswith, \
669 endswith) and cannot be combined with the operator that is \
670 also set on this field",
671 transforms.join(", |")
672 )));
673 }
674 }
675
676 if !ctx.re {
678 let mut regex_flags: Vec<&'static str> = Vec::new();
679 if ctx.ignore_case {
680 regex_flags.push("i");
681 }
682 if ctx.multiline {
683 regex_flags.push("m");
684 }
685 if ctx.dotall {
686 regex_flags.push("s");
687 }
688 if !regex_flags.is_empty() {
689 return Err(EvalError::InvalidModifiers(format!(
690 "regex flag modifiers |{} have no effect without |re; \
691 case sensitivity for substring or equality matching is \
692 controlled by |cased (or its absence, which keeps the \
693 default case-insensitive behavior)",
694 regex_flags.join(", |")
695 )));
696 }
697 }
698
699 Ok(())
700}
701
702fn fieldref_conflicts(ctx: &ModCtx) -> bool {
705 let string_ops = u8::from(ctx.contains) + u8::from(ctx.startswith) + u8::from(ctx.endswith);
706 let blocked = ctx.re
707 || ctx.cidr
708 || ctx.exists
709 || ctx.has_numeric_comparison()
710 || ctx.timestamp_part.is_some();
711 blocked || string_ops > 1
712}
713
714fn string_modifier_before_fieldref(modifiers: &[Modifier]) -> Option<&'static str> {
717 let fieldref_at = modifiers
718 .iter()
719 .position(|m| matches!(m, Modifier::FieldRef))?;
720 modifiers[..fieldref_at].iter().find_map(|m| match m {
721 Modifier::Contains => Some("contains"),
722 Modifier::StartsWith => Some("startswith"),
723 Modifier::EndsWith => Some("endswith"),
724 _ => None,
725 })
726}
727
728fn fieldref_name(value: &SigmaValue) -> Result<String> {
730 match value {
731 SigmaValue::String(s) => s.as_plain().ok_or_else(|| {
732 EvalError::IncompatibleValue("field reference must not contain wildcards".into())
733 }),
734 other => value_to_plain_string(other),
735 }
736}
737
738fn fieldref_str_op(ctx: &ModCtx) -> IrStrOp {
739 if ctx.contains {
740 IrStrOp::Contains
741 } else if ctx.startswith {
742 IrStrOp::StartsWith
743 } else if ctx.endswith {
744 IrStrOp::EndsWith
745 } else {
746 IrStrOp::Exact
747 }
748}
749
750fn compile_value(value: &SigmaValue, ctx: &ModCtx) -> Result<CompiledMatcher> {
756 let ci = ctx.is_case_insensitive();
757
758 if ctx.expand {
762 let plain = value_to_plain_string(value)?;
763 let template = crate::matcher::parse_expand_template(&plain);
764 return Ok(CompiledMatcher::Expand {
765 template,
766 case_insensitive: ci,
767 });
768 }
769
770 if let Some(part) = ctx.timestamp_part {
772 let inner = match value {
775 SigmaValue::Integer(n) => CompiledMatcher::NumericEq(*n as f64),
776 SigmaValue::Float(n) => CompiledMatcher::NumericEq(*n),
777 SigmaValue::String(s) => {
778 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
779 let n: f64 = plain.parse().map_err(|_| {
780 EvalError::IncompatibleValue(format!(
781 "timestamp part modifier requires numeric value, got: {plain}"
782 ))
783 })?;
784 CompiledMatcher::NumericEq(n)
785 }
786 _ => {
787 return Err(EvalError::IncompatibleValue(
788 "timestamp part modifier requires numeric value".into(),
789 ));
790 }
791 };
792 return Ok(CompiledMatcher::TimestampPart {
793 part,
794 inner: Box::new(inner),
795 });
796 }
797
798 if ctx.fieldref {
800 let field_name = fieldref_name(value)?;
801 return Ok(CompiledMatcher::FieldRef {
802 field: field_name,
803 op: fieldref_str_op(ctx),
804 case_insensitive: ci,
805 });
806 }
807
808 if ctx.re {
812 let pattern = value_to_plain_string(value)?;
813 let regex = build_regex(&pattern, ctx.ignore_case, ctx.multiline, ctx.dotall)?;
814 return Ok(CompiledMatcher::Regex(regex));
815 }
816
817 if ctx.cidr {
819 let cidr_str = value_to_plain_string(value)?;
820 let net: ipnet::IpNet = cidr_str
821 .parse()
822 .map_err(|e: ipnet::AddrParseError| EvalError::InvalidCidr(e))?;
823 return Ok(CompiledMatcher::Cidr(net));
824 }
825
826 if ctx.has_numeric_comparison() {
828 let n = value_to_f64(value)?;
829 if ctx.gt {
830 return Ok(CompiledMatcher::NumericGt(n));
831 }
832 if ctx.gte {
833 return Ok(CompiledMatcher::NumericGte(n));
834 }
835 if ctx.lt {
836 return Ok(CompiledMatcher::NumericLt(n));
837 }
838 if ctx.lte {
839 return Ok(CompiledMatcher::NumericLte(n));
840 }
841 }
842
843 match value {
845 SigmaValue::Integer(n) => {
846 if ctx.contains || ctx.startswith || ctx.endswith {
847 return compile_string_value(&n.to_string(), ctx);
849 }
850 return Ok(CompiledMatcher::NumericEq(*n as f64));
851 }
852 SigmaValue::Float(n) => {
853 if ctx.contains || ctx.startswith || ctx.endswith {
854 return compile_string_value(&n.to_string(), ctx);
855 }
856 return Ok(CompiledMatcher::NumericEq(*n));
857 }
858 SigmaValue::Bool(b) => return Ok(CompiledMatcher::BoolEq(*b)),
859 SigmaValue::Null => return Ok(CompiledMatcher::Null),
860 SigmaValue::String(_) => {} }
862
863 let sigma_str = match value {
865 SigmaValue::String(s) => s,
866 _ => unreachable!(),
867 };
868
869 let mut bytes = sigma_string_to_bytes(sigma_str);
871
872 if ctx.wide {
874 bytes = to_utf16le_bytes(&bytes);
875 }
876
877 if ctx.utf16be {
879 bytes = to_utf16be_bytes(&bytes);
880 }
881
882 if ctx.utf16 {
884 bytes = to_utf16_bom_bytes(&bytes);
885 }
886
887 if ctx.base64 {
889 let encoded = BASE64_STANDARD.encode(&bytes);
890 return compile_string_value(&encoded, ctx);
891 }
892
893 if ctx.base64offset {
895 let patterns = base64_offset_patterns(&bytes);
896 let matchers: Vec<CompiledMatcher> = patterns
897 .into_iter()
898 .map(|p| {
899 CompiledMatcher::Contains {
901 value: if ci { p.to_lowercase() } else { p },
902 case_insensitive: ci,
903 }
904 })
905 .collect();
906 return Ok(CompiledMatcher::AnyOf(matchers));
907 }
908
909 if ctx.windash {
911 let plain = sigma_str
912 .as_plain()
913 .unwrap_or_else(|| sigma_str.original.clone());
914 let variants = expand_windash(&plain)?;
915 let matchers: Result<Vec<CompiledMatcher>> = variants
916 .into_iter()
917 .map(|v| compile_string_value(&v, ctx))
918 .collect();
919 return Ok(CompiledMatcher::AnyOf(matchers?));
920 }
921
922 compile_sigma_string(sigma_str, ctx)
924}
925
926fn compile_sigma_string(sigma_str: &SigmaString, ctx: &ModCtx) -> Result<CompiledMatcher> {
928 let ci = ctx.is_case_insensitive();
929
930 if sigma_str.is_plain() {
932 let plain = sigma_str.as_plain().unwrap_or_default();
933 return compile_string_value(&plain, ctx);
934 }
935
936 let mut pattern = String::new();
941 if ci {
942 pattern.push_str("(?i)");
943 }
944
945 if !ctx.contains && !ctx.startswith {
946 pattern.push('^');
947 }
948
949 for part in &sigma_str.parts {
950 match part {
951 StringPart::Plain(text) => {
952 pattern.push_str(®ex::escape(text));
953 }
954 StringPart::Special(SpecialChar::WildcardMulti) => {
955 pattern.push_str(".*");
956 }
957 StringPart::Special(SpecialChar::WildcardSingle) => {
958 pattern.push('.');
959 }
960 }
961 }
962
963 if !ctx.contains && !ctx.endswith {
964 pattern.push('$');
965 }
966
967 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
968 Ok(CompiledMatcher::Regex(regex))
969}
970
971fn compile_string_value(plain: &str, ctx: &ModCtx) -> Result<CompiledMatcher> {
973 let ci = ctx.is_case_insensitive();
974
975 if ctx.contains {
976 Ok(CompiledMatcher::Contains {
977 value: if ci {
978 plain.to_lowercase()
979 } else {
980 plain.to_string()
981 },
982 case_insensitive: ci,
983 })
984 } else if ctx.startswith {
985 Ok(CompiledMatcher::StartsWith {
986 value: if ci {
987 plain.to_lowercase()
988 } else {
989 plain.to_string()
990 },
991 case_insensitive: ci,
992 })
993 } else if ctx.endswith {
994 Ok(CompiledMatcher::EndsWith {
995 value: if ci {
996 plain.to_lowercase()
997 } else {
998 plain.to_string()
999 },
1000 case_insensitive: ci,
1001 })
1002 } else {
1003 Ok(CompiledMatcher::Exact {
1004 value: if ci {
1005 plain.to_lowercase()
1006 } else {
1007 plain.to_string()
1008 },
1009 case_insensitive: ci,
1010 })
1011 }
1012}
1013
1014fn compile_value_default(value: &SigmaValue, case_insensitive: bool) -> Result<CompiledMatcher> {
1016 match value {
1017 SigmaValue::String(s) => {
1018 if s.is_plain() {
1019 let plain = s.as_plain().unwrap_or_default();
1020 Ok(CompiledMatcher::Contains {
1021 value: if case_insensitive {
1022 plain.to_lowercase()
1023 } else {
1024 plain
1025 },
1026 case_insensitive,
1027 })
1028 } else {
1029 let pattern = sigma_string_to_regex(&s.parts, case_insensitive);
1031 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
1032 Ok(CompiledMatcher::Regex(regex))
1033 }
1034 }
1035 SigmaValue::Integer(n) => Ok(CompiledMatcher::NumericEq(*n as f64)),
1036 SigmaValue::Float(n) => Ok(CompiledMatcher::NumericEq(*n)),
1037 SigmaValue::Bool(b) => Ok(CompiledMatcher::BoolEq(*b)),
1038 SigmaValue::Null => Ok(CompiledMatcher::Null),
1039 }
1040}
1041
1042pub fn eval_condition(
1051 expr: &ConditionExpr,
1052 detections: &HashMap<String, CompiledDetection>,
1053 event: &impl Event,
1054 matched_selections: &mut Vec<String>,
1055) -> bool {
1056 eval_condition_with_bloom(
1057 expr,
1058 detections,
1059 event,
1060 matched_selections,
1061 &crate::engine::bloom_index::NoBloom,
1062 )
1063}
1064
1065fn eval_condition_matches_with_bloom<E, B>(
1071 expr: &ConditionExpr,
1072 detections: &HashMap<String, CompiledDetection>,
1073 event: &E,
1074 bloom: &B,
1075) -> bool
1076where
1077 E: Event,
1078 B: crate::engine::bloom_index::BloomLookup,
1079{
1080 match expr {
1081 ConditionExpr::Identifier(name) => detections
1082 .get(name)
1083 .is_some_and(|det| eval_detection_with_bloom(det, event, bloom)),
1084 ConditionExpr::And(exprs) => exprs
1085 .iter()
1086 .all(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
1087 ConditionExpr::Or(exprs) => exprs
1088 .iter()
1089 .any(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
1090 ConditionExpr::Not(inner) => {
1091 !eval_condition_matches_with_bloom(inner, detections, event, bloom)
1092 }
1093 ConditionExpr::Selector {
1094 quantifier,
1095 pattern,
1096 } => {
1097 let mut matching = detections
1098 .iter()
1099 .filter(|(name, _)| pattern.matches_detection_name(name));
1100 match quantifier {
1101 Quantifier::Any => {
1102 matching.any(|(_, det)| eval_detection_with_bloom(det, event, bloom))
1103 }
1104 Quantifier::All => {
1105 matching.all(|(_, det)| eval_detection_with_bloom(det, event, bloom))
1106 }
1107 Quantifier::Count(required) => {
1108 if *required == 0 {
1109 return true;
1110 }
1111 let mut matched = 0u64;
1112 matching.any(|(_, det)| {
1113 if eval_detection_with_bloom(det, event, bloom) {
1114 matched += 1;
1115 }
1116 matched >= *required
1117 })
1118 }
1119 }
1120 }
1121 }
1122}
1123
1124pub(crate) fn eval_condition_with_bloom<E, B>(
1130 expr: &ConditionExpr,
1131 detections: &HashMap<String, CompiledDetection>,
1132 event: &E,
1133 matched_selections: &mut Vec<String>,
1134 bloom: &B,
1135) -> bool
1136where
1137 E: Event,
1138 B: crate::engine::bloom_index::BloomLookup,
1139{
1140 match expr {
1141 ConditionExpr::Identifier(name) => {
1142 if let Some(det) = detections.get(name) {
1143 let result = eval_detection_with_bloom(det, event, bloom);
1144 if result {
1145 matched_selections.push(name.clone());
1146 }
1147 result
1148 } else {
1149 false
1150 }
1151 }
1152
1153 ConditionExpr::And(exprs) => exprs
1154 .iter()
1155 .all(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1156
1157 ConditionExpr::Or(exprs) => exprs
1158 .iter()
1159 .any(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1160
1161 ConditionExpr::Not(inner) => {
1162 !eval_condition_with_bloom(inner, detections, event, matched_selections, bloom)
1163 }
1164
1165 ConditionExpr::Selector {
1166 quantifier,
1167 pattern,
1168 } => {
1169 let matching_names: Vec<&String> = detections
1170 .keys()
1171 .filter(|name| pattern.matches_detection_name(name))
1172 .collect();
1173
1174 let mut match_count = 0u64;
1175 for name in &matching_names {
1176 if let Some(det) = detections.get(*name)
1177 && eval_detection_with_bloom(det, event, bloom)
1178 {
1179 match_count += 1;
1180 matched_selections.push((*name).clone());
1181 }
1182 }
1183
1184 match quantifier {
1185 Quantifier::Any => match_count >= 1,
1186 Quantifier::All => match_count == matching_names.len() as u64,
1187 Quantifier::Count(n) => match_count >= *n,
1188 }
1189 }
1190 }
1191}
1192
1193#[cfg(test)]
1198fn eval_detection_item(item: &CompiledDetectionItem, event: &impl Event) -> bool {
1199 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1200}
1201
1202pub(crate) fn eval_detection_item_no_bloom(
1206 item: &CompiledDetectionItem,
1207 event: &impl Event,
1208) -> bool {
1209 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1210}
1211
1212fn eval_detection_with_bloom<E, B>(detection: &CompiledDetection, event: &E, bloom: &B) -> bool
1214where
1215 E: Event,
1216 B: crate::engine::bloom_index::BloomLookup,
1217{
1218 match detection {
1219 CompiledDetection::AllOf(items) => items
1220 .iter()
1221 .all(|item| eval_detection_item_with_bloom(item, event, bloom)),
1222 CompiledDetection::AnyOf(dets) => dets
1223 .iter()
1224 .any(|d| eval_detection_with_bloom(d, event, bloom)),
1225 CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
1226 CompiledDetection::ArrayMatch {
1227 field,
1228 quantifier,
1229 body,
1230 } => match event.get_field(field) {
1231 Some(value) => eval_array_quantified(&value, *quantifier, body, event),
1232 None => array_quantifier_matches_empty(*quantifier),
1233 },
1234 CompiledDetection::And(dets) => dets
1235 .iter()
1236 .all(|d| eval_detection_with_bloom(d, event, bloom)),
1237 CompiledDetection::Conditional { named, condition } => {
1241 eval_condition_with_bloom(condition, named, event, &mut Vec::new(), bloom)
1242 }
1243 }
1244}
1245
1246fn eval_detection_item_with_bloom<E, B>(item: &CompiledDetectionItem, event: &E, bloom: &B) -> bool
1253where
1254 E: Event,
1255 B: crate::engine::bloom_index::BloomLookup,
1256{
1257 if let Some(expect_exists) = item.exists {
1258 if let Some(field) = &item.field {
1259 let exists = event.get_field(field).is_some_and(|v| !v.is_null());
1260 return exists == expect_exists;
1261 }
1262 return !expect_exists;
1263 }
1264
1265 match &item.field {
1266 Some(field_name) => {
1267 if let Some(value) = event.get_field(field_name) {
1268 if item.bloom_eligible
1269 && bloom.verdict_for_field(field_name)
1270 == crate::engine::bloom_index::BloomVerdict::DefinitelyNoMatch
1271 {
1272 return false;
1273 }
1274 item.matcher.matches(&value, event)
1275 } else {
1276 matches!(item.matcher, CompiledMatcher::Null)
1277 }
1278 }
1279 None => item.matcher.matches_keyword(event),
1280 }
1281}
1282
1283const MAX_KEYWORD_MATCHES: usize = 16;
1287
1288fn collect_field_matches(
1296 selection_names: &[String],
1297 detections: &HashMap<String, CompiledDetection>,
1298 event: &impl Event,
1299 level: MatchDetailLevel,
1300) -> Vec<FieldMatch> {
1301 let mut matches = Vec::new();
1302 for name in selection_names {
1303 if let Some(det) = detections.get(name) {
1304 collect_detection_fields(name, det, event, level, &mut matches);
1305 }
1306 }
1307 matches
1308}
1309
1310fn collect_detection_fields(
1311 selection: &str,
1312 detection: &CompiledDetection,
1313 event: &impl Event,
1314 level: MatchDetailLevel,
1315 out: &mut Vec<FieldMatch>,
1316) {
1317 match detection {
1318 CompiledDetection::AllOf(items) => {
1319 for item in items {
1320 match &item.field {
1321 Some(field_name) => {
1322 if let Some(value) = event.get_field(field_name) {
1323 if item.matcher.matches(&value, event) {
1324 out.push(make_field_match(
1325 selection,
1326 field_name,
1327 value.to_json(),
1328 &item.matcher,
1329 level,
1330 ));
1331 }
1332 } else if level != MatchDetailLevel::Off
1333 && matches!(
1334 item.matcher,
1335 CompiledMatcher::Null | CompiledMatcher::Exists(false)
1336 )
1337 {
1338 out.push(make_field_match(
1342 selection,
1343 field_name,
1344 serde_json::Value::Null,
1345 &item.matcher,
1346 level,
1347 ));
1348 }
1349 }
1350 None => {
1351 if level != MatchDetailLevel::Off {
1353 collect_keyword_matches(selection, &item.matcher, event, level, out);
1354 }
1355 }
1356 }
1357 }
1358 }
1359 CompiledDetection::AnyOf(dets) => {
1360 for d in dets {
1361 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1362 collect_detection_fields(selection, d, event, level, out);
1363 }
1364 }
1365 }
1366 CompiledDetection::ArrayMatch { field, body, .. } => {
1367 let value = event.get_field(field);
1368 collect_array_match_fields(
1369 selection,
1370 field,
1371 body,
1372 value.as_ref(),
1373 event,
1374 level,
1375 "",
1376 out,
1377 );
1378 }
1379 CompiledDetection::And(dets) => {
1380 for d in dets {
1381 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1382 collect_detection_fields(selection, d, event, level, out);
1383 }
1384 }
1385 }
1386 CompiledDetection::Conditional { .. } => {}
1389 CompiledDetection::Keywords(matcher) => {
1390 if level != MatchDetailLevel::Off {
1393 collect_keyword_matches(selection, matcher, event, level, out);
1394 }
1395 }
1396 }
1397}
1398
1399#[allow(clippy::too_many_arguments)]
1406fn collect_array_match_fields<E: Event>(
1407 selection: &str,
1408 field: &str,
1409 body: &CompiledDetection,
1410 value: Option<&EventValue>,
1411 outer: &E,
1412 level: MatchDetailLevel,
1413 path_prefix: &str,
1414 out: &mut Vec<FieldMatch>,
1415) {
1416 let container_path = if path_prefix.is_empty() {
1417 field.to_string()
1418 } else {
1419 array::join_member_field(path_prefix, Some(field))
1420 };
1421
1422 let (scalar, members): (bool, Vec<&EventValue>) = match value {
1423 None => return,
1424 Some(EventValue::Null) => {
1425 out.push(FieldMatch::new(container_path, serde_json::Value::Null));
1426 return;
1427 }
1428 Some(EventValue::Array(items)) if items.is_empty() => {
1429 out.push(FieldMatch::new(
1430 container_path,
1431 serde_json::Value::Array(Vec::new()),
1432 ));
1433 return;
1434 }
1435 Some(EventValue::Array(items)) => (false, items.iter().collect()),
1436 Some(single) => (true, vec![single]),
1437 };
1438
1439 let matching: Vec<usize> = members
1440 .iter()
1441 .enumerate()
1442 .filter(|(_, m)| array::eval_array_body(body, m, outer))
1443 .map(|(i, _)| i)
1444 .take(array::ARRAY_MEMBER_CAP)
1445 .collect();
1446
1447 if matching.is_empty() {
1448 if let Some(v) = value {
1449 out.push(FieldMatch::new(container_path, v.to_json()));
1450 }
1451 return;
1452 }
1453
1454 for i in matching {
1455 let member_path = array::array_member_path(&container_path, i, scalar);
1456 let before = out.len();
1457 collect_array_body_fields(selection, body, members[i], outer, level, &member_path, out);
1458 if out.len() == before && level != MatchDetailLevel::Off {
1464 out.push(FieldMatch::new(member_path, members[i].to_json()));
1465 }
1466 }
1467}
1468
1469fn collect_array_body_fields<E: Event>(
1470 selection: &str,
1471 body: &CompiledDetection,
1472 member: &EventValue,
1473 outer: &E,
1474 level: MatchDetailLevel,
1475 member_path: &str,
1476 out: &mut Vec<FieldMatch>,
1477) {
1478 match body {
1479 CompiledDetection::AllOf(items) => {
1480 for item in items {
1481 if !array::eval_array_item(item, member, outer) {
1482 continue;
1483 }
1484 let relative = item.field.as_deref();
1485 let absent =
1486 relative.is_some_and(|name| array::element_field(member, name).is_none());
1487 if absent && level == MatchDetailLevel::Off {
1488 continue;
1489 }
1490 let path = array::join_member_field(member_path, relative);
1491 let value = match relative {
1492 Some(name) => array::element_field(member, name)
1493 .map(|v| v.to_json())
1494 .unwrap_or(serde_json::Value::Null),
1495 None => member.to_json(),
1496 };
1497 out.push(make_field_match(
1498 selection,
1499 &path,
1500 value,
1501 &item.matcher,
1502 level,
1503 ));
1504 }
1505 }
1506 CompiledDetection::AnyOf(dets) | CompiledDetection::And(dets) => {
1507 for d in dets {
1508 if array::eval_array_body(d, member, outer) {
1509 collect_array_body_fields(selection, d, member, outer, level, member_path, out);
1510 }
1511 }
1512 }
1513 CompiledDetection::ArrayMatch {
1514 field, body: inner, ..
1515 } => {
1516 collect_array_match_fields(
1517 selection,
1518 field,
1519 inner,
1520 array::element_field(member, field),
1521 outer,
1522 level,
1523 member_path,
1524 out,
1525 );
1526 }
1527 CompiledDetection::Keywords(matcher) => {
1528 if level != MatchDetailLevel::Off && matcher.matches(member, outer) {
1529 let d = matcher.describe();
1530 out.push(FieldMatch {
1531 field: member_path.to_string(),
1532 value: member.to_json(),
1533 selection: Some(selection.to_string()),
1534 matcher: Some(MatcherKind::Keyword),
1535 pattern: if level == MatchDetailLevel::Full {
1536 d.pattern
1537 } else {
1538 None
1539 },
1540 case_sensitive: d.case_sensitive,
1541 negated: d.negated,
1542 });
1543 }
1544 }
1545 CompiledDetection::Conditional { named, condition } => {
1546 collect_array_condition_fields(
1547 selection,
1548 condition,
1549 named,
1550 member,
1551 outer,
1552 level,
1553 member_path,
1554 out,
1555 );
1556 }
1557 }
1558}
1559
1560#[allow(clippy::too_many_arguments)]
1561fn collect_array_condition_fields<E: Event>(
1562 selection: &str,
1563 expr: &ConditionExpr,
1564 named: &HashMap<String, CompiledDetection>,
1565 member: &EventValue,
1566 outer: &E,
1567 level: MatchDetailLevel,
1568 member_path: &str,
1569 out: &mut Vec<FieldMatch>,
1570) {
1571 match expr {
1572 ConditionExpr::Identifier(name) => {
1573 if let Some(det) = named.get(name)
1574 && array::eval_array_body(det, member, outer)
1575 {
1576 collect_array_body_fields(selection, det, member, outer, level, member_path, out);
1577 }
1578 }
1579 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1580 for e in exprs {
1581 collect_array_condition_fields(
1582 selection,
1583 e,
1584 named,
1585 member,
1586 outer,
1587 level,
1588 member_path,
1589 out,
1590 );
1591 }
1592 }
1593 ConditionExpr::Not(_) => {}
1594 ConditionExpr::Selector { pattern, .. } => {
1595 let mut names: Vec<&String> = named
1596 .keys()
1597 .filter(|n| pattern.matches_detection_name(n))
1598 .collect();
1599 names.sort();
1600 for name in names {
1601 if let Some(det) = named.get(name)
1602 && array::eval_array_body(det, member, outer)
1603 {
1604 collect_array_body_fields(
1605 selection,
1606 det,
1607 member,
1608 outer,
1609 level,
1610 member_path,
1611 out,
1612 );
1613 }
1614 }
1615 }
1616 }
1617}
1618
1619fn make_field_match(
1623 selection: &str,
1624 field: &str,
1625 value: serde_json::Value,
1626 matcher: &CompiledMatcher,
1627 level: MatchDetailLevel,
1628) -> FieldMatch {
1629 match level {
1630 MatchDetailLevel::Off => FieldMatch::new(field, value),
1631 MatchDetailLevel::Summary | MatchDetailLevel::Full => {
1632 let d = matcher.describe();
1633 FieldMatch {
1634 field: field.to_string(),
1635 value,
1636 selection: Some(selection.to_string()),
1637 matcher: Some(d.kind),
1638 pattern: if level == MatchDetailLevel::Full {
1639 d.pattern
1640 } else {
1641 None
1642 },
1643 case_sensitive: d.case_sensitive,
1644 negated: d.negated,
1645 }
1646 }
1647 }
1648}
1649
1650fn collect_keyword_matches(
1654 selection: &str,
1655 matcher: &CompiledMatcher,
1656 event: &impl Event,
1657 level: MatchDetailLevel,
1658 out: &mut Vec<FieldMatch>,
1659) {
1660 let descriptor = matcher.describe();
1661 let mut count = 0;
1662 for s in event.all_string_values() {
1663 if count >= MAX_KEYWORD_MATCHES {
1664 break;
1665 }
1666 if matcher.matches_str(&s) {
1667 count += 1;
1668 out.push(FieldMatch {
1669 field: "keyword".to_string(),
1670 value: serde_json::Value::String(s.into_owned()),
1671 selection: Some(selection.to_string()),
1672 matcher: Some(MatcherKind::Keyword),
1673 pattern: if level == MatchDetailLevel::Full {
1674 descriptor.pattern.clone()
1675 } else {
1676 None
1677 },
1678 case_sensitive: descriptor.case_sensitive,
1679 negated: descriptor.negated,
1680 });
1681 }
1682 }
1683}