dd_sds/scanner/
scope.rs

1use crate::Path;
2use serde::{Deserialize, Serialize};
3
4#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
5#[serde(rename_all = "snake_case")]
6pub(crate) enum ScanArea {
7    #[default]
8    Everything,
9    /// An empty list means the rule never runs.
10    Only(Vec<Path<'static>>),
11}
12
13/// Which fields of an event a rule looks at.
14///
15/// A scope starts with the `include` fields (either everything or a list of specific paths), then
16/// `exclude` prevents some of those from being scanned. `exclude` is useful when scanning
17/// everything except a few fields, or when including some fields but not some of their children.
18///
19/// A path covers itself and all of its children. When a field is covered by both `include`
20/// and `exclude`, `exclude` wins, even if the `include` path is more specific.
21///
22/// Excluded fields never produce a match. Regex rules may still scan them internally for
23/// multipass V0, which uses the values found there to drop identical matches elsewhere in the
24/// event as false positives.
25#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
26// Rejecting unknown fields makes a scope in an older JSON format fail loudly instead of
27// parsing as "scan everything".
28#[serde(deny_unknown_fields)]
29pub struct Scope {
30    #[serde(default)]
31    pub(crate) include: ScanArea,
32    /// These fields never produce a match (see the type docs for multipass V0).
33    #[serde(default)]
34    pub(crate) exclude: Vec<Path<'static>>,
35}
36
37impl Scope {
38    /// Covers every field of the event.
39    pub fn all() -> Self {
40        Self {
41            include: ScanArea::Everything,
42            exclude: vec![],
43        }
44    }
45
46    /// Covers fields under any `include` path, minus fields under any `exclude` path.
47    pub fn include_and_exclude(include: Vec<Path<'static>>, exclude: Vec<Path<'static>>) -> Self {
48        Self {
49            include: ScanArea::Only(include),
50            exclude,
51        }
52    }
53
54    /// Covers only fields under any `include` path. An empty `include` means the rule never runs.
55    pub fn include(include: Vec<Path<'static>>) -> Self {
56        Self {
57            include: ScanArea::Only(include),
58            exclude: vec![],
59        }
60    }
61
62    /// Covers every field, minus fields under any `exclude` path.
63    pub fn exclude(exclude: Vec<Path<'static>>) -> Self {
64        Self {
65            include: ScanArea::Everything,
66            exclude,
67        }
68    }
69}
70
71impl Default for Scope {
72    fn default() -> Self {
73        Self::all()
74    }
75}
76
77#[cfg(test)]
78mod test {
79    use super::*;
80    use serde_json::{Value, json};
81
82    fn field_path(name: &'static str) -> Path<'static> {
83        Path::from(vec![name.into()])
84    }
85
86    fn field_path_json(name: &str) -> Value {
87        json!({"segments": [{"type": "Field", "value": name}]})
88    }
89
90    fn assert_json_round_trip(scope: Scope, expected: Value) {
91        assert_eq!(serde_json::to_value(&scope).unwrap(), expected);
92        assert_eq!(serde_json::from_value::<Scope>(expected).unwrap(), scope);
93    }
94
95    #[test]
96    fn test_all_scope_json() {
97        assert_json_round_trip(
98            Scope::all(),
99            json!({"include": "everything", "exclude": []}),
100        );
101    }
102
103    #[test]
104    fn test_default_scope_is_all() {
105        assert_eq!(Scope::default(), Scope::all());
106    }
107
108    #[test]
109    fn test_include_scope_json() {
110        assert_json_round_trip(
111            Scope::include(vec![field_path("a")]),
112            json!({"include": {"only": [field_path_json("a")]}, "exclude": []}),
113        );
114    }
115
116    #[test]
117    fn test_include_nothing_scope_json_differs_from_all() {
118        assert_json_round_trip(
119            Scope::include(vec![]),
120            json!({"include": {"only": []}, "exclude": []}),
121        );
122    }
123
124    #[test]
125    fn test_exclude_scope_json() {
126        assert_json_round_trip(
127            Scope::exclude(vec![field_path("a")]),
128            json!({"include": "everything", "exclude": [field_path_json("a")]}),
129        );
130    }
131
132    #[test]
133    fn test_include_and_exclude_scope_json() {
134        assert_json_round_trip(
135            Scope::include_and_exclude(vec![field_path("a")], vec![field_path("b")]),
136            json!({"include": {"only": [field_path_json("a")]}, "exclude": [field_path_json("b")]}),
137        );
138    }
139
140    #[test]
141    fn test_missing_scope_fields_default_to_all() {
142        assert_eq!(
143            serde_json::from_value::<Scope>(json!({})).unwrap(),
144            Scope::all()
145        );
146    }
147
148    // Old-format JSON must fail loudly: with defaulted fields it would otherwise
149    // parse as "scan everything".
150    #[test]
151    fn test_old_scope_json_format_is_rejected() {
152        assert!(serde_json::from_value::<Scope>(json!({"type": "Exclude", "paths": []})).is_err());
153        assert!(
154            serde_json::from_value::<Scope>(
155                json!({"type": "Include", "paths": {"include": [], "exclude": []}})
156            )
157            .is_err()
158        );
159    }
160}