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