1use rsigma_parser::{ConditionExpr, FilterRule, LogSource, SigmaCollection, SigmaRule};
8
9use crate::compiler::{CompiledRule, compile_detection, compile_rule, evaluate_rule};
10use crate::error::Result;
11use crate::event::Event;
12use crate::pipeline::{Pipeline, apply_pipelines};
13use crate::result::MatchResult;
14
15pub struct Engine {
50 rules: Vec<CompiledRule>,
51 pipelines: Vec<Pipeline>,
52 include_event: bool,
55 filter_counter: usize,
58}
59
60impl Engine {
61 pub fn new() -> Self {
63 Engine {
64 rules: Vec::new(),
65 pipelines: Vec::new(),
66 include_event: false,
67 filter_counter: 0,
68 }
69 }
70
71 pub fn new_with_pipeline(pipeline: Pipeline) -> Self {
73 Engine {
74 rules: Vec::new(),
75 pipelines: vec![pipeline],
76 include_event: false,
77 filter_counter: 0,
78 }
79 }
80
81 pub fn set_include_event(&mut self, include: bool) {
84 self.include_event = include;
85 }
86
87 pub fn add_pipeline(&mut self, pipeline: Pipeline) {
92 self.pipelines.push(pipeline);
93 self.pipelines.sort_by_key(|p| p.priority);
94 }
95
96 pub fn add_rule(&mut self, rule: &SigmaRule) -> Result<()> {
100 let compiled = if self.pipelines.is_empty() {
101 compile_rule(rule)?
102 } else {
103 let mut transformed = rule.clone();
104 apply_pipelines(&self.pipelines, &mut transformed)?;
105 compile_rule(&transformed)?
106 };
107 self.rules.push(compiled);
108 Ok(())
109 }
110
111 pub fn add_collection(&mut self, collection: &SigmaCollection) -> Result<()> {
116 for rule in &collection.rules {
117 self.add_rule(rule)?;
118 }
119 for filter in &collection.filters {
121 self.apply_filter(filter)?;
122 }
123 Ok(())
124 }
125
126 pub fn add_collection_with_pipelines(
131 &mut self,
132 collection: &SigmaCollection,
133 pipelines: &[Pipeline],
134 ) -> Result<()> {
135 let prev = std::mem::take(&mut self.pipelines);
136 self.pipelines = pipelines.to_vec();
137 self.pipelines.sort_by_key(|p| p.priority);
138 let result = self.add_collection(collection);
139 self.pipelines = prev;
140 result
141 }
142
143 pub fn apply_filter(&mut self, filter: &FilterRule) -> Result<()> {
148 let mut filter_detections = Vec::new();
150 for (name, detection) in &filter.detection.named {
151 let compiled = compile_detection(detection)?;
152 filter_detections.push((name.clone(), compiled));
153 }
154
155 if filter_detections.is_empty() {
156 return Ok(());
157 }
158
159 let fc = self.filter_counter;
160 self.filter_counter += 1;
161
162 let filter_cond = if filter_detections.len() == 1 {
166 ConditionExpr::Identifier(format!("__filter_{fc}_{}", filter_detections[0].0))
167 } else {
168 ConditionExpr::And(
169 filter_detections
170 .iter()
171 .map(|(name, _)| ConditionExpr::Identifier(format!("__filter_{fc}_{name}")))
172 .collect(),
173 )
174 };
175
176 let mut matched_any = false;
178 for rule in &mut self.rules {
179 let rule_matches = filter.rules.is_empty() || filter.rules.iter().any(|r| {
181 rule.id.as_deref() == Some(r.as_str())
182 || rule.title == *r
183 });
184
185 if rule_matches {
187 if let Some(ref filter_ls) = filter.logsource
188 && !logsource_compatible(&rule.logsource, filter_ls)
189 {
190 continue;
191 }
192
193 for (name, compiled) in &filter_detections {
195 rule.detections
196 .insert(format!("__filter_{fc}_{name}"), compiled.clone());
197 }
198
199 rule.conditions = rule
201 .conditions
202 .iter()
203 .map(|cond| {
204 ConditionExpr::And(vec![
205 cond.clone(),
206 ConditionExpr::Not(Box::new(filter_cond.clone())),
207 ])
208 })
209 .collect();
210 matched_any = true;
211 }
212 }
213
214 if !filter.rules.is_empty() && !matched_any {
215 log::warn!(
216 "filter '{}' references rules {:?} but none matched any loaded rule",
217 filter.title,
218 filter.rules
219 );
220 }
221
222 Ok(())
223 }
224
225 pub fn add_compiled_rule(&mut self, rule: CompiledRule) {
227 self.rules.push(rule);
228 }
229
230 pub fn evaluate(&self, event: &Event) -> Vec<MatchResult> {
232 let mut results = Vec::new();
233 for rule in &self.rules {
234 if let Some(mut m) = evaluate_rule(rule, event) {
235 if self.include_event && m.event.is_none() {
236 m.event = Some(event.as_value().clone());
237 }
238 results.push(m);
239 }
240 }
241 results
242 }
243
244 pub fn evaluate_with_logsource(
251 &self,
252 event: &Event,
253 event_logsource: &LogSource,
254 ) -> Vec<MatchResult> {
255 let mut results = Vec::new();
256 for rule in &self.rules {
257 if logsource_matches(&rule.logsource, event_logsource)
258 && let Some(mut m) = evaluate_rule(rule, event)
259 {
260 if self.include_event && m.event.is_none() {
261 m.event = Some(event.as_value().clone());
262 }
263 results.push(m);
264 }
265 }
266 results
267 }
268
269 pub fn rule_count(&self) -> usize {
271 self.rules.len()
272 }
273
274 pub fn rules(&self) -> &[CompiledRule] {
276 &self.rules
277 }
278}
279
280impl Default for Engine {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286fn logsource_compatible(a: &LogSource, b: &LogSource) -> bool {
296 fn field_compatible(a: &Option<String>, b: &Option<String>) -> bool {
297 match (a, b) {
298 (Some(va), Some(vb)) => va.eq_ignore_ascii_case(vb),
299 _ => true, }
301 }
302
303 field_compatible(&a.category, &b.category)
304 && field_compatible(&a.product, &b.product)
305 && field_compatible(&a.service, &b.service)
306}
307
308fn logsource_matches(rule_ls: &LogSource, event_ls: &LogSource) -> bool {
311 if let Some(ref cat) = rule_ls.category {
312 match &event_ls.category {
313 Some(ec) if ec.eq_ignore_ascii_case(cat) => {}
314 _ => return false,
315 }
316 }
317 if let Some(ref prod) = rule_ls.product {
318 match &event_ls.product {
319 Some(ep) if ep.eq_ignore_ascii_case(prod) => {}
320 _ => return false,
321 }
322 }
323 if let Some(ref svc) = rule_ls.service {
324 match &event_ls.service {
325 Some(es) if es.eq_ignore_ascii_case(svc) => {}
326 _ => return false,
327 }
328 }
329 true
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use rsigma_parser::parse_sigma_yaml;
336 use serde_json::json;
337
338 fn make_engine_with_rule(yaml: &str) -> Engine {
339 let collection = parse_sigma_yaml(yaml).unwrap();
340 let mut engine = Engine::new();
341 engine.add_collection(&collection).unwrap();
342 engine
343 }
344
345 #[test]
346 fn test_simple_match() {
347 let engine = make_engine_with_rule(
348 r#"
349title: Detect Whoami
350logsource:
351 product: windows
352 category: process_creation
353detection:
354 selection:
355 CommandLine|contains: 'whoami'
356 condition: selection
357level: medium
358"#,
359 );
360
361 let ev = json!({"CommandLine": "cmd /c whoami /all"});
362 let event = Event::from_value(&ev);
363 let matches = engine.evaluate(&event);
364 assert_eq!(matches.len(), 1);
365 assert_eq!(matches[0].rule_title, "Detect Whoami");
366 }
367
368 #[test]
369 fn test_no_match() {
370 let engine = make_engine_with_rule(
371 r#"
372title: Detect Whoami
373logsource:
374 product: windows
375 category: process_creation
376detection:
377 selection:
378 CommandLine|contains: 'whoami'
379 condition: selection
380level: medium
381"#,
382 );
383
384 let ev = json!({"CommandLine": "ipconfig /all"});
385 let event = Event::from_value(&ev);
386 let matches = engine.evaluate(&event);
387 assert!(matches.is_empty());
388 }
389
390 #[test]
391 fn test_and_not_filter() {
392 let engine = make_engine_with_rule(
393 r#"
394title: Suspicious Process
395logsource:
396 product: windows
397detection:
398 selection:
399 CommandLine|contains: 'whoami'
400 filter:
401 User: 'SYSTEM'
402 condition: selection and not filter
403level: high
404"#,
405 );
406
407 let ev = json!({"CommandLine": "whoami", "User": "admin"});
409 let event = Event::from_value(&ev);
410 assert_eq!(engine.evaluate(&event).len(), 1);
411
412 let ev2 = json!({"CommandLine": "whoami", "User": "SYSTEM"});
414 let event2 = Event::from_value(&ev2);
415 assert!(engine.evaluate(&event2).is_empty());
416 }
417
418 #[test]
419 fn test_multiple_values_or() {
420 let engine = make_engine_with_rule(
421 r#"
422title: Recon Commands
423logsource:
424 product: windows
425detection:
426 selection:
427 CommandLine|contains:
428 - 'whoami'
429 - 'ipconfig'
430 - 'net user'
431 condition: selection
432level: medium
433"#,
434 );
435
436 let ev = json!({"CommandLine": "ipconfig /all"});
437 let event = Event::from_value(&ev);
438 assert_eq!(engine.evaluate(&event).len(), 1);
439
440 let ev2 = json!({"CommandLine": "dir"});
441 let event2 = Event::from_value(&ev2);
442 assert!(engine.evaluate(&event2).is_empty());
443 }
444
445 #[test]
446 fn test_logsource_routing() {
447 let engine = make_engine_with_rule(
448 r#"
449title: Windows Process
450logsource:
451 product: windows
452 category: process_creation
453detection:
454 selection:
455 CommandLine|contains: 'whoami'
456 condition: selection
457level: medium
458"#,
459 );
460
461 let ev = json!({"CommandLine": "whoami"});
462 let event = Event::from_value(&ev);
463
464 let ls_match = LogSource {
466 product: Some("windows".into()),
467 category: Some("process_creation".into()),
468 ..Default::default()
469 };
470 assert_eq!(engine.evaluate_with_logsource(&event, &ls_match).len(), 1);
471
472 let ls_nomatch = LogSource {
474 product: Some("linux".into()),
475 category: Some("process_creation".into()),
476 ..Default::default()
477 };
478 assert!(
479 engine
480 .evaluate_with_logsource(&event, &ls_nomatch)
481 .is_empty()
482 );
483 }
484
485 #[test]
486 fn test_selector_1_of() {
487 let engine = make_engine_with_rule(
488 r#"
489title: Multiple Selections
490logsource:
491 product: windows
492detection:
493 selection_cmd:
494 CommandLine|contains: 'cmd'
495 selection_ps:
496 CommandLine|contains: 'powershell'
497 condition: 1 of selection_*
498level: medium
499"#,
500 );
501
502 let ev = json!({"CommandLine": "powershell.exe -enc"});
503 let event = Event::from_value(&ev);
504 assert_eq!(engine.evaluate(&event).len(), 1);
505 }
506
507 #[test]
508 fn test_filter_rule_application() {
509 let yaml = r#"
511title: Suspicious Process
512id: rule-001
513logsource:
514 product: windows
515 category: process_creation
516detection:
517 selection:
518 CommandLine|contains: 'whoami'
519 condition: selection
520level: high
521---
522title: Filter SYSTEM
523filter:
524 rules:
525 - rule-001
526 selection:
527 User: 'SYSTEM'
528 condition: selection
529"#;
530 let collection = parse_sigma_yaml(yaml).unwrap();
531 assert_eq!(collection.rules.len(), 1);
532 assert_eq!(collection.filters.len(), 1);
533
534 let mut engine = Engine::new();
535 engine.add_collection(&collection).unwrap();
536
537 let ev = json!({"CommandLine": "whoami", "User": "admin"});
539 let event = Event::from_value(&ev);
540 assert_eq!(engine.evaluate(&event).len(), 1);
541
542 let ev2 = json!({"CommandLine": "whoami", "User": "SYSTEM"});
544 let event2 = Event::from_value(&ev2);
545 assert!(engine.evaluate(&event2).is_empty());
546 }
547
548 #[test]
549 fn test_filter_rule_no_ref_applies_to_all() {
550 let yaml = r#"
552title: Detection A
553id: det-a
554logsource:
555 product: windows
556detection:
557 sel:
558 EventType: alert
559 condition: sel
560---
561title: Filter Out Test Env
562filter:
563 rules: []
564 selection:
565 Environment: 'test'
566 condition: selection
567"#;
568 let collection = parse_sigma_yaml(yaml).unwrap();
569 let mut engine = Engine::new();
570 engine.add_collection(&collection).unwrap();
571
572 let ev = json!({"EventType": "alert", "Environment": "prod"});
573 let event = Event::from_value(&ev);
574 assert_eq!(engine.evaluate(&event).len(), 1);
575
576 let ev2 = json!({"EventType": "alert", "Environment": "test"});
577 let event2 = Event::from_value(&ev2);
578 assert!(engine.evaluate(&event2).is_empty());
579 }
580
581 #[test]
582 fn test_multiple_rules() {
583 let yaml = r#"
584title: Rule A
585logsource:
586 product: windows
587detection:
588 selection:
589 CommandLine|contains: 'whoami'
590 condition: selection
591level: low
592---
593title: Rule B
594logsource:
595 product: windows
596detection:
597 selection:
598 CommandLine|contains: 'ipconfig'
599 condition: selection
600level: low
601"#;
602 let collection = parse_sigma_yaml(yaml).unwrap();
603 let mut engine = Engine::new();
604 engine.add_collection(&collection).unwrap();
605 assert_eq!(engine.rule_count(), 2);
606
607 let ev = json!({"CommandLine": "whoami"});
609 let event = Event::from_value(&ev);
610 let matches = engine.evaluate(&event);
611 assert_eq!(matches.len(), 1);
612 assert_eq!(matches[0].rule_title, "Rule A");
613 }
614
615 #[test]
620 fn test_filter_by_rule_name() {
621 let yaml = r#"
623title: Detect Mimikatz
624logsource:
625 product: windows
626detection:
627 selection:
628 CommandLine|contains: 'mimikatz'
629 condition: selection
630level: critical
631---
632title: Exclude Admin Tools
633filter:
634 rules:
635 - Detect Mimikatz
636 selection:
637 ParentImage|endswith: '\admin_toolkit.exe'
638 condition: selection
639"#;
640 let collection = parse_sigma_yaml(yaml).unwrap();
641 let mut engine = Engine::new();
642 engine.add_collection(&collection).unwrap();
643
644 let ev = json!({"CommandLine": "mimikatz.exe", "ParentImage": "C:\\cmd.exe"});
646 let event = Event::from_value(&ev);
647 assert_eq!(engine.evaluate(&event).len(), 1);
648
649 let ev2 = json!({"CommandLine": "mimikatz.exe", "ParentImage": "C:\\admin_toolkit.exe"});
651 let event2 = Event::from_value(&ev2);
652 assert!(engine.evaluate(&event2).is_empty());
653 }
654
655 #[test]
656 fn test_filter_multiple_detections() {
657 let yaml = r#"
659title: Suspicious Network
660id: net-001
661logsource:
662 product: windows
663detection:
664 selection:
665 DestinationPort: 443
666 condition: selection
667level: medium
668---
669title: Exclude Trusted
670filter:
671 rules:
672 - net-001
673 trusted_dst:
674 DestinationIp|startswith: '10.'
675 trusted_user:
676 User: 'svc_account'
677 condition: trusted_dst and trusted_user
678"#;
679 let collection = parse_sigma_yaml(yaml).unwrap();
680 let mut engine = Engine::new();
681 engine.add_collection(&collection).unwrap();
682
683 let ev = json!({"DestinationPort": 443, "DestinationIp": "8.8.8.8", "User": "admin"});
685 let event = Event::from_value(&ev);
686 assert_eq!(engine.evaluate(&event).len(), 1);
687
688 let ev2 = json!({"DestinationPort": 443, "DestinationIp": "10.0.0.1", "User": "admin"});
690 let event2 = Event::from_value(&ev2);
691 assert_eq!(engine.evaluate(&event2).len(), 1);
692
693 let ev3 =
695 json!({"DestinationPort": 443, "DestinationIp": "10.0.0.1", "User": "svc_account"});
696 let event3 = Event::from_value(&ev3);
697 assert!(engine.evaluate(&event3).is_empty());
698 }
699
700 #[test]
701 fn test_filter_applied_to_multiple_rules() {
702 let yaml = r#"
704title: Rule One
705id: r1
706logsource:
707 product: windows
708detection:
709 sel:
710 EventID: 1
711 condition: sel
712---
713title: Rule Two
714id: r2
715logsource:
716 product: windows
717detection:
718 sel:
719 EventID: 2
720 condition: sel
721---
722title: Exclude Test
723filter:
724 rules: []
725 selection:
726 Environment: 'test'
727 condition: selection
728"#;
729 let collection = parse_sigma_yaml(yaml).unwrap();
730 let mut engine = Engine::new();
731 engine.add_collection(&collection).unwrap();
732
733 let ev1 = json!({"EventID": 1, "Environment": "prod"});
735 assert_eq!(engine.evaluate(&Event::from_value(&ev1)).len(), 1);
736 let ev2 = json!({"EventID": 2, "Environment": "prod"});
737 assert_eq!(engine.evaluate(&Event::from_value(&ev2)).len(), 1);
738
739 let ev3 = json!({"EventID": 1, "Environment": "test"});
741 assert!(engine.evaluate(&Event::from_value(&ev3)).is_empty());
742 let ev4 = json!({"EventID": 2, "Environment": "test"});
743 assert!(engine.evaluate(&Event::from_value(&ev4)).is_empty());
744 }
745
746 #[test]
751 fn test_expand_modifier_yaml() {
752 let yaml = r#"
753title: User Profile Access
754logsource:
755 product: windows
756detection:
757 selection:
758 TargetFilename|expand: 'C:\Users\%username%\AppData\sensitive.dat'
759 condition: selection
760level: high
761"#;
762 let engine = make_engine_with_rule(yaml);
763
764 let ev = json!({
766 "TargetFilename": "C:\\Users\\admin\\AppData\\sensitive.dat",
767 "username": "admin"
768 });
769 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
770
771 let ev2 = json!({
773 "TargetFilename": "C:\\Users\\admin\\AppData\\sensitive.dat",
774 "username": "guest"
775 });
776 assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
777 }
778
779 #[test]
780 fn test_expand_modifier_multiple_placeholders() {
781 let yaml = r#"
782title: Registry Path
783logsource:
784 product: windows
785detection:
786 selection:
787 RegistryKey|expand: 'HKLM\SOFTWARE\%vendor%\%product%'
788 condition: selection
789level: medium
790"#;
791 let engine = make_engine_with_rule(yaml);
792
793 let ev = json!({
794 "RegistryKey": "HKLM\\SOFTWARE\\Acme\\Widget",
795 "vendor": "Acme",
796 "product": "Widget"
797 });
798 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
799
800 let ev2 = json!({
801 "RegistryKey": "HKLM\\SOFTWARE\\Acme\\Widget",
802 "vendor": "Other",
803 "product": "Widget"
804 });
805 assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
806 }
807
808 #[test]
813 fn test_timestamp_hour_modifier_yaml() {
814 let yaml = r#"
815title: Off-Hours Login
816logsource:
817 product: windows
818detection:
819 selection:
820 EventType: 'login'
821 time_filter:
822 Timestamp|hour: 3
823 condition: selection and time_filter
824level: high
825"#;
826 let engine = make_engine_with_rule(yaml);
827
828 let ev = json!({"EventType": "login", "Timestamp": "2024-07-10T03:45:00Z"});
830 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
831
832 let ev2 = json!({"EventType": "login", "Timestamp": "2024-07-10T14:45:00Z"});
834 assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
835 }
836
837 #[test]
838 fn test_timestamp_day_modifier_yaml() {
839 let yaml = r#"
840title: Weekend Activity
841logsource:
842 product: windows
843detection:
844 selection:
845 EventType: 'access'
846 day_check:
847 CreatedAt|day: 25
848 condition: selection and day_check
849level: medium
850"#;
851 let engine = make_engine_with_rule(yaml);
852
853 let ev = json!({"EventType": "access", "CreatedAt": "2024-12-25T10:00:00Z"});
854 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
855
856 let ev2 = json!({"EventType": "access", "CreatedAt": "2024-12-26T10:00:00Z"});
857 assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
858 }
859
860 #[test]
861 fn test_timestamp_year_modifier_yaml() {
862 let yaml = r#"
863title: Legacy System
864logsource:
865 product: windows
866detection:
867 selection:
868 EventType: 'auth'
869 old_events:
870 EventTime|year: 2020
871 condition: selection and old_events
872level: low
873"#;
874 let engine = make_engine_with_rule(yaml);
875
876 let ev = json!({"EventType": "auth", "EventTime": "2020-06-15T10:00:00Z"});
877 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
878
879 let ev2 = json!({"EventType": "auth", "EventTime": "2024-06-15T10:00:00Z"});
880 assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
881 }
882
883 #[test]
888 fn test_action_repeat_evaluates_correctly() {
889 let yaml = r#"
891title: Detect Whoami
892logsource:
893 product: windows
894 category: process_creation
895detection:
896 selection:
897 CommandLine|contains: 'whoami'
898 condition: selection
899level: medium
900---
901action: repeat
902title: Detect Ipconfig
903detection:
904 selection:
905 CommandLine|contains: 'ipconfig'
906 condition: selection
907"#;
908 let collection = parse_sigma_yaml(yaml).unwrap();
909 assert_eq!(collection.rules.len(), 2);
910
911 let mut engine = Engine::new();
912 engine.add_collection(&collection).unwrap();
913 assert_eq!(engine.rule_count(), 2);
914
915 let ev1 = json!({"CommandLine": "whoami /all"});
917 let matches1 = engine.evaluate(&Event::from_value(&ev1));
918 assert_eq!(matches1.len(), 1);
919 assert_eq!(matches1[0].rule_title, "Detect Whoami");
920
921 let ev2 = json!({"CommandLine": "ipconfig /all"});
923 let matches2 = engine.evaluate(&Event::from_value(&ev2));
924 assert_eq!(matches2.len(), 1);
925 assert_eq!(matches2[0].rule_title, "Detect Ipconfig");
926
927 let ev3 = json!({"CommandLine": "dir"});
929 assert!(engine.evaluate(&Event::from_value(&ev3)).is_empty());
930 }
931
932 #[test]
933 fn test_action_repeat_with_global() {
934 let yaml = r#"
937action: global
938logsource:
939 product: windows
940 category: process_creation
941level: high
942---
943title: Detect Net User
944detection:
945 selection:
946 CommandLine|contains: 'net user'
947 condition: selection
948---
949action: repeat
950title: Detect Net Group
951detection:
952 selection:
953 CommandLine|contains: 'net group'
954 condition: selection
955"#;
956 let collection = parse_sigma_yaml(yaml).unwrap();
957 assert_eq!(collection.rules.len(), 2);
958
959 let mut engine = Engine::new();
960 engine.add_collection(&collection).unwrap();
961
962 let ev1 = json!({"CommandLine": "net user admin"});
963 let m1 = engine.evaluate(&Event::from_value(&ev1));
964 assert_eq!(m1.len(), 1);
965 assert_eq!(m1[0].rule_title, "Detect Net User");
966
967 let ev2 = json!({"CommandLine": "net group admins"});
968 let m2 = engine.evaluate(&Event::from_value(&ev2));
969 assert_eq!(m2.len(), 1);
970 assert_eq!(m2[0].rule_title, "Detect Net Group");
971 }
972
973 #[test]
978 fn test_neq_modifier_yaml() {
979 let yaml = r#"
980title: Non-Standard Port
981logsource:
982 product: windows
983detection:
984 selection:
985 Protocol: TCP
986 filter:
987 DestinationPort|neq: 443
988 condition: selection and filter
989level: medium
990"#;
991 let engine = make_engine_with_rule(yaml);
992
993 let ev = json!({"Protocol": "TCP", "DestinationPort": "80"});
995 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
996
997 let ev2 = json!({"Protocol": "TCP", "DestinationPort": "443"});
999 assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
1000 }
1001
1002 #[test]
1003 fn test_neq_modifier_integer() {
1004 let yaml = r#"
1005title: Non-Standard Port Numeric
1006logsource:
1007 product: windows
1008detection:
1009 selection:
1010 DestinationPort|neq: 443
1011 condition: selection
1012level: medium
1013"#;
1014 let engine = make_engine_with_rule(yaml);
1015
1016 let ev = json!({"DestinationPort": 80});
1017 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1018
1019 let ev2 = json!({"DestinationPort": 443});
1020 assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
1021 }
1022
1023 #[test]
1028 fn test_selector_them_excludes_underscore() {
1029 let yaml = r#"
1031title: Underscore Test
1032logsource:
1033 product: windows
1034detection:
1035 selection:
1036 CommandLine|contains: 'whoami'
1037 _helper:
1038 User: 'SYSTEM'
1039 condition: all of them
1040level: medium
1041"#;
1042 let engine = make_engine_with_rule(yaml);
1043
1044 let ev = json!({"CommandLine": "whoami", "User": "admin"});
1046 assert_eq!(
1047 engine.evaluate(&Event::from_value(&ev)).len(),
1048 1,
1049 "all of them should exclude _helper, so only selection is required"
1050 );
1051 }
1052
1053 #[test]
1054 fn test_selector_them_includes_non_underscore() {
1055 let yaml = r#"
1056title: Multiple Selections
1057logsource:
1058 product: windows
1059detection:
1060 sel_cmd:
1061 CommandLine|contains: 'cmd'
1062 sel_ps:
1063 CommandLine|contains: 'powershell'
1064 _private:
1065 User: 'admin'
1066 condition: 1 of them
1067level: medium
1068"#;
1069 let engine = make_engine_with_rule(yaml);
1070
1071 let ev = json!({"CommandLine": "cmd.exe", "User": "guest"});
1073 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1074
1075 let ev2 = json!({"CommandLine": "notepad", "User": "admin"});
1077 assert!(
1078 engine.evaluate(&Event::from_value(&ev2)).is_empty(),
1079 "_private should be excluded from 'them'"
1080 );
1081 }
1082
1083 #[test]
1088 fn test_utf16le_modifier_yaml() {
1089 let yaml = r#"
1091title: Wide String
1092logsource:
1093 product: windows
1094detection:
1095 selection:
1096 Payload|wide|base64: 'Test'
1097 condition: selection
1098level: medium
1099"#;
1100 let engine = make_engine_with_rule(yaml);
1101
1102 let ev = json!({"Payload": "VABlAHMAdAA="});
1106 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1107 }
1108
1109 #[test]
1110 fn test_utf16be_modifier_yaml() {
1111 let yaml = r#"
1112title: UTF16BE String
1113logsource:
1114 product: windows
1115detection:
1116 selection:
1117 Payload|utf16be|base64: 'AB'
1118 condition: selection
1119level: medium
1120"#;
1121 let engine = make_engine_with_rule(yaml);
1122
1123 let ev = json!({"Payload": "AEEAQg=="});
1126 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1127 }
1128
1129 #[test]
1130 fn test_utf16_bom_modifier_yaml() {
1131 let yaml = r#"
1132title: UTF16 BOM String
1133logsource:
1134 product: windows
1135detection:
1136 selection:
1137 Payload|utf16|base64: 'A'
1138 condition: selection
1139level: medium
1140"#;
1141 let engine = make_engine_with_rule(yaml);
1142
1143 let ev = json!({"Payload": "//5BAA=="});
1146 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1147 }
1148
1149 #[test]
1154 fn test_pipeline_field_mapping_e2e() {
1155 use crate::pipeline::parse_pipeline;
1156
1157 let pipeline_yaml = r#"
1158name: Sysmon to ECS
1159transformations:
1160 - type: field_name_mapping
1161 mapping:
1162 CommandLine: process.command_line
1163 rule_conditions:
1164 - type: logsource
1165 product: windows
1166"#;
1167 let pipeline = parse_pipeline(pipeline_yaml).unwrap();
1168
1169 let rule_yaml = r#"
1170title: Detect Whoami
1171logsource:
1172 product: windows
1173 category: process_creation
1174detection:
1175 selection:
1176 CommandLine|contains: 'whoami'
1177 condition: selection
1178level: medium
1179"#;
1180 let collection = parse_sigma_yaml(rule_yaml).unwrap();
1181
1182 let mut engine = Engine::new_with_pipeline(pipeline);
1183 engine.add_collection(&collection).unwrap();
1184
1185 let ev = json!({"process.command_line": "cmd /c whoami"});
1191 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1192
1193 let ev2 = json!({"CommandLine": "cmd /c whoami"});
1195 assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
1196 }
1197
1198 #[test]
1199 fn test_pipeline_add_condition_e2e() {
1200 use crate::pipeline::parse_pipeline;
1201
1202 let pipeline_yaml = r#"
1203name: Add index condition
1204transformations:
1205 - type: add_condition
1206 conditions:
1207 source: windows
1208 rule_conditions:
1209 - type: logsource
1210 product: windows
1211"#;
1212 let pipeline = parse_pipeline(pipeline_yaml).unwrap();
1213
1214 let rule_yaml = r#"
1215title: Detect Cmd
1216logsource:
1217 product: windows
1218detection:
1219 selection:
1220 CommandLine|contains: 'cmd'
1221 condition: selection
1222level: low
1223"#;
1224 let collection = parse_sigma_yaml(rule_yaml).unwrap();
1225
1226 let mut engine = Engine::new_with_pipeline(pipeline);
1227 engine.add_collection(&collection).unwrap();
1228
1229 let ev = json!({"CommandLine": "cmd.exe", "source": "windows"});
1231 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1232
1233 let ev2 = json!({"CommandLine": "cmd.exe"});
1235 assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
1236 }
1237
1238 #[test]
1239 fn test_pipeline_change_logsource_e2e() {
1240 use crate::pipeline::parse_pipeline;
1241
1242 let pipeline_yaml = r#"
1243name: Change logsource
1244transformations:
1245 - type: change_logsource
1246 product: elastic
1247 category: endpoint
1248 rule_conditions:
1249 - type: logsource
1250 product: windows
1251"#;
1252 let pipeline = parse_pipeline(pipeline_yaml).unwrap();
1253
1254 let rule_yaml = r#"
1255title: Test Rule
1256logsource:
1257 product: windows
1258 category: process_creation
1259detection:
1260 selection:
1261 action: test
1262 condition: selection
1263level: low
1264"#;
1265 let collection = parse_sigma_yaml(rule_yaml).unwrap();
1266
1267 let mut engine = Engine::new_with_pipeline(pipeline);
1268 engine.add_collection(&collection).unwrap();
1269
1270 let ev = json!({"action": "test"});
1272 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1273
1274 let ls = LogSource {
1276 product: Some("windows".to_string()),
1277 category: Some("process_creation".to_string()),
1278 ..Default::default()
1279 };
1280 assert!(
1281 engine
1282 .evaluate_with_logsource(&Event::from_value(&ev), &ls)
1283 .is_empty(),
1284 "logsource was changed; windows/process_creation should not match"
1285 );
1286
1287 let ls2 = LogSource {
1288 product: Some("elastic".to_string()),
1289 category: Some("endpoint".to_string()),
1290 ..Default::default()
1291 };
1292 assert_eq!(
1293 engine
1294 .evaluate_with_logsource(&Event::from_value(&ev), &ls2)
1295 .len(),
1296 1,
1297 "elastic/endpoint should match the transformed logsource"
1298 );
1299 }
1300
1301 #[test]
1302 fn test_pipeline_replace_string_e2e() {
1303 use crate::pipeline::parse_pipeline;
1304
1305 let pipeline_yaml = r#"
1306name: Replace backslash
1307transformations:
1308 - type: replace_string
1309 regex: "\\\\"
1310 replacement: "/"
1311"#;
1312 let pipeline = parse_pipeline(pipeline_yaml).unwrap();
1313
1314 let rule_yaml = r#"
1315title: Path Detection
1316logsource:
1317 product: windows
1318detection:
1319 selection:
1320 FilePath|contains: 'C:\Windows'
1321 condition: selection
1322level: low
1323"#;
1324 let collection = parse_sigma_yaml(rule_yaml).unwrap();
1325
1326 let mut engine = Engine::new_with_pipeline(pipeline);
1327 engine.add_collection(&collection).unwrap();
1328
1329 let ev = json!({"FilePath": "C:/Windows/System32/cmd.exe"});
1331 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1332 }
1333
1334 #[test]
1335 fn test_pipeline_skips_non_matching_rules() {
1336 use crate::pipeline::parse_pipeline;
1337
1338 let pipeline_yaml = r#"
1339name: Windows Only
1340transformations:
1341 - type: field_name_prefix
1342 prefix: "win."
1343 rule_conditions:
1344 - type: logsource
1345 product: windows
1346"#;
1347 let pipeline = parse_pipeline(pipeline_yaml).unwrap();
1348
1349 let rule_yaml = r#"
1351title: Windows Rule
1352logsource:
1353 product: windows
1354detection:
1355 selection:
1356 CommandLine|contains: 'whoami'
1357 condition: selection
1358level: low
1359---
1360title: Linux Rule
1361logsource:
1362 product: linux
1363detection:
1364 selection:
1365 CommandLine|contains: 'whoami'
1366 condition: selection
1367level: low
1368"#;
1369 let collection = parse_sigma_yaml(rule_yaml).unwrap();
1370 assert_eq!(collection.rules.len(), 2);
1371
1372 let mut engine = Engine::new_with_pipeline(pipeline);
1373 engine.add_collection(&collection).unwrap();
1374
1375 let ev_win = json!({"win.CommandLine": "whoami"});
1377 let m = engine.evaluate(&Event::from_value(&ev_win));
1378 assert_eq!(m.len(), 1);
1379 assert_eq!(m[0].rule_title, "Windows Rule");
1380
1381 let ev_linux = json!({"CommandLine": "whoami"});
1383 let m2 = engine.evaluate(&Event::from_value(&ev_linux));
1384 assert_eq!(m2.len(), 1);
1385 assert_eq!(m2[0].rule_title, "Linux Rule");
1386 }
1387
1388 #[test]
1389 fn test_multiple_pipelines_e2e() {
1390 use crate::pipeline::parse_pipeline;
1391
1392 let p1_yaml = r#"
1393name: First Pipeline
1394priority: 10
1395transformations:
1396 - type: field_name_mapping
1397 mapping:
1398 CommandLine: process.args
1399"#;
1400 let p2_yaml = r#"
1401name: Second Pipeline
1402priority: 20
1403transformations:
1404 - type: field_name_suffix
1405 suffix: ".keyword"
1406"#;
1407 let p1 = parse_pipeline(p1_yaml).unwrap();
1408 let p2 = parse_pipeline(p2_yaml).unwrap();
1409
1410 let rule_yaml = r#"
1411title: Test
1412logsource:
1413 product: windows
1414detection:
1415 selection:
1416 CommandLine|contains: 'test'
1417 condition: selection
1418level: low
1419"#;
1420 let collection = parse_sigma_yaml(rule_yaml).unwrap();
1421
1422 let mut engine = Engine::new();
1423 engine.add_pipeline(p1);
1424 engine.add_pipeline(p2);
1425 engine.add_collection(&collection).unwrap();
1426
1427 let ev = json!({"process.args.keyword": "testing"});
1430 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1431 }
1432
1433 #[test]
1434 fn test_pipeline_drop_detection_item_e2e() {
1435 use crate::pipeline::parse_pipeline;
1436
1437 let pipeline_yaml = r#"
1438name: Drop EventID
1439transformations:
1440 - type: drop_detection_item
1441 field_name_conditions:
1442 - type: include_fields
1443 fields:
1444 - EventID
1445"#;
1446 let pipeline = parse_pipeline(pipeline_yaml).unwrap();
1447
1448 let rule_yaml = r#"
1449title: Sysmon Process
1450logsource:
1451 product: windows
1452detection:
1453 selection:
1454 EventID: 1
1455 CommandLine|contains: 'whoami'
1456 condition: selection
1457level: medium
1458"#;
1459 let collection = parse_sigma_yaml(rule_yaml).unwrap();
1460
1461 let mut engine = Engine::new_with_pipeline(pipeline);
1462 engine.add_collection(&collection).unwrap();
1463
1464 let ev = json!({"CommandLine": "whoami"});
1466 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1467
1468 let mut engine2 = Engine::new();
1470 engine2.add_collection(&collection).unwrap();
1471 assert!(engine2.evaluate(&Event::from_value(&ev)).is_empty());
1473 }
1474
1475 #[test]
1476 fn test_pipeline_set_state_and_conditional() {
1477 use crate::pipeline::parse_pipeline;
1478
1479 let pipeline_yaml = r#"
1480name: Stateful Pipeline
1481transformations:
1482 - id: mark_windows
1483 type: set_state
1484 key: is_windows
1485 value: "true"
1486 rule_conditions:
1487 - type: logsource
1488 product: windows
1489 - type: field_name_prefix
1490 prefix: "winlog."
1491 rule_conditions:
1492 - type: processing_state
1493 key: is_windows
1494 val: "true"
1495"#;
1496 let pipeline = parse_pipeline(pipeline_yaml).unwrap();
1497
1498 let rule_yaml = r#"
1499title: Windows Detect
1500logsource:
1501 product: windows
1502detection:
1503 selection:
1504 CommandLine|contains: 'test'
1505 condition: selection
1506level: low
1507"#;
1508 let collection = parse_sigma_yaml(rule_yaml).unwrap();
1509
1510 let mut engine = Engine::new_with_pipeline(pipeline);
1511 engine.add_collection(&collection).unwrap();
1512
1513 let ev = json!({"winlog.CommandLine": "testing"});
1515 assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
1516 }
1517}