rsigma_parser/selector.rs
1//! Detection-name glob matching for `... of selection_*` selector expressions.
2//!
3//! The selector matcher is shared by the parser, the evaluator, and the
4//! converter so that a pattern like `sel*main` resolves to the same set of
5//! detection identifiers regardless of which subsystem expands it. Keeping the
6//! semantics in one place also avoids the historical drift between `eval` and
7//! `convert`, where `convert` supported a middle `*` (`sel*main`) but `eval`
8//! did not.
9
10use crate::ast::SelectorPattern;
11
12/// Check whether a detection identifier matches a glob `pattern`.
13///
14/// A single `*` is treated as a wildcard. The supported shapes are:
15///
16/// - `*` — match any identifier
17/// - `selection_*` — match identifiers starting with `selection_`
18/// - `*_main` — match identifiers ending with `_main`
19/// - `sel*main` — match identifiers starting with `sel` and ending with `main`
20/// - `selection` — exact match
21///
22/// All other characters are matched literally. The function does not interpret
23/// any other meta-character; in particular `?` is not a wildcard.
24///
25/// # Examples
26///
27/// ```
28/// use rsigma_parser::detection_name_matches;
29/// assert!(detection_name_matches("selection_*", "selection_main"));
30/// assert!(detection_name_matches("*_main", "selection_main"));
31/// assert!(detection_name_matches("sel*main", "selection_main"));
32/// assert!(!detection_name_matches("sel*main", "filter_main"));
33/// ```
34pub fn detection_name_matches(pattern: &str, name: &str) -> bool {
35 if pattern == "*" {
36 return true;
37 }
38 if let Some(prefix) = pattern.strip_suffix('*') {
39 return name.starts_with(prefix);
40 }
41 if let Some(suffix) = pattern.strip_prefix('*') {
42 return name.ends_with(suffix);
43 }
44 if let Some((prefix, suffix)) = pattern.split_once('*') {
45 return name.starts_with(prefix) && name.ends_with(suffix);
46 }
47 pattern == name
48}
49
50impl SelectorPattern {
51 /// Return true if this selector pattern matches a detection identifier.
52 ///
53 /// Identifiers beginning with `_` are conventionally hidden from `them`
54 /// expansions (matching the behavior already shared between the evaluator
55 /// and the converter). For [`SelectorPattern::Pattern`], dispatch goes
56 /// through [`detection_name_matches`].
57 pub fn matches_detection_name(&self, name: &str) -> bool {
58 match self {
59 SelectorPattern::Them => !name.starts_with('_'),
60 SelectorPattern::Pattern(pat) => detection_name_matches(pat, name),
61 }
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn star_only_matches_anything() {
71 assert!(detection_name_matches("*", "anything"));
72 assert!(detection_name_matches("*", ""));
73 }
74
75 #[test]
76 fn star_suffix_matches_prefix() {
77 assert!(detection_name_matches("selection_*", "selection_main"));
78 assert!(detection_name_matches("selection_*", "selection_"));
79 assert!(!detection_name_matches("selection_*", "filter_main"));
80 }
81
82 #[test]
83 fn star_prefix_matches_suffix() {
84 assert!(detection_name_matches("*_main", "selection_main"));
85 assert!(!detection_name_matches("*_main", "selection_alt"));
86 }
87
88 #[test]
89 fn star_middle_matches_prefix_and_suffix() {
90 // The regression that previously diverged between eval and convert:
91 // eval did not implement the middle `*` branch, so the same selector
92 // pattern resolved to different detection sets in the two crates.
93 assert!(detection_name_matches("sel*main", "selection_main"));
94 assert!(!detection_name_matches("sel*main", "filter_main"));
95 assert!(!detection_name_matches("sel*main", "selection_alt"));
96 }
97
98 #[test]
99 fn exact_match_without_star() {
100 assert!(detection_name_matches("selection", "selection"));
101 assert!(!detection_name_matches("selection", "filter"));
102 assert!(!detection_name_matches("selection", "selection_main"));
103 }
104
105 #[test]
106 fn underscore_pattern_is_literal() {
107 // A leading underscore in the pattern is treated as a literal character
108 // (the `_`-prefix convention only suppresses identifiers from `them`).
109 assert!(detection_name_matches("_helper", "_helper"));
110 assert!(!detection_name_matches("_helper", "helper"));
111 }
112
113 #[test]
114 fn selector_pattern_them_skips_underscore_names() {
115 let them = SelectorPattern::Them;
116 assert!(them.matches_detection_name("selection"));
117 assert!(!them.matches_detection_name("_internal"));
118 }
119
120 #[test]
121 fn selector_pattern_pattern_uses_glob() {
122 let pat = SelectorPattern::Pattern("selection_*".to_string());
123 assert!(pat.matches_detection_name("selection_main"));
124 assert!(!pat.matches_detection_name("filter_main"));
125 // A pattern with a literal `_` prefix still applies normally; the
126 // `_`-prefix convention only matters for the `them` form.
127 let internal = SelectorPattern::Pattern("_internal".to_string());
128 assert!(internal.matches_detection_name("_internal"));
129 }
130}