rsigma_runtime/alert_pipeline/
matcher.rs1use regex::Regex;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use rsigma_eval::EvaluationResult;
15
16use crate::selector::{Selector, SelectorParseError};
17
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
20pub enum MatchOp {
21 #[default]
23 #[serde(rename = "=")]
24 Eq,
25 #[serde(rename = "!=")]
27 Ne,
28 #[serde(rename = "=~")]
30 ReMatch,
31 #[serde(rename = "!~")]
33 ReNotMatch,
34}
35
36#[derive(Debug, Clone, Deserialize, Serialize)]
38pub struct MatcherSpec {
39 pub selector: String,
41 #[serde(default)]
43 pub op: MatchOp,
44 pub value: String,
46}
47
48#[derive(Debug, Clone)]
51pub enum MatcherError {
52 Selector(SelectorParseError),
54 Regex { value: String, message: String },
56}
57
58impl std::fmt::Display for MatcherError {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 match self {
61 MatcherError::Selector(e) => write!(f, "{e}"),
62 MatcherError::Regex { value, message } => {
63 write!(f, "invalid matcher regex '{value}': {message}")
64 }
65 }
66 }
67}
68
69impl std::error::Error for MatcherError {}
70
71#[derive(Debug, Clone)]
73pub struct Matcher {
74 selector: Selector,
75 op: MatchOp,
76 value: String,
77 regex: Option<Regex>,
78}
79
80impl Matcher {
81 pub fn compile(spec: &MatcherSpec) -> Result<Self, MatcherError> {
83 let selector = Selector::parse(&spec.selector).map_err(MatcherError::Selector)?;
84 let regex = match spec.op {
85 MatchOp::ReMatch | MatchOp::ReNotMatch => {
86 let anchored = format!("^(?:{})$", spec.value);
88 Some(Regex::new(&anchored).map_err(|e| MatcherError::Regex {
89 value: spec.value.clone(),
90 message: e.to_string(),
91 })?)
92 }
93 MatchOp::Eq | MatchOp::Ne => None,
94 };
95 Ok(Matcher {
96 selector,
97 op: spec.op,
98 value: spec.value.clone(),
99 regex,
100 })
101 }
102
103 pub fn to_spec(&self) -> MatcherSpec {
105 MatcherSpec {
106 selector: self.selector.as_str(),
107 op: self.op,
108 value: self.value.clone(),
109 }
110 }
111
112 fn matches(&self, result: &EvaluationResult) -> bool {
114 let resolved = self
115 .selector
116 .resolve(result)
117 .map(value_to_string)
118 .unwrap_or_default();
119 match self.op {
120 MatchOp::Eq => resolved == self.value,
121 MatchOp::Ne => resolved != self.value,
122 MatchOp::ReMatch => self.regex.as_ref().is_some_and(|r| r.is_match(&resolved)),
123 MatchOp::ReNotMatch => self.regex.as_ref().is_some_and(|r| !r.is_match(&resolved)),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Default)]
131pub struct MatcherSet {
132 matchers: Vec<Matcher>,
133}
134
135impl MatcherSet {
136 pub fn compile(specs: &[MatcherSpec]) -> Result<Self, MatcherError> {
138 let matchers = specs
139 .iter()
140 .map(Matcher::compile)
141 .collect::<Result<Vec<_>, _>>()?;
142 Ok(MatcherSet { matchers })
143 }
144
145 pub fn is_empty(&self) -> bool {
147 self.matchers.is_empty()
148 }
149
150 pub fn matches(&self, result: &EvaluationResult) -> bool {
153 !self.matchers.is_empty() && self.matchers.iter().all(|m| m.matches(result))
154 }
155
156 pub fn to_specs(&self) -> Vec<MatcherSpec> {
158 self.matchers.iter().map(Matcher::to_spec).collect()
159 }
160}
161
162fn value_to_string(value: Value) -> String {
164 match value {
165 Value::String(s) => s,
166 other => other.to_string(),
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use rsigma_eval::{DetectionBody, EvaluationResult, FieldMatch, ResultBody, RuleHeader};
174 use rsigma_parser::Level;
175 use std::collections::HashMap;
176 use std::sync::Arc;
177
178 fn detection(ip: &str, level: Level) -> EvaluationResult {
179 EvaluationResult {
180 header: RuleHeader {
181 rule_title: "t".to_string(),
182 rule_id: Some("rule-1".to_string()),
183 level: Some(level),
184 tags: vec![],
185 custom_attributes: Arc::new(HashMap::new()),
186 enrichments: None,
187 },
188 body: ResultBody::Detection(DetectionBody {
189 matched_selections: vec![],
190 matched_fields: vec![FieldMatch::new("SourceIp", serde_json::json!(ip))],
191 event: None,
192 }),
193 }
194 }
195
196 fn spec(selector: &str, op: MatchOp, value: &str) -> MatcherSpec {
197 MatcherSpec {
198 selector: selector.to_string(),
199 op,
200 value: value.to_string(),
201 }
202 }
203
204 #[test]
205 fn eq_and_ne() {
206 let m = Matcher::compile(&spec("match.SourceIp", MatchOp::Eq, "10.0.0.1")).unwrap();
207 assert!(m.matches(&detection("10.0.0.1", Level::High)));
208 assert!(!m.matches(&detection("10.0.0.2", Level::High)));
209 let n = Matcher::compile(&spec("match.SourceIp", MatchOp::Ne, "10.0.0.1")).unwrap();
210 assert!(!n.matches(&detection("10.0.0.1", Level::High)));
211 assert!(n.matches(&detection("10.0.0.2", Level::High)));
212 }
213
214 #[test]
215 fn regex_is_anchored() {
216 let m =
217 Matcher::compile(&spec("match.SourceIp", MatchOp::ReMatch, r"10\.0\.0\.\d+")).unwrap();
218 assert!(m.matches(&detection("10.0.0.5", Level::High)));
219 assert!(!m.matches(&detection("10.0.0.5x", Level::High)));
221 }
222
223 #[test]
224 fn set_ands_matchers() {
225 let set = MatcherSet::compile(&[
226 spec("match.SourceIp", MatchOp::Eq, "10.0.0.1"),
227 spec("level", MatchOp::Eq, "high"),
228 ])
229 .unwrap();
230 assert!(set.matches(&detection("10.0.0.1", Level::High)));
231 assert!(!set.matches(&detection("10.0.0.1", Level::Low)));
232 assert!(!set.matches(&detection("10.0.0.2", Level::High)));
233 }
234
235 #[test]
236 fn empty_set_never_matches() {
237 let set = MatcherSet::default();
238 assert!(!set.matches(&detection("10.0.0.1", Level::High)));
239 }
240
241 #[test]
242 fn bad_regex_is_rejected() {
243 let err = Matcher::compile(&spec("rule", MatchOp::ReMatch, "(")).unwrap_err();
244 assert!(matches!(err, MatcherError::Regex { .. }));
245 }
246}