use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use rsigma_eval::EvaluationResult;
use crate::selector::{Selector, SelectorParseError};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
pub enum MatchOp {
#[default]
#[serde(rename = "=")]
Eq,
#[serde(rename = "!=")]
Ne,
#[serde(rename = "=~")]
ReMatch,
#[serde(rename = "!~")]
ReNotMatch,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MatcherSpec {
pub selector: String,
#[serde(default)]
pub op: MatchOp,
pub value: String,
}
#[derive(Debug, Clone)]
pub enum MatcherError {
Selector(SelectorParseError),
Regex { value: String, message: String },
}
impl std::fmt::Display for MatcherError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatcherError::Selector(e) => write!(f, "{e}"),
MatcherError::Regex { value, message } => {
write!(f, "invalid matcher regex '{value}': {message}")
}
}
}
}
impl std::error::Error for MatcherError {}
#[derive(Debug, Clone)]
pub struct Matcher {
selector: Selector,
op: MatchOp,
value: String,
regex: Option<Regex>,
}
impl Matcher {
pub fn compile(spec: &MatcherSpec) -> Result<Self, MatcherError> {
let selector = Selector::parse(&spec.selector).map_err(MatcherError::Selector)?;
let regex = match spec.op {
MatchOp::ReMatch | MatchOp::ReNotMatch => {
let anchored = format!("^(?:{})$", spec.value);
Some(Regex::new(&anchored).map_err(|e| MatcherError::Regex {
value: spec.value.clone(),
message: e.to_string(),
})?)
}
MatchOp::Eq | MatchOp::Ne => None,
};
Ok(Matcher {
selector,
op: spec.op,
value: spec.value.clone(),
regex,
})
}
pub fn to_spec(&self) -> MatcherSpec {
MatcherSpec {
selector: self.selector.as_str(),
op: self.op,
value: self.value.clone(),
}
}
fn matches(&self, result: &EvaluationResult) -> bool {
let resolved = self
.selector
.resolve(result)
.map(value_to_string)
.unwrap_or_default();
match self.op {
MatchOp::Eq => resolved == self.value,
MatchOp::Ne => resolved != self.value,
MatchOp::ReMatch => self.regex.as_ref().is_some_and(|r| r.is_match(&resolved)),
MatchOp::ReNotMatch => self.regex.as_ref().is_some_and(|r| !r.is_match(&resolved)),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MatcherSet {
matchers: Vec<Matcher>,
}
impl MatcherSet {
pub fn compile(specs: &[MatcherSpec]) -> Result<Self, MatcherError> {
let matchers = specs
.iter()
.map(Matcher::compile)
.collect::<Result<Vec<_>, _>>()?;
Ok(MatcherSet { matchers })
}
pub fn is_empty(&self) -> bool {
self.matchers.is_empty()
}
pub fn matches(&self, result: &EvaluationResult) -> bool {
!self.matchers.is_empty() && self.matchers.iter().all(|m| m.matches(result))
}
pub fn to_specs(&self) -> Vec<MatcherSpec> {
self.matchers.iter().map(Matcher::to_spec).collect()
}
}
fn value_to_string(value: Value) -> String {
match value {
Value::String(s) => s,
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use rsigma_eval::{DetectionBody, EvaluationResult, FieldMatch, ResultBody, RuleHeader};
use rsigma_parser::Level;
use std::collections::HashMap;
use std::sync::Arc;
fn detection(ip: &str, level: Level) -> EvaluationResult {
EvaluationResult {
header: RuleHeader {
rule_title: "t".to_string(),
rule_id: Some("rule-1".to_string()),
level: Some(level),
tags: vec![],
custom_attributes: Arc::new(HashMap::new()),
enrichments: None,
},
body: ResultBody::Detection(DetectionBody {
matched_selections: vec![],
matched_fields: vec![FieldMatch::new("SourceIp", serde_json::json!(ip))],
event: None,
}),
}
}
fn spec(selector: &str, op: MatchOp, value: &str) -> MatcherSpec {
MatcherSpec {
selector: selector.to_string(),
op,
value: value.to_string(),
}
}
#[test]
fn eq_and_ne() {
let m = Matcher::compile(&spec("match.SourceIp", MatchOp::Eq, "10.0.0.1")).unwrap();
assert!(m.matches(&detection("10.0.0.1", Level::High)));
assert!(!m.matches(&detection("10.0.0.2", Level::High)));
let n = Matcher::compile(&spec("match.SourceIp", MatchOp::Ne, "10.0.0.1")).unwrap();
assert!(!n.matches(&detection("10.0.0.1", Level::High)));
assert!(n.matches(&detection("10.0.0.2", Level::High)));
}
#[test]
fn regex_is_anchored() {
let m =
Matcher::compile(&spec("match.SourceIp", MatchOp::ReMatch, r"10\.0\.0\.\d+")).unwrap();
assert!(m.matches(&detection("10.0.0.5", Level::High)));
assert!(!m.matches(&detection("10.0.0.5x", Level::High)));
}
#[test]
fn set_ands_matchers() {
let set = MatcherSet::compile(&[
spec("match.SourceIp", MatchOp::Eq, "10.0.0.1"),
spec("level", MatchOp::Eq, "high"),
])
.unwrap();
assert!(set.matches(&detection("10.0.0.1", Level::High)));
assert!(!set.matches(&detection("10.0.0.1", Level::Low)));
assert!(!set.matches(&detection("10.0.0.2", Level::High)));
}
#[test]
fn empty_set_never_matches() {
let set = MatcherSet::default();
assert!(!set.matches(&detection("10.0.0.1", Level::High)));
}
#[test]
fn bad_regex_is_rejected() {
let err = Matcher::compile(&spec("rule", MatchOp::ReMatch, "(")).unwrap_err();
assert!(matches!(err, MatcherError::Regex { .. }));
}
}