1use std::collections::HashSet;
26
27use rsigma_parser::SelectorPattern;
28
29use crate::hir::{IrCondition, IrDetection, IrDetectionItem, IrRule};
30
31pub fn optimize_rule(mut rule: IrRule) -> IrRule {
34 rule.conditions = rule.conditions.into_iter().map(flatten_condition).collect();
35 eliminate_dead_detections(&mut rule);
36 rule
37}
38
39pub fn flatten_condition(cond: IrCondition) -> IrCondition {
53 match cond {
54 IrCondition::And(children) => flatten_bool(children, true),
55 IrCondition::Or(children) => flatten_bool(children, false),
56 IrCondition::Not(inner) => match flatten_condition(*inner) {
57 IrCondition::Not(doubly) => *doubly,
58 other => IrCondition::Not(Box::new(other)),
59 },
60 leaf @ (IrCondition::Detection(_) | IrCondition::Selector { .. }) => leaf,
61 }
62}
63
64fn flatten_bool(children: Vec<IrCondition>, is_and: bool) -> IrCondition {
65 let mut flat: Vec<IrCondition> = Vec::with_capacity(children.len());
66 for child in children {
67 match flatten_condition(child) {
68 IrCondition::And(inner) if is_and => flat.extend(inner),
69 IrCondition::Or(inner) if !is_and => flat.extend(inner),
70 other => flat.push(other),
71 }
72 }
73
74 let mut deduped: Vec<IrCondition> = Vec::with_capacity(flat.len());
76 for c in flat {
77 if !deduped.contains(&c) {
78 deduped.push(c);
79 }
80 }
81
82 if deduped.len() == 1 {
83 return deduped.into_iter().next().unwrap();
84 }
85 if is_and {
86 IrCondition::And(deduped)
87 } else {
88 IrCondition::Or(deduped)
89 }
90}
91
92pub fn eliminate_dead_detections(rule: &mut IrRule) {
104 prune_named(&mut rule.detections, &rule.conditions);
105 for det in rule.detections.values_mut() {
106 prune_detection_tree(det);
107 }
108}
109
110fn prune_named(
111 named: &mut std::collections::HashMap<String, IrDetection>,
112 conditions: &[IrCondition],
113) {
114 let names: Vec<String> = named.keys().cloned().collect();
115 let mut live: HashSet<String> = HashSet::new();
116 for cond in conditions {
117 collect_referenced(cond, &names, &mut live);
118 }
119 named.retain(|name, _| live.contains(name));
120}
121
122fn collect_referenced(cond: &IrCondition, names: &[String], out: &mut HashSet<String>) {
123 match cond {
124 IrCondition::Detection(name) => {
125 out.insert(name.clone());
126 }
127 IrCondition::And(children) | IrCondition::Or(children) => {
128 for c in children {
129 collect_referenced(c, names, out);
130 }
131 }
132 IrCondition::Not(inner) => collect_referenced(inner, names, out),
133 IrCondition::Selector { pattern, .. } => {
134 for name in names {
135 if selector_matches(pattern, name) {
136 out.insert(name.clone());
137 }
138 }
139 }
140 }
141}
142
143fn selector_matches(pattern: &SelectorPattern, name: &str) -> bool {
144 pattern.matches_detection_name(name)
145}
146
147fn prune_detection_tree(det: &mut IrDetection) {
150 match det {
151 IrDetection::Conditional { named, condition } => {
152 prune_named(named, std::slice::from_ref(condition));
153 for inner in named.values_mut() {
154 prune_detection_tree(inner);
155 }
156 }
157 IrDetection::AnyOf(children) | IrDetection::And(children) => {
158 for child in children {
159 prune_detection_tree(child);
160 }
161 }
162 IrDetection::ArrayMatch { body, .. } => prune_detection_tree(body),
163 IrDetection::AllOf(_) | IrDetection::Keywords(_) => {}
164 }
165}
166
167#[derive(Debug, Clone, PartialEq)]
173pub struct RepeatedItem {
174 pub item: IrDetectionItem,
175 pub count: usize,
176}
177
178#[derive(Debug, Clone, PartialEq, Default)]
180pub struct CseReport {
181 pub repeated_items: Vec<RepeatedItem>,
183}
184
185pub fn common_subexpressions(rule: &IrRule) -> CseReport {
192 let mut items: Vec<&IrDetectionItem> = Vec::new();
193 for det in rule.detections.values() {
194 collect_items(det, &mut items);
195 }
196
197 let mut counts: Vec<(IrDetectionItem, usize)> = Vec::new();
198 for it in items {
199 if let Some(entry) = counts.iter_mut().find(|(existing, _)| existing == it) {
200 entry.1 += 1;
201 } else {
202 counts.push((it.clone(), 1));
203 }
204 }
205
206 let repeated_items = counts
207 .into_iter()
208 .filter(|(_, count)| *count > 1)
209 .map(|(item, count)| RepeatedItem { item, count })
210 .collect();
211
212 CseReport { repeated_items }
213}
214
215fn collect_items<'a>(det: &'a IrDetection, out: &mut Vec<&'a IrDetectionItem>) {
216 match det {
217 IrDetection::AllOf(items) => out.extend(items.iter()),
218 IrDetection::AnyOf(children) | IrDetection::And(children) => {
219 for child in children {
220 collect_items(child, out);
221 }
222 }
223 IrDetection::ArrayMatch { body, .. } => collect_items(body, out),
224 IrDetection::Conditional { named, .. } => {
225 for inner in named.values() {
226 collect_items(inner, out);
227 }
228 }
229 IrDetection::Keywords(_) => {}
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use std::collections::HashMap;
236
237 use rsigma_parser::{LogSource, Quantifier, SelectorPattern};
238
239 use super::*;
240 use crate::hir::{IrMatcher, IrPattern, IrPatternPart, IrRuleMetadata, IrStrOp};
241
242 fn cond_det(name: &str) -> IrCondition {
243 IrCondition::Detection(name.to_string())
244 }
245
246 fn str_item(field: &str, literal: &str) -> IrDetectionItem {
247 IrDetectionItem {
248 field: Some(field.to_string()),
249 matcher: IrMatcher::Str {
250 op: IrStrOp::Exact,
251 pattern: IrPattern {
252 parts: vec![IrPatternPart::Literal(literal.to_string())],
253 },
254 case_insensitive: true,
255 },
256 exists: None,
257 }
258 }
259
260 fn rule(detections: HashMap<String, IrDetection>, conditions: Vec<IrCondition>) -> IrRule {
261 IrRule {
262 metadata: IrRuleMetadata::default(),
263 logsource: LogSource::default(),
264 sigma_version: None,
265 detections,
266 conditions,
267 }
268 }
269
270 #[test]
271 fn flatten_merges_nested_same_kind() {
272 let cond = IrCondition::And(vec![
273 cond_det("a"),
274 IrCondition::And(vec![cond_det("b"), cond_det("c")]),
275 ]);
276 assert_eq!(
277 flatten_condition(cond),
278 IrCondition::And(vec![cond_det("a"), cond_det("b"), cond_det("c")])
279 );
280 }
281
282 #[test]
283 fn flatten_dedups_idempotent_siblings() {
284 let cond = IrCondition::Or(vec![cond_det("a"), cond_det("a"), cond_det("b")]);
285 assert_eq!(
286 flatten_condition(cond),
287 IrCondition::Or(vec![cond_det("a"), cond_det("b")])
288 );
289 }
290
291 #[test]
292 fn flatten_unwraps_singletons_and_double_not() {
293 assert_eq!(
294 flatten_condition(IrCondition::And(vec![cond_det("only")])),
295 cond_det("only")
296 );
297 assert_eq!(
298 flatten_condition(IrCondition::Not(Box::new(IrCondition::Not(Box::new(
299 cond_det("x")
300 ))))),
301 cond_det("x")
302 );
303 }
304
305 #[test]
306 fn flatten_preserves_selector_leaves() {
307 let cond = IrCondition::Selector {
308 quantifier: Quantifier::All,
309 pattern: SelectorPattern::Them,
310 };
311 assert_eq!(flatten_condition(cond.clone()), cond);
312 }
313
314 #[test]
315 fn dead_elimination_drops_unreferenced() {
316 let mut detections = HashMap::new();
317 detections.insert(
318 "used".to_string(),
319 IrDetection::AllOf(vec![str_item("a", "x")]),
320 );
321 detections.insert(
322 "orphan".to_string(),
323 IrDetection::AllOf(vec![str_item("b", "y")]),
324 );
325 let mut r = rule(detections, vec![cond_det("used")]);
326 eliminate_dead_detections(&mut r);
327 assert!(r.detections.contains_key("used"));
328 assert!(!r.detections.contains_key("orphan"));
329 }
330
331 #[test]
332 fn dead_elimination_keeps_selector_matched() {
333 let mut detections = HashMap::new();
334 detections.insert(
335 "selection_a".to_string(),
336 IrDetection::AllOf(vec![str_item("a", "x")]),
337 );
338 detections.insert(
339 "_internal".to_string(),
340 IrDetection::AllOf(vec![str_item("b", "y")]),
341 );
342 let mut r = rule(
344 detections,
345 vec![IrCondition::Selector {
346 quantifier: Quantifier::All,
347 pattern: SelectorPattern::Them,
348 }],
349 );
350 eliminate_dead_detections(&mut r);
351 assert!(r.detections.contains_key("selection_a"));
352 assert!(!r.detections.contains_key("_internal"));
353 }
354
355 #[test]
356 fn cse_reports_repeated_items() {
357 let mut detections = HashMap::new();
358 detections.insert(
359 "sel1".to_string(),
360 IrDetection::AllOf(vec![str_item("Image", "\\cmd.exe")]),
361 );
362 detections.insert(
363 "sel2".to_string(),
364 IrDetection::AllOf(vec![str_item("Image", "\\cmd.exe")]),
365 );
366 let r = rule(
367 detections,
368 vec![IrCondition::Or(vec![cond_det("sel1"), cond_det("sel2")])],
369 );
370 let report = common_subexpressions(&r);
371 assert_eq!(report.repeated_items.len(), 1);
372 assert_eq!(report.repeated_items[0].count, 2);
373 assert_eq!(
374 report.repeated_items[0].item,
375 str_item("Image", "\\cmd.exe")
376 );
377 }
378}