From dae0835529abe7ad0ce8de5f11bcb81306962303 Mon Sep 17 00:00:00 2001 From: qjerome Date: Fri, 26 Jul 2024 15:49:17 +0200 Subject: [PATCH] add: new conditions + tests --- gene/src/rules.rs | 237 ++++++++++++++ gene/src/rules/condition.rs | 340 ++++++++++++++++++++- gene/src/rules/condition/condition_test.rs | 2 +- gene/src/rules/grammars/condition.pest | 20 +- 4 files changed, 588 insertions(+), 11 deletions(-) diff --git a/gene/src/rules.rs b/gene/src/rules.rs index bcc0ae4..bf1f8d4 100644 --- a/gene/src/rules.rs +++ b/gene/src/rules.rs @@ -577,4 +577,241 @@ condition: $a and $b let m = matches.get("$a").unwrap(); assert_eq!(m, r#".data.file.exe == "8.8.4.4""#); } + + #[test] + fn test_all_of_them() { + let test = r#" +--- +name: test +matches: + $a: .ip == "8.8.4.4" + $b: .ip ~= "^8\.8\." +condition: all of them +..."#; + + let d: Rule = serde_yaml::from_str(test).unwrap(); + let cr = CompiledRule::try_from(d).unwrap(); + + def_event!( + pub struct Dummy { + ip: IpAddr, + } + ); + + let event = Dummy { + ip: "8.8.4.4".parse().unwrap(), + }; + + assert_eq!(cr.match_event(&event), Ok(true)); + } + + #[test] + fn test_all_of_vars() { + let test = r#" +--- +name: test +matches: + $ip1: .ip == "8.8.4.4" + $ip2: .ip ~= "^8\.8\." + $t : .ip == "4.4.4.4" +condition: all of $ip +..."#; + + let d: Rule = serde_yaml::from_str(test).unwrap(); + let cr = CompiledRule::try_from(d).unwrap(); + + def_event!( + pub struct Dummy { + ip: IpAddr, + } + ); + + let event = Dummy { + ip: "8.8.4.4".parse().unwrap(), + }; + + assert_eq!(cr.match_event(&event), Ok(true)); + } + + #[test] + fn test_any_of_them() { + let test = r#" +--- +name: test +matches: + $a: .ip == "8.8.4.4" + $b: .ip ~= "^8\.8\." +condition: any of them +..."#; + + let d: Rule = serde_yaml::from_str(test).unwrap(); + let cr = CompiledRule::try_from(d).unwrap(); + + def_event!( + pub struct Dummy { + ip: IpAddr, + } + ); + + let event = Dummy { + ip: "8.8.42.42".parse().unwrap(), + }; + + assert_eq!(cr.match_event(&event), Ok(true)); + } + + #[test] + fn test_any_of_vars() { + let test = r#" +--- +name: test +matches: + $ip2: .ip == "42.42.42.42" + $ip3: .ip == "8.8.4.4" +condition: any of $ip +..."#; + + let d: Rule = serde_yaml::from_str(test).unwrap(); + let cr = CompiledRule::try_from(d).unwrap(); + + def_event!( + pub struct Dummy { + ip: IpAddr, + } + ); + + for (ip, expect) in [ + ("42.42.42.42", true), + ("8.8.4.4", true), + ("255.0.0.0", false), + ] { + let event = Dummy { + ip: ip.parse().unwrap(), + }; + + assert_eq!(cr.match_event(&event), Ok(expect)); + } + } + + #[test] + fn test_n_of_them() { + let test = r#" +--- +name: test +matches: + $path1: .path == "/bin/ls" + $ip2: .ip == "42.42.42.42" + $ip3: .ip == "8.8.4.4" +condition: 2 of them +..."#; + + let d: Rule = serde_yaml::from_str(test).unwrap(); + let cr = CompiledRule::try_from(d).unwrap(); + + def_event!( + pub struct Dummy { + path: String, + ip: IpAddr, + } + ); + + let event = Dummy { + path: "/bin/ls".into(), + ip: "42.42.42.42".parse().unwrap(), + }; + + assert_eq!(cr.match_event(&event), Ok(true)); + } + + #[test] + fn test_n_of_vars() { + let test = r#" +--- +name: test +matches: + $path1: .path == "/bin/ls" + $path2: .path == "/bin/true" + $ip1: .ip == "42.42.42.42" + $ip2: .ip == "8.8.4.4" +condition: 1 of $path or 1 of $ip +..."#; + + let d: Rule = serde_yaml::from_str(test).unwrap(); + let cr = CompiledRule::try_from(d).unwrap(); + + def_event!( + pub struct Dummy { + path: String, + ip: IpAddr, + } + ); + + let event = Dummy { + path: "/bin/ls".into(), + ip: "42.42.42.42".parse().unwrap(), + }; + + assert_eq!(cr.match_event(&event), Ok(true)); + + let event = Dummy { + path: "/bin/true".into(), + ip: "8.8.4.4".parse().unwrap(), + }; + + assert_eq!(cr.match_event(&event), Ok(true)); + } + + #[test] + fn test_none_of_them() { + let test = r#" +--- +name: test +matches: + $a: .ip == "8.8.4.4" + $b: .ip ~= "^8\.8\." +condition: none of them +..."#; + + let d: Rule = serde_yaml::from_str(test).unwrap(); + let cr = CompiledRule::try_from(d).unwrap(); + + def_event!( + pub struct Dummy { + ip: IpAddr, + } + ); + + let event = Dummy { + ip: "42.42.42.42".parse().unwrap(), + }; + + assert_eq!(cr.match_event(&event), Ok(true)); + } + + #[test] + fn test_none_of_vars() { + let test = r#" +--- +name: test +matches: + $ip: .ip == "8.8.4.4" + $ip: .ip ~= "^8\.8\." +condition: none of $ip +..."#; + + let d: Rule = serde_yaml::from_str(test).unwrap(); + let cr = CompiledRule::try_from(d).unwrap(); + + def_event!( + pub struct Dummy { + ip: IpAddr, + } + ); + + let event = Dummy { + ip: "42.42.42.42".parse().unwrap(), + }; + + assert_eq!(cr.match_event(&event), Ok(true)); + } } diff --git a/gene/src/rules/condition.rs b/gene/src/rules/condition.rs index 94ec1cc..85f9ad0 100644 --- a/gene/src/rules/condition.rs +++ b/gene/src/rules/condition.rs @@ -32,6 +32,14 @@ pub(crate) enum Op { #[derive(Debug, Clone, PartialEq)] pub(crate) enum Expr { Variable(String), + AllOfThem, + AllOfVars(String), + AnyOfThem, + AnyOfVars(String), + NoneOfThem, + NoneOfVars(String), + NOfThem(usize), + NOfVars(usize, String), BinOp { lhs: Box, op: Op, @@ -81,6 +89,94 @@ impl Expr { event: &E, ) -> Result { match self { + Expr::AllOfThem => { + for m in operands.values() { + if !m.match_event(event)? { + return Ok(false); + } + } + Ok(true) + } + Expr::AllOfVars(start) => { + for m in operands + .iter() + .filter(|(v, _)| v.starts_with(start)) + .map(|(_, m)| m) + { + if !m.match_event(event)? { + return Ok(false); + } + } + Ok(true) + } + Expr::NOfThem(n) => { + let mut c = 0; + for m in operands.values() { + if m.match_event(event)? { + c += 1; + if c >= *n { + return Ok(true); + } + } + } + Ok(c >= *n) + } + Expr::NOfVars(n, start) => { + let mut c = 0; + for m in operands + .iter() + .filter(|(v, _)| v.starts_with(start)) + .map(|(_, m)| m) + { + if m.match_event(event)? { + c += 1; + if c >= *n { + return Ok(true); + } + } + } + Ok(c >= *n) + } + Expr::AnyOfThem => { + for m in operands.values() { + if m.match_event(event)? { + return Ok(true); + } + } + Ok(false) + } + Expr::AnyOfVars(start) => { + for m in operands + .iter() + .filter(|(v, _)| v.starts_with(start)) + .map(|(_, m)| m) + { + if m.match_event(event)? { + return Ok(true); + } + } + Ok(false) + } + Expr::NoneOfThem => { + for m in operands.values() { + if m.match_event(event)? { + return Ok(false); + } + } + Ok(true) + } + Expr::NoneOfVars(start) => { + for m in operands + .iter() + .filter(|(v, _)| v.starts_with(start)) + .map(|(_, m)| m) + { + if m.match_event(event)? { + return Ok(false); + } + } + Ok(true) + } Expr::Variable(var) => { if let Some(m) = operands.get(var) { return m.match_event(event).map_err(|e| e.into()); @@ -100,15 +196,58 @@ impl Expr { #[allow(dead_code)] // this function is used in test - fn compute(&self, operands: &HashMap<&str, bool>) -> bool { + fn compute(&self, operands: &HashMap<&str, bool>) -> Result { match self { - Expr::Variable(var) => *(operands.get(var.as_str()).unwrap()), + Expr::AllOfThem => Ok(operands.iter().all(|(_, &b)| b)), + Expr::AllOfVars(start) => Ok(operands + .iter() + .filter(|(v, _)| v.starts_with(start)) + .all(|(_, &b)| b)), + Expr::AnyOfThem => Ok(operands.iter().any(|(_, &b)| b)), + Expr::AnyOfVars(start) => Ok(operands + .iter() + .filter(|(v, _)| v.starts_with(start)) + .any(|(_, &b)| b)), + Expr::NoneOfThem => Ok(!operands.iter().any(|(_, &b)| b)), + Expr::NoneOfVars(start) => Ok(!operands + .iter() + .filter(|(v, _)| v.starts_with(start)) + .any(|(_, &b)| b)), + Expr::NOfThem(x) => { + if operands.len() < *x { + return Ok(false); + } + for (c, _) in operands.iter().filter(|(_, &b)| b).enumerate() { + // +1 because we start iterating at 0 >< + if c + 1 >= *x { + return Ok(true); + } + } + Ok(false) + } + Expr::NOfVars(x, start) => { + if operands.len() < *x { + return Ok(false); + } + for (c, _) in operands + .iter() + .filter(|(v, &b)| b && v.starts_with(start)) + .enumerate() + { + // +1 because we start iterating at 0 >< + if c + 1 >= *x { + return Ok(true); + } + } + Ok(false) + } + Expr::Variable(var) => Ok(*(operands.get(var.as_str()).unwrap())), Expr::BinOp { lhs, op, rhs } => match op { - Op::And => lhs.compute(operands) && rhs.compute(operands), - Op::Or => lhs.compute(operands) || rhs.compute(operands), + Op::And => Ok(lhs.compute(operands)? && rhs.compute(operands)?), + Op::Or => Ok(lhs.compute(operands)? || rhs.compute(operands)?), }, - Expr::Negate(expr) => !expr.compute(operands), - Expr::None => true, + Expr::Negate(expr) => Ok(!expr.compute(operands)?), + Expr::None => Ok(true), } } } @@ -116,8 +255,50 @@ impl Expr { fn parse_expr(pairs: Pairs) -> Expr { PRATT_PARSER .map_primary(|primary| match primary.as_rule() { - Rule::ident => Expr::Variable(primary.as_str().into()), - Rule::expr => parse_expr(primary.into_inner()), + Rule::var => Expr::Variable(primary.as_str().into()), + Rule::all_of_them => Expr::AllOfThem, + Rule::all_of_vars => { + Expr::AllOfVars(primary.as_str().rsplit_once(' ').unwrap().1.into()) + } + Rule::none_of_them => Expr::NoneOfThem, + Rule::none_of_vars => { + Expr::NoneOfVars(primary.as_str().rsplit_once(' ').unwrap().1.into()) + } + Rule::any_of_them => Expr::AnyOfThem, + Rule::any_of_vars => { + Expr::AnyOfVars(primary.as_str().rsplit_once(' ').unwrap().1.into()) + } + Rule::n_of_them => { + // this should not panic in anyways as it is validated by pest + let n = // this should not panic in anyways as it is validated by pest + primary + .as_str() + .split_once(' ') + .unwrap() + .0 + .parse::() + .unwrap(); + if n == 0 { + // equivalent to none of them + return Expr::NoneOfThem; + } + Expr::NOfThem(n) + } + Rule::n_of_vars => { + let n = primary + .as_str() + .split_once(' ') + .unwrap() + .0 + .parse::() + .unwrap(); + let vars = primary.as_str().rsplit_once(' ').unwrap().1.into(); + if n == 0 { + return Expr::NoneOfVars(vars); + } + Expr::NOfVars(n, vars) + } + Rule::expr | Rule::ident | Rule::group => parse_expr(primary.into_inner()), rule => unreachable!("Expr::parse expected atom, found {:?}", rule), }) .map_infix(|lhs, op, rhs| { @@ -201,10 +382,151 @@ mod tests { "$a and $b", "$a and ($b or ($c and $d))", "($a or $b) and $c", + "any of them", + "any of $app_", + "all of them", + "all of $app_", + "none of them", + "none of $app_", + "42 of them", + "42 of $app_", ]; valid.iter().for_each(|ident| { - Expr::from_str(ident).unwrap(); + println!("{:?}", Expr::from_str(ident).unwrap()); }); + + // special cases + assert_eq!("0 of them".parse::().unwrap(), Expr::NoneOfThem); + + assert_eq!( + "0 of $app".parse::().unwrap(), + Expr::NoneOfVars("$app".into()) + ); + } + + #[test] + fn test_all_of_them() { + let expr = Expr::from_str("all of them").unwrap(); + let mut operands = { + let mut m = HashMap::new(); + m.insert("$a", true); + m.insert("$b", true); + m + }; + + assert_eq!(expr.compute(&operands), Ok(true)); + operands.insert("$c", false); + assert_eq!(expr.compute(&operands), Ok(false)); + } + + #[test] + fn test_all_of_vars() { + let expr = Expr::from_str("all of $app").unwrap(); + let mut operands = { + let mut m = HashMap::new(); + m.insert("$app1", true); + m.insert("$app2", true); + m.insert("$b", false); + m + }; + + assert_eq!(expr.compute(&operands), Ok(true)); + operands.insert("$app3", false); + assert_eq!(expr.compute(&operands), Ok(false)); + } + + #[test] + fn test_any_of_them() { + let expr = Expr::from_str("any of them").unwrap(); + let mut operands = { + let mut m = HashMap::new(); + m.insert("$a", true); + m.insert("$b", false); + m + }; + + assert_eq!(expr.compute(&operands), Ok(true)); + operands.entry("$a").and_modify(|b| *b = false); + assert_eq!(expr.compute(&operands), Ok(false)); + } + + #[test] + fn test_any_of_vars() { + let expr = Expr::from_str("any of $app").unwrap(); + let mut operands = { + let mut m = HashMap::new(); + m.insert("$app1", true); + m.insert("$app2", false); + m.insert("$b", true); + m + }; + + assert_eq!(expr.compute(&operands), Ok(true)); + operands.entry("$app1").and_modify(|b| *b = false); + assert_eq!(expr.compute(&operands), Ok(false)); + } + + #[test] + fn test_none_of_them() { + let expr = Expr::from_str("none of them").unwrap(); + let mut operands = { + let mut m = HashMap::new(); + m.insert("$a", true); + m.insert("$b", false); + m + }; + + assert_eq!(expr.compute(&operands), Ok(false)); + operands.entry("$a").and_modify(|b| *b = false); + assert_eq!(expr.compute(&operands), Ok(true)); + } + + #[test] + fn test_none_of_vars() { + let expr = Expr::from_str("none of $app").unwrap(); + let mut operands = { + let mut m = HashMap::new(); + m.insert("$app1", true); + m.insert("$app2", true); + m.insert("$b", false); + m + }; + + assert_eq!(expr.compute(&operands), Ok(false)); + operands.entry("$app1").and_modify(|b| *b = false); + operands.entry("$app2").and_modify(|b| *b = false); + assert_eq!(expr.compute(&operands), Ok(true)); + } + + #[test] + fn test_x_of_them() { + let expr = Expr::from_str("1 of them").unwrap(); + let mut operands = { + let mut m = HashMap::new(); + m.insert("$a", true); + m.insert("$b", false); + m + }; + + assert_eq!(expr.compute(&operands), Ok(true)); + operands.entry("$a").and_modify(|b| *b = false); + assert_eq!(expr.compute(&operands), Ok(false)); + } + + #[test] + fn test_x_of_vars() { + let expr = Expr::from_str("1 of $app").unwrap(); + let mut operands = { + let mut m = HashMap::new(); + m.insert("$app1", true); + m.insert("$app2", false); + m.insert("$b", true); + m + }; + + assert_eq!(expr.compute(&operands), Ok(true)); + operands.entry("$app1").and_modify(|b| *b = false); + assert_eq!(expr.compute(&operands), Ok(false)); } } diff --git a/gene/src/rules/condition/condition_test.rs b/gene/src/rules/condition/condition_test.rs index b0ab16d..e7337eb 100644 --- a/gene/src/rules/condition/condition_test.rs +++ b/gene/src/rules/condition/condition_test.rs @@ -11,7 +11,7 @@ mod test { fn test_condition_computation() { TEST_CONDITIONS.iter().for_each(|(condition, result)| { let cond = Expr::from_str(condition).unwrap(); - assert_eq!(cond.compute(&OPERANDS), *result) + assert_eq!(cond.compute(&OPERANDS).unwrap(), *result) }); } } diff --git a/gene/src/rules/grammars/condition.pest b/gene/src/rules/grammars/condition.pest index 50361f4..6cae74a 100644 --- a/gene/src/rules/grammars/condition.pest +++ b/gene/src/rules/grammars/condition.pest @@ -1,4 +1,22 @@ -ident = @{ "$" ~ (ASCII_ALPHANUMERIC | "_")+ } +ident = _{ var | group } +var = @{ "$" ~ (ASCII_ALPHANUMERIC | "_")+ } + +// meta expressions +group = { n_of_them | n_of_vars | all_of_them | all_of_vars | any_of_them | any_of_vars | none_of_them | none_of_vars } +of_them = _{ "of" ~ "them" } +of_vars = _{ "of" ~ var } +// x_of +n_of_them = { ASCII_DIGIT+ ~ of_them } +n_of_vars = { ASCII_DIGIT+ ~ of_vars } +// all_of +all_of_them = { "all" ~ of_them } +all_of_vars = { "all" ~ of_vars } +// any_of +any_of_them = { "any" ~ of_them } +any_of_vars = { "any" ~ of_vars } +// none_of +none_of_them = { "none" ~ of_them } +none_of_vars = { "none" ~ of_vars } negate = { ("!" | "not") } op = _{ or | and }