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.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}