1use std::collections::HashMap;
11
12use base64::Engine as Base64Engine;
13use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
14use regex::Regex;
15
16use rsigma_parser::value::{SpecialChar, StringPart};
17use rsigma_parser::{
18 ConditionExpr, Detection, DetectionItem, Level, LogSource, Modifier, Quantifier,
19 SelectorPattern, SigmaRule, SigmaString, SigmaValue,
20};
21
22use crate::error::{EvalError, Result};
23use crate::event::Event;
24use crate::matcher::{CompiledMatcher, sigma_string_to_regex};
25use crate::result::{FieldMatch, MatchResult};
26
27#[derive(Debug, Clone)]
33pub struct CompiledRule {
34 pub title: String,
35 pub id: Option<String>,
36 pub level: Option<Level>,
37 pub tags: Vec<String>,
38 pub logsource: LogSource,
39 pub detections: HashMap<String, CompiledDetection>,
41 pub conditions: Vec<ConditionExpr>,
43 pub include_event: bool,
46}
47
48#[derive(Debug, Clone)]
50pub enum CompiledDetection {
51 AllOf(Vec<CompiledDetectionItem>),
53 AnyOf(Vec<CompiledDetection>),
55 Keywords(CompiledMatcher),
57}
58
59#[derive(Debug, Clone)]
61pub struct CompiledDetectionItem {
62 pub field: Option<String>,
64 pub matcher: CompiledMatcher,
66 pub exists: Option<bool>,
68}
69
70#[derive(Clone, Copy)]
76struct ModCtx {
77 contains: bool,
78 startswith: bool,
79 endswith: bool,
80 all: bool,
81 base64: bool,
82 base64offset: bool,
83 wide: bool,
84 utf16be: bool,
85 utf16: bool,
86 windash: bool,
87 re: bool,
88 cidr: bool,
89 cased: bool,
90 exists: bool,
91 fieldref: bool,
92 gt: bool,
93 gte: bool,
94 lt: bool,
95 lte: bool,
96 neq: bool,
97 ignore_case: bool,
98 multiline: bool,
99 dotall: bool,
100 expand: bool,
101 timestamp_part: Option<crate::matcher::TimePart>,
102}
103
104impl ModCtx {
105 fn from_modifiers(modifiers: &[Modifier]) -> Self {
106 let mut ctx = ModCtx {
107 contains: false,
108 startswith: false,
109 endswith: false,
110 all: false,
111 base64: false,
112 base64offset: false,
113 wide: false,
114 utf16be: false,
115 utf16: false,
116 windash: false,
117 re: false,
118 cidr: false,
119 cased: false,
120 exists: false,
121 fieldref: false,
122 gt: false,
123 gte: false,
124 lt: false,
125 lte: false,
126 neq: false,
127 ignore_case: false,
128 multiline: false,
129 dotall: false,
130 expand: false,
131 timestamp_part: None,
132 };
133 for m in modifiers {
134 match m {
135 Modifier::Contains => ctx.contains = true,
136 Modifier::StartsWith => ctx.startswith = true,
137 Modifier::EndsWith => ctx.endswith = true,
138 Modifier::All => ctx.all = true,
139 Modifier::Base64 => ctx.base64 = true,
140 Modifier::Base64Offset => ctx.base64offset = true,
141 Modifier::Wide => ctx.wide = true,
142 Modifier::Utf16be => ctx.utf16be = true,
143 Modifier::Utf16 => ctx.utf16 = true,
144 Modifier::WindAsh => ctx.windash = true,
145 Modifier::Re => ctx.re = true,
146 Modifier::Cidr => ctx.cidr = true,
147 Modifier::Cased => ctx.cased = true,
148 Modifier::Exists => ctx.exists = true,
149 Modifier::FieldRef => ctx.fieldref = true,
150 Modifier::Gt => ctx.gt = true,
151 Modifier::Gte => ctx.gte = true,
152 Modifier::Lt => ctx.lt = true,
153 Modifier::Lte => ctx.lte = true,
154 Modifier::Neq => ctx.neq = true,
155 Modifier::IgnoreCase => ctx.ignore_case = true,
156 Modifier::Multiline => ctx.multiline = true,
157 Modifier::DotAll => ctx.dotall = true,
158 Modifier::Expand => ctx.expand = true,
159 Modifier::Hour => ctx.timestamp_part = Some(crate::matcher::TimePart::Hour),
160 Modifier::Day => ctx.timestamp_part = Some(crate::matcher::TimePart::Day),
161 Modifier::Week => ctx.timestamp_part = Some(crate::matcher::TimePart::Week),
162 Modifier::Month => ctx.timestamp_part = Some(crate::matcher::TimePart::Month),
163 Modifier::Year => ctx.timestamp_part = Some(crate::matcher::TimePart::Year),
164 Modifier::Minute => ctx.timestamp_part = Some(crate::matcher::TimePart::Minute),
165 }
166 }
167 ctx
168 }
169
170 fn is_case_insensitive(&self) -> bool {
173 !self.cased
174 }
175
176 fn has_numeric_comparison(&self) -> bool {
178 self.gt || self.gte || self.lt || self.lte
179 }
180
181 fn has_neq(&self) -> bool {
183 self.neq
184 }
185}
186
187pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
193 let mut detections = HashMap::new();
194 for (name, detection) in &rule.detection.named {
195 detections.insert(name.clone(), compile_detection(detection)?);
196 }
197
198 let include_event = rule
199 .custom_attributes
200 .get("rsigma.include_event")
201 .is_some_and(|v| v == "true");
202
203 Ok(CompiledRule {
204 title: rule.title.clone(),
205 id: rule.id.clone(),
206 level: rule.level,
207 tags: rule.tags.clone(),
208 logsource: rule.logsource.clone(),
209 detections,
210 conditions: rule.detection.conditions.clone(),
211 include_event,
212 })
213}
214
215pub fn evaluate_rule(rule: &CompiledRule, event: &Event) -> Option<MatchResult> {
217 for condition in &rule.conditions {
219 let mut matched_selections = Vec::new();
220 if eval_condition(condition, &rule.detections, event, &mut matched_selections) {
221 let matched_fields =
223 collect_field_matches(&matched_selections, &rule.detections, event);
224
225 let event_data = if rule.include_event {
226 Some(event.as_value().clone())
227 } else {
228 None
229 };
230
231 return Some(MatchResult {
232 rule_title: rule.title.clone(),
233 rule_id: rule.id.clone(),
234 level: rule.level,
235 tags: rule.tags.clone(),
236 matched_selections,
237 matched_fields,
238 event: event_data,
239 });
240 }
241 }
242 None
243}
244
245pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
254 match detection {
255 Detection::AllOf(items) => {
256 if items.is_empty() {
257 return Err(EvalError::InvalidModifiers(
258 "AllOf detection must not be empty (vacuous truth)".into(),
259 ));
260 }
261 let compiled: Result<Vec<_>> = items.iter().map(compile_detection_item).collect();
262 Ok(CompiledDetection::AllOf(compiled?))
263 }
264 Detection::AnyOf(dets) => {
265 if dets.is_empty() {
266 return Err(EvalError::InvalidModifiers(
267 "AnyOf detection must not be empty (would never match)".into(),
268 ));
269 }
270 let compiled: Result<Vec<_>> = dets.iter().map(compile_detection).collect();
271 Ok(CompiledDetection::AnyOf(compiled?))
272 }
273 Detection::Keywords(values) => {
274 let ci = true; let matchers: Vec<CompiledMatcher> = values
276 .iter()
277 .map(|v| compile_value_default(v, ci))
278 .collect::<Result<Vec<_>>>()?;
279 let matcher = if matchers.len() == 1 {
280 matchers
282 .into_iter()
283 .next()
284 .unwrap_or(CompiledMatcher::AnyOf(vec![]))
285 } else {
286 CompiledMatcher::AnyOf(matchers)
287 };
288 Ok(CompiledDetection::Keywords(matcher))
289 }
290 }
291}
292
293fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
294 let ctx = ModCtx::from_modifiers(&item.field.modifiers);
295
296 if ctx.exists {
298 let expect = match item.values.first() {
299 Some(SigmaValue::Bool(b)) => *b,
300 Some(SigmaValue::String(s)) => match s.as_plain().as_deref() {
301 Some("true") | Some("yes") => true,
302 Some("false") | Some("no") => false,
303 _ => true,
304 },
305 _ => true,
306 };
307 return Ok(CompiledDetectionItem {
308 field: item.field.name.clone(),
309 matcher: CompiledMatcher::Exists(expect),
310 exists: Some(expect),
311 });
312 }
313
314 if ctx.all && item.values.len() <= 1 {
316 return Err(EvalError::InvalidModifiers(
317 "|all modifier requires more than one value".to_string(),
318 ));
319 }
320
321 let matchers: Result<Vec<CompiledMatcher>> =
323 item.values.iter().map(|v| compile_value(v, &ctx)).collect();
324 let matchers = matchers?;
325
326 let combined = if matchers.len() == 1 {
328 matchers
330 .into_iter()
331 .next()
332 .unwrap_or(CompiledMatcher::AnyOf(vec![]))
333 } else if ctx.all {
334 CompiledMatcher::AllOf(matchers)
335 } else {
336 CompiledMatcher::AnyOf(matchers)
337 };
338
339 Ok(CompiledDetectionItem {
340 field: item.field.name.clone(),
341 matcher: combined,
342 exists: None,
343 })
344}
345
346fn compile_value(value: &SigmaValue, ctx: &ModCtx) -> Result<CompiledMatcher> {
352 let ci = ctx.is_case_insensitive();
353
354 if ctx.expand {
358 let plain = value_to_plain_string(value)?;
359 let template = crate::matcher::parse_expand_template(&plain);
360 return Ok(CompiledMatcher::Expand {
361 template,
362 case_insensitive: ci,
363 });
364 }
365
366 if let Some(part) = ctx.timestamp_part {
368 let inner = match value {
371 SigmaValue::Integer(n) => CompiledMatcher::NumericEq(*n as f64),
372 SigmaValue::Float(n) => CompiledMatcher::NumericEq(*n),
373 SigmaValue::String(s) => {
374 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
375 let n: f64 = plain.parse().map_err(|_| {
376 EvalError::IncompatibleValue(format!(
377 "timestamp part modifier requires numeric value, got: {plain}"
378 ))
379 })?;
380 CompiledMatcher::NumericEq(n)
381 }
382 _ => {
383 return Err(EvalError::IncompatibleValue(
384 "timestamp part modifier requires numeric value".into(),
385 ));
386 }
387 };
388 return Ok(CompiledMatcher::TimestampPart {
389 part,
390 inner: Box::new(inner),
391 });
392 }
393
394 if ctx.fieldref {
396 let field_name = value_to_plain_string(value)?;
397 return Ok(CompiledMatcher::FieldRef {
398 field: field_name,
399 case_insensitive: ci,
400 });
401 }
402
403 if ctx.re {
407 let pattern = value_to_plain_string(value)?;
408 let regex = build_regex(&pattern, ctx.ignore_case, ctx.multiline, ctx.dotall)?;
409 return Ok(CompiledMatcher::Regex(regex));
410 }
411
412 if ctx.cidr {
414 let cidr_str = value_to_plain_string(value)?;
415 let net: ipnet::IpNet = cidr_str
416 .parse()
417 .map_err(|e: ipnet::AddrParseError| EvalError::InvalidCidr(e))?;
418 return Ok(CompiledMatcher::Cidr(net));
419 }
420
421 if ctx.has_numeric_comparison() {
423 let n = value_to_f64(value)?;
424 if ctx.gt {
425 return Ok(CompiledMatcher::NumericGt(n));
426 }
427 if ctx.gte {
428 return Ok(CompiledMatcher::NumericGte(n));
429 }
430 if ctx.lt {
431 return Ok(CompiledMatcher::NumericLt(n));
432 }
433 if ctx.lte {
434 return Ok(CompiledMatcher::NumericLte(n));
435 }
436 }
437
438 if ctx.has_neq() {
440 let mut inner_ctx = ModCtx { ..*ctx };
442 inner_ctx.neq = false;
443 let inner = compile_value(value, &inner_ctx)?;
444 return Ok(CompiledMatcher::Not(Box::new(inner)));
445 }
446
447 match value {
449 SigmaValue::Integer(n) => {
450 if ctx.contains || ctx.startswith || ctx.endswith {
451 return compile_string_value(&n.to_string(), ctx);
453 }
454 return Ok(CompiledMatcher::NumericEq(*n as f64));
455 }
456 SigmaValue::Float(n) => {
457 if ctx.contains || ctx.startswith || ctx.endswith {
458 return compile_string_value(&n.to_string(), ctx);
459 }
460 return Ok(CompiledMatcher::NumericEq(*n));
461 }
462 SigmaValue::Bool(b) => return Ok(CompiledMatcher::BoolEq(*b)),
463 SigmaValue::Null => return Ok(CompiledMatcher::Null),
464 SigmaValue::String(_) => {} }
466
467 let sigma_str = match value {
469 SigmaValue::String(s) => s,
470 _ => unreachable!(),
471 };
472
473 let mut bytes = sigma_string_to_bytes(sigma_str);
475
476 if ctx.wide {
478 bytes = to_utf16le_bytes(&bytes);
479 }
480
481 if ctx.utf16be {
483 bytes = to_utf16be_bytes(&bytes);
484 }
485
486 if ctx.utf16 {
488 bytes = to_utf16_bom_bytes(&bytes);
489 }
490
491 if ctx.base64 {
493 let encoded = BASE64_STANDARD.encode(&bytes);
494 return compile_string_value(&encoded, ctx);
495 }
496
497 if ctx.base64offset {
499 let patterns = base64_offset_patterns(&bytes);
500 let matchers: Vec<CompiledMatcher> = patterns
501 .into_iter()
502 .map(|p| {
503 CompiledMatcher::Contains {
505 value: if ci { p.to_lowercase() } else { p },
506 case_insensitive: ci,
507 }
508 })
509 .collect();
510 return Ok(CompiledMatcher::AnyOf(matchers));
511 }
512
513 if ctx.windash {
515 let plain = sigma_str
516 .as_plain()
517 .unwrap_or_else(|| sigma_str.original.clone());
518 let variants = expand_windash(&plain)?;
519 let matchers: Result<Vec<CompiledMatcher>> = variants
520 .into_iter()
521 .map(|v| compile_string_value(&v, ctx))
522 .collect();
523 return Ok(CompiledMatcher::AnyOf(matchers?));
524 }
525
526 compile_sigma_string(sigma_str, ctx)
528}
529
530fn compile_sigma_string(sigma_str: &SigmaString, ctx: &ModCtx) -> Result<CompiledMatcher> {
532 let ci = ctx.is_case_insensitive();
533
534 if sigma_str.is_plain() {
536 let plain = sigma_str.as_plain().unwrap_or_default();
537 return compile_string_value(&plain, ctx);
538 }
539
540 let mut pattern = String::new();
545 if ci {
546 pattern.push_str("(?i)");
547 }
548
549 if !ctx.contains && !ctx.startswith {
550 pattern.push('^');
551 }
552
553 for part in &sigma_str.parts {
554 match part {
555 StringPart::Plain(text) => {
556 pattern.push_str(®ex::escape(text));
557 }
558 StringPart::Special(SpecialChar::WildcardMulti) => {
559 pattern.push_str(".*");
560 }
561 StringPart::Special(SpecialChar::WildcardSingle) => {
562 pattern.push('.');
563 }
564 }
565 }
566
567 if !ctx.contains && !ctx.endswith {
568 pattern.push('$');
569 }
570
571 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
572 Ok(CompiledMatcher::Regex(regex))
573}
574
575fn compile_string_value(plain: &str, ctx: &ModCtx) -> Result<CompiledMatcher> {
577 let ci = ctx.is_case_insensitive();
578
579 if ctx.contains {
580 Ok(CompiledMatcher::Contains {
581 value: if ci {
582 plain.to_lowercase()
583 } else {
584 plain.to_string()
585 },
586 case_insensitive: ci,
587 })
588 } else if ctx.startswith {
589 Ok(CompiledMatcher::StartsWith {
590 value: if ci {
591 plain.to_lowercase()
592 } else {
593 plain.to_string()
594 },
595 case_insensitive: ci,
596 })
597 } else if ctx.endswith {
598 Ok(CompiledMatcher::EndsWith {
599 value: if ci {
600 plain.to_lowercase()
601 } else {
602 plain.to_string()
603 },
604 case_insensitive: ci,
605 })
606 } else {
607 Ok(CompiledMatcher::Exact {
608 value: if ci {
609 plain.to_lowercase()
610 } else {
611 plain.to_string()
612 },
613 case_insensitive: ci,
614 })
615 }
616}
617
618fn compile_value_default(value: &SigmaValue, case_insensitive: bool) -> Result<CompiledMatcher> {
620 match value {
621 SigmaValue::String(s) => {
622 if s.is_plain() {
623 let plain = s.as_plain().unwrap_or_default();
624 Ok(CompiledMatcher::Contains {
625 value: if case_insensitive {
626 plain.to_lowercase()
627 } else {
628 plain
629 },
630 case_insensitive,
631 })
632 } else {
633 let pattern = sigma_string_to_regex(&s.parts, case_insensitive);
635 let regex = Regex::new(&pattern).map_err(EvalError::InvalidRegex)?;
636 Ok(CompiledMatcher::Regex(regex))
637 }
638 }
639 SigmaValue::Integer(n) => Ok(CompiledMatcher::NumericEq(*n as f64)),
640 SigmaValue::Float(n) => Ok(CompiledMatcher::NumericEq(*n)),
641 SigmaValue::Bool(b) => Ok(CompiledMatcher::BoolEq(*b)),
642 SigmaValue::Null => Ok(CompiledMatcher::Null),
643 }
644}
645
646pub fn eval_condition(
655 expr: &ConditionExpr,
656 detections: &HashMap<String, CompiledDetection>,
657 event: &Event,
658 matched_selections: &mut Vec<String>,
659) -> bool {
660 match expr {
661 ConditionExpr::Identifier(name) => {
662 if let Some(det) = detections.get(name) {
663 let result = eval_detection(det, event);
664 if result {
665 matched_selections.push(name.clone());
666 }
667 result
668 } else {
669 false
670 }
671 }
672
673 ConditionExpr::And(exprs) => exprs
674 .iter()
675 .all(|e| eval_condition(e, detections, event, matched_selections)),
676
677 ConditionExpr::Or(exprs) => exprs
678 .iter()
679 .any(|e| eval_condition(e, detections, event, matched_selections)),
680
681 ConditionExpr::Not(inner) => !eval_condition(inner, detections, event, matched_selections),
682
683 ConditionExpr::Selector {
684 quantifier,
685 pattern,
686 } => {
687 let matching_names: Vec<&String> = match pattern {
688 SelectorPattern::Them => detections
689 .keys()
690 .filter(|name| !name.starts_with('_'))
691 .collect(),
692 SelectorPattern::Pattern(pat) => detections
693 .keys()
694 .filter(|name| pattern_matches(pat, name))
695 .collect(),
696 };
697
698 let mut match_count = 0u64;
699 for name in &matching_names {
700 if let Some(det) = detections.get(*name)
701 && eval_detection(det, event)
702 {
703 match_count += 1;
704 matched_selections.push((*name).clone());
705 }
706 }
707
708 match quantifier {
709 Quantifier::Any => match_count >= 1,
710 Quantifier::All => match_count == matching_names.len() as u64,
711 Quantifier::Count(n) => match_count >= *n,
712 }
713 }
714 }
715}
716
717fn eval_detection(detection: &CompiledDetection, event: &Event) -> bool {
719 match detection {
720 CompiledDetection::AllOf(items) => {
721 items.iter().all(|item| eval_detection_item(item, event))
722 }
723 CompiledDetection::AnyOf(dets) => dets.iter().any(|d| eval_detection(d, event)),
724 CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
725 }
726}
727
728fn eval_detection_item(item: &CompiledDetectionItem, event: &Event) -> bool {
730 if let Some(expect_exists) = item.exists {
732 if let Some(field) = &item.field {
733 let exists = event.get_field(field).is_some_and(|v| !v.is_null());
734 return exists == expect_exists;
735 }
736 return !expect_exists; }
738
739 match &item.field {
740 Some(field_name) => {
741 if let Some(value) = event.get_field(field_name) {
743 item.matcher.matches(value, event)
744 } else {
745 matches!(item.matcher, CompiledMatcher::Null)
747 }
748 }
749 None => {
750 item.matcher.matches_keyword(event)
752 }
753 }
754}
755
756fn collect_field_matches(
758 selection_names: &[String],
759 detections: &HashMap<String, CompiledDetection>,
760 event: &Event,
761) -> Vec<FieldMatch> {
762 let mut matches = Vec::new();
763 for name in selection_names {
764 if let Some(det) = detections.get(name) {
765 collect_detection_fields(det, event, &mut matches);
766 }
767 }
768 matches
769}
770
771fn collect_detection_fields(
772 detection: &CompiledDetection,
773 event: &Event,
774 out: &mut Vec<FieldMatch>,
775) {
776 match detection {
777 CompiledDetection::AllOf(items) => {
778 for item in items {
779 if let Some(field_name) = &item.field
780 && let Some(value) = event.get_field(field_name)
781 && item.matcher.matches(value, event)
782 {
783 out.push(FieldMatch {
784 field: field_name.clone(),
785 value: value.clone(),
786 });
787 }
788 }
789 }
790 CompiledDetection::AnyOf(dets) => {
791 for d in dets {
792 if eval_detection(d, event) {
793 collect_detection_fields(d, event, out);
794 }
795 }
796 }
797 CompiledDetection::Keywords(_) => {
798 }
800 }
801}
802
803fn pattern_matches(pattern: &str, name: &str) -> bool {
809 if pattern == "*" {
810 return true;
811 }
812 if let Some(prefix) = pattern.strip_suffix('*') {
813 return name.starts_with(prefix);
814 }
815 if let Some(suffix) = pattern.strip_prefix('*') {
816 return name.ends_with(suffix);
817 }
818 pattern == name
819}
820
821fn value_to_plain_string(value: &SigmaValue) -> Result<String> {
827 match value {
828 SigmaValue::String(s) => Ok(s.as_plain().unwrap_or_else(|| s.original.clone())),
829 SigmaValue::Integer(n) => Ok(n.to_string()),
830 SigmaValue::Float(n) => Ok(n.to_string()),
831 SigmaValue::Bool(b) => Ok(b.to_string()),
832 SigmaValue::Null => Err(EvalError::IncompatibleValue(
833 "null value for string modifier".into(),
834 )),
835 }
836}
837
838fn value_to_f64(value: &SigmaValue) -> Result<f64> {
840 match value {
841 SigmaValue::Integer(n) => Ok(*n as f64),
842 SigmaValue::Float(n) => Ok(*n),
843 SigmaValue::String(s) => {
844 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
845 plain
846 .parse::<f64>()
847 .map_err(|_| EvalError::ExpectedNumeric(plain))
848 }
849 _ => Err(EvalError::ExpectedNumeric(format!("{value:?}"))),
850 }
851}
852
853fn sigma_string_to_bytes(s: &SigmaString) -> Vec<u8> {
855 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
856 plain.into_bytes()
857}
858
859fn to_utf16le_bytes(bytes: &[u8]) -> Vec<u8> {
865 let s = String::from_utf8_lossy(bytes);
866 let mut wide = Vec::with_capacity(s.len() * 2);
867 for c in s.chars() {
868 let mut buf = [0u16; 2];
869 let encoded = c.encode_utf16(&mut buf);
870 for u in encoded {
871 wide.extend_from_slice(&u.to_le_bytes());
872 }
873 }
874 wide
875}
876
877fn to_utf16be_bytes(bytes: &[u8]) -> Vec<u8> {
879 let s = String::from_utf8_lossy(bytes);
880 let mut wide = Vec::with_capacity(s.len() * 2);
881 for c in s.chars() {
882 let mut buf = [0u16; 2];
883 let encoded = c.encode_utf16(&mut buf);
884 for u in encoded {
885 wide.extend_from_slice(&u.to_be_bytes());
886 }
887 }
888 wide
889}
890
891fn to_utf16_bom_bytes(bytes: &[u8]) -> Vec<u8> {
893 let mut result = vec![0xFF, 0xFE]; result.extend_from_slice(&to_utf16le_bytes(bytes));
895 result
896}
897
898fn base64_offset_patterns(value: &[u8]) -> Vec<String> {
904 let mut patterns = Vec::with_capacity(3);
905
906 for offset in 0..3usize {
907 let mut padded = vec![0u8; offset];
908 padded.extend_from_slice(value);
909
910 let encoded = BASE64_STANDARD.encode(&padded);
911
912 let start = (offset * 4).div_ceil(3);
914 let trimmed = encoded.trim_end_matches('=');
916 let end = trimmed.len();
917
918 if start < end {
919 patterns.push(trimmed[start..end].to_string());
920 }
921 }
922
923 patterns
924}
925
926fn build_regex(
928 pattern: &str,
929 case_insensitive: bool,
930 multiline: bool,
931 dotall: bool,
932) -> Result<Regex> {
933 let mut flags = String::new();
934 if case_insensitive {
935 flags.push('i');
936 }
937 if multiline {
938 flags.push('m');
939 }
940 if dotall {
941 flags.push('s');
942 }
943
944 let full_pattern = if flags.is_empty() {
945 pattern.to_string()
946 } else {
947 format!("(?{flags}){pattern}")
948 };
949
950 Regex::new(&full_pattern).map_err(EvalError::InvalidRegex)
951}
952
953const WINDASH_CHARS: [char; 5] = ['-', '/', '\u{2013}', '\u{2014}', '\u{2015}'];
956
957const MAX_WINDASH_DASHES: usize = 8;
960
961fn expand_windash(input: &str) -> Result<Vec<String>> {
964 let dash_positions: Vec<usize> = input
966 .char_indices()
967 .filter(|(_, c)| *c == '-')
968 .map(|(i, _)| i)
969 .collect();
970
971 if dash_positions.is_empty() {
972 return Ok(vec![input.to_string()]);
973 }
974
975 let n = dash_positions.len();
976 if n > MAX_WINDASH_DASHES {
977 return Err(EvalError::InvalidModifiers(format!(
978 "windash modifier: value contains {n} dashes, max is {MAX_WINDASH_DASHES} \
979 (would generate {} variants)",
980 5u64.saturating_pow(n as u32)
981 )));
982 }
983
984 let total = WINDASH_CHARS.len().pow(n as u32);
986 let mut variants = Vec::with_capacity(total);
987
988 for combo in 0..total {
989 let mut variant = input.to_string();
990 let mut idx = combo;
991 for &pos in dash_positions.iter().rev() {
993 let replacement = WINDASH_CHARS[idx % WINDASH_CHARS.len()];
994 variant.replace_range(pos..pos + 1, &replacement.to_string());
995 idx /= WINDASH_CHARS.len();
996 }
997 variants.push(variant);
998 }
999
1000 Ok(variants)
1001}
1002
1003#[cfg(test)]
1008mod tests {
1009 use super::*;
1010 use rsigma_parser::FieldSpec;
1011 use serde_json::json;
1012
1013 fn make_field_spec(name: &str, modifiers: &[Modifier]) -> FieldSpec {
1014 FieldSpec::new(Some(name.to_string()), modifiers.to_vec())
1015 }
1016
1017 fn make_item(name: &str, modifiers: &[Modifier], values: Vec<SigmaValue>) -> DetectionItem {
1018 DetectionItem {
1019 field: make_field_spec(name, modifiers),
1020 values,
1021 }
1022 }
1023
1024 #[test]
1025 fn test_compile_exact_match() {
1026 let item = make_item(
1027 "CommandLine",
1028 &[],
1029 vec![SigmaValue::String(SigmaString::new("whoami"))],
1030 );
1031 let compiled = compile_detection_item(&item).unwrap();
1032 assert_eq!(compiled.field, Some("CommandLine".into()));
1033
1034 let ev = json!({"CommandLine": "whoami"});
1035 let event = Event::from_value(&ev);
1036 assert!(eval_detection_item(&compiled, &event));
1037
1038 let ev2 = json!({"CommandLine": "WHOAMI"});
1039 let event2 = Event::from_value(&ev2);
1040 assert!(eval_detection_item(&compiled, &event2)); }
1042
1043 #[test]
1044 fn test_compile_contains() {
1045 let item = make_item(
1046 "CommandLine",
1047 &[Modifier::Contains],
1048 vec![SigmaValue::String(SigmaString::new("whoami"))],
1049 );
1050 let compiled = compile_detection_item(&item).unwrap();
1051
1052 let ev = json!({"CommandLine": "cmd /c whoami /all"});
1053 let event = Event::from_value(&ev);
1054 assert!(eval_detection_item(&compiled, &event));
1055
1056 let ev2 = json!({"CommandLine": "ipconfig"});
1057 let event2 = Event::from_value(&ev2);
1058 assert!(!eval_detection_item(&compiled, &event2));
1059 }
1060
1061 #[test]
1062 fn test_compile_endswith() {
1063 let item = make_item(
1064 "Image",
1065 &[Modifier::EndsWith],
1066 vec![SigmaValue::String(SigmaString::new(".exe"))],
1067 );
1068 let compiled = compile_detection_item(&item).unwrap();
1069
1070 let ev = json!({"Image": "C:\\Windows\\cmd.exe"});
1071 let event = Event::from_value(&ev);
1072 assert!(eval_detection_item(&compiled, &event));
1073
1074 let ev2 = json!({"Image": "C:\\Windows\\cmd.bat"});
1075 let event2 = Event::from_value(&ev2);
1076 assert!(!eval_detection_item(&compiled, &event2));
1077 }
1078
1079 #[test]
1080 fn test_compile_contains_all() {
1081 let item = make_item(
1082 "CommandLine",
1083 &[Modifier::Contains, Modifier::All],
1084 vec![
1085 SigmaValue::String(SigmaString::new("net")),
1086 SigmaValue::String(SigmaString::new("user")),
1087 ],
1088 );
1089 let compiled = compile_detection_item(&item).unwrap();
1090
1091 let ev = json!({"CommandLine": "net user admin"});
1092 let event = Event::from_value(&ev);
1093 assert!(eval_detection_item(&compiled, &event));
1094
1095 let ev2 = json!({"CommandLine": "net localgroup"});
1096 let event2 = Event::from_value(&ev2);
1097 assert!(!eval_detection_item(&compiled, &event2)); }
1099
1100 #[test]
1101 fn test_all_modifier_single_value_rejected() {
1102 let item = make_item(
1103 "CommandLine",
1104 &[Modifier::Contains, Modifier::All],
1105 vec![SigmaValue::String(SigmaString::new("net"))],
1106 );
1107 let result = compile_detection_item(&item);
1108 assert!(result.is_err());
1109 let err = result.unwrap_err().to_string();
1110 assert!(err.contains("|all modifier requires more than one value"));
1111 }
1112
1113 #[test]
1114 fn test_all_modifier_empty_values_rejected() {
1115 let item = make_item("CommandLine", &[Modifier::Contains, Modifier::All], vec![]);
1116 let result = compile_detection_item(&item);
1117 assert!(result.is_err());
1118 }
1119
1120 #[test]
1121 fn test_all_modifier_multiple_values_accepted() {
1122 let item = make_item(
1124 "CommandLine",
1125 &[Modifier::Contains, Modifier::All],
1126 vec![
1127 SigmaValue::String(SigmaString::new("net")),
1128 SigmaValue::String(SigmaString::new("user")),
1129 ],
1130 );
1131 assert!(compile_detection_item(&item).is_ok());
1132 }
1133
1134 #[test]
1135 fn test_compile_regex() {
1136 let item = make_item(
1137 "CommandLine",
1138 &[Modifier::Re],
1139 vec![SigmaValue::String(SigmaString::from_raw(r"cmd\.exe.*/c"))],
1140 );
1141 let compiled = compile_detection_item(&item).unwrap();
1142
1143 let ev = json!({"CommandLine": "cmd.exe /c whoami"});
1144 let event = Event::from_value(&ev);
1145 assert!(eval_detection_item(&compiled, &event));
1146 }
1147
1148 #[test]
1149 fn test_regex_case_sensitive_by_default() {
1150 let item = make_item(
1152 "User",
1153 &[Modifier::Re],
1154 vec![SigmaValue::String(SigmaString::from_raw("Admin"))],
1155 );
1156 let compiled = compile_detection_item(&item).unwrap();
1157
1158 let ev_match = json!({"User": "Admin"});
1159 assert!(eval_detection_item(
1160 &compiled,
1161 &Event::from_value(&ev_match)
1162 ));
1163
1164 let ev_no_match = json!({"User": "admin"});
1165 assert!(!eval_detection_item(
1166 &compiled,
1167 &Event::from_value(&ev_no_match)
1168 ));
1169 }
1170
1171 #[test]
1172 fn test_regex_case_insensitive_with_i_modifier() {
1173 let item = make_item(
1175 "User",
1176 &[Modifier::Re, Modifier::IgnoreCase],
1177 vec![SigmaValue::String(SigmaString::from_raw("Admin"))],
1178 );
1179 let compiled = compile_detection_item(&item).unwrap();
1180
1181 let ev_exact = json!({"User": "Admin"});
1182 assert!(eval_detection_item(
1183 &compiled,
1184 &Event::from_value(&ev_exact)
1185 ));
1186
1187 let ev_lower = json!({"User": "admin"});
1188 assert!(eval_detection_item(
1189 &compiled,
1190 &Event::from_value(&ev_lower)
1191 ));
1192 }
1193
1194 #[test]
1195 fn test_compile_cidr() {
1196 let item = make_item(
1197 "SourceIP",
1198 &[Modifier::Cidr],
1199 vec![SigmaValue::String(SigmaString::new("10.0.0.0/8"))],
1200 );
1201 let compiled = compile_detection_item(&item).unwrap();
1202
1203 let ev = json!({"SourceIP": "10.1.2.3"});
1204 let event = Event::from_value(&ev);
1205 assert!(eval_detection_item(&compiled, &event));
1206
1207 let ev2 = json!({"SourceIP": "192.168.1.1"});
1208 let event2 = Event::from_value(&ev2);
1209 assert!(!eval_detection_item(&compiled, &event2));
1210 }
1211
1212 #[test]
1213 fn test_compile_exists() {
1214 let item = make_item(
1215 "SomeField",
1216 &[Modifier::Exists],
1217 vec![SigmaValue::Bool(true)],
1218 );
1219 let compiled = compile_detection_item(&item).unwrap();
1220
1221 let ev = json!({"SomeField": "value"});
1222 let event = Event::from_value(&ev);
1223 assert!(eval_detection_item(&compiled, &event));
1224
1225 let ev2 = json!({"OtherField": "value"});
1226 let event2 = Event::from_value(&ev2);
1227 assert!(!eval_detection_item(&compiled, &event2));
1228 }
1229
1230 #[test]
1231 fn test_compile_wildcard() {
1232 let item = make_item(
1233 "Image",
1234 &[],
1235 vec![SigmaValue::String(SigmaString::new(r"*\cmd.exe"))],
1236 );
1237 let compiled = compile_detection_item(&item).unwrap();
1238
1239 let ev = json!({"Image": "C:\\Windows\\System32\\cmd.exe"});
1240 let event = Event::from_value(&ev);
1241 assert!(eval_detection_item(&compiled, &event));
1242
1243 let ev2 = json!({"Image": "C:\\Windows\\powershell.exe"});
1244 let event2 = Event::from_value(&ev2);
1245 assert!(!eval_detection_item(&compiled, &event2));
1246 }
1247
1248 #[test]
1249 fn test_compile_numeric_comparison() {
1250 let item = make_item("EventID", &[Modifier::Gte], vec![SigmaValue::Integer(4688)]);
1251 let compiled = compile_detection_item(&item).unwrap();
1252
1253 let ev = json!({"EventID": 4688});
1254 let event = Event::from_value(&ev);
1255 assert!(eval_detection_item(&compiled, &event));
1256
1257 let ev2 = json!({"EventID": 1000});
1258 let event2 = Event::from_value(&ev2);
1259 assert!(!eval_detection_item(&compiled, &event2));
1260 }
1261
1262 #[test]
1263 fn test_windash_expansion() {
1264 let variants = expand_windash("-param -value").unwrap();
1266 assert_eq!(variants.len(), 25);
1267 assert!(variants.contains(&"-param -value".to_string()));
1269 assert!(variants.contains(&"/param -value".to_string()));
1270 assert!(variants.contains(&"-param /value".to_string()));
1271 assert!(variants.contains(&"/param /value".to_string()));
1272 assert!(variants.contains(&"\u{2013}param \u{2013}value".to_string()));
1274 assert!(variants.contains(&"\u{2014}param \u{2014}value".to_string()));
1276 assert!(variants.contains(&"\u{2015}param \u{2015}value".to_string()));
1278 assert!(variants.contains(&"/param \u{2013}value".to_string()));
1280 }
1281
1282 #[test]
1283 fn test_windash_no_dash() {
1284 let variants = expand_windash("nodash").unwrap();
1285 assert_eq!(variants.len(), 1);
1286 assert_eq!(variants[0], "nodash");
1287 }
1288
1289 #[test]
1290 fn test_windash_single_dash() {
1291 let variants = expand_windash("-v").unwrap();
1293 assert_eq!(variants.len(), 5);
1294 assert!(variants.contains(&"-v".to_string()));
1295 assert!(variants.contains(&"/v".to_string()));
1296 assert!(variants.contains(&"\u{2013}v".to_string()));
1297 assert!(variants.contains(&"\u{2014}v".to_string()));
1298 assert!(variants.contains(&"\u{2015}v".to_string()));
1299 }
1300
1301 #[test]
1302 fn test_base64_offset_patterns() {
1303 let patterns = base64_offset_patterns(b"Test");
1304 assert!(!patterns.is_empty());
1305 assert!(
1307 patterns
1308 .iter()
1309 .any(|p| p.contains("VGVzdA") || p.contains("Rlc3"))
1310 );
1311 }
1312
1313 #[test]
1314 fn test_pattern_matches() {
1315 assert!(pattern_matches("selection_*", "selection_main"));
1316 assert!(pattern_matches("selection_*", "selection_"));
1317 assert!(!pattern_matches("selection_*", "filter_main"));
1318 assert!(pattern_matches("*", "anything"));
1319 assert!(pattern_matches("*_filter", "my_filter"));
1320 assert!(pattern_matches("exact", "exact"));
1321 assert!(!pattern_matches("exact", "other"));
1322 }
1323
1324 #[test]
1325 fn test_eval_condition_and() {
1326 let items_sel = vec![make_item(
1327 "CommandLine",
1328 &[Modifier::Contains],
1329 vec![SigmaValue::String(SigmaString::new("whoami"))],
1330 )];
1331 let items_filter = vec![make_item(
1332 "User",
1333 &[],
1334 vec![SigmaValue::String(SigmaString::new("SYSTEM"))],
1335 )];
1336
1337 let mut detections = HashMap::new();
1338 detections.insert(
1339 "selection".into(),
1340 compile_detection(&Detection::AllOf(items_sel)).unwrap(),
1341 );
1342 detections.insert(
1343 "filter".into(),
1344 compile_detection(&Detection::AllOf(items_filter)).unwrap(),
1345 );
1346
1347 let cond = ConditionExpr::And(vec![
1348 ConditionExpr::Identifier("selection".into()),
1349 ConditionExpr::Not(Box::new(ConditionExpr::Identifier("filter".into()))),
1350 ]);
1351
1352 let ev = json!({"CommandLine": "whoami", "User": "admin"});
1353 let event = Event::from_value(&ev);
1354 let mut matched = Vec::new();
1355 assert!(eval_condition(&cond, &detections, &event, &mut matched));
1356
1357 let ev2 = json!({"CommandLine": "whoami", "User": "SYSTEM"});
1358 let event2 = Event::from_value(&ev2);
1359 let mut matched2 = Vec::new();
1360 assert!(!eval_condition(&cond, &detections, &event2, &mut matched2));
1361 }
1362
1363 #[test]
1364 fn test_compile_expand_modifier() {
1365 let items = vec![make_item(
1366 "path",
1367 &[Modifier::Expand],
1368 vec![SigmaValue::String(SigmaString::new(
1369 "C:\\Users\\%username%\\Downloads",
1370 ))],
1371 )];
1372 let detection = compile_detection(&Detection::AllOf(items)).unwrap();
1373
1374 let mut detections = HashMap::new();
1375 detections.insert("selection".into(), detection);
1376
1377 let cond = ConditionExpr::Identifier("selection".into());
1378
1379 let ev = json!({
1381 "path": "C:\\Users\\admin\\Downloads",
1382 "username": "admin"
1383 });
1384 let event = Event::from_value(&ev);
1385 let mut matched = Vec::new();
1386 assert!(eval_condition(&cond, &detections, &event, &mut matched));
1387
1388 let ev2 = json!({
1390 "path": "C:\\Users\\admin\\Downloads",
1391 "username": "guest"
1392 });
1393 let event2 = Event::from_value(&ev2);
1394 let mut matched2 = Vec::new();
1395 assert!(!eval_condition(&cond, &detections, &event2, &mut matched2));
1396 }
1397
1398 #[test]
1399 fn test_compile_timestamp_hour_modifier() {
1400 let items = vec![make_item(
1401 "timestamp",
1402 &[Modifier::Hour],
1403 vec![SigmaValue::Integer(3)],
1404 )];
1405 let detection = compile_detection(&Detection::AllOf(items)).unwrap();
1406
1407 let mut detections = HashMap::new();
1408 detections.insert("selection".into(), detection);
1409
1410 let cond = ConditionExpr::Identifier("selection".into());
1411
1412 let ev = json!({"timestamp": "2024-07-10T03:30:00Z"});
1414 let event = Event::from_value(&ev);
1415 let mut matched = Vec::new();
1416 assert!(eval_condition(&cond, &detections, &event, &mut matched));
1417
1418 let ev2 = json!({"timestamp": "2024-07-10T12:30:00Z"});
1420 let event2 = Event::from_value(&ev2);
1421 let mut matched2 = Vec::new();
1422 assert!(!eval_condition(&cond, &detections, &event2, &mut matched2));
1423 }
1424
1425 #[test]
1426 fn test_compile_timestamp_month_modifier() {
1427 let items = vec![make_item(
1428 "created",
1429 &[Modifier::Month],
1430 vec![SigmaValue::Integer(12)],
1431 )];
1432 let detection = compile_detection(&Detection::AllOf(items)).unwrap();
1433
1434 let mut detections = HashMap::new();
1435 detections.insert("selection".into(), detection);
1436
1437 let cond = ConditionExpr::Identifier("selection".into());
1438
1439 let ev = json!({"created": "2024-12-25T10:00:00Z"});
1441 let event = Event::from_value(&ev);
1442 let mut matched = Vec::new();
1443 assert!(eval_condition(&cond, &detections, &event, &mut matched));
1444
1445 let ev2 = json!({"created": "2024-07-10T10:00:00Z"});
1447 let event2 = Event::from_value(&ev2);
1448 let mut matched2 = Vec::new();
1449 assert!(!eval_condition(&cond, &detections, &event2, &mut matched2));
1450 }
1451
1452 fn make_test_sigma_rule(title: &str, custom_attributes: HashMap<String, String>) -> SigmaRule {
1453 use rsigma_parser::{Detections, LogSource};
1454 SigmaRule {
1455 title: title.to_string(),
1456 id: Some("test-id".to_string()),
1457 name: None,
1458 related: vec![],
1459 taxonomy: None,
1460 status: None,
1461 level: Some(Level::Medium),
1462 description: None,
1463 license: None,
1464 author: None,
1465 references: vec![],
1466 date: None,
1467 modified: None,
1468 tags: vec![],
1469 scope: vec![],
1470 logsource: LogSource {
1471 category: Some("test".to_string()),
1472 product: None,
1473 service: None,
1474 definition: None,
1475 custom: HashMap::new(),
1476 },
1477 detection: Detections {
1478 named: {
1479 let mut m = HashMap::new();
1480 m.insert(
1481 "selection".to_string(),
1482 Detection::AllOf(vec![make_item(
1483 "action",
1484 &[],
1485 vec![SigmaValue::String(SigmaString::new("login"))],
1486 )]),
1487 );
1488 m
1489 },
1490 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
1491 condition_strings: vec!["selection".to_string()],
1492 timeframe: None,
1493 },
1494 fields: vec![],
1495 falsepositives: vec![],
1496 custom_attributes,
1497 }
1498 }
1499
1500 #[test]
1501 fn test_include_event_custom_attribute() {
1502 let mut attrs = HashMap::new();
1503 attrs.insert("rsigma.include_event".to_string(), "true".to_string());
1504 let rule = make_test_sigma_rule("Include Event Test", attrs);
1505
1506 let compiled = compile_rule(&rule).unwrap();
1507 assert!(compiled.include_event);
1508
1509 let ev = json!({"action": "login", "user": "alice"});
1510 let event = Event::from_value(&ev);
1511 let result = evaluate_rule(&compiled, &event).unwrap();
1512 assert!(result.event.is_some());
1513 assert_eq!(result.event.unwrap(), ev);
1514 }
1515
1516 #[test]
1517 fn test_no_include_event_by_default() {
1518 let rule = make_test_sigma_rule("No Include Event Test", HashMap::new());
1519
1520 let compiled = compile_rule(&rule).unwrap();
1521 assert!(!compiled.include_event);
1522
1523 let ev = json!({"action": "login", "user": "alice"});
1524 let event = Event::from_value(&ev);
1525 let result = evaluate_rule(&compiled, &event).unwrap();
1526 assert!(result.event.is_none());
1527 }
1528}
1529
1530#[cfg(test)]
1535mod proptests {
1536 use super::*;
1537 use proptest::prelude::*;
1538
1539 proptest! {
1543 #[test]
1544 fn windash_count_is_5_pow_n(
1545 prefix in "[a-z]{0,5}",
1547 dashes in prop::collection::vec(Just('-'), 0..=3),
1548 suffix in "[a-z]{0,5}",
1549 ) {
1550 let mut input = prefix;
1551 for d in &dashes {
1552 input.push(*d);
1553 }
1554 input.push_str(&suffix);
1555
1556 let n = input.chars().filter(|c| *c == '-').count();
1557 let variants = expand_windash(&input).unwrap();
1558 let expected = 5usize.pow(n as u32);
1559 prop_assert_eq!(variants.len(), expected,
1560 "expand_windash({:?}) should produce {} variants, got {}",
1561 input, expected, variants.len());
1562 }
1563 }
1564
1565 proptest! {
1569 #[test]
1570 fn windash_no_duplicates(
1571 prefix in "[a-z]{0,4}",
1572 dashes in prop::collection::vec(Just('-'), 0..=2),
1573 suffix in "[a-z]{0,4}",
1574 ) {
1575 let mut input = prefix;
1576 for d in &dashes {
1577 input.push(*d);
1578 }
1579 input.push_str(&suffix);
1580
1581 let variants = expand_windash(&input).unwrap();
1582 let unique: std::collections::HashSet<&String> = variants.iter().collect();
1583 prop_assert_eq!(variants.len(), unique.len(),
1584 "expand_windash({:?}) produced duplicates", input);
1585 }
1586 }
1587
1588 proptest! {
1592 #[test]
1593 fn windash_contains_original(
1594 prefix in "[a-z]{0,5}",
1595 dashes in prop::collection::vec(Just('-'), 0..=3),
1596 suffix in "[a-z]{0,5}",
1597 ) {
1598 let mut input = prefix;
1599 for d in &dashes {
1600 input.push(*d);
1601 }
1602 input.push_str(&suffix);
1603
1604 let variants = expand_windash(&input).unwrap();
1605 prop_assert!(variants.contains(&input),
1606 "expand_windash({:?}) should contain the original", input);
1607 }
1608 }
1609
1610 proptest! {
1615 #[test]
1616 fn windash_variants_preserve_non_dash_chars(
1617 prefix in "[a-z]{1,5}",
1618 suffix in "[a-z]{1,5}",
1619 ) {
1620 let input = format!("{prefix}-{suffix}");
1621 let variants = expand_windash(&input).unwrap();
1622 for variant in &variants {
1623 prop_assert!(variant.starts_with(&prefix),
1625 "variant {:?} should start with {:?}", variant, prefix);
1626 prop_assert!(variant.ends_with(&suffix),
1627 "variant {:?} should end with {:?}", variant, suffix);
1628 }
1629 }
1630 }
1631
1632 proptest! {
1636 #[test]
1637 fn windash_no_dashes_passthrough(text in "[a-zA-Z0-9]{1,20}") {
1638 prop_assume!(!text.contains('-'));
1639 let variants = expand_windash(&text).unwrap();
1640 prop_assert_eq!(variants.len(), 1);
1641 prop_assert_eq!(&variants[0], &text);
1642 }
1643 }
1644}