dd_sds/scoped_ruleset/
mod.rs

1mod bool_set;
2
3use crate::event::{EventVisitor, VisitStringResult};
4use crate::scanner::scope::{ScanArea, Scope};
5use crate::scoped_ruleset::bool_set::BoolSet;
6use crate::{Event, Path, PathSegment, ScannerError};
7use ahash::AHashMap;
8
9/// A `ScopedRuleSet` determines which rules will be used to scan each field of an event, and which
10/// paths are considered `excluded`.
11#[derive(Debug)]
12pub struct ScopedRuleSet {
13    tree: RuleTree,
14    // The number of rules stored in this set
15    num_rules: usize,
16    add_implicit_index_wildcards: bool,
17}
18
19impl ScopedRuleSet {
20    /// The scopes for each rule. The indices of the scopes MUST match the rule index.
21    pub fn new(rules_scopes: &[Scope]) -> Self {
22        let mut tree = RuleTree::new();
23
24        for (rule_index, scope) in rules_scopes.iter().enumerate() {
25            match &scope.include {
26                ScanArea::Everything => tree.insert_rule_add(&Path::root(), rule_index),
27                ScanArea::Only(paths) => {
28                    for path in paths {
29                        tree.insert_rule_add(path, rule_index);
30                    }
31                }
32            }
33            for path in &scope.exclude {
34                tree.insert_rule_removal(path, rule_index);
35            }
36        }
37        Self {
38            tree,
39            num_rules: rules_scopes.len(),
40            add_implicit_index_wildcards: false,
41        }
42    }
43
44    pub fn with_implicit_index_wildcards(mut self, value: bool) -> Self {
45        self.add_implicit_index_wildcards = value;
46        self
47    }
48
49    pub fn visit_string_rule_combinations<'path, 'c: 'path>(
50        &'c self,
51        event: &'path mut impl Event,
52        content_visitor: impl ContentVisitor<'path>,
53    ) -> Result<(), ScannerError> {
54        let bool_set = if self.add_implicit_index_wildcards {
55            Some(BoolSet::new(self.num_rules))
56        } else {
57            None
58        };
59        let mut visitor = ScopedRuledSetEventVisitor {
60            content_visitor,
61            tree_nodes: vec![ActiveRuleTree {
62                rule_tree: &self.tree,
63                index_wildcard_match: false,
64            }],
65            active_node_counter: vec![NodeCounter {
66                active_tree_count: 1,
67            }],
68            path: Path::root(),
69            bool_set,
70            add_implicit_index_wildcards: self.add_implicit_index_wildcards,
71            // should_keywords_match_event_paths: self.should_keywords_match_event_paths,
72        };
73
74        event.visit_event(&mut visitor)
75    }
76}
77
78pub struct ExclusionCheck<'a> {
79    tree_nodes: &'a [ActiveRuleTree<'a>],
80}
81
82impl ExclusionCheck<'_> {
83    pub fn is_excluded(&self, rule_index: usize) -> bool {
84        for include_node in self.tree_nodes {
85            for change in &include_node.rule_tree.rule_changes {
86                match change {
87                    RuleChange::Add(_) => { /* ignore */ }
88                    RuleChange::Remove(i) => {
89                        if *i == rule_index {
90                            return true;
91                        }
92                    }
93                }
94            }
95        }
96        false
97    }
98}
99
100pub trait ContentVisitor<'path> {
101    fn visit_content<'content_visitor>(
102        &'content_visitor mut self,
103        path: &Path<'path>,
104        content: &str,
105        rules: RuleIndexVisitor,
106        is_excluded: ExclusionCheck<'content_visitor>,
107    ) -> Result<bool, ScannerError>;
108}
109
110// This is just a reference to a RuleTree with some additional information
111struct ActiveRuleTree<'a> {
112    rule_tree: &'a RuleTree,
113    // If this tree was pushed because of a wildcard index, indices aren't
114    // allowed to match immediately after, so they are ignored.
115    index_wildcard_match: bool,
116}
117
118struct NodeCounter {
119    // This counts how many trees are currently active, which can
120    // happen due to (implicit) wildcard segments. The last value in this
121    // list is the current number of active trees (n), which is the last
122    // n trees in `tree_nodes`.
123    active_tree_count: usize,
124}
125
126struct ScopedRuledSetEventVisitor<'a, C> {
127    // The struct that will receive content / rules.
128    content_visitor: C,
129
130    // This is a list of parent tree nodes, which is a list of all of the "active" rule changes.
131    // If an "Add" exists for a rule in this list, it will be scanned. If a single "Remove" exists, it will cause the `ExclusionCheck` to return true.
132    tree_nodes: Vec<ActiveRuleTree<'a>>,
133
134    // This is a counter that helps keep track of how many elements we have pushed
135    // In the tree_nodes list and in the true_positive_rule_idx list
136    active_node_counter: Vec<NodeCounter>,
137
138    // The current path being visited
139    path: Path<'a>,
140
141    // A re-usable boolean set to de-duplicate rules
142    bool_set: Option<BoolSet>,
143
144    add_implicit_index_wildcards: bool,
145}
146
147impl<'path, C> EventVisitor<'path> for ScopedRuledSetEventVisitor<'path, C>
148where
149    C: ContentVisitor<'path>,
150{
151    fn push_segment(&mut self, segment: PathSegment<'path>) {
152        // update the tree
153        // The current path may go beyond what is stored in the "include" tree, so the tree is only updated if they are at the same height.
154
155        let num_active_trees = self.active_node_counter.last().unwrap().active_tree_count;
156        let tree_nodes_len = self.tree_nodes.len();
157        let active_trees_range = tree_nodes_len - num_active_trees..tree_nodes_len;
158
159        for tree_index in active_trees_range {
160            if (!self.tree_nodes[tree_index].index_wildcard_match || !segment.is_index())
161                && let Some(child) = self.tree_nodes[tree_index].rule_tree.children.get(&segment)
162            {
163                self.tree_nodes.push(ActiveRuleTree {
164                    rule_tree: child,
165                    index_wildcard_match: false,
166                });
167            }
168
169            if self.add_implicit_index_wildcards && segment.is_index() {
170                // Optionally skip the index (it acts as a wildcard) by
171                // pushing the same tree back onto the stack.
172                self.tree_nodes.push(ActiveRuleTree {
173                    rule_tree: self.tree_nodes[tree_index].rule_tree,
174                    index_wildcard_match: true,
175                });
176            }
177        }
178
179        // The new number of active trees is the number of new trees pushed
180        self.active_node_counter.push(NodeCounter {
181            active_tree_count: self.tree_nodes.len() - tree_nodes_len,
182        });
183
184        self.path.segments.push(segment);
185    }
186
187    fn pop_segment(&mut self) {
188        let node_counter = self.active_node_counter.pop().unwrap();
189        for _ in 0..node_counter.active_tree_count {
190            // The rules from the last node are no longer active, so remove them.
191            let _popped = self.tree_nodes.pop();
192        }
193        self.path.segments.pop();
194    }
195
196    fn visit_string<'s>(
197        &'s mut self,
198        value: &str,
199    ) -> Result<VisitStringResult<'s, 'path>, ScannerError> {
200        let will_mutate = self.content_visitor.visit_content(
201            &self.path,
202            value,
203            RuleIndexVisitor {
204                tree_nodes: &self.tree_nodes,
205                used_rule_set: self.bool_set.as_mut(),
206            },
207            ExclusionCheck {
208                tree_nodes: &self.tree_nodes,
209            },
210        );
211        if let Some(bool_set) = &mut self.bool_set {
212            bool_set.reset();
213        }
214        will_mutate.map(|will_mutate| VisitStringResult {
215            might_mutate: will_mutate,
216            path: &self.path,
217        })
218    }
219}
220
221pub struct RuleIndexVisitor<'a> {
222    tree_nodes: &'a Vec<ActiveRuleTree<'a>>,
223    used_rule_set: Option<&'a mut BoolSet>,
224}
225
226impl RuleIndexVisitor<'_> {
227    /// Visits all rules associated with the current string. This may
228    /// potentially return no rule indices at all.
229    pub fn visit_rule_indices(
230        &mut self,
231        mut visit: impl FnMut(usize) -> Result<(), ScannerError>,
232    ) -> Result<(), ScannerError> {
233        // visit rules with an `Include` scope
234        for include_node in self.tree_nodes {
235            if include_node.index_wildcard_match {
236                // This is guaranteed to be a duplicated node. Skip it
237                continue;
238            }
239            for change in &include_node.rule_tree.rule_changes {
240                match change {
241                    RuleChange::Add(rule_index) => {
242                        if let Some(used_rule_set) = &mut self.used_rule_set {
243                            if !used_rule_set.get_and_set(*rule_index) {
244                                (visit)(*rule_index)?;
245                            }
246                        } else {
247                            (visit)(*rule_index)?;
248                        }
249                    }
250                    RuleChange::Remove(_) => { /* Nothing to do here */ }
251                }
252            }
253        }
254        Ok(())
255    }
256}
257
258#[derive(Clone, Copy, PartialEq, Debug)]
259enum RuleChange {
260    Add(usize),
261    Remove(usize),
262}
263
264#[derive(Clone, Debug)]
265struct RuleTree {
266    rule_changes: Vec<RuleChange>,
267    children: AHashMap<PathSegment<'static>, RuleTree>,
268}
269
270impl RuleTree {
271    pub fn new() -> Self {
272        Self {
273            rule_changes: vec![],
274            children: AHashMap::new(),
275        }
276    }
277
278    pub fn insert_rule_add(&mut self, path: &Path<'static>, rule_index: usize) {
279        self.insert_rule_inner(&path.segments, RuleChange::Add(rule_index));
280    }
281
282    pub fn insert_rule_removal(&mut self, path: &Path<'static>, rule_index: usize) {
283        self.insert_rule_inner(&path.segments, RuleChange::Remove(rule_index));
284    }
285
286    fn insert_rule_inner(&mut self, path: &[PathSegment<'static>], rule_change: RuleChange) {
287        if self.rule_changes.contains(&rule_change) {
288            return;
289        }
290        if let Some((first, remaining)) = path.split_first() {
291            let child_tree = self
292                .children
293                .entry(first.clone())
294                .or_insert_with(RuleTree::new);
295            child_tree.insert_rule_inner(remaining, rule_change);
296        } else {
297            // remove any recursive children, since they will be duplicated
298            self.recursively_remove(rule_change);
299            self.rule_changes.push(rule_change);
300        }
301    }
302
303    // Remove the given change from all children (recursive). This is used
304    // to remove duplicates.
305    fn recursively_remove(&mut self, rule_change: RuleChange) {
306        self.rule_changes.retain(|x| *x != rule_change);
307        for child_tree in self.children.values_mut() {
308            child_tree.recursively_remove(rule_change);
309        }
310    }
311}
312
313#[cfg(test)]
314mod test {
315    use crate::simple_event::SimpleEvent;
316
317    use super::*;
318
319    #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
320    struct VisitedPath {
321        path: Path<'static>,
322        content: String,
323        rules: Vec<VisitedRule>,
324    }
325
326    #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
327    struct VisitedRule {
328        rule_index: usize,
329        is_excluded: bool,
330    }
331
332    #[derive(Debug, PartialEq, Eq)]
333    struct Visited {
334        paths: Vec<VisitedPath>,
335    }
336
337    // Visits the event and returns the paths that were visited
338    fn visit_event(event: &mut impl Event, ruleset: &ScopedRuleSet) -> Visited {
339        let mut visited = Visited { paths: vec![] };
340
341        struct RecordingContentVisitor<'a> {
342            visited: &'a mut Visited,
343        }
344
345        impl<'a> ContentVisitor<'a> for RecordingContentVisitor<'a> {
346            fn visit_content<'content_visitor>(
347                &'content_visitor mut self,
348                path: &Path<'a>,
349                content: &str,
350                mut rule_iter: RuleIndexVisitor,
351                exclusion_check: ExclusionCheck<'content_visitor>,
352            ) -> Result<bool, ScannerError> {
353                let mut rules = vec![];
354                rule_iter.visit_rule_indices(|rule_index| {
355                    rules.push(VisitedRule {
356                        rule_index,
357                        is_excluded: exclusion_check.is_excluded(rule_index),
358                    });
359                    Ok(())
360                })?;
361                rules.sort();
362                self.visited.paths.push(VisitedPath {
363                    path: path.into_static(),
364                    content: content.to_string(),
365                    rules,
366                });
367                Ok(true)
368            }
369        }
370
371        ruleset
372            .visit_string_rule_combinations(
373                event,
374                RecordingContentVisitor {
375                    visited: &mut visited,
376                },
377            )
378            .unwrap();
379        visited.paths.sort();
380        visited
381    }
382
383    #[test]
384    fn test_inclusive_scopes() {
385        // Fields are scanned as long as they are a child of any `include` path
386        let ruleset = ScopedRuleSet::new(&[
387            Scope::include(vec![Path::from(vec!["a".into()])]),
388            Scope::include(vec![Path::from(vec!["a".into(), "b".into()])]),
389            Scope::include(vec![]),
390        ]);
391
392        let mut event = SimpleEvent::Map(
393            [
394                (
395                    "a".into(),
396                    SimpleEvent::Map(
397                        [
398                            ("b".into(), SimpleEvent::String("value-ab".into())),
399                            ("c".into(), SimpleEvent::String("value-ac".into())),
400                        ]
401                        .into(),
402                    ),
403                ),
404                ("d".into(), SimpleEvent::String("value-d".into())),
405            ]
406            .into(),
407        );
408
409        let paths = visit_event(&mut event, &ruleset);
410
411        assert_eq!(
412            paths,
413            Visited {
414                paths: vec![
415                    VisitedPath {
416                        path: Path::from(vec!["a".into(), "b".into()]),
417                        content: "value-ab".into(),
418                        rules: vec![
419                            VisitedRule {
420                                rule_index: 0,
421                                is_excluded: false
422                            },
423                            VisitedRule {
424                                rule_index: 1,
425                                is_excluded: false
426                            }
427                        ]
428                    },
429                    VisitedPath {
430                        path: Path::from(vec!["a".into(), "c".into()]),
431                        content: "value-ac".into(),
432                        rules: vec![VisitedRule {
433                            rule_index: 0,
434                            is_excluded: false
435                        }]
436                    },
437                    VisitedPath {
438                        path: Path::from(vec!["d".into()]),
439                        content: "value-d".into(),
440                        rules: vec![]
441                    }
442                ],
443            }
444        );
445    }
446
447    #[test]
448    fn test_inclusive_scopes_array() {
449        // Fields are scanned as long as they are a child of any `include` path
450        let ruleset = ScopedRuleSet::new(&[
451            Scope::include(vec![Path::from(vec![0.into()])]),
452            Scope::include(vec![Path::from(vec![1.into(), 0.into()])]),
453            Scope::include(vec![Path::from(vec![2.into(), 0.into()])]),
454        ]);
455
456        let mut event = SimpleEvent::List(vec![
457            SimpleEvent::String("value-0".into()),
458            SimpleEvent::String("value-1".into()),
459            SimpleEvent::List(vec![SimpleEvent::String("value-2-0".into())]),
460            SimpleEvent::String("value-3".into()),
461        ]);
462
463        let paths = visit_event(&mut event, &ruleset);
464
465        assert_eq!(
466            paths,
467            Visited {
468                paths: vec![
469                    VisitedPath {
470                        path: Path::from(vec![0.into()]),
471                        content: "value-0".into(),
472                        rules: vec![VisitedRule {
473                            rule_index: 0,
474                            is_excluded: false
475                        }]
476                    },
477                    VisitedPath {
478                        path: Path::from(vec![1.into()]),
479                        content: "value-1".into(),
480                        rules: vec![]
481                    },
482                    VisitedPath {
483                        path: Path::from(vec![2.into(), 0.into()]),
484                        content: "value-2-0".into(),
485                        rules: vec![VisitedRule {
486                            rule_index: 2,
487                            is_excluded: false
488                        }]
489                    },
490                    VisitedPath {
491                        path: Path::from(vec![3.into()]),
492                        content: "value-3".into(),
493                        rules: vec![]
494                    }
495                ],
496            }
497        );
498    }
499
500    #[test]
501    fn test_exclusive_scopes() {
502        // All fields are scanned, but fields that are children of any `exclude` path are marked as excluded
503        let ruleset = ScopedRuleSet::new(&[
504            Scope::exclude(vec![Path::from(vec!["a".into()])]),
505            Scope::exclude(vec![Path::from(vec!["a".into(), "b".into()])]),
506            // matches everything (it will always be excluded)
507            Scope::all(),
508        ]);
509
510        let mut event = SimpleEvent::Map(
511            [
512                (
513                    "a".into(),
514                    SimpleEvent::Map(
515                        [
516                            ("b".into(), SimpleEvent::String("value-ab".into())),
517                            ("c".into(), SimpleEvent::String("value-ac".into())),
518                        ]
519                        .into(),
520                    ),
521                ),
522                ("d".into(), SimpleEvent::String("value-d".into())),
523                ("e".into(), SimpleEvent::List(vec![])),
524            ]
525            .into(),
526        );
527
528        let paths = visit_event(&mut event, &ruleset);
529
530        assert_eq!(
531            paths,
532            Visited {
533                paths: vec![
534                    VisitedPath {
535                        path: Path::from(vec!["a".into(), "b".into()]),
536                        content: "value-ab".into(),
537                        rules: vec![
538                            VisitedRule {
539                                rule_index: 0,
540                                is_excluded: true
541                            },
542                            VisitedRule {
543                                rule_index: 1,
544                                is_excluded: true
545                            },
546                            VisitedRule {
547                                rule_index: 2,
548                                is_excluded: false
549                            }
550                        ]
551                    },
552                    VisitedPath {
553                        path: Path::from(vec!["a".into(), "c".into()]),
554                        content: "value-ac".into(),
555                        rules: vec![
556                            VisitedRule {
557                                rule_index: 0,
558                                is_excluded: true
559                            },
560                            VisitedRule {
561                                rule_index: 1,
562                                is_excluded: false
563                            },
564                            VisitedRule {
565                                rule_index: 2,
566                                is_excluded: false
567                            }
568                        ]
569                    },
570                    VisitedPath {
571                        path: Path::from(vec!["d".into()]),
572                        content: "value-d".into(),
573                        rules: vec![
574                            VisitedRule {
575                                rule_index: 0,
576                                is_excluded: false
577                            },
578                            VisitedRule {
579                                rule_index: 1,
580                                is_excluded: false
581                            },
582                            VisitedRule {
583                                rule_index: 2,
584                                is_excluded: false
585                            }
586                        ]
587                    }
588                ],
589            },
590        );
591    }
592
593    #[test]
594    fn test_exclusive_scopes_array() {
595        // All fields are scanned, but fields that are children of any `exclude` path are marked as excluded
596        let ruleset = ScopedRuleSet::new(&[
597            Scope::exclude(vec![Path::from(vec![0.into()])]),
598            Scope::exclude(vec![Path::from(vec![1.into(), 0.into()])]),
599            Scope::exclude(vec![Path::from(vec![2.into(), 0.into()])]),
600        ]);
601
602        let mut event = SimpleEvent::List(vec![
603            SimpleEvent::String("value-0".into()),
604            SimpleEvent::String("value-1".into()),
605            SimpleEvent::List(vec![SimpleEvent::String("value-2-0".into())]),
606            SimpleEvent::String("value-3".into()),
607        ]);
608
609        let paths = visit_event(&mut event, &ruleset);
610
611        assert_eq!(
612            paths,
613            Visited {
614                paths: vec![
615                    VisitedPath {
616                        path: Path::from(vec![0.into()]),
617                        content: "value-0".into(),
618                        rules: vec![
619                            VisitedRule {
620                                rule_index: 0,
621                                is_excluded: true
622                            },
623                            VisitedRule {
624                                rule_index: 1,
625                                is_excluded: false
626                            },
627                            VisitedRule {
628                                rule_index: 2,
629                                is_excluded: false
630                            }
631                        ]
632                    },
633                    VisitedPath {
634                        path: Path::from(vec![1.into()]),
635                        content: "value-1".into(),
636                        rules: vec![
637                            VisitedRule {
638                                rule_index: 0,
639                                is_excluded: false
640                            },
641                            VisitedRule {
642                                rule_index: 1,
643                                is_excluded: false
644                            },
645                            VisitedRule {
646                                rule_index: 2,
647                                is_excluded: false
648                            }
649                        ]
650                    },
651                    VisitedPath {
652                        path: Path::from(vec![2.into(), 0.into()]),
653                        content: "value-2-0".into(),
654                        rules: vec![
655                            VisitedRule {
656                                rule_index: 0,
657                                is_excluded: false
658                            },
659                            VisitedRule {
660                                rule_index: 1,
661                                is_excluded: false
662                            },
663                            VisitedRule {
664                                rule_index: 2,
665                                is_excluded: true
666                            }
667                        ]
668                    },
669                    VisitedPath {
670                        path: Path::from(vec![3.into()]),
671                        content: "value-3".into(),
672                        rules: vec![
673                            VisitedRule {
674                                rule_index: 0,
675                                is_excluded: false
676                            },
677                            VisitedRule {
678                                rule_index: 1,
679                                is_excluded: false
680                            },
681                            VisitedRule {
682                                rule_index: 2,
683                                is_excluded: false
684                            }
685                        ]
686                    }
687                ],
688            }
689        );
690    }
691
692    #[test]
693    fn test_include_and_exclude() {
694        // Fields that are children of any `include` path are scanned, but they are only marked as
695        // excluded if they are children of any `exclude` path
696        let ruleset = ScopedRuleSet::new(&[Scope::include_and_exclude(
697            vec![Path::from(vec![2.into()])],
698            vec![Path::from(vec![2.into(), 0.into()])],
699        )]);
700
701        let mut event = SimpleEvent::List(vec![
702            SimpleEvent::String("value-0".into()),
703            SimpleEvent::String("value-1".into()),
704            SimpleEvent::List(vec![
705                SimpleEvent::String("value-2-0".into()),
706                SimpleEvent::String("value-2-1".into()),
707            ]),
708            SimpleEvent::String("value-3".into()),
709        ]);
710
711        let paths = visit_event(&mut event, &ruleset);
712
713        assert_eq!(
714            paths,
715            Visited {
716                paths: vec![
717                    VisitedPath {
718                        path: Path::from(vec![0.into()]),
719                        content: "value-0".into(),
720                        rules: vec![]
721                    },
722                    VisitedPath {
723                        path: Path::from(vec![1.into()]),
724                        content: "value-1".into(),
725                        rules: vec![]
726                    },
727                    VisitedPath {
728                        path: Path::from(vec![2.into(), 0.into()]),
729                        content: "value-2-0".into(),
730                        rules: vec![VisitedRule {
731                            rule_index: 0,
732                            is_excluded: true
733                        }]
734                    },
735                    VisitedPath {
736                        path: Path::from(vec![2.into(), 1.into()]),
737                        content: "value-2-1".into(),
738                        rules: vec![VisitedRule {
739                            rule_index: 0,
740                            is_excluded: false
741                        }]
742                    },
743                    VisitedPath {
744                        path: Path::from(vec![3.into()]),
745                        content: "value-3".into(),
746                        rules: vec![]
747                    },
748                ]
749            }
750        );
751    }
752
753    #[test]
754    fn test_include_and_exclude_priority() {
755        // exclude paths have priority, and override any include path. (even if the include is more specific)
756
757        let ruleset = ScopedRuleSet::new(&[Scope::include_and_exclude(
758            // This include is ignored, since an exclude overrides it
759            vec![Path::from(vec![1.into(), 0.into()])],
760            vec![Path::from(vec![1.into()])],
761        )]);
762
763        let mut event = SimpleEvent::List(vec![
764            SimpleEvent::String("value-0".into()),
765            SimpleEvent::List(vec![
766                SimpleEvent::String("value-1-0".into()),
767                SimpleEvent::String("value-1-1".into()),
768            ]),
769        ]);
770
771        let paths = visit_event(&mut event, &ruleset);
772
773        assert_eq!(
774            paths,
775            Visited {
776                paths: vec![
777                    VisitedPath {
778                        path: Path::from(vec![0.into()]),
779                        content: "value-0".into(),
780                        rules: vec![]
781                    },
782                    VisitedPath {
783                        path: Path::from(vec![1.into(), 0.into()]),
784                        content: "value-1-0".into(),
785                        rules: vec![VisitedRule {
786                            rule_index: 0,
787                            is_excluded: true
788                        }]
789                    },
790                    VisitedPath {
791                        path: Path::from(vec![1.into(), 1.into()]),
792                        content: "value-1-1".into(),
793                        rules: vec![]
794                    }
795                ],
796            }
797        );
798    }
799
800    fn visited_path(path: Path<'static>, content: &str, rules: &[(usize, bool)]) -> VisitedPath {
801        VisitedPath {
802            path,
803            content: content.into(),
804            rules: rules
805                .iter()
806                .map(|&(rule_index, is_excluded)| VisitedRule {
807                    rule_index,
808                    is_excluded,
809                })
810                .collect(),
811        }
812    }
813
814    fn event_with_fields_a_and_b_c() -> SimpleEvent {
815        SimpleEvent::Map(
816            [
817                ("a".into(), SimpleEvent::String("value-a".into())),
818                (
819                    "b".into(),
820                    SimpleEvent::Map(
821                        [("c".into(), SimpleEvent::String("value-b-c".into()))].into(),
822                    ),
823                ),
824            ]
825            .into(),
826        )
827    }
828
829    #[test]
830    fn test_exclude_root_marks_every_field_excluded() {
831        let ruleset = ScopedRuleSet::new(&[Scope::exclude(vec![Path::root()])]);
832
833        let paths = visit_event(&mut event_with_fields_a_and_b_c(), &ruleset);
834
835        assert_eq!(
836            paths,
837            Visited {
838                paths: vec![
839                    visited_path(Path::from(vec!["a".into()]), "value-a", &[(0, true)]),
840                    visited_path(
841                        Path::from(vec!["b".into(), "c".into()]),
842                        "value-b-c",
843                        &[(0, true)]
844                    ),
845                ],
846            }
847        );
848    }
849
850    #[test]
851    fn test_include_and_exclude_same_path_marks_it_excluded() {
852        let a = Path::from(vec!["a".into()]);
853        let ruleset =
854            ScopedRuleSet::new(&[Scope::include_and_exclude(vec![a.clone()], vec![a.clone()])]);
855
856        let paths = visit_event(&mut event_with_fields_a_and_b_c(), &ruleset);
857
858        assert_eq!(
859            paths,
860            Visited {
861                paths: vec![
862                    visited_path(a, "value-a", &[(0, true)]),
863                    visited_path(Path::from(vec!["b".into(), "c".into()]), "value-b-c", &[]),
864                ],
865            }
866        );
867    }
868
869    #[test]
870    fn test_exclude_and_include_scopes_on_same_path_are_independent_per_rule() {
871        let a = Path::from(vec!["a".into()]);
872        let ruleset = ScopedRuleSet::new(&[
873            Scope::exclude(vec![a.clone()]),
874            Scope::include(vec![a.clone()]),
875        ]);
876
877        let paths = visit_event(&mut event_with_fields_a_and_b_c(), &ruleset);
878
879        assert_eq!(
880            paths,
881            Visited {
882                paths: vec![
883                    visited_path(a, "value-a", &[(0, true), (1, false)]),
884                    visited_path(
885                        Path::from(vec!["b".into(), "c".into()]),
886                        "value-b-c",
887                        &[(0, false)]
888                    ),
889                ],
890            }
891        );
892    }
893
894    #[test]
895    fn test_include_same_change_multiple_times() {
896        // If multiple "include" scopes cover the same field, it should only be returned once.
897
898        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![
899            Path::from(vec!["a".into()]),
900            Path::from(vec!["a".into(), "b".into()]),
901        ])]);
902
903        let mut event = SimpleEvent::Map(
904            [
905                (
906                    "a".into(),
907                    SimpleEvent::Map(
908                        [
909                            ("b".into(), SimpleEvent::String("value-ab".into())),
910                            ("c".into(), SimpleEvent::String("value-ac".into())),
911                        ]
912                        .into(),
913                    ),
914                ),
915                ("d".into(), SimpleEvent::String("value-d".into())),
916            ]
917            .into(),
918        );
919
920        let paths = visit_event(&mut event, &ruleset);
921
922        assert_eq!(
923            paths,
924            Visited {
925                paths: vec![
926                    VisitedPath {
927                        path: Path::from(vec!["a".into(), "b".into()]),
928                        content: "value-ab".into(),
929                        rules: vec![VisitedRule {
930                            rule_index: 0,
931                            is_excluded: false
932                        }]
933                    },
934                    VisitedPath {
935                        path: Path::from(vec!["a".into(), "c".into()]),
936                        content: "value-ac".into(),
937                        rules: vec![VisitedRule {
938                            rule_index: 0,
939                            is_excluded: false
940                        }]
941                    },
942                    VisitedPath {
943                        path: Path::from(vec!["d".into()]),
944                        content: "value-d".into(),
945                        rules: vec![]
946                    }
947                ],
948            }
949        );
950    }
951
952    #[test]
953    fn test_include_root_multiple_times() {
954        // This makes sure duplicate rule changes (at the root of the tree) are removed
955        let ruleset =
956            ScopedRuleSet::new(&[Scope::include(vec![Path::from(vec![]), Path::from(vec![])])]);
957
958        let mut event = SimpleEvent::Map(
959            [
960                (
961                    "a".into(),
962                    SimpleEvent::Map(
963                        [
964                            ("b".into(), SimpleEvent::String("value-ab".into())),
965                            ("c".into(), SimpleEvent::String("value-ac".into())),
966                        ]
967                        .into(),
968                    ),
969                ),
970                ("d".into(), SimpleEvent::String("value-d".into())),
971            ]
972            .into(),
973        );
974
975        let paths = visit_event(&mut event, &ruleset);
976
977        assert_eq!(
978            paths,
979            Visited {
980                paths: vec![
981                    VisitedPath {
982                        path: Path::from(vec!["a".into(), "b".into()]),
983                        content: "value-ab".into(),
984                        rules: vec![VisitedRule {
985                            rule_index: 0,
986                            is_excluded: false
987                        }]
988                    },
989                    VisitedPath {
990                        path: Path::from(vec!["a".into(), "c".into()]),
991                        content: "value-ac".into(),
992                        rules: vec![VisitedRule {
993                            rule_index: 0,
994                            is_excluded: false
995                        }]
996                    },
997                    VisitedPath {
998                        path: Path::from(vec!["d".into()]),
999                        content: "value-d".into(),
1000                        rules: vec![VisitedRule {
1001                            rule_index: 0,
1002                            is_excluded: false
1003                        }]
1004                    }
1005                ],
1006            }
1007        );
1008    }
1009
1010    #[test]
1011    fn test_include_same_change_multiple_times_reversed() {
1012        // If multiple "include" scopes cover the same field, it should only be returned once.
1013        // This one specifically tests when a more generic path is added _after_ a specific path.
1014        // (The more specific one should be removed from the tree)
1015
1016        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![
1017            Path::from(vec!["a".into(), "b".into()]),
1018            Path::from(vec!["a".into()]),
1019        ])]);
1020
1021        let mut event = SimpleEvent::Map(
1022            [
1023                (
1024                    "a".into(),
1025                    SimpleEvent::Map(
1026                        [
1027                            ("b".into(), SimpleEvent::String("value-ab".into())),
1028                            ("c".into(), SimpleEvent::String("value-ac".into())),
1029                        ]
1030                        .into(),
1031                    ),
1032                ),
1033                ("d".into(), SimpleEvent::String("value-d".into())),
1034            ]
1035            .into(),
1036        );
1037
1038        let paths = visit_event(&mut event, &ruleset);
1039
1040        assert_eq!(
1041            paths,
1042            Visited {
1043                paths: vec![
1044                    VisitedPath {
1045                        path: Path::from(vec!["a".into(), "b".into()]),
1046                        content: "value-ab".into(),
1047                        rules: vec![VisitedRule {
1048                            rule_index: 0,
1049                            is_excluded: false
1050                        }]
1051                    },
1052                    VisitedPath {
1053                        path: Path::from(vec!["a".into(), "c".into()]),
1054                        content: "value-ac".into(),
1055                        rules: vec![VisitedRule {
1056                            rule_index: 0,
1057                            is_excluded: false
1058                        }]
1059                    },
1060                    VisitedPath {
1061                        path: Path::from(vec!["d".into()]),
1062                        content: "value-d".into(),
1063                        rules: vec![]
1064                    }
1065                ],
1066            }
1067        );
1068    }
1069
1070    #[test]
1071    fn test_fields_should_act_as_wildcard_on_lists() {
1072        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![Path::from(vec![
1073            "a".into(),
1074            "b".into(),
1075        ])])])
1076        .with_implicit_index_wildcards(true);
1077
1078        let mut event = SimpleEvent::Map(
1079            [(
1080                "a".into(),
1081                SimpleEvent::List(vec![SimpleEvent::Map(
1082                    [("b".into(), SimpleEvent::String("value-a-0-b".to_string()))].into(),
1083                )]),
1084            )]
1085            .into(),
1086        );
1087
1088        let paths = visit_event(&mut event, &ruleset);
1089
1090        assert_eq!(
1091            paths,
1092            Visited {
1093                paths: vec![VisitedPath {
1094                    path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1095                    content: "value-a-0-b".into(),
1096                    // Rule 0 matches this path even though there is a "0" index between the "a" and "b" field
1097                    rules: vec![VisitedRule {
1098                        rule_index: 0,
1099                        is_excluded: false
1100                    }]
1101                },],
1102            }
1103        );
1104    }
1105
1106    #[test]
1107    fn test_exclude_implicit_wildcard_path() {
1108        // This makes sure that exclusions are applied even when a path matches due to an
1109        // implicit array wildcard.
1110
1111        let ab_path = Path::from(vec!["a".into(), "b".into()]);
1112        let ruleset = ScopedRuleSet::new(&[Scope::include_and_exclude(
1113            vec![ab_path.clone()],
1114            vec![ab_path],
1115        )])
1116        .with_implicit_index_wildcards(true);
1117
1118        let mut event = SimpleEvent::Map(
1119            [(
1120                "a".into(),
1121                SimpleEvent::List(vec![SimpleEvent::Map(
1122                    [("b".into(), SimpleEvent::String("value-a-0-b".to_string()))].into(),
1123                )]),
1124            )]
1125            .into(),
1126        );
1127
1128        let paths = visit_event(&mut event, &ruleset);
1129
1130        assert_eq!(
1131            paths,
1132            Visited {
1133                paths: vec![VisitedPath {
1134                    path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1135                    content: "value-a-0-b".into(),
1136                    // Rule 0 matches this path even though there is a "0" index between the "a" and "b" field
1137                    rules: vec![VisitedRule {
1138                        rule_index: 0,
1139                        is_excluded: true
1140                    }]
1141                },],
1142            }
1143        );
1144    }
1145
1146    #[test]
1147    fn test_included_scope_both_implicit_and_explicit_index() {
1148        let a_0_c_path = Path::from(vec!["a".into(), 0.into(), "c".into()]);
1149        let ab_path = Path::from(vec!["a".into(), "b".into()]);
1150        let a_1_d_path = Path::from(vec!["a".into(), 1.into(), "d".into()]);
1151
1152        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![a_0_c_path, ab_path, a_1_d_path])])
1153            .with_implicit_index_wildcards(true);
1154
1155        let mut event = SimpleEvent::Map(
1156            [(
1157                "a".into(),
1158                SimpleEvent::List(vec![
1159                    SimpleEvent::Map(
1160                        [
1161                            ("b".into(), SimpleEvent::String("value-a-0-b".to_string())),
1162                            ("c".into(), SimpleEvent::String("value-a-0-c".to_string())),
1163                            ("d".into(), SimpleEvent::String("value-a-0-d".to_string())),
1164                        ]
1165                        .into(),
1166                    ),
1167                    SimpleEvent::Map(
1168                        [
1169                            ("b".into(), SimpleEvent::String("value-a-1-b".to_string())),
1170                            ("c".into(), SimpleEvent::String("value-a-1-c".to_string())),
1171                            ("d".into(), SimpleEvent::String("value-a-1-d".to_string())),
1172                        ]
1173                        .into(),
1174                    ),
1175                ]),
1176            )]
1177            .into(),
1178        );
1179
1180        let paths = visit_event(&mut event, &ruleset);
1181
1182        assert_eq!(
1183            paths,
1184            Visited {
1185                paths: vec![
1186                    VisitedPath {
1187                        path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1188                        content: "value-a-0-b".into(),
1189                        rules: vec![VisitedRule {
1190                            rule_index: 0,
1191                            is_excluded: false
1192                        }]
1193                    },
1194                    VisitedPath {
1195                        path: Path::from(vec!["a".into(), 0.into(), "c".into()]),
1196                        content: "value-a-0-c".into(),
1197                        rules: vec![VisitedRule {
1198                            rule_index: 0,
1199                            is_excluded: false
1200                        }]
1201                    },
1202                    VisitedPath {
1203                        path: Path::from(vec!["a".into(), 0.into(), "d".into()]),
1204                        content: "value-a-0-d".into(),
1205                        rules: vec![]
1206                    },
1207                    VisitedPath {
1208                        path: Path::from(vec!["a".into(), 1.into(), "b".into()]),
1209                        content: "value-a-1-b".into(),
1210                        rules: vec![VisitedRule {
1211                            rule_index: 0,
1212                            is_excluded: false
1213                        }]
1214                    },
1215                    VisitedPath {
1216                        path: Path::from(vec!["a".into(), 1.into(), "c".into()]),
1217                        content: "value-a-1-c".into(),
1218                        rules: vec![]
1219                    },
1220                    VisitedPath {
1221                        path: Path::from(vec!["a".into(), 1.into(), "d".into()]),
1222                        content: "value-a-1-d".into(),
1223                        rules: vec![VisitedRule {
1224                            rule_index: 0,
1225                            is_excluded: false
1226                        }]
1227                    },
1228                ],
1229            }
1230        );
1231    }
1232
1233    #[test]
1234    fn test_duplicate_rules_are_filtered_out() {
1235        // Internally there may be multiple "active" trees which could end up storing duplicate rules.
1236        // Those duplicates must be filtered out.
1237
1238        // Both of these scopes match the path "a[0].b" (one with an explicit index, one with an implicit wildcard index)
1239        let a_b_path = Path::from(vec!["a".into(), "b".into()]);
1240        let a_0_b_path = Path::from(vec!["a".into(), 0.into(), "b".into()]);
1241
1242        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![a_b_path, a_0_b_path])])
1243            .with_implicit_index_wildcards(true);
1244
1245        let mut event = SimpleEvent::Map(
1246            [(
1247                "a".into(),
1248                SimpleEvent::List(vec![SimpleEvent::Map(
1249                    [("b".into(), SimpleEvent::String("value-a-0-b".to_string()))].into(),
1250                )]),
1251            )]
1252            .into(),
1253        );
1254
1255        let paths = visit_event(&mut event, &ruleset);
1256
1257        assert_eq!(
1258            paths,
1259            Visited {
1260                paths: vec![VisitedPath {
1261                    path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1262                    content: "value-a-0-b".into(),
1263                    rules: vec![VisitedRule {
1264                        rule_index: 0,
1265                        is_excluded: false
1266                    }]
1267                },],
1268            }
1269        );
1270    }
1271
1272    #[test]
1273    fn test_deeply_nested_implicit_wildcard_index() {
1274        // A wildcard index can skip multiple levels of indexing, not just 1
1275
1276        let a_b_path = Path::from(vec!["a".into(), "b".into()]);
1277        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![a_b_path])])
1278            .with_implicit_index_wildcards(true);
1279
1280        let mut event = SimpleEvent::Map(
1281            [(
1282                "a".into(),
1283                SimpleEvent::List(vec![SimpleEvent::List(vec![SimpleEvent::Map(
1284                    [("b".into(), SimpleEvent::String("value-a-0-0-b".to_string()))].into(),
1285                )])]),
1286            )]
1287            .into(),
1288        );
1289
1290        let paths = visit_event(&mut event, &ruleset);
1291
1292        assert_eq!(
1293            paths,
1294            Visited {
1295                paths: vec![VisitedPath {
1296                    path: Path::from(vec!["a".into(), 0.into(), 0.into(), "b".into()]),
1297                    content: "value-a-0-0-b".into(),
1298                    rules: vec![VisitedRule {
1299                        rule_index: 0,
1300                        is_excluded: false
1301                    }]
1302                },],
1303            }
1304        );
1305    }
1306
1307    #[test]
1308    fn test_implicit_index_wildcard_is_disabled_by_default() {
1309        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![Path::from(vec![
1310            "a".into(),
1311            "b".into(),
1312        ])])]);
1313
1314        let mut event = SimpleEvent::Map(
1315            [(
1316                "a".into(),
1317                SimpleEvent::List(vec![SimpleEvent::Map(
1318                    [("b".into(), SimpleEvent::String("value-a-0-b".to_string()))].into(),
1319                )]),
1320            )]
1321            .into(),
1322        );
1323
1324        let paths = visit_event(&mut event, &ruleset);
1325
1326        assert_eq!(
1327            paths,
1328            Visited {
1329                paths: vec![VisitedPath {
1330                    path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1331                    content: "value-a-0-b".into(),
1332                    // Rule 0 does NOT match, since the implicit index wildcard is disabled
1333                    rules: vec![]
1334                },],
1335            }
1336        );
1337    }
1338}