diff --git a/gene/src/engine.rs b/gene/src/engine.rs index 3a9a284..d8f71d4 100644 --- a/gene/src/engine.rs +++ b/gene/src/engine.rs @@ -20,7 +20,7 @@ use gene_derive::FieldGetter; /// A severity score (sum of all matching rules severity bounded to [MAX_SEVERITY](rules::MAX_SEVERITY)) is also part of a `ScanResult`. /// Some [Rules](Rule) matching an [Event] might be filter rules. In this /// case only the [filtered](ScanResult::filtered) flag is updated. -#[derive(Debug, Default, FieldGetter, Serialize, Deserialize, Clone)] +#[derive(Debug, Default, FieldGetter, Serialize, Deserialize, Clone, PartialEq)] pub struct ScanResult { /// union of the rule names matching the event #[getter(skip)] @@ -405,6 +405,114 @@ actions: ["do_something"] assert!(sr.is_only_filter()); } + #[test] + fn test_include_all_empty_filter() { + // test that we must take all events when nothing is + // included / excluded + let mut e = Engine::new(); + let r = rule!( + r#" +--- +name: test +params: + filter: true +match-on: + events: + test: [] +..."# + ); + + e.insert_rule(r).unwrap(); + fake_event!(IpEvt, id = 1, source = "test", (".ip", "8.8.4.4")); + e.scan(&IpEvt {}).unwrap().unwrap(); + + fake_event!(PathEvt, id = 2, source = "test", (".path", "/bin/ls")); + e.scan(&PathEvt {}).unwrap().unwrap(); + } + + #[test] + fn test_include_filter() { + // test that only events included must be included + let mut e = Engine::new(); + let r = rule!( + r#" +--- +name: test +params: + filter: true +match-on: + events: + test: [ 2 ] +..."# + ); + + e.insert_rule(r).unwrap(); + fake_event!(IpEvt, id = 1, source = "test", (".ip", "8.8.4.4")); + // not explicitly included so it should not be + assert_eq!(e.scan(&IpEvt {}).unwrap(), None); + + fake_event!(PathEvt, id = 2, source = "test", (".path", "/bin/ls")); + e.scan(&PathEvt {}).unwrap().unwrap(); + } + + #[test] + fn test_exclude_filter() { + // test that only stuff excluded must be excluded + let mut e = Engine::new(); + let r = rule!( + r#" +--- +name: test +params: + filter: true +match-on: + events: + test: [ -1 ] +..."# + ); + + e.insert_rule(r).unwrap(); + fake_event!(IpEvt, id = 1, source = "test", (".ip", "8.8.4.4")); + assert_eq!(e.scan(&IpEvt {}).unwrap(), None); + + // if not explicitely excluded it is included + fake_event!(PathEvt, id = 2, source = "test", (".path", "/bin/ls")); + assert!(e.scan(&PathEvt {}).unwrap().is_some()); + + fake_event!(DnsEvt, id = 3, source = "test", (".domain", "test.com")); + assert!(e.scan(&DnsEvt {}).unwrap().is_some()); + } + + #[test] + fn test_mix_include_exclude_filter() { + // test that when include and exclude filters are + // specified we take only events in those + let mut e = Engine::new(); + let r = rule!( + r#" +--- +name: test +params: + filter: true +match-on: + events: + test: [ -1, 2 ] +..."# + ); + + e.insert_rule(r).unwrap(); + fake_event!(IpEvt, id = 1, source = "test", (".ip", "8.8.4.4")); + assert_eq!(e.scan(&IpEvt {}).unwrap(), None); + + fake_event!(PathEvt, id = 2, source = "test", (".path", "/bin/ls")); + assert!(e.scan(&PathEvt {}).unwrap().is_some()); + + // this has not been excluded but not included so it should + // not match + fake_event!(DnsEvt, id = 3, source = "test", (".domain", "test.com")); + assert_eq!(e.scan(&DnsEvt {}).unwrap(), None); + } + #[test] fn test_match_and_filter() { let mut e = Engine::new(); diff --git a/gene/src/rules.rs b/gene/src/rules.rs index bf1f8d4..fff5cb0 100644 --- a/gene/src/rules.rs +++ b/gene/src/rules.rs @@ -151,9 +151,48 @@ impl Rule { self } + // build filter for events to include (positive values in + // `match-on` section) + fn build_include_events( + filters: &HashMap>, + ) -> HashMap> { + let mut inc = HashMap::new(); + for (source, events) in filters { + let events = events + .iter() + .filter(|&&id| id >= 0) + .cloned() + .collect::>(); + if !events.is_empty() { + inc.insert(source.clone(), events); + } + } + inc + } + + // build filter for events to exclude (negative values in + // `match-on` section) + fn build_exclude_events( + filters: &HashMap>, + ) -> HashMap> { + let mut excl = HashMap::new(); + for (source, events) in filters { + let events = events + .iter() + .filter(|&&id| id < 0) + .map(|id| id.abs()) + .collect::>(); + if !events.is_empty() { + excl.insert(source.clone(), events); + } + } + excl + } + #[inline] pub fn compile_into(self) -> Result { let name = self.name.clone(); + let filters = self.match_on.and_then(|mo| mo.events).unwrap_or_default(); // to wrap error with rule name || -> Result { @@ -162,7 +201,8 @@ impl Rule { filter: self.params.and_then(|p| p.filter).unwrap_or_default(), tags: HashSet::new(), attack: HashSet::new(), - events: self.match_on.and_then(|mo| mo.events).unwrap_or_default(), + include_events: Self::build_include_events(&filters), + exclude_events: Self::build_exclude_events(&filters), matches: HashMap::new(), condition: match self.condition { Some(cond) => { @@ -221,7 +261,8 @@ pub struct CompiledRule { pub(crate) filter: bool, pub(crate) tags: HashSet, pub(crate) attack: HashSet, - pub(crate) events: HashMap>, + pub(crate) include_events: HashMap>, + pub(crate) exclude_events: HashMap>, pub(crate) matches: HashMap, pub(crate) condition: condition::Condition, pub(crate) severity: u8, @@ -271,17 +312,33 @@ impl CompiledRule { #[inline(always)] pub(crate) fn can_match_on(&self, src: &String, id: i64) -> bool { - if self.events.is_empty() { + // we have no filter at all + if self.include_events.is_empty() && self.exclude_events.is_empty() { return true; } - if let Some(set) = self.events.get(src) { - if set.is_empty() { - return true; + // explicit event excluding logic + let opt_exclude = self.exclude_events.get(src); + if let Some(exclude) = opt_exclude { + // we definitely want to exclude that event + if exclude.contains(&id) { + return false; } - return set.contains(&id); } + let opt_include = self.include_events.get(src); + // we include if we have no include filter for this source + // but we have an exclude filter (that didn't match) + if opt_include.is_none() && opt_exclude.is_some() { + return true; + } + + // we return result of lookup in include filter if there is one + if let Some(include) = opt_include { + return include.contains(&id); + } + + // default we cannot match on event false } @@ -476,10 +533,6 @@ condition: $c let test = r#" --- name: test -match-on: - events: - test: [42] -condition: ..."#; let d: Rule = serde_yaml::from_str(test).unwrap(); diff --git a/gene/tests/data/compiled.gen b/gene/tests/data/compiled.gen index 7ba9cb9..428d47b 100644 --- a/gene/tests/data/compiled.gen +++ b/gene/tests/data/compiled.gen @@ -873,7 +873,7 @@ meta: name: HeurLongDomain params: filter: false - disable: false + disable: true match-on: events: Microsoft-Windows-DNS-Client/Operational: [] @@ -1785,7 +1785,7 @@ actions: null name: PSC#Win32API params: filter: false - disable: false + disable: true match-on: events: Microsoft-Windows-PowerShell/Operational: [] @@ -2206,7 +2206,7 @@ meta: name: SuspWriteAccess params: filter: false - disable: false + disable: true match-on: events: Microsoft-Windows-Sysmon/Operational: @@ -2691,7 +2691,7 @@ meta: name: UnknownServices params: filter: false - disable: false + disable: true match-on: events: Microsoft-Windows-Sysmon/Operational: diff --git a/gene/tests/rust_events_test.rs b/gene/tests/rust_events_test.rs index 61c0cfc..bd17bf3 100644 --- a/gene/tests/rust_events_test.rs +++ b/gene/tests/rust_events_test.rs @@ -76,7 +76,7 @@ fn test_fast() { let scan_dur = time_it(|| { for _ in 0..run_count { for e in events.iter() { - if let Some(sr) = engine.scan(e).ok().and_then(|v| v) { + if let Some(sr) = engine.scan(e).unwrap_or_default() { if sr.is_detection() { positives += 1 }