1mod array;
12mod from_ir;
13mod helpers;
14#[doc(hidden)]
15pub mod optimizer;
16#[cfg(test)]
17mod tests;
18mod value;
19
20pub(crate) use array::{
21 array_quantifier_from_member_matches, array_quantifier_matches_empty, decisive_member_verdict,
22 element_field, eval_array_body, eval_array_item, eval_array_quantified,
23 select_recorded_member_indices,
24};
25
26pub use from_ir::compile_to_compiled;
27
28#[cfg(test)]
31pub(crate) use optimizer::optimize_any_of as optimize_any_of_for_test;
32
33use std::collections::HashMap;
34use std::sync::Arc;
35
36#[cfg(test)]
37use rsigma_parser::DetectionItem;
38use rsigma_parser::{
39 ArrayQuantifier, ConditionExpr, Detection, Level, LogSource, Quantifier, SigmaRule,
40};
41
42use crate::error::Result;
43use crate::event::{Event, EventValue};
44use crate::matcher::CompiledMatcher;
45use crate::result::{
46 DetectionBody, EvaluationResult, FieldMatch, MatchDetailLevel, MatcherKind, ResultBody,
47 RuleHeader,
48};
49
50pub(crate) use helpers::{yaml_to_json, yaml_to_json_map};
51
52#[derive(Debug, Clone)]
58pub struct CompiledRule {
59 pub title: String,
60 pub id: Option<String>,
61 pub name: Option<String>,
62 pub level: Option<Level>,
63 pub tags: Vec<String>,
64 pub description: Option<String>,
67 pub falsepositives: Vec<String>,
70 pub logsource: LogSource,
71 pub detections: HashMap<String, CompiledDetection>,
73 pub conditions: Vec<ConditionExpr>,
75 pub include_event: bool,
78 pub custom_attributes: Arc<HashMap<String, serde_json::Value>>,
83}
84
85#[derive(Debug, Clone)]
87pub enum CompiledDetection {
88 AllOf(Vec<CompiledDetectionItem>),
90 AnyOf(Vec<CompiledDetection>),
92 Keywords(CompiledMatcher),
94 ArrayMatch {
98 field: String,
99 quantifier: ArrayQuantifier,
100 body: Box<CompiledDetection>,
101 },
102 And(Vec<CompiledDetection>),
105 Conditional {
110 named: HashMap<String, CompiledDetection>,
111 condition: ConditionExpr,
112 },
113}
114
115#[derive(Debug, Clone)]
117pub struct CompiledDetectionItem {
118 pub field: Option<String>,
120 pub matcher: CompiledMatcher,
122 pub exists: Option<bool>,
124 pub bloom_eligible: bool,
129}
130
131pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
139 let ir = rsigma_ir::lower_rule(rule, &rsigma_ir::LowerOptions::default())?;
140 compile_to_compiled(&ir)
141}
142
143pub fn evaluate_rule(rule: &CompiledRule, event: &impl Event) -> Option<EvaluationResult> {
151 evaluate_rule_with_bloom(
152 rule,
153 event,
154 &crate::engine::bloom_index::NoBloom,
155 MatchDetailLevel::Off,
156 )
157}
158
159pub(crate) fn evaluate_rule_with_bloom<E, B>(
167 rule: &CompiledRule,
168 event: &E,
169 bloom: &B,
170 level: MatchDetailLevel,
171) -> Option<EvaluationResult>
172where
173 E: Event,
174 B: crate::engine::bloom_index::BloomLookup,
175{
176 for condition in &rule.conditions {
177 if eval_condition_matches_with_bloom(condition, &rule.detections, event, bloom) {
178 let mut matched_selections = Vec::new();
179 let matched = eval_condition_with_bloom(
180 condition,
181 &rule.detections,
182 event,
183 &mut matched_selections,
184 bloom,
185 );
186 debug_assert!(matched, "detail pass must agree with boolean pass");
187 let matched_fields =
188 collect_field_matches(&matched_selections, &rule.detections, event, level);
189
190 let event_data = if rule.include_event {
191 Some(event.to_json())
192 } else {
193 None
194 };
195
196 return Some(EvaluationResult {
197 header: RuleHeader {
198 rule_title: rule.title.clone(),
199 rule_id: rule.id.clone(),
200 level: rule.level,
201 tags: rule.tags.clone(),
202 custom_attributes: rule.custom_attributes.clone(),
203 enrichments: None,
204 },
205 body: ResultBody::Detection(DetectionBody {
206 matched_selections,
207 matched_fields,
208 event: event_data,
209 }),
210 });
211 }
212 }
213 None
214}
215
216pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
226 let opts = rsigma_ir::LowerOptions {
227 permissive_placeholders: true,
228 };
229 from_ir::compile_ir_detection(&rsigma_ir::lower::lower_detection(detection, &opts)?)
230}
231
232#[cfg(test)]
233fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
234 let ir = rsigma_ir::lower::lower_detection_item(item, &rsigma_ir::LowerOptions::default())?;
235 from_ir::compile_ir_detection_item(&ir)
236}
237
238pub fn eval_condition(
247 expr: &ConditionExpr,
248 detections: &HashMap<String, CompiledDetection>,
249 event: &impl Event,
250 matched_selections: &mut Vec<String>,
251) -> bool {
252 eval_condition_with_bloom(
253 expr,
254 detections,
255 event,
256 matched_selections,
257 &crate::engine::bloom_index::NoBloom,
258 )
259}
260
261fn eval_condition_matches_with_bloom<E, B>(
267 expr: &ConditionExpr,
268 detections: &HashMap<String, CompiledDetection>,
269 event: &E,
270 bloom: &B,
271) -> bool
272where
273 E: Event,
274 B: crate::engine::bloom_index::BloomLookup,
275{
276 match expr {
277 ConditionExpr::Identifier(name) => detections
278 .get(name)
279 .is_some_and(|det| eval_detection_with_bloom(det, event, bloom)),
280 ConditionExpr::And(exprs) => exprs
281 .iter()
282 .all(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
283 ConditionExpr::Or(exprs) => exprs
284 .iter()
285 .any(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
286 ConditionExpr::Not(inner) => {
287 !eval_condition_matches_with_bloom(inner, detections, event, bloom)
288 }
289 ConditionExpr::Selector {
290 quantifier,
291 pattern,
292 } => {
293 let mut matching = detections
294 .iter()
295 .filter(|(name, _)| pattern.matches_detection_name(name));
296 match quantifier {
297 Quantifier::Any => {
298 matching.any(|(_, det)| eval_detection_with_bloom(det, event, bloom))
299 }
300 Quantifier::All => {
301 matching.all(|(_, det)| eval_detection_with_bloom(det, event, bloom))
302 }
303 Quantifier::Count(required) => {
304 if *required == 0 {
305 return true;
306 }
307 let mut matched = 0u64;
308 matching.any(|(_, det)| {
309 if eval_detection_with_bloom(det, event, bloom) {
310 matched += 1;
311 }
312 matched >= *required
313 })
314 }
315 }
316 }
317 }
318}
319
320pub(crate) fn eval_condition_with_bloom<E, B>(
326 expr: &ConditionExpr,
327 detections: &HashMap<String, CompiledDetection>,
328 event: &E,
329 matched_selections: &mut Vec<String>,
330 bloom: &B,
331) -> bool
332where
333 E: Event,
334 B: crate::engine::bloom_index::BloomLookup,
335{
336 match expr {
337 ConditionExpr::Identifier(name) => {
338 if let Some(det) = detections.get(name) {
339 let result = eval_detection_with_bloom(det, event, bloom);
340 if result {
341 matched_selections.push(name.clone());
342 }
343 result
344 } else {
345 false
346 }
347 }
348
349 ConditionExpr::And(exprs) => exprs
350 .iter()
351 .all(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
352
353 ConditionExpr::Or(exprs) => exprs
354 .iter()
355 .any(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
356
357 ConditionExpr::Not(inner) => {
358 !eval_condition_with_bloom(inner, detections, event, matched_selections, bloom)
359 }
360
361 ConditionExpr::Selector {
362 quantifier,
363 pattern,
364 } => {
365 let matching_names: Vec<&String> = detections
366 .keys()
367 .filter(|name| pattern.matches_detection_name(name))
368 .collect();
369
370 let mut match_count = 0u64;
371 for name in &matching_names {
372 if let Some(det) = detections.get(*name)
373 && eval_detection_with_bloom(det, event, bloom)
374 {
375 match_count += 1;
376 matched_selections.push((*name).clone());
377 }
378 }
379
380 match quantifier {
381 Quantifier::Any => match_count >= 1,
382 Quantifier::All => match_count == matching_names.len() as u64,
383 Quantifier::Count(n) => match_count >= *n,
384 }
385 }
386 }
387}
388
389#[cfg(test)]
394fn eval_detection_item(item: &CompiledDetectionItem, event: &impl Event) -> bool {
395 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
396}
397
398pub(crate) fn eval_detection_item_no_bloom(
402 item: &CompiledDetectionItem,
403 event: &impl Event,
404) -> bool {
405 eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
406}
407
408fn eval_detection_with_bloom<E, B>(detection: &CompiledDetection, event: &E, bloom: &B) -> bool
410where
411 E: Event,
412 B: crate::engine::bloom_index::BloomLookup,
413{
414 match detection {
415 CompiledDetection::AllOf(items) => items
416 .iter()
417 .all(|item| eval_detection_item_with_bloom(item, event, bloom)),
418 CompiledDetection::AnyOf(dets) => dets
419 .iter()
420 .any(|d| eval_detection_with_bloom(d, event, bloom)),
421 CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
422 CompiledDetection::ArrayMatch {
423 field,
424 quantifier,
425 body,
426 } => match event.get_field(field) {
427 Some(value) => eval_array_quantified(&value, *quantifier, body, event),
428 None => array_quantifier_matches_empty(*quantifier),
429 },
430 CompiledDetection::And(dets) => dets
431 .iter()
432 .all(|d| eval_detection_with_bloom(d, event, bloom)),
433 CompiledDetection::Conditional { named, condition } => {
437 eval_condition_with_bloom(condition, named, event, &mut Vec::new(), bloom)
438 }
439 }
440}
441
442fn eval_detection_item_with_bloom<E, B>(item: &CompiledDetectionItem, event: &E, bloom: &B) -> bool
449where
450 E: Event,
451 B: crate::engine::bloom_index::BloomLookup,
452{
453 if let Some(expect_exists) = item.exists {
454 if let Some(field) = &item.field {
455 return event.get_field(field).is_some() == expect_exists;
456 }
457 return !expect_exists;
458 }
459
460 match &item.field {
461 Some(field_name) => {
462 if let Some(value) = event.get_field(field_name) {
463 if item.bloom_eligible
464 && bloom.verdict_for_field(field_name)
465 == crate::engine::bloom_index::BloomVerdict::DefinitelyNoMatch
466 {
467 return false;
468 }
469 item.matcher.matches(&value, event)
470 } else {
471 item.matcher.matches_absent()
472 }
473 }
474 None => item.matcher.matches_keyword(event),
475 }
476}
477
478const MAX_KEYWORD_MATCHES: usize = 16;
482
483fn collect_field_matches(
491 selection_names: &[String],
492 detections: &HashMap<String, CompiledDetection>,
493 event: &impl Event,
494 level: MatchDetailLevel,
495) -> Vec<FieldMatch> {
496 let mut matches = Vec::new();
497 for name in selection_names {
498 if let Some(det) = detections.get(name) {
499 collect_detection_fields(name, det, event, level, &mut matches);
500 }
501 }
502 matches
503}
504
505fn collect_detection_fields(
506 selection: &str,
507 detection: &CompiledDetection,
508 event: &impl Event,
509 level: MatchDetailLevel,
510 out: &mut Vec<FieldMatch>,
511) {
512 match detection {
513 CompiledDetection::AllOf(items) => {
514 for item in items {
515 match &item.field {
516 Some(field_name) => {
517 if let Some(value) = event.get_field(field_name) {
518 if item.matcher.matches(&value, event) {
519 out.push(make_field_match(
520 selection,
521 field_name,
522 value.to_json(),
523 &item.matcher,
524 level,
525 ));
526 }
527 } else if level != MatchDetailLevel::Off
528 && (item.exists == Some(false) || item.matcher.matches_absent())
529 {
530 out.push(make_field_match(
535 selection,
536 field_name,
537 serde_json::Value::Null,
538 &item.matcher,
539 level,
540 ));
541 }
542 }
543 None => {
544 if level != MatchDetailLevel::Off {
546 collect_keyword_matches(selection, &item.matcher, event, level, out);
547 }
548 }
549 }
550 }
551 }
552 CompiledDetection::AnyOf(dets) => {
553 for d in dets {
554 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
555 collect_detection_fields(selection, d, event, level, out);
556 }
557 }
558 }
559 CompiledDetection::ArrayMatch { field, body, .. } => {
560 let value = event.get_field(field);
561 collect_array_match_fields(
562 selection,
563 field,
564 body,
565 value.as_ref(),
566 event,
567 level,
568 "",
569 out,
570 );
571 }
572 CompiledDetection::And(dets) => {
573 for d in dets {
574 if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
575 collect_detection_fields(selection, d, event, level, out);
576 }
577 }
578 }
579 CompiledDetection::Conditional { .. } => {}
582 CompiledDetection::Keywords(matcher) => {
583 if level != MatchDetailLevel::Off {
586 collect_keyword_matches(selection, matcher, event, level, out);
587 }
588 }
589 }
590}
591
592#[allow(clippy::too_many_arguments)]
599fn collect_array_match_fields<E: Event>(
600 selection: &str,
601 field: &str,
602 body: &CompiledDetection,
603 value: Option<&EventValue>,
604 outer: &E,
605 level: MatchDetailLevel,
606 path_prefix: &str,
607 out: &mut Vec<FieldMatch>,
608) {
609 let container_path = if path_prefix.is_empty() {
610 field.to_string()
611 } else {
612 array::join_member_field(path_prefix, Some(field))
613 };
614
615 let (scalar, members): (bool, Vec<&EventValue>) = match value {
616 None => return,
617 Some(EventValue::Null) => {
618 out.push(FieldMatch::new(container_path, serde_json::Value::Null));
619 return;
620 }
621 Some(EventValue::Array(items)) if items.is_empty() => {
622 out.push(FieldMatch::new(
623 container_path,
624 serde_json::Value::Array(Vec::new()),
625 ));
626 return;
627 }
628 Some(EventValue::Array(items)) => (false, items.iter().collect()),
629 Some(single) => (true, vec![single]),
630 };
631
632 let matching: Vec<usize> = members
633 .iter()
634 .enumerate()
635 .filter(|(_, m)| array::eval_array_body(body, m, outer))
636 .map(|(i, _)| i)
637 .take(array::ARRAY_MEMBER_CAP)
638 .collect();
639
640 if matching.is_empty() {
641 if let Some(v) = value {
642 out.push(FieldMatch::new(container_path, v.to_json()));
643 }
644 return;
645 }
646
647 for i in matching {
648 let member_path = array::array_member_path(&container_path, i, scalar);
649 let before = out.len();
650 collect_array_body_fields(selection, body, members[i], outer, level, &member_path, out);
651 if out.len() == before && level != MatchDetailLevel::Off {
657 out.push(FieldMatch::new(member_path, members[i].to_json()));
658 }
659 }
660}
661
662fn collect_array_body_fields<E: Event>(
663 selection: &str,
664 body: &CompiledDetection,
665 member: &EventValue,
666 outer: &E,
667 level: MatchDetailLevel,
668 member_path: &str,
669 out: &mut Vec<FieldMatch>,
670) {
671 match body {
672 CompiledDetection::AllOf(items) => {
673 for item in items {
674 if !array::eval_array_item(item, member, outer) {
675 continue;
676 }
677 let relative = item.field.as_deref();
678 let absent =
679 relative.is_some_and(|name| array::element_field(member, name).is_none());
680 if absent && level == MatchDetailLevel::Off {
681 continue;
682 }
683 let path = array::join_member_field(member_path, relative);
684 let value = match relative {
685 Some(name) => array::element_field(member, name)
686 .map(|v| v.to_json())
687 .unwrap_or(serde_json::Value::Null),
688 None => member.to_json(),
689 };
690 out.push(make_field_match(
691 selection,
692 &path,
693 value,
694 &item.matcher,
695 level,
696 ));
697 }
698 }
699 CompiledDetection::AnyOf(dets) | CompiledDetection::And(dets) => {
700 for d in dets {
701 if array::eval_array_body(d, member, outer) {
702 collect_array_body_fields(selection, d, member, outer, level, member_path, out);
703 }
704 }
705 }
706 CompiledDetection::ArrayMatch {
707 field, body: inner, ..
708 } => {
709 collect_array_match_fields(
710 selection,
711 field,
712 inner,
713 array::element_field(member, field),
714 outer,
715 level,
716 member_path,
717 out,
718 );
719 }
720 CompiledDetection::Keywords(matcher) => {
721 if level != MatchDetailLevel::Off && matcher.matches(member, outer) {
722 let d = matcher.describe();
723 out.push(FieldMatch {
724 field: member_path.to_string(),
725 value: member.to_json(),
726 selection: Some(selection.to_string()),
727 matcher: Some(MatcherKind::Keyword),
728 pattern: if level == MatchDetailLevel::Full {
729 d.pattern
730 } else {
731 None
732 },
733 case_sensitive: d.case_sensitive,
734 negated: d.negated,
735 });
736 }
737 }
738 CompiledDetection::Conditional { named, condition } => {
739 collect_array_condition_fields(
740 selection,
741 condition,
742 named,
743 member,
744 outer,
745 level,
746 member_path,
747 out,
748 );
749 }
750 }
751}
752
753#[allow(clippy::too_many_arguments)]
754fn collect_array_condition_fields<E: Event>(
755 selection: &str,
756 expr: &ConditionExpr,
757 named: &HashMap<String, CompiledDetection>,
758 member: &EventValue,
759 outer: &E,
760 level: MatchDetailLevel,
761 member_path: &str,
762 out: &mut Vec<FieldMatch>,
763) {
764 match expr {
765 ConditionExpr::Identifier(name) => {
766 if let Some(det) = named.get(name)
767 && array::eval_array_body(det, member, outer)
768 {
769 collect_array_body_fields(selection, det, member, outer, level, member_path, out);
770 }
771 }
772 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
773 for e in exprs {
774 collect_array_condition_fields(
775 selection,
776 e,
777 named,
778 member,
779 outer,
780 level,
781 member_path,
782 out,
783 );
784 }
785 }
786 ConditionExpr::Not(_) => {}
787 ConditionExpr::Selector { pattern, .. } => {
788 let mut names: Vec<&String> = named
789 .keys()
790 .filter(|n| pattern.matches_detection_name(n))
791 .collect();
792 names.sort();
793 for name in names {
794 if let Some(det) = named.get(name)
795 && array::eval_array_body(det, member, outer)
796 {
797 collect_array_body_fields(
798 selection,
799 det,
800 member,
801 outer,
802 level,
803 member_path,
804 out,
805 );
806 }
807 }
808 }
809 }
810}
811
812fn make_field_match(
816 selection: &str,
817 field: &str,
818 value: serde_json::Value,
819 matcher: &CompiledMatcher,
820 level: MatchDetailLevel,
821) -> FieldMatch {
822 match level {
823 MatchDetailLevel::Off => FieldMatch::new(field, value),
824 MatchDetailLevel::Summary | MatchDetailLevel::Full => {
825 let d = matcher.describe();
826 FieldMatch {
827 field: field.to_string(),
828 value,
829 selection: Some(selection.to_string()),
830 matcher: Some(d.kind),
831 pattern: if level == MatchDetailLevel::Full {
832 d.pattern
833 } else {
834 None
835 },
836 case_sensitive: d.case_sensitive,
837 negated: d.negated,
838 }
839 }
840 }
841}
842
843fn collect_keyword_matches(
848 selection: &str,
849 matcher: &CompiledMatcher,
850 event: &impl Event,
851 level: MatchDetailLevel,
852 out: &mut Vec<FieldMatch>,
853) {
854 match matcher {
855 CompiledMatcher::AllOf(children)
856 | CompiledMatcher::CaseInsensitiveGroup {
857 children,
858 mode: crate::matcher::GroupMode::All,
859 } => {
860 for child in children {
861 collect_keyword_matches(selection, child, event, level, out);
862 }
863 return;
864 }
865 CompiledMatcher::Not(_) => return,
866 _ => {}
867 }
868 let descriptor = matcher.describe();
869 let mut count = 0;
870 for s in event.all_string_values() {
871 if count >= MAX_KEYWORD_MATCHES {
872 break;
873 }
874 if matcher.matches_str(&s) {
875 count += 1;
876 out.push(FieldMatch {
877 field: "keyword".to_string(),
878 value: serde_json::Value::String(s.into_owned()),
879 selection: Some(selection.to_string()),
880 matcher: Some(MatcherKind::Keyword),
881 pattern: if level == MatchDetailLevel::Full {
882 descriptor.pattern.clone()
883 } else {
884 None
885 },
886 case_sensitive: descriptor.case_sensitive,
887 negated: descriptor.negated,
888 });
889 }
890 }
891}