use secir::template::matchers::MatcherCondition;
use secir::{Error, Result};
use std::collections::BTreeSet;
mod parser;
pub(crate) use parser::{ConditionParser, tokenize_condition};
#[derive(Debug, Clone)]
pub(crate) enum ConditionExpr {
Selection(String),
Not(Box<ConditionExpr>),
And(Box<ConditionExpr>, Box<ConditionExpr>),
Or(Box<ConditionExpr>, Box<ConditionExpr>),
Of {
count: Option<usize>,
pattern: String,
},
Near(Box<ConditionExpr>, Box<ConditionExpr>),
}
#[derive(Debug, Clone)]
pub(crate) struct ConditionTerm {
pub(crate) name: String,
pub(crate) negative: bool,
}
pub(crate) fn parse_condition(input: &str) -> Result<ConditionExpr> {
let tokens = tokenize_condition(input);
let mut parser = ConditionParser {
tokens,
index: 0,
depth: 0,
};
let expr = parser.parse_expr()?;
if parser.index != parser.tokens.len() {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: format!("unexpected token '{}'", parser.tokens[parser.index]),
});
}
Ok(expr)
}
pub(crate) fn flatten_condition(
expr: &ConditionExpr,
selection_names: &[String],
) -> Result<(MatcherCondition, Vec<ConditionTerm>)> {
match expr {
ConditionExpr::Selection(name) => Ok((
MatcherCondition::Or,
vec![ConditionTerm {
name: name.clone(),
negative: false,
}],
)),
ConditionExpr::Not(inner) => match inner.as_ref() {
ConditionExpr::Selection(name) => Ok((
MatcherCondition::Or,
vec![ConditionTerm {
name: name.clone(),
negative: true,
}],
)),
_ => Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "NOT is only supported directly on selections".to_string(),
}),
},
ConditionExpr::And(_, _) => flatten_and_condition(expr, selection_names),
ConditionExpr::Or(_, _) => {
let mut terms = Vec::new();
flatten_or_group(expr, selection_names, &mut terms)?;
Ok((MatcherCondition::Or, dedupe_terms(terms)))
}
ConditionExpr::Of { count, pattern } => {
let names = resolve_selection_pattern(pattern, selection_names)?;
let matchers_condition = match count {
None => MatcherCondition::And,
Some(1) => MatcherCondition::Or,
Some(n) => {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: format!(
"'{n} of {pattern}' is not supported: count > 1 requires a streaming \
evaluator. Fix: use 'all of {pattern}' or '1 of {pattern}', or \
evaluate the count condition in a post-processing aggregation step."
),
});
}
};
Ok((
matchers_condition,
names
.into_iter()
.map(|name| ConditionTerm {
name,
negative: false,
})
.collect(),
))
}
ConditionExpr::Near(left, right) => {
let terms = Vec::new();
flatten_and_clauses(left, selection_names, &mut vec![terms.clone()])?;
flatten_and_clauses(right, selection_names, &mut vec![terms])?;
Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "near is not supported by secir matchers".to_string(),
})
}
}
}
fn flatten_and_condition(
expr: &ConditionExpr,
selection_names: &[String],
) -> Result<(MatcherCondition, Vec<ConditionTerm>)> {
let mut clauses = Vec::new();
flatten_and_clauses(expr, selection_names, &mut clauses)?;
let terms = dedupe_terms(clauses.into_iter().flatten().collect());
Ok((MatcherCondition::And, terms))
}
fn flatten_and_clauses(
expr: &ConditionExpr,
selection_names: &[String],
clauses: &mut Vec<Vec<ConditionTerm>>,
) -> Result<()> {
match expr {
ConditionExpr::And(left, right) => {
flatten_and_clauses(left, selection_names, clauses)?;
flatten_and_clauses(right, selection_names, clauses)
}
ConditionExpr::Near(_, _) => Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "near is not supported by secir matchers".to_string(),
}),
_ => {
let mut terms = Vec::new();
flatten_or_group(expr, selection_names, &mut terms)?;
clauses.push(dedupe_terms(terms));
Ok(())
}
}
}
fn flatten_or_group(
expr: &ConditionExpr,
selection_names: &[String],
out: &mut Vec<ConditionTerm>,
) -> Result<()> {
match expr {
ConditionExpr::Or(left, right) => {
flatten_or_group(left, selection_names, out)?;
flatten_or_group(right, selection_names, out)
}
ConditionExpr::Selection(name) => {
out.push(ConditionTerm {
name: name.clone(),
negative: false,
});
Ok(())
}
ConditionExpr::Not(inner) => match inner.as_ref() {
ConditionExpr::Selection(name) => {
out.push(ConditionTerm {
name: name.clone(),
negative: true,
});
Ok(())
}
_ => Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "NOT is only supported directly on selections".to_string(),
}),
},
ConditionExpr::Of { count, pattern } => {
if let Some(n) = count {
if *n > 1 {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: format!(
"'{n} of {pattern}' is not supported: count > 1 requires a streaming \
evaluator. Fix: use 'all of {pattern}' or '1 of {pattern}'."
),
});
}
}
for name in resolve_selection_pattern(pattern, selection_names)? {
out.push(ConditionTerm {
name,
negative: false,
});
}
Ok(())
}
ConditionExpr::And(left, right) => {
flatten_or_group(left, selection_names, out)?;
flatten_or_group(right, selection_names, out)
}
ConditionExpr::Near(_, _) => Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "near is not supported by secir matchers".to_string(),
}),
}
}
fn dedupe_terms(terms: Vec<ConditionTerm>) -> Vec<ConditionTerm> {
let mut seen = BTreeSet::new();
let mut deduped = Vec::with_capacity(terms.len());
for term in terms {
if seen.insert((term.name.clone(), term.negative)) {
deduped.push(term);
}
}
deduped
}
pub(crate) fn resolve_selection_pattern(
pattern: &str,
selection_names: &[String],
) -> Result<Vec<String>> {
let mut names: Vec<String> = if pattern.eq_ignore_ascii_case("them") {
selection_names.to_vec()
} else if pattern.contains('*') {
let pattern_lc = pattern.to_ascii_lowercase();
selection_names
.iter()
.filter(|name| wildcard_matches_lowered(&pattern_lc, name))
.cloned()
.collect()
} else {
selection_names
.iter()
.filter(|name| pattern.eq_ignore_ascii_case(name))
.cloned()
.collect()
};
names.sort();
if names.is_empty() {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: format!("condition references unknown selection pattern '{pattern}'"),
});
}
Ok(names)
}
fn wildcard_matches_lowered(pattern_lc: &str, candidate: &str) -> bool {
let candidate = candidate.to_ascii_lowercase();
let mut remainder = candidate.as_str();
let mut anchored_start = true;
for part in pattern_lc.split('*') {
if part.is_empty() {
anchored_start = false;
continue;
}
if anchored_start {
if !remainder.starts_with(part) {
return false;
}
remainder = &remainder[part.len()..];
anchored_start = false;
continue;
}
let Some(index) = remainder.find(part) else {
return false;
};
remainder = &remainder[index + part.len()..];
}
pattern_lc.ends_with('*') || remainder.is_empty()
}
#[cfg(test)]
mod tests;