1mod helpers;
11#[cfg(test)]
12mod tests;
13
14use std::collections::HashMap;
15use std::sync::Arc;
16
17use base64::Engine as Base64Engine;
18use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
19use regex::Regex;
20
21use rsigma_parser::value::{SpecialChar, StringPart};
22use rsigma_parser::{
23 ConditionExpr, Detection, DetectionItem, Level, LogSource, Modifier, Quantifier,
24 SelectorPattern, SigmaRule, SigmaString, SigmaValue,
25};
26
27use crate::error::{EvalError, Result};
28use crate::event::Event;
29use crate::matcher::{CompiledMatcher, sigma_string_to_regex};
30use crate::result::{FieldMatch, MatchResult};
31
32pub(crate) use helpers::yaml_to_json_map;
33use helpers::{
34 base64_offset_patterns, build_regex, expand_windash, pattern_matches, sigma_string_to_bytes,
35 to_utf16_bom_bytes, to_utf16be_bytes, to_utf16le_bytes, value_to_f64, value_to_plain_string,
36};
37
38#[derive(Debug, Clone)]
44pub struct CompiledRule {
45 pub title: String,
46 pub id: Option<String>,
47 pub level: Option<Level>,
48 pub tags: Vec<String>,
49 pub logsource: LogSource,
50 pub detections: HashMap<String, CompiledDetection>,
52 pub conditions: Vec<ConditionExpr>,
54 pub include_event: bool,
57 pub custom_attributes: Arc<HashMap<String, serde_json::Value>>,
62}
63
64#[derive(Debug, Clone)]
66pub enum CompiledDetection {
67 AllOf(Vec<CompiledDetectionItem>),
69 AnyOf(Vec<CompiledDetection>),
71 Keywords(CompiledMatcher),
73}
74
75#[derive(Debug, Clone)]
77pub struct CompiledDetectionItem {
78 pub field: Option<String>,
80 pub matcher: CompiledMatcher,
82 pub exists: Option<bool>,
84}
85
86#[derive(Clone, Copy)]
92struct ModCtx {
93 contains: bool,
94 startswith: bool,
95 endswith: bool,
96 all: bool,
97 base64: bool,
98 base64offset: bool,
99 wide: bool,
100 utf16be: bool,
101 utf16: bool,
102 windash: bool,
103 re: bool,
104 cidr: bool,
105 cased: bool,
106 exists: bool,
107 fieldref: bool,
108 gt: bool,
109 gte: bool,
110 lt: bool,
111 lte: bool,
112 neq: bool,
113 ignore_case: bool,
114 multiline: bool,
115 dotall: bool,
116 expand: bool,
117 timestamp_part: Option<crate::matcher::TimePart>,
118}
119
120impl ModCtx {
121 fn from_modifiers(modifiers: &[Modifier]) -> Self {
122 let mut ctx = ModCtx {
123 contains: false,
124 startswith: false,
125 endswith: false,
126 all: false,
127 base64: false,
128 base64offset: false,
129 wide: false,
130 utf16be: false,
131 utf16: false,
132 windash: false,
133 re: false,
134 cidr: false,
135 cased: false,
136 exists: false,
137 fieldref: false,
138 gt: false,
139 gte: false,
140 lt: false,
141 lte: false,
142 neq: false,
143 ignore_case: false,
144 multiline: false,
145 dotall: false,
146 expand: false,
147 timestamp_part: None,
148 };
149 for m in modifiers {
150 match m {
151 Modifier::Contains => ctx.contains = true,
152 Modifier::StartsWith => ctx.startswith = true,
153 Modifier::EndsWith => ctx.endswith = true,
154 Modifier::All => ctx.all = true,
155 Modifier::Base64 => ctx.base64 = true,
156 Modifier::Base64Offset => ctx.base64offset = true,
157 Modifier::Wide => ctx.wide = true,
158 Modifier::Utf16be => ctx.utf16be = true,
159 Modifier::Utf16 => ctx.utf16 = true,
160 Modifier::WindAsh => ctx.windash = true,
161 Modifier::Re => ctx.re = true,
162 Modifier::Cidr => ctx.cidr = true,
163 Modifier::Cased => ctx.cased = true,
164 Modifier::Exists => ctx.exists = true,
165 Modifier::FieldRef => ctx.fieldref = true,
166 Modifier::Gt => ctx.gt = true,
167 Modifier::Gte => ctx.gte = true,
168 Modifier::Lt => ctx.lt = true,
169 Modifier::Lte => ctx.lte = true,
170 Modifier::Neq => ctx.neq = true,
171 Modifier::IgnoreCase => ctx.ignore_case = true,
172 Modifier::Multiline => ctx.multiline = true,
173 Modifier::DotAll => ctx.dotall = true,
174 Modifier::Expand => ctx.expand = true,
175 Modifier::Hour => ctx.timestamp_part = Some(crate::matcher::TimePart::Hour),
176 Modifier::Day => ctx.timestamp_part = Some(crate::matcher::TimePart::Day),
177 Modifier::Week => ctx.timestamp_part = Some(crate::matcher::TimePart::Week),
178 Modifier::Month => ctx.timestamp_part = Some(crate::matcher::TimePart::Month),
179 Modifier::Year => ctx.timestamp_part = Some(crate::matcher::TimePart::Year),
180 Modifier::Minute => ctx.timestamp_part = Some(crate::matcher::TimePart::Minute),
181 }
182 }
183 ctx
184 }
185
186 fn is_case_insensitive(&self) -> bool {
189 !self.cased
190 }
191
192 fn has_numeric_comparison(&self) -> bool {
194 self.gt || self.gte || self.lt || self.lte
195 }
196
197 fn has_neq(&self) -> bool {
199 self.neq
200 }
201}
202
203pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
209 let mut detections = HashMap::new();
210 for (name, detection) in &rule.detection.named {
211 detections.insert(name.clone(), compile_detection(detection)?);
212 }
213
214 for condition in &rule.detection.conditions {
215 validate_condition_refs(condition, &detections)?;
216 }
217
218 let include_event = rule
219 .custom_attributes
220 .get("rsigma.include_event")
221 .and_then(|v| v.as_str())
222 == Some("true");
223
224 let custom_attributes = Arc::new(yaml_to_json_map(&rule.custom_attributes));
225
226 Ok(CompiledRule {
227 title: rule.title.clone(),
228 id: rule.id.clone(),
229 level: rule.level,
230 tags: rule.tags.clone(),
231 logsource: rule.logsource.clone(),
232 detections,
233 conditions: rule.detection.conditions.clone(),
234 include_event,
235 custom_attributes,
236 })
237}
238
239fn validate_condition_refs(
243 expr: &ConditionExpr,
244 detections: &HashMap<String, CompiledDetection>,
245) -> Result<()> {
246 match expr {
247 ConditionExpr::Identifier(name) => {
248 if !detections.contains_key(name) {
249 return Err(EvalError::UnknownDetection(name.clone()));
250 }
251 Ok(())
252 }
253 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
254 for e in exprs {
255 validate_condition_refs(e, detections)?;
256 }
257 Ok(())
258 }
259 ConditionExpr::Not(inner) => validate_condition_refs(inner, detections),
260 ConditionExpr::Selector { .. } => Ok(()),
261 }
262}
263
264pub fn evaluate_rule(rule: &CompiledRule, event: &impl Event) -> Option<MatchResult> {
266 for condition in &rule.conditions {
267 let mut matched_selections = Vec::new();
268 if eval_condition(condition, &rule.detections, event, &mut matched_selections) {
269 let matched_fields =
270 collect_field_matches(&matched_selections, &rule.detections, event);
271
272 let event_data = if rule.include_event {
273 Some(event.to_json())
274 } else {
275 None
276 };
277
278 return Some(MatchResult {
279 rule_title: rule.title.clone(),
280 rule_id: rule.id.clone(),
281 level: rule.level,
282 tags: rule.tags.clone(),
283 matched_selections,
284 matched_fields,
285 event: event_data,
286 custom_attributes: rule.custom_attributes.clone(),
287 });
288 }
289 }
290 None
291}
292
293pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
302 match detection {
303 Detection::AllOf(items) => {
304 if items.is_empty() {
305 return Err(EvalError::InvalidModifiers(
306 "AllOf detection must not be empty (vacuous truth)".into(),
307 ));
308 }
309 let compiled: Result<Vec<_>> = items.iter().map(compile_detection_item).collect();
310 Ok(CompiledDetection::AllOf(compiled?))
311 }
312 Detection::AnyOf(dets) => {
313 if dets.is_empty() {
314 return Err(EvalError::InvalidModifiers(
315 "AnyOf detection must not be empty (would never match)".into(),
316 ));
317 }
318 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
319 Ok(CompiledDetection::AnyOf(compiled?))
320 }
321 Detection::Keywords(values) => {
322 let ci = true; let matchers: Vec<CompiledMatcher> = values
324 .iter()
325 .map(|v| compile_value_default(v, ci))
326 .collect::<Result<Vec<_>>>()?;
327 let matcher = if matchers.len() == 1 {
328 matchers
330 .into_iter()
331 .next()
332 .unwrap_or(CompiledMatcher::AnyOf(vec![]))
333 } else {
334 CompiledMatcher::AnyOf(matchers)
335 };
336 Ok(CompiledDetection::Keywords(matcher))
337 }
338 }
339}
340
341fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
342 let ctx = ModCtx::from_modifiers(&item.field.modifiers);
343
344 if ctx.exists {
346 let expect = match item.values.first() {
347 Some(SigmaValue::Bool(b)) => *b,
348 Some(SigmaValue::String(s)) => match s.as_plain().as_deref() {
349 Some("true") | Some("yes") => true,
350 Some("false") | Some("no") => false,
351 _ => true,
352 },
353 _ => true,
354 };
355 return Ok(CompiledDetectionItem {
356 field: item.field.name.clone(),
357 matcher: CompiledMatcher::Exists(expect),
358 exists: Some(expect),
359 });
360 }
361
362 if ctx.all && item.values.len() <= 1 {
364 return Err(EvalError::InvalidModifiers(
365 "|all modifier requires more than one value".to_string(),
366 ));
367 }
368
369 let matchers: Result<Vec<CompiledMatcher>> =
371 item.values.iter().map(|v| compile_value(v, &ctx)).collect();
372 let matchers = matchers?;
373
374 let combined = if matchers.len() == 1 {
376 matchers
378 .into_iter()
379 .next()
380 .unwrap_or(CompiledMatcher::AnyOf(vec![]))
381 } else if ctx.all {
382 CompiledMatcher::AllOf(matchers)
383 } else {
384 CompiledMatcher::AnyOf(matchers)
385 };
386
387 Ok(CompiledDetectionItem {
388 field: item.field.name.clone(),
389 matcher: combined,
390 exists: None,
391 })
392}
393
394fn compile_value(value: &SigmaValue, ctx: &ModCtx) -> Result<CompiledMatcher> {
400 let ci = ctx.is_case_insensitive();
401
402 if ctx.expand {
406 let plain = value_to_plain_string(value)?;
407 let template = crate::matcher::parse_expand_template(&plain);
408 return Ok(CompiledMatcher::Expand {
409 template,
410 case_insensitive: ci,
411 });
412 }
413
414 if let Some(part) = ctx.timestamp_part {
416 let inner = match value {
419 SigmaValue::Integer(n) => CompiledMatcher::NumericEq(*n as f64),
420 SigmaValue::Float(n) => CompiledMatcher::NumericEq(*n),
421 SigmaValue::String(s) => {
422 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
423 let n: f64 = plain.parse().map_err(|_| {
424 EvalError::IncompatibleValue(format!(
425 "timestamp part modifier requires numeric value, got: {plain}"
426 ))
427 })?;
428 CompiledMatcher::NumericEq(n)
429 }
430 _ => {
431 return Err(EvalError::IncompatibleValue(
432 "timestamp part modifier requires numeric value".into(),
433 ));
434 }
435 };
436 return Ok(CompiledMatcher::TimestampPart {
437 part,
438 inner: Box::new(inner),
439 });
440 }
441
442 if ctx.fieldref {
444 let field_name = value_to_plain_string(value)?;
445 return Ok(CompiledMatcher::FieldRef {
446 field: field_name,
447 case_insensitive: ci,
448 });
449 }
450
451 if ctx.re {
455 let pattern = value_to_plain_string(value)?;
456 let regex = build_regex(&pattern, ctx.ignore_case, ctx.multiline, ctx.dotall)?;
457 return Ok(CompiledMatcher::Regex(regex));
458 }
459
460 if ctx.cidr {
462 let cidr_str = value_to_plain_string(value)?;
463 let net: ipnet::IpNet = cidr_str
464 .parse()
465 .map_err(|e: ipnet::AddrParseError| EvalError::InvalidCidr(e))?;
466 return Ok(CompiledMatcher::Cidr(net));
467 }
468
469 if ctx.has_numeric_comparison() {
471 let n = value_to_f64(value)?;
472 if ctx.gt {
473 return Ok(CompiledMatcher::NumericGt(n));
474 }
475 if ctx.gte {
476 return Ok(CompiledMatcher::NumericGte(n));
477 }
478 if ctx.lt {
479 return Ok(CompiledMatcher::NumericLt(n));
480 }
481 if ctx.lte {
482 return Ok(CompiledMatcher::NumericLte(n));
483 }
484 }
485
486 if ctx.has_neq() {
488 let mut inner_ctx = ModCtx { ..*ctx };
490 inner_ctx.neq = false;
491 let inner = compile_value(value, &inner_ctx)?;
492 return Ok(CompiledMatcher::Not(Box::new(inner)));
493 }
494
495 match value {
497 SigmaValue::Integer(n) => {
498 if ctx.contains || ctx.startswith || ctx.endswith {
499 return compile_string_value(&n.to_string(), ctx);
501 }
502 return Ok(CompiledMatcher::NumericEq(*n as f64));
503 }
504 SigmaValue::Float(n) => {
505 if ctx.contains || ctx.startswith || ctx.endswith {
506 return compile_string_value(&n.to_string(), ctx);
507 }
508 return Ok(CompiledMatcher::NumericEq(*n));
509 }
510 SigmaValue::Bool(b) => return Ok(CompiledMatcher::BoolEq(*b)),
511 SigmaValue::Null => return Ok(CompiledMatcher::Null),
512 SigmaValue::String(_) => {} }
514
515 let sigma_str = match value {
517 SigmaValue::String(s) => s,
518 _ => unreachable!(),
519 };
520
521 let mut bytes = sigma_string_to_bytes(sigma_str);
523
524 if ctx.wide {
526 bytes = to_utf16le_bytes(&bytes);
527 }
528
529 if ctx.utf16be {
531 bytes = to_utf16be_bytes(&bytes);
532 }
533
534 if ctx.utf16 {
536 bytes = to_utf16_bom_bytes(&bytes);
537 }
538
539 if ctx.base64 {
541 let encoded = BASE64_STANDARD.encode(&bytes);
542 return compile_string_value(&encoded, ctx);
543 }
544
545 if ctx.base64offset {
547 let patterns = base64_offset_patterns(&bytes);
548 let matchers: Vec<CompiledMatcher> = patterns
549 .into_iter()
550 .map(|p| {
551 CompiledMatcher::Contains {
553 value: if ci { p.to_lowercase() } else { p },
554 case_insensitive: ci,
555 }
556 })
557 .collect();
558 return Ok(CompiledMatcher::AnyOf(matchers));
559 }
560
561 if ctx.windash {
563 let plain = sigma_str
564 .as_plain()
565 .unwrap_or_else(|| sigma_str.original.clone());
566 let variants = expand_windash(&plain)?;
567 let matchers: Result<Vec<CompiledMatcher>> = variants
568 .into_iter()
569 .map(|v| compile_string_value(&v, ctx))
570 .collect();
571 return Ok(CompiledMatcher::AnyOf(matchers?));
572 }
573
574 compile_sigma_string(sigma_str, ctx)
576}
577
578fn compile_sigma_string(sigma_str: &SigmaString, ctx: &ModCtx) -> Result<CompiledMatcher> {
580 let ci = ctx.is_case_insensitive();
581
582 if sigma_str.is_plain() {
584 let plain = sigma_str.as_plain().unwrap_or_default();
585 return compile_string_value(&plain, ctx);
586 }
587
588 let mut pattern = String::new();
593 if ci {
594 pattern.push_str("(?i)");
595 }
596
597 if !ctx.contains && !ctx.startswith {
598 pattern.push('^');
599 }
600
601 for part in &sigma_str.parts {
602 match part {
603 StringPart::Plain(text) => {
604 pattern.push_str(®ex::escape(text));
605 }
606 StringPart::Special(SpecialChar::WildcardMulti) => {
607 pattern.push_str(".*");
608 }
609 StringPart::Special(SpecialChar::WildcardSingle) => {
610 pattern.push('.');
611 }
612 }
613 }
614
615 if !ctx.contains && !ctx.endswith {
616 pattern.push('$');
617 }
618
619 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
620 Ok(CompiledMatcher::Regex(regex))
621}
622
623fn compile_string_value(plain: &str, ctx: &ModCtx) -> Result<CompiledMatcher> {
625 let ci = ctx.is_case_insensitive();
626
627 if ctx.contains {
628 Ok(CompiledMatcher::Contains {
629 value: if ci {
630 plain.to_lowercase()
631 } else {
632 plain.to_string()
633 },
634 case_insensitive: ci,
635 })
636 } else if ctx.startswith {
637 Ok(CompiledMatcher::StartsWith {
638 value: if ci {
639 plain.to_lowercase()
640 } else {
641 plain.to_string()
642 },
643 case_insensitive: ci,
644 })
645 } else if ctx.endswith {
646 Ok(CompiledMatcher::EndsWith {
647 value: if ci {
648 plain.to_lowercase()
649 } else {
650 plain.to_string()
651 },
652 case_insensitive: ci,
653 })
654 } else {
655 Ok(CompiledMatcher::Exact {
656 value: if ci {
657 plain.to_lowercase()
658 } else {
659 plain.to_string()
660 },
661 case_insensitive: ci,
662 })
663 }
664}
665
666fn compile_value_default(value: &SigmaValue, case_insensitive: bool) -> Result<CompiledMatcher> {
668 match value {
669 SigmaValue::String(s) => {
670 if s.is_plain() {
671 let plain = s.as_plain().unwrap_or_default();
672 Ok(CompiledMatcher::Contains {
673 value: if case_insensitive {
674 plain.to_lowercase()
675 } else {
676 plain
677 },
678 case_insensitive,
679 })
680 } else {
681 let pattern = sigma_string_to_regex(&s.parts, case_insensitive);
683 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
684 Ok(CompiledMatcher::Regex(regex))
685 }
686 }
687 SigmaValue::Integer(n) => Ok(CompiledMatcher::NumericEq(*n as f64)),
688 SigmaValue::Float(n) => Ok(CompiledMatcher::NumericEq(*n)),
689 SigmaValue::Bool(b) => Ok(CompiledMatcher::BoolEq(*b)),
690 SigmaValue::Null => Ok(CompiledMatcher::Null),
691 }
692}
693
694pub fn eval_condition(
703 expr: &ConditionExpr,
704 detections: &HashMap<String, CompiledDetection>,
705 event: &impl Event,
706 matched_selections: &mut Vec<String>,
707) -> bool {
708 match expr {
709 ConditionExpr::Identifier(name) => {
710 if let Some(det) = detections.get(name) {
711 let result = eval_detection(det, event);
712 if result {
713 matched_selections.push(name.clone());
714 }
715 result
716 } else {
717 false
718 }
719 }
720
721 ConditionExpr::And(exprs) => exprs
722 .iter()
723 .all(|e| eval_condition(e, detections, event, matched_selections)),
724
725 ConditionExpr::Or(exprs) => exprs
726 .iter()
727 .any(|e| eval_condition(e, detections, event, matched_selections)),
728
729 ConditionExpr::Not(inner) => !eval_condition(inner, detections, event, matched_selections),
730
731 ConditionExpr::Selector {
732 quantifier,
733 pattern,
734 } => {
735 let matching_names: Vec<&String> = match pattern {
736 SelectorPattern::Them => detections
737 .keys()
738 .filter(|name| !name.starts_with('_'))
739 .collect(),
740 SelectorPattern::Pattern(pat) => detections
741 .keys()
742 .filter(|name| pattern_matches(pat, name))
743 .collect(),
744 };
745
746 let mut match_count = 0u64;
747 for name in &matching_names {
748 if let Some(det) = detections.get(*name)
749 && eval_detection(det, event)
750 {
751 match_count += 1;
752 matched_selections.push((*name).clone());
753 }
754 }
755
756 match quantifier {
757 Quantifier::Any => match_count >= 1,
758 Quantifier::All => match_count == matching_names.len() as u64,
759 Quantifier::Count(n) => match_count >= *n,
760 }
761 }
762 }
763}
764
765fn eval_detection(detection: &CompiledDetection, event: &impl Event) -> bool {
767 match detection {
768 CompiledDetection::AllOf(items) => {
769 items.iter().all(|item| eval_detection_item(item, event))
770 }
771 CompiledDetection::AnyOf(dets) => dets.iter().any(|d| eval_detection(d, event)),
772 CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
773 }
774}
775
776fn eval_detection_item(item: &CompiledDetectionItem, event: &impl Event) -> bool {
778 if let Some(expect_exists) = item.exists {
779 if let Some(field) = &item.field {
780 let exists = event.get_field(field).is_some_and(|v| !v.is_null());
781 return exists == expect_exists;
782 }
783 return !expect_exists;
784 }
785
786 match &item.field {
787 Some(field_name) => {
788 if let Some(value) = event.get_field(field_name) {
789 item.matcher.matches(&value, event)
790 } else {
791 matches!(item.matcher, CompiledMatcher::Null)
792 }
793 }
794 None => item.matcher.matches_keyword(event),
795 }
796}
797
798fn collect_field_matches(
800 selection_names: &[String],
801 detections: &HashMap<String, CompiledDetection>,
802 event: &impl Event,
803) -> Vec<FieldMatch> {
804 let mut matches = Vec::new();
805 for name in selection_names {
806 if let Some(det) = detections.get(name) {
807 collect_detection_fields(det, event, &mut matches);
808 }
809 }
810 matches
811}
812
813fn collect_detection_fields(
814 detection: &CompiledDetection,
815 event: &impl Event,
816 out: &mut Vec<FieldMatch>,
817) {
818 match detection {
819 CompiledDetection::AllOf(items) => {
820 for item in items {
821 if let Some(field_name) = &item.field
822 && let Some(value) = event.get_field(field_name)
823 && item.matcher.matches(&value, event)
824 {
825 out.push(FieldMatch {
826 field: field_name.clone(),
827 value: value.to_json(),
828 });
829 }
830 }
831 }
832 CompiledDetection::AnyOf(dets) => {
833 for d in dets {
834 if eval_detection(d, event) {
835 collect_detection_fields(d, event, out);
836 }
837 }
838 }
839 CompiledDetection::Keywords(_) => {}
840 }
841}