Skip to content

Commit

Permalink
add: implement exclude filters
Browse files Browse the repository at this point in the history
  • Loading branch information
qjerome committed Jul 29, 2024
1 parent dae0835 commit 06823d5
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 17 deletions.
110 changes: 109 additions & 1 deletion gene/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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();
Expand Down
75 changes: 64 additions & 11 deletions gene/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, HashSet<i64>>,
) -> HashMap<String, HashSet<i64>> {
let mut inc = HashMap::new();
for (source, events) in filters {
let events = events
.iter()
.filter(|&&id| id >= 0)
.cloned()
.collect::<HashSet<i64>>();
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<String, HashSet<i64>>,
) -> HashMap<String, HashSet<i64>> {
let mut excl = HashMap::new();
for (source, events) in filters {
let events = events
.iter()
.filter(|&&id| id < 0)
.map(|id| id.abs())
.collect::<HashSet<i64>>();
if !events.is_empty() {
excl.insert(source.clone(), events);
}
}
excl
}

#[inline]
pub fn compile_into(self) -> Result<CompiledRule, Error> {
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<CompiledRule, Error> {
Expand All @@ -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) => {
Expand Down Expand Up @@ -221,7 +261,8 @@ pub struct CompiledRule {
pub(crate) filter: bool,
pub(crate) tags: HashSet<String>,
pub(crate) attack: HashSet<String>,
pub(crate) events: HashMap<String, HashSet<i64>>,
pub(crate) include_events: HashMap<String, HashSet<i64>>,
pub(crate) exclude_events: HashMap<String, HashSet<i64>>,
pub(crate) matches: HashMap<String, Match>,
pub(crate) condition: condition::Condition,
pub(crate) severity: u8,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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();
Expand Down
8 changes: 4 additions & 4 deletions gene/tests/data/compiled.gen
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ meta:
name: HeurLongDomain
params:
filter: false
disable: false
disable: true
match-on:
events:
Microsoft-Windows-DNS-Client/Operational: []
Expand Down Expand Up @@ -1785,7 +1785,7 @@ actions: null
name: PSC#Win32API
params:
filter: false
disable: false
disable: true
match-on:
events:
Microsoft-Windows-PowerShell/Operational: []
Expand Down Expand Up @@ -2206,7 +2206,7 @@ meta:
name: SuspWriteAccess
params:
filter: false
disable: false
disable: true
match-on:
events:
Microsoft-Windows-Sysmon/Operational:
Expand Down Expand Up @@ -2691,7 +2691,7 @@ meta:
name: UnknownServices
params:
filter: false
disable: false
disable: true
match-on:
events:
Microsoft-Windows-Sysmon/Operational:
Expand Down
2 changes: 1 addition & 1 deletion gene/tests/rust_events_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 06823d5

Please sign in to comment.