1mod helpers;
11#[doc(hidden)]
12pub mod optimizer;
13#[cfg(test)]
14mod tests;
15
16#[cfg(test)]
19pub(crate) use optimizer::optimize_any_of as optimize_any_of_for_test;
20
21use std::borrow::Cow;
22use std::collections::HashMap;
23use std::sync::Arc;
24
25use base64::Engine as Base64Engine;
26use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
27use regex::Regex;
28
29use rsigma_parser::fieldpath::{first_unescaped, unescape_brackets};
30use rsigma_parser::value::{SpecialChar, StringPart};
31use rsigma_parser::{
32 ArrayQuantifier, ConditionExpr, Detection, DetectionItem, Level, LogSource, Modifier,
33 Quantifier, SigmaRule, SigmaString, SigmaValue,
34};
35
36use crate::error::{EvalError, Result};
37use crate::event::{Event, EventValue};
38use crate::matcher::{CompiledMatcher, sigma_string_to_regex};
39use crate::result::{
40 DetectionBody, EvaluationResult, FieldMatch, MatchDetailLevel, MatcherKind, ResultBody,
41 RuleHeader,
42};
43
44pub(crate) use helpers::yaml_to_json_map;
45use helpers::{
46 base64_offset_patterns, build_regex, expand_windash, sigma_string_to_bytes, to_utf16_bom_bytes,
47 to_utf16be_bytes, to_utf16le_bytes, value_to_f64, value_to_plain_string,
48};
49
50#[derive(Debug, Clone)]
56pub struct CompiledRule {
57 pub title: String,
58 pub id: Option<String>,
59 pub level: Option<Level>,
60 pub tags: Vec<String>,
61 pub logsource: LogSource,
62 pub detections: HashMap<String, CompiledDetection>,
64 pub conditions: Vec<ConditionExpr>,
66 pub include_event: bool,
69 pub custom_attributes: Arc<HashMap<String, serde_json::Value>>,
74}
75
76#[derive(Debug, Clone)]
78pub enum CompiledDetection {
79 AllOf(Vec<CompiledDetectionItem>),
81 AnyOf(Vec<CompiledDetection>),
83 Keywords(CompiledMatcher),
85 ArrayMatch {
89 field: String,
90 quantifier: ArrayQuantifier,
91 body: Box<CompiledDetection>,
92 },
93 And(Vec<CompiledDetection>),
96 Conditional {
101 named: HashMap<String, CompiledDetection>,
102 condition: ConditionExpr,
103 },
104}
105
106#[derive(Debug, Clone)]
108pub struct CompiledDetectionItem {
109 pub field: Option<String>,
111 pub matcher: CompiledMatcher,
113 pub exists: Option<bool>,
115 pub bloom_eligible: bool,
120}
121
122#[derive(Clone, Copy)]
128struct ModCtx {
129 contains: bool,
130 startswith: bool,
131 endswith: bool,
132 all: bool,
133 base64: bool,
134 base64offset: bool,
135 wide: bool,
136 utf16be: bool,
137 utf16: bool,
138 windash: bool,
139 re: bool,
140 cidr: bool,
141 cased: bool,
142 exists: bool,
143 fieldref: bool,
144 gt: bool,
145 gte: bool,
146 lt: bool,
147 lte: bool,
148 neq: bool,
149 ignore_case: bool,
150 multiline: bool,
151 dotall: bool,
152 expand: bool,
153 timestamp_part: Option<crate::matcher::TimePart>,
154}
155
156impl ModCtx {
157 fn from_modifiers(modifiers: &[Modifier]) -> Self {
158 let mut ctx = ModCtx {
159 contains: false,
160 startswith: false,
161 endswith: false,
162 all: false,
163 base64: false,
164 base64offset: false,
165 wide: false,
166 utf16be: false,
167 utf16: false,
168 windash: false,
169 re: false,
170 cidr: false,
171 cased: false,
172 exists: false,
173 fieldref: false,
174 gt: false,
175 gte: false,
176 lt: false,
177 lte: false,
178 neq: false,
179 ignore_case: false,
180 multiline: false,
181 dotall: false,
182 expand: false,
183 timestamp_part: None,
184 };
185 for m in modifiers {
186 match m {
187 Modifier::Contains => ctx.contains = true,
188 Modifier::StartsWith => ctx.startswith = true,
189 Modifier::EndsWith => ctx.endswith = true,
190 Modifier::All => ctx.all = true,
191 Modifier::Base64 => ctx.base64 = true,
192 Modifier::Base64Offset => ctx.base64offset = true,
193 Modifier::Wide => ctx.wide = true,
194 Modifier::Utf16be => ctx.utf16be = true,
195 Modifier::Utf16 => ctx.utf16 = true,
196 Modifier::WindAsh => ctx.windash = true,
197 Modifier::Re => ctx.re = true,
198 Modifier::Cidr => ctx.cidr = true,
199 Modifier::Cased => ctx.cased = true,
200 Modifier::Exists => ctx.exists = true,
201 Modifier::FieldRef => ctx.fieldref = true,
202 Modifier::Gt => ctx.gt = true,
203 Modifier::Gte => ctx.gte = true,
204 Modifier::Lt => ctx.lt = true,
205 Modifier::Lte => ctx.lte = true,
206 Modifier::Neq => ctx.neq = true,
207 Modifier::IgnoreCase => ctx.ignore_case = true,
208 Modifier::Multiline => ctx.multiline = true,
209 Modifier::DotAll => ctx.dotall = true,
210 Modifier::Expand => ctx.expand = true,
211 Modifier::Hour => ctx.timestamp_part = Some(crate::matcher::TimePart::Hour),
212 Modifier::Day => ctx.timestamp_part = Some(crate::matcher::TimePart::Day),
213 Modifier::Week => ctx.timestamp_part = Some(crate::matcher::TimePart::Week),
214 Modifier::Month => ctx.timestamp_part = Some(crate::matcher::TimePart::Month),
215 Modifier::Year => ctx.timestamp_part = Some(crate::matcher::TimePart::Year),
216 Modifier::Minute => ctx.timestamp_part = Some(crate::matcher::TimePart::Minute),
217 }
218 }
219 ctx
220 }
221
222 fn is_case_insensitive(&self) -> bool {
225 !self.cased
226 }
227
228 fn has_numeric_comparison(&self) -> bool {
230 self.gt || self.gte || self.lt || self.lte
231 }
232
233 fn has_neq(&self) -> bool {
235 self.neq
236 }
237}
238
239pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
245 let mut detections = HashMap::new();
246 for (name, detection) in &rule.detection.named {
247 detections.insert(name.clone(), compile_detection(detection)?);
248 }
249
250 for condition in &rule.detection.conditions {
251 validate_condition_refs(condition, &detections)?;
252 }
253
254 let include_event = rule
255 .custom_attributes
256 .get("rsigma.include_event")
257 .and_then(|v| v.as_str())
258 == Some("true");
259
260 let custom_attributes = Arc::new(yaml_to_json_map(&rule.custom_attributes));
261
262 Ok(CompiledRule {
263 title: rule.title.clone(),
264 id: rule.id.clone(),
265 level: rule.level,
266 tags: rule.tags.clone(),
267 logsource: rule.logsource.clone(),
268 detections,
269 conditions: rule.detection.conditions.clone(),
270 include_event,
271 custom_attributes,
272 })
273}
274
275fn validate_condition_refs(
279 expr: &ConditionExpr,
280 detections: &HashMap<String, CompiledDetection>,
281) -> Result<()> {
282 match expr {
283 ConditionExpr::Identifier(name) => {
284 if !detections.contains_key(name) {
285 return Err(EvalError::UnknownDetection(name.clone()));
286 }
287 Ok(())
288 }
289 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
290 for e in exprs {
291 validate_condition_refs(e, detections)?;
292 }
293 Ok(())
294 }
295 ConditionExpr::Not(inner) => validate_condition_refs(inner, detections),
296 ConditionExpr::Selector { .. } => Ok(()),
297 }
298}
299
300pub fn evaluate_rule(rule: &CompiledRule, event: &impl Event) -> Option<EvaluationResult> {
308 evaluate_rule_with_bloom(
309 rule,
310 event,
311 &crate::engine::bloom_index::NoBloom,
312 MatchDetailLevel::Off,
313 )
314}
315
316pub(crate) fn evaluate_rule_with_bloom<E, B>(
324 rule: &CompiledRule,
325 event: &E,
326 bloom: &B,
327 level: MatchDetailLevel,
328) -> Option<EvaluationResult>
329where
330 E: Event,
331 B: crate::engine::bloom_index::BloomLookup,
332{
333 for condition in &rule.conditions {
334 let mut matched_selections = Vec::new();
335 if eval_condition_with_bloom(
336 condition,
337 &rule.detections,
338 event,
339 &mut matched_selections,
340 bloom,
341 ) {
342 let matched_fields =
343 collect_field_matches(&matched_selections, &rule.detections, event, level);
344
345 let event_data = if rule.include_event {
346 Some(event.to_json())
347 } else {
348 None
349 };
350
351 return Some(EvaluationResult {
352 header: RuleHeader {
353 rule_title: rule.title.clone(),
354 rule_id: rule.id.clone(),
355 level: rule.level,
356 tags: rule.tags.clone(),
357 custom_attributes: rule.custom_attributes.clone(),
358 enrichments: None,
359 },
360 body: ResultBody::Detection(DetectionBody {
361 matched_selections,
362 matched_fields,
363 event: event_data,
364 }),
365 });
366 }
367 }
368 None
369}
370
371pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
380 match detection {
381 Detection::AllOf(items) => {
382 if items.is_empty() {
383 return Err(EvalError::InvalidModifiers(
384 "AllOf detection must not be empty (vacuous truth)".into(),
385 ));
386 }
387 let compiled: Result<Vec<_>> = items.iter().map(compile_detection_item).collect();
388 Ok(CompiledDetection::AllOf(compiled?))
389 }
390 Detection::AnyOf(dets) => {
391 if dets.is_empty() {
392 return Err(EvalError::InvalidModifiers(
393 "AnyOf detection must not be empty (would never match)".into(),
394 ));
395 }
396 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
397 Ok(CompiledDetection::AnyOf(compiled?))
398 }
399 Detection::ArrayMatch {
400 field,
401 quantifier,
402 body,
403 } => {
404 let compiled_body = compile_detection(body)?;
405 Ok(CompiledDetection::ArrayMatch {
406 field: field.clone(),
407 quantifier: *quantifier,
408 body: Box::new(compiled_body),
409 })
410 }
411 Detection::And(dets) => {
412 if dets.is_empty() {
413 return Err(EvalError::InvalidModifiers(
414 "And detection must not be empty".into(),
415 ));
416 }
417 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
418 Ok(CompiledDetection::And(compiled?))
419 }
420 Detection::Conditional { named, condition } => {
421 if named.is_empty() {
422 return Err(EvalError::InvalidModifiers(
423 "Conditional detection must have at least one named sub-selection".into(),
424 ));
425 }
426 let compiled: Result<HashMap<String, CompiledDetection>> = named
427 .iter()
428 .map(|(k, d)| Ok((k.clone(), compile_detection(d)?)))
429 .collect();
430 Ok(CompiledDetection::Conditional {
431 named: compiled?,
432 condition: condition.clone(),
433 })
434 }
435 Detection::Keywords(values) => {
436 let ci = true; let matchers: Vec<CompiledMatcher> = values
438 .iter()
439 .map(|v| compile_value_default(v, ci))
440 .collect::<Result<Vec<_>>>()?;
441 let matcher = optimizer::optimize_any_of(matchers);
443 Ok(CompiledDetection::Keywords(matcher))
444 }
445 }
446}
447
448fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
449 let ctx = ModCtx::from_modifiers(&item.field.modifiers);
450
451 validate_modifiers(&ctx, &item.field.modifiers)?;
460
461 if ctx.exists {
463 let expect = match item.values.first() {
464 Some(SigmaValue::Bool(b)) => *b,
465 Some(SigmaValue::String(s)) => match s.as_plain().as_deref() {
466 Some("true") | Some("yes") => true,
467 Some("false") | Some("no") => false,
468 _ => true,
469 },
470 _ => true,
471 };
472 return Ok(CompiledDetectionItem {
473 field: item.field.name.clone(),
474 matcher: CompiledMatcher::Exists(expect),
475 exists: Some(expect),
476 bloom_eligible: false,
477 });
478 }
479
480 if ctx.all && item.values.len() <= 1 {
482 return Err(EvalError::InvalidModifiers(
483 "|all modifier requires more than one value".to_string(),
484 ));
485 }
486
487 let matchers: Result<Vec<CompiledMatcher>> =
489 item.values.iter().map(|v| compile_value(v, &ctx)).collect();
490 let matchers = matchers?;
491
492 let combined = if ctx.all {
499 if matchers.len() == 1 {
500 matchers
501 .into_iter()
502 .next()
503 .unwrap_or(CompiledMatcher::AllOf(vec![]))
504 } else {
505 CompiledMatcher::AllOf(matchers)
506 }
507 } else {
508 optimizer::optimize_any_of(matchers)
509 };
510
511 let bloom_eligible = item.field.name.is_some()
512 && crate::engine::bloom_index::is_positive_substring_matcher(&combined);
513
514 Ok(CompiledDetectionItem {
515 field: item.field.name.clone(),
516 matcher: combined,
517 exists: None,
518 bloom_eligible,
519 })
520}
521
522fn validate_modifiers(ctx: &ModCtx, modifiers: &[Modifier]) -> Result<()> {
559 let mut operators: Vec<&'static str> = Vec::new();
561 if ctx.contains {
562 operators.push("contains");
563 }
564 if ctx.startswith {
565 operators.push("startswith");
566 }
567 if ctx.endswith {
568 operators.push("endswith");
569 }
570 if ctx.re {
571 operators.push("re");
572 }
573 if ctx.cidr {
574 operators.push("cidr");
575 }
576 if ctx.exists {
577 operators.push("exists");
578 }
579 if ctx.fieldref {
580 operators.push("fieldref");
581 }
582 if ctx.gt {
583 operators.push("gt");
584 }
585 if ctx.gte {
586 operators.push("gte");
587 }
588 if ctx.lt {
589 operators.push("lt");
590 }
591 if ctx.lte {
592 operators.push("lte");
593 }
594 for m in modifiers {
595 match m {
596 Modifier::Minute => operators.push("minute"),
597 Modifier::Hour => operators.push("hour"),
598 Modifier::Day => operators.push("day"),
599 Modifier::Week => operators.push("week"),
600 Modifier::Month => operators.push("month"),
601 Modifier::Year => operators.push("year"),
602 _ => {}
603 }
604 }
605 if operators.len() > 1 {
606 return Err(EvalError::InvalidModifiers(format!(
607 "conflicting modifiers: at most one operator may be set per field; \
608 got |{}",
609 operators.join(", |")
610 )));
611 }
612
613 let mut wide_encodings: Vec<&'static str> = Vec::new();
615 if ctx.wide {
616 wide_encodings.push("wide");
617 }
618 if ctx.utf16 {
619 wide_encodings.push("utf16");
620 }
621 if ctx.utf16be {
622 wide_encodings.push("utf16be");
623 }
624 if wide_encodings.len() > 1 {
625 return Err(EvalError::InvalidModifiers(format!(
626 "conflicting modifiers: |wide, |utf16, and |utf16be are mutually \
627 exclusive UTF-16 encodings; got |{}",
628 wide_encodings.join(", |")
629 )));
630 }
631
632 if ctx.base64 && ctx.base64offset {
634 return Err(EvalError::InvalidModifiers(
635 "conflicting modifiers: |base64 and |base64offset are mutually \
636 exclusive base64 strategies; pick one"
637 .into(),
638 ));
639 }
640
641 let has_non_string_operator = ctx.re
646 || ctx.cidr
647 || ctx.exists
648 || ctx.fieldref
649 || ctx.has_numeric_comparison()
650 || ctx.timestamp_part.is_some();
651 if has_non_string_operator {
652 let mut transforms: Vec<&'static str> = Vec::new();
653 if ctx.base64 {
654 transforms.push("base64");
655 }
656 if ctx.base64offset {
657 transforms.push("base64offset");
658 }
659 if ctx.wide {
660 transforms.push("wide");
661 }
662 if ctx.utf16 {
663 transforms.push("utf16");
664 }
665 if ctx.utf16be {
666 transforms.push("utf16be");
667 }
668 if ctx.windash {
669 transforms.push("windash");
670 }
671 if ctx.expand {
672 transforms.push("expand");
673 }
674 if !transforms.is_empty() {
675 return Err(EvalError::InvalidModifiers(format!(
676 "conflicting modifiers: value transformations |{} only apply \
677 to string match operators (default eq, contains, startswith, \
678 endswith) and cannot be combined with the operator that is \
679 also set on this field",
680 transforms.join(", |")
681 )));
682 }
683 }
684
685 if !ctx.re {
687 let mut regex_flags: Vec<&'static str> = Vec::new();
688 if ctx.ignore_case {
689 regex_flags.push("i");
690 }
691 if ctx.multiline {
692 regex_flags.push("m");
693 }
694 if ctx.dotall {
695 regex_flags.push("s");
696 }
697 if !regex_flags.is_empty() {
698 return Err(EvalError::InvalidModifiers(format!(
699 "regex flag modifiers |{} have no effect without |re; \
700 case sensitivity for substring or equality matching is \
701 controlled by |cased (or its absence, which keeps the \
702 default case-insensitive behavior)",
703 regex_flags.join(", |")
704 )));
705 }
706 }
707
708 Ok(())
709}
710
711fn compile_value(value: &SigmaValue, ctx: &ModCtx) -> Result<CompiledMatcher> {
717 let ci = ctx.is_case_insensitive();
718
719 if ctx.expand {
723 let plain = value_to_plain_string(value)?;
724 let template = crate::matcher::parse_expand_template(&plain);
725 return Ok(CompiledMatcher::Expand {
726 template,
727 case_insensitive: ci,
728 });
729 }
730
731 if let Some(part) = ctx.timestamp_part {
733 let inner = match value {
736 SigmaValue::Integer(n) => CompiledMatcher::NumericEq(*n as f64),
737 SigmaValue::Float(n) => CompiledMatcher::NumericEq(*n),
738 SigmaValue::String(s) => {
739 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
740 let n: f64 = plain.parse().map_err(|_| {
741 EvalError::IncompatibleValue(format!(
742 "timestamp part modifier requires numeric value, got: {plain}"
743 ))
744 })?;
745 CompiledMatcher::NumericEq(n)
746 }
747 _ => {
748 return Err(EvalError::IncompatibleValue(
749 "timestamp part modifier requires numeric value".into(),
750 ));
751 }
752 };
753 return Ok(CompiledMatcher::TimestampPart {
754 part,
755 inner: Box::new(inner),
756 });
757 }
758
759 if ctx.fieldref {
761 let field_name = value_to_plain_string(value)?;
762 return Ok(CompiledMatcher::FieldRef {
763 field: field_name,
764 case_insensitive: ci,
765 });
766 }
767
768 if ctx.re {
772 let pattern = value_to_plain_string(value)?;
773 let regex = build_regex(&pattern, ctx.ignore_case, ctx.multiline, ctx.dotall)?;
774 return Ok(CompiledMatcher::Regex(regex));
775 }
776
777 if ctx.cidr {
779 let cidr_str = value_to_plain_string(value)?;
780 let net: ipnet::IpNet = cidr_str
781 .parse()
782 .map_err(|e: ipnet::AddrParseError| EvalError::InvalidCidr(e))?;
783 return Ok(CompiledMatcher::Cidr(net));
784 }
785
786 if ctx.has_numeric_comparison() {
788 let n = value_to_f64(value)?;
789 if ctx.gt {
790 return Ok(CompiledMatcher::NumericGt(n));
791 }
792 if ctx.gte {
793 return Ok(CompiledMatcher::NumericGte(n));
794 }
795 if ctx.lt {
796 return Ok(CompiledMatcher::NumericLt(n));
797 }
798 if ctx.lte {
799 return Ok(CompiledMatcher::NumericLte(n));
800 }
801 }
802
803 if ctx.has_neq() {
805 let mut inner_ctx = ModCtx { ..*ctx };
807 inner_ctx.neq = false;
808 let inner = compile_value(value, &inner_ctx)?;
809 return Ok(CompiledMatcher::Not(Box::new(inner)));
810 }
811
812 match value {
814 SigmaValue::Integer(n) => {
815 if ctx.contains || ctx.startswith || ctx.endswith {
816 return compile_string_value(&n.to_string(), ctx);
818 }
819 return Ok(CompiledMatcher::NumericEq(*n as f64));
820 }
821 SigmaValue::Float(n) => {
822 if ctx.contains || ctx.startswith || ctx.endswith {
823 return compile_string_value(&n.to_string(), ctx);
824 }
825 return Ok(CompiledMatcher::NumericEq(*n));
826 }
827 SigmaValue::Bool(b) => return Ok(CompiledMatcher::BoolEq(*b)),
828 SigmaValue::Null => return Ok(CompiledMatcher::Null),
829 SigmaValue::String(_) => {} }
831
832 let sigma_str = match value {
834 SigmaValue::String(s) => s,
835 _ => unreachable!(),
836 };
837
838 let mut bytes = sigma_string_to_bytes(sigma_str);
840
841 if ctx.wide {
843 bytes = to_utf16le_bytes(&bytes);
844 }
845
846 if ctx.utf16be {
848 bytes = to_utf16be_bytes(&bytes);
849 }
850
851 if ctx.utf16 {
853 bytes = to_utf16_bom_bytes(&bytes);
854 }
855
856 if ctx.base64 {
858 let encoded = BASE64_STANDARD.encode(&bytes);
859 return compile_string_value(&encoded, ctx);
860 }
861
862 if ctx.base64offset {
864 let patterns = base64_offset_patterns(&bytes);
865 let matchers: Vec<CompiledMatcher> = patterns
866 .into_iter()
867 .map(|p| {
868 CompiledMatcher::Contains {
870 value: if ci { p.to_lowercase() } else { p },
871 case_insensitive: ci,
872 }
873 })
874 .collect();
875 return Ok(CompiledMatcher::AnyOf(matchers));
876 }
877
878 if ctx.windash {
880 let plain = sigma_str
881 .as_plain()
882 .unwrap_or_else(|| sigma_str.original.clone());
883 let variants = expand_windash(&plain)?;
884 let matchers: Result<Vec<CompiledMatcher>> = variants
885 .into_iter()
886 .map(|v| compile_string_value(&v, ctx))
887 .collect();
888 return Ok(CompiledMatcher::AnyOf(matchers?));
889 }
890
891 compile_sigma_string(sigma_str, ctx)
893}
894
895fn compile_sigma_string(sigma_str: &SigmaString, ctx: &ModCtx) -> Result<CompiledMatcher> {
897 let ci = ctx.is_case_insensitive();
898
899 if sigma_str.is_plain() {
901 let plain = sigma_str.as_plain().unwrap_or_default();
902 return compile_string_value(&plain, ctx);
903 }
904
905 let mut pattern = String::new();
910 if ci {
911 pattern.push_str("(?i)");
912 }
913
914 if !ctx.contains && !ctx.startswith {
915 pattern.push('^');
916 }
917
918 for part in &sigma_str.parts {
919 match part {
920 StringPart::Plain(text) => {
921 pattern.push_str(®ex::escape(text));
922 }
923 StringPart::Special(SpecialChar::WildcardMulti) => {
924 pattern.push_str(".*");
925 }
926 StringPart::Special(SpecialChar::WildcardSingle) => {
927 pattern.push('.');
928 }
929 }
930 }
931
932 if !ctx.contains && !ctx.endswith {
933 pattern.push('$');
934 }
935
936 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
937 Ok(CompiledMatcher::Regex(regex))
938}
939
940fn compile_string_value(plain: &str, ctx: &ModCtx) -> Result<CompiledMatcher> {
942 let ci = ctx.is_case_insensitive();
943
944 if ctx.contains {
945 Ok(CompiledMatcher::Contains {
946 value: if ci {
947 plain.to_lowercase()
948 } else {
949 plain.to_string()
950 },
951 case_insensitive: ci,
952 })
953 } else if ctx.startswith {
954 Ok(CompiledMatcher::StartsWith {
955 value: if ci {
956 plain.to_lowercase()
957 } else {
958 plain.to_string()
959 },
960 case_insensitive: ci,
961 })
962 } else if ctx.endswith {
963 Ok(CompiledMatcher::EndsWith {
964 value: if ci {
965 plain.to_lowercase()
966 } else {
967 plain.to_string()
968 },
969 case_insensitive: ci,
970 })
971 } else {
972 Ok(CompiledMatcher::Exact {
973 value: if ci {
974 plain.to_lowercase()
975 } else {
976 plain.to_string()
977 },
978 case_insensitive: ci,
979 })
980 }
981}
982
983fn compile_value_default(value: &SigmaValue, case_insensitive: bool) -> Result<CompiledMatcher> {
985 match value {
986 SigmaValue::String(s) => {
987 if s.is_plain() {
988 let plain = s.as_plain().unwrap_or_default();
989 Ok(CompiledMatcher::Contains {
990 value: if case_insensitive {
991 plain.to_lowercase()
992 } else {
993 plain
994 },
995 case_insensitive,
996 })
997 } else {
998 let pattern = sigma_string_to_regex(&s.parts, case_insensitive);
1000 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
1001 Ok(CompiledMatcher::Regex(regex))
1002 }
1003 }
1004 SigmaValue::Integer(n) => Ok(CompiledMatcher::NumericEq(*n as f64)),
1005 SigmaValue::Float(n) => Ok(CompiledMatcher::NumericEq(*n)),
1006 SigmaValue::Bool(b) => Ok(CompiledMatcher::BoolEq(*b)),
1007 SigmaValue::Null => Ok(CompiledMatcher::Null),
1008 }
1009}
1010
1011pub fn eval_condition(
1020 expr: &ConditionExpr,
1021 detections: &HashMap<String, CompiledDetection>,
1022 event: &impl Event,
1023 matched_selections: &mut Vec<String>,
1024) -> bool {
1025 eval_condition_with_bloom(
1026 expr,
1027 detections,
1028 event,
1029 matched_selections,
1030 &crate::engine::bloom_index::NoBloom,
1031 )
1032}
1033
1034pub(crate) fn eval_condition_with_bloom<E, B>(
1040 expr: &ConditionExpr,
1041 detections: &HashMap<String, CompiledDetection>,
1042 event: &E,
1043 matched_selections: &mut Vec<String>,
1044 bloom: &B,
1045) -> bool
1046where
1047 E: Event,
1048 B: crate::engine::bloom_index::BloomLookup,
1049{
1050 match expr {
1051 ConditionExpr::Identifier(name) => {
1052 if let Some(det) = detections.get(name) {
1053 let result = eval_detection_with_bloom(det, event, bloom);
1054 if result {
1055 matched_selections.push(name.clone());
1056 }
1057 result
1058 } else {
1059 false
1060 }
1061 }
1062
1063 ConditionExpr::And(exprs) => exprs
1064 .iter()
1065 .all(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1066
1067 ConditionExpr::Or(exprs) => exprs
1068 .iter()
1069 .any(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
1070
1071 ConditionExpr::Not(inner) => {
1072 !eval_condition_with_bloom(inner, detections, event, matched_selections, bloom)
1073 }
1074
1075 ConditionExpr::Selector {
1076 quantifier,
1077 pattern,
1078 } => {
1079 let matching_names: Vec<&String> = detections
1080 .keys()
1081 .filter(|name| pattern.matches_detection_name(name))
1082 .collect();
1083
1084 let mut match_count = 0u64;
1085 for name in &matching_names {
1086 if let Some(det) = detections.get(*name)
1087 && eval_detection_with_bloom(det, event, bloom)
1088 {
1089 match_count += 1;
1090 matched_selections.push((*name).clone());
1091 }
1092 }
1093
1094 match quantifier {
1095 Quantifier::Any => match_count >= 1,
1096 Quantifier::All => match_count == matching_names.len() as u64,
1097 Quantifier::Count(n) => match_count >= *n,
1098 }
1099 }
1100 }
1101}
1102
1103#[cfg(test)]
1108fn eval_detection_item(item: &CompiledDetectionItem, event: &impl Event) -> bool {
1109 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1110}
1111
1112pub(crate) fn eval_detection_no_bloom(detection: &CompiledDetection, event: &impl Event) -> bool {
1118 eval_detection_with_bloom(detection, event, &crate::engine::bloom_index::NoBloom)
1119}
1120
1121pub(crate) fn eval_detection_item_no_bloom(
1125 item: &CompiledDetectionItem,
1126 event: &impl Event,
1127) -> bool {
1128 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
1129}
1130
1131fn eval_detection_with_bloom<E, B>(detection: &CompiledDetection, event: &E, bloom: &B) -> bool
1133where
1134 E: Event,
1135 B: crate::engine::bloom_index::BloomLookup,
1136{
1137 match detection {
1138 CompiledDetection::AllOf(items) => items
1139 .iter()
1140 .all(|item| eval_detection_item_with_bloom(item, event, bloom)),
1141 CompiledDetection::AnyOf(dets) => dets
1142 .iter()
1143 .any(|d| eval_detection_with_bloom(d, event, bloom)),
1144 CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
1145 CompiledDetection::ArrayMatch {
1146 field,
1147 quantifier,
1148 body,
1149 } => match event.get_field(field) {
1150 Some(value) => eval_array_quantified(&value, *quantifier, body, event),
1151 None => array_quantifier_matches_empty(*quantifier),
1152 },
1153 CompiledDetection::And(dets) => dets
1154 .iter()
1155 .all(|d| eval_detection_with_bloom(d, event, bloom)),
1156 CompiledDetection::Conditional { named, condition } => {
1160 eval_condition_with_bloom(condition, named, event, &mut Vec::new(), bloom)
1161 }
1162 }
1163}
1164
1165fn eval_array_quantified<E: Event>(
1171 value: &EventValue,
1172 quantifier: ArrayQuantifier,
1173 body: &CompiledDetection,
1174 outer: &E,
1175) -> bool {
1176 match value {
1177 EventValue::Array(members) => match quantifier {
1178 ArrayQuantifier::Any => members.iter().any(|m| eval_array_body(body, m, outer)),
1179 ArrayQuantifier::All => {
1180 !members.is_empty() && members.iter().all(|m| eval_array_body(body, m, outer))
1181 }
1182 ArrayQuantifier::AllOrEmpty => members.iter().all(|m| eval_array_body(body, m, outer)),
1183 ArrayQuantifier::None => !members.iter().any(|m| eval_array_body(body, m, outer)),
1184 },
1185 EventValue::Null => array_quantifier_matches_empty(quantifier),
1188 single => match quantifier {
1190 ArrayQuantifier::None => !eval_array_body(body, single, outer),
1191 _ => eval_array_body(body, single, outer),
1192 },
1193 }
1194}
1195
1196fn array_quantifier_matches_empty(quantifier: ArrayQuantifier) -> bool {
1198 matches!(
1199 quantifier,
1200 ArrayQuantifier::None | ArrayQuantifier::AllOrEmpty
1201 )
1202}
1203
1204fn eval_array_body<E: Event>(body: &CompiledDetection, member: &EventValue, outer: &E) -> bool {
1209 match body {
1210 CompiledDetection::AllOf(items) => items
1211 .iter()
1212 .all(|item| eval_array_item(item, member, outer)),
1213 CompiledDetection::AnyOf(dets) => dets.iter().any(|d| eval_array_body(d, member, outer)),
1214 CompiledDetection::And(dets) => dets.iter().all(|d| eval_array_body(d, member, outer)),
1215 CompiledDetection::ArrayMatch {
1216 field,
1217 quantifier,
1218 body: inner,
1219 } => match element_field(member, field) {
1220 Some(value) => eval_array_quantified(value, *quantifier, inner, outer),
1221 None => array_quantifier_matches_empty(*quantifier),
1222 },
1223 CompiledDetection::Keywords(matcher) => matcher.matches(member, outer),
1225 CompiledDetection::Conditional { named, condition } => {
1228 eval_array_condition(condition, named, member, outer)
1229 }
1230 }
1231}
1232
1233fn eval_array_condition<E: Event>(
1241 expr: &ConditionExpr,
1242 named: &HashMap<String, CompiledDetection>,
1243 member: &EventValue,
1244 outer: &E,
1245) -> bool {
1246 match expr {
1247 ConditionExpr::Identifier(name) => named
1248 .get(name)
1249 .is_some_and(|d| eval_array_body(d, member, outer)),
1250 ConditionExpr::And(exprs) => exprs
1251 .iter()
1252 .all(|e| eval_array_condition(e, named, member, outer)),
1253 ConditionExpr::Or(exprs) => exprs
1254 .iter()
1255 .any(|e| eval_array_condition(e, named, member, outer)),
1256 ConditionExpr::Not(inner) => !eval_array_condition(inner, named, member, outer),
1257 ConditionExpr::Selector {
1258 quantifier,
1259 pattern,
1260 } => {
1261 let names: Vec<&String> = named
1262 .keys()
1263 .filter(|n| pattern.matches_detection_name(n))
1264 .collect();
1265 let count = names
1266 .iter()
1267 .filter(|n| {
1268 named
1269 .get(**n)
1270 .is_some_and(|d| eval_array_body(d, member, outer))
1271 })
1272 .count() as u64;
1273 match quantifier {
1274 Quantifier::Any => count >= 1,
1275 Quantifier::All => count == names.len() as u64,
1276 Quantifier::Count(n) => count >= *n,
1277 }
1278 }
1279 }
1280}
1281
1282fn eval_array_item<E: Event>(item: &CompiledDetectionItem, member: &EventValue, outer: &E) -> bool {
1284 if let Some(expect_exists) = item.exists {
1285 let exists = match &item.field {
1286 Some(name) => element_field(member, name).is_some_and(|v| !v.is_null()),
1287 None => !member.is_null(),
1288 };
1289 return exists == expect_exists;
1290 }
1291
1292 match &item.field {
1293 Some(name) => match element_field(member, name) {
1294 Some(value) => item.matcher.matches(value, outer),
1295 None => matches!(item.matcher, CompiledMatcher::Null),
1296 },
1297 None => item.matcher.matches(member, outer),
1299 }
1300}
1301
1302fn element_field<'a>(member: &'a EventValue<'a>, path: &str) -> Option<&'a EventValue<'a>> {
1308 if let EventValue::Map(entries) = member
1309 && let Some((_, v)) = entries.iter().find(|(k, _)| k.as_ref() == path)
1310 {
1311 return Some(v);
1312 }
1313 let ops = parse_event_ops(path);
1314 nav_event_value(member, &ops)
1315}
1316
1317enum EventOp<'a> {
1318 Key(Cow<'a, str>),
1319 Index(i64),
1320}
1321
1322fn parse_event_ops(path: &str) -> Vec<EventOp<'_>> {
1326 let mut ops = Vec::new();
1327 for part in path.split('.') {
1328 match first_unescaped(part, b'[') {
1329 Some(bpos) if index_groups(&part[bpos..]).is_some() => {
1330 let name = &part[..bpos];
1331 if !name.is_empty() {
1332 ops.push(EventOp::Key(unescape_brackets(name)));
1333 }
1334 for idx in index_groups(&part[bpos..]).expect("checked") {
1335 ops.push(EventOp::Index(idx));
1336 }
1337 }
1338 _ => ops.push(EventOp::Key(unescape_brackets(part))),
1339 }
1340 }
1341 ops
1342}
1343
1344fn index_groups(s: &str) -> Option<Vec<i64>> {
1347 let mut out = Vec::new();
1348 let mut rem = s;
1349 while !rem.is_empty() {
1350 let rest = rem.strip_prefix('[')?;
1351 let close = rest.find(']')?;
1352 out.push(rest[..close].parse().ok()?);
1353 rem = &rest[close + 1..];
1354 }
1355 Some(out)
1356}
1357
1358fn nav_event_value<'a>(
1359 current: &'a EventValue<'a>,
1360 ops: &[EventOp<'_>],
1361) -> Option<&'a EventValue<'a>> {
1362 let Some((op, rest)) = ops.split_first() else {
1363 return Some(current);
1364 };
1365 match op {
1366 EventOp::Key(key) => match current {
1367 EventValue::Map(entries) => {
1368 let next = entries
1369 .iter()
1370 .find(|(k, _)| k.as_ref() == key.as_ref())
1371 .map(|(_, v)| v)?;
1372 nav_event_value(next, rest)
1373 }
1374 EventValue::Array(members) => members.iter().find_map(|m| nav_event_value(m, ops)),
1375 _ => None,
1376 },
1377 EventOp::Index(i) => match current {
1378 EventValue::Array(members) => {
1379 let idx = crate::event::resolve_array_index(*i, members.len())?;
1380 nav_event_value(members.get(idx)?, rest)
1381 }
1382 _ => None,
1383 },
1384 }
1385}
1386
1387fn eval_detection_item_with_bloom<E, B>(item: &CompiledDetectionItem, event: &E, bloom: &B) -> bool
1394where
1395 E: Event,
1396 B: crate::engine::bloom_index::BloomLookup,
1397{
1398 if let Some(expect_exists) = item.exists {
1399 if let Some(field) = &item.field {
1400 let exists = event.get_field(field).is_some_and(|v| !v.is_null());
1401 return exists == expect_exists;
1402 }
1403 return !expect_exists;
1404 }
1405
1406 match &item.field {
1407 Some(field_name) => {
1408 if let Some(value) = event.get_field(field_name) {
1409 if item.bloom_eligible
1410 && bloom.verdict_for_field(field_name)
1411 == crate::engine::bloom_index::BloomVerdict::DefinitelyNoMatch
1412 {
1413 return false;
1414 }
1415 item.matcher.matches(&value, event)
1416 } else {
1417 matches!(item.matcher, CompiledMatcher::Null)
1418 }
1419 }
1420 None => item.matcher.matches_keyword(event),
1421 }
1422}
1423
1424const MAX_KEYWORD_MATCHES: usize = 16;
1428
1429fn collect_field_matches(
1437 selection_names: &[String],
1438 detections: &HashMap<String, CompiledDetection>,
1439 event: &impl Event,
1440 level: MatchDetailLevel,
1441) -> Vec<FieldMatch> {
1442 let mut matches = Vec::new();
1443 for name in selection_names {
1444 if let Some(det) = detections.get(name) {
1445 collect_detection_fields(name, det, event, level, &mut matches);
1446 }
1447 }
1448 matches
1449}
1450
1451fn collect_detection_fields(
1452 selection: &str,
1453 detection: &CompiledDetection,
1454 event: &impl Event,
1455 level: MatchDetailLevel,
1456 out: &mut Vec<FieldMatch>,
1457) {
1458 match detection {
1459 CompiledDetection::AllOf(items) => {
1460 for item in items {
1461 match &item.field {
1462 Some(field_name) => {
1463 if let Some(value) = event.get_field(field_name) {
1464 if item.matcher.matches(&value, event) {
1465 out.push(make_field_match(
1466 selection,
1467 field_name,
1468 value.to_json(),
1469 &item.matcher,
1470 level,
1471 ));
1472 }
1473 } else if level != MatchDetailLevel::Off
1474 && matches!(item.matcher, CompiledMatcher::Null)
1475 {
1476 out.push(make_field_match(
1479 selection,
1480 field_name,
1481 serde_json::Value::Null,
1482 &item.matcher,
1483 level,
1484 ));
1485 }
1486 }
1487 None => {
1488 if level != MatchDetailLevel::Off {
1490 collect_keyword_matches(selection, &item.matcher, event, level, out);
1491 }
1492 }
1493 }
1494 }
1495 }
1496 CompiledDetection::AnyOf(dets) => {
1497 for d in dets {
1498 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1499 collect_detection_fields(selection, d, event, level, out);
1500 }
1501 }
1502 }
1503 CompiledDetection::ArrayMatch { field, .. } => {
1504 if let Some(value) = event.get_field(field) {
1508 out.push(FieldMatch::new(field.clone(), value.to_json()));
1509 }
1510 }
1511 CompiledDetection::And(dets) => {
1512 for d in dets {
1513 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
1514 collect_detection_fields(selection, d, event, level, out);
1515 }
1516 }
1517 }
1518 CompiledDetection::Conditional { .. } => {}
1521 CompiledDetection::Keywords(matcher) => {
1522 if level != MatchDetailLevel::Off {
1525 collect_keyword_matches(selection, matcher, event, level, out);
1526 }
1527 }
1528 }
1529}
1530
1531fn make_field_match(
1535 selection: &str,
1536 field: &str,
1537 value: serde_json::Value,
1538 matcher: &CompiledMatcher,
1539 level: MatchDetailLevel,
1540) -> FieldMatch {
1541 match level {
1542 MatchDetailLevel::Off => FieldMatch::new(field, value),
1543 MatchDetailLevel::Summary | MatchDetailLevel::Full => {
1544 let d = matcher.describe();
1545 FieldMatch {
1546 field: field.to_string(),
1547 value,
1548 selection: Some(selection.to_string()),
1549 matcher: Some(d.kind),
1550 pattern: if level == MatchDetailLevel::Full {
1551 d.pattern
1552 } else {
1553 None
1554 },
1555 case_sensitive: d.case_sensitive,
1556 negated: d.negated,
1557 }
1558 }
1559 }
1560}
1561
1562fn collect_keyword_matches(
1566 selection: &str,
1567 matcher: &CompiledMatcher,
1568 event: &impl Event,
1569 level: MatchDetailLevel,
1570 out: &mut Vec<FieldMatch>,
1571) {
1572 let descriptor = matcher.describe();
1573 let mut count = 0;
1574 for s in event.all_string_values() {
1575 if count >= MAX_KEYWORD_MATCHES {
1576 break;
1577 }
1578 if matcher.matches_str(&s) {
1579 count += 1;
1580 out.push(FieldMatch {
1581 field: "keyword".to_string(),
1582 value: serde_json::Value::String(s.into_owned()),
1583 selection: Some(selection.to_string()),
1584 matcher: Some(MatcherKind::Keyword),
1585 pattern: if level == MatchDetailLevel::Full {
1586 descriptor.pattern.clone()
1587 } else {
1588 None
1589 },
1590 case_sensitive: descriptor.case_sensitive,
1591 negated: descriptor.negated,
1592 });
1593 }
1594 }
1595}