Skip to main content

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.len() >= prefix.len() + suffix.len()
46            && name.starts_with(prefix)
47            && name.ends_with(suffix);
48    }
49    pattern == name
50}
51
52impl SelectorPattern {
53    /// Return true if this selector pattern matches a detection identifier.
54    ///
55    /// Identifiers beginning with `_` are hidden from `them` and from every
56    /// pattern that does not itself begin with `_`, as in the Sigma
57    /// specification and pySigma. Otherwise
58    /// [`SelectorPattern::Pattern`] dispatches through
59    /// [`detection_name_matches`].
60    pub fn matches_detection_name(&self, name: &str) -> bool {
61        match self {
62            SelectorPattern::Them => !name.starts_with('_'),
63            SelectorPattern::Pattern(pat) => {
64                (pat.starts_with('_') || !name.starts_with('_'))
65                    && detection_name_matches(pat, name)
66            }
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn star_only_matches_anything() {
77        assert!(detection_name_matches("*", "anything"));
78        assert!(detection_name_matches("*", ""));
79    }
80
81    #[test]
82    fn star_suffix_matches_prefix() {
83        assert!(detection_name_matches("selection_*", "selection_main"));
84        assert!(detection_name_matches("selection_*", "selection_"));
85        assert!(!detection_name_matches("selection_*", "filter_main"));
86    }
87
88    #[test]
89    fn star_prefix_matches_suffix() {
90        assert!(detection_name_matches("*_main", "selection_main"));
91        assert!(!detection_name_matches("*_main", "selection_alt"));
92    }
93
94    #[test]
95    fn star_middle_matches_prefix_and_suffix() {
96        // The regression that previously diverged between eval and convert:
97        // eval did not implement the middle `*` branch, so the same selector
98        // pattern resolved to different detection sets in the two crates.
99        assert!(detection_name_matches("sel*main", "selection_main"));
100        assert!(!detection_name_matches("sel*main", "filter_main"));
101        assert!(!detection_name_matches("sel*main", "selection_alt"));
102    }
103
104    #[test]
105    fn star_middle_prefix_and_suffix_do_not_overlap() {
106        assert!(!detection_name_matches("sel*lection", "selection"));
107        assert!(detection_name_matches("sel*lection", "sellection"));
108        assert!(!detection_name_matches("a_*_main", "a_main"));
109        assert!(detection_name_matches("a_*_main", "a__main"));
110    }
111
112    #[test]
113    fn exact_match_without_star() {
114        assert!(detection_name_matches("selection", "selection"));
115        assert!(!detection_name_matches("selection", "filter"));
116        assert!(!detection_name_matches("selection", "selection_main"));
117    }
118
119    #[test]
120    fn underscore_pattern_is_literal() {
121        // A leading underscore in the glob is an ordinary character; hiding
122        // `_` identifiers is the selector's job, not the glob's.
123        assert!(detection_name_matches("_helper", "_helper"));
124        assert!(!detection_name_matches("_helper", "helper"));
125        assert!(detection_name_matches("*", "_helper"));
126    }
127
128    #[test]
129    fn selector_pattern_them_skips_underscore_names() {
130        let them = SelectorPattern::Them;
131        assert!(them.matches_detection_name("selection"));
132        assert!(!them.matches_detection_name("_internal"));
133    }
134
135    #[test]
136    fn selector_pattern_pattern_uses_glob() {
137        let pat = SelectorPattern::Pattern("selection_*".to_string());
138        assert!(pat.matches_detection_name("selection_main"));
139        assert!(!pat.matches_detection_name("filter_main"));
140    }
141
142    #[test]
143    fn selector_pattern_skips_underscore_names_unless_it_starts_with_one() {
144        let star = SelectorPattern::Pattern("*".to_string());
145        assert!(star.matches_detection_name("selection"));
146        assert!(!star.matches_detection_name("_internal"));
147        let suffix = SelectorPattern::Pattern("*_main".to_string());
148        assert!(!suffix.matches_detection_name("_sel_main"));
149
150        let internal = SelectorPattern::Pattern("_internal".to_string());
151        assert!(internal.matches_detection_name("_internal"));
152        let hidden = SelectorPattern::Pattern("_*".to_string());
153        assert!(hidden.matches_detection_name("_internal"));
154        assert!(!hidden.matches_detection_name("selection"));
155    }
156}