dd_sds/scanner/
mod.rs

1use crate::encoding::Encoding;
2use crate::event::Event;
3use std::future::Future;
4
5use crate::match_validation::{
6    config::InternalMatchValidationType, config::MatchValidationType, match_status::MatchStatus,
7    match_validator::MatchValidator,
8};
9
10#[cfg(feature = "third-party-active-checkers")]
11use error::MatchValidatorCreationError;
12
13use self::metrics::ScannerMetrics;
14#[cfg(feature = "third-party-active-checkers")]
15use crate::match_validation::match_validator::RAYON_THREAD_POOL;
16use crate::observability::labels::Labels;
17use crate::rule_match::{InternalRuleMatch, RuleMatch};
18use crate::scanner::config::RuleConfig;
19use crate::scanner::internal_rule_match_set::InternalRuleMatchSet;
20use crate::scanner::regex_rule::compiled::RegexCompiledRule;
21use crate::scanner::regex_rule::{RegexCaches, access_regex_caches};
22use crate::scanner::scope::Scope;
23pub use crate::scanner::shared_data::SharedData;
24use crate::scanner::suppression::{CompiledSuppressions, SuppressionValidationError, Suppressions};
25use crate::scoped_ruleset::{ContentVisitor, ExclusionCheck, ScopedRuleSet};
26pub use crate::secondary_validation::Validator;
27use crate::stats::GLOBAL_STATS;
28use crate::tokio::TOKIO_RUNTIME;
29use crate::{CreateScannerError, EncodeIndices, MatchAction, Path, ScannerError};
30use ::metrics::counter;
31use ahash::AHashMap;
32use futures::executor::block_on;
33use serde::{Deserialize, Serialize};
34use serde_with::serde_as;
35use std::ops::Deref;
36use std::pin::Pin;
37use std::sync::Arc;
38use std::time::{Duration, Instant};
39use tokio::task::JoinHandle;
40use tokio::time::timeout;
41
42pub mod config;
43pub mod debug_scan;
44pub mod error;
45pub mod metrics;
46pub mod regex_rule;
47pub mod scope;
48pub mod shared_data;
49pub mod shared_pool;
50pub mod suppression;
51
52mod internal_rule_match_set;
53#[cfg(test)]
54mod test;
55
56#[derive(Clone)]
57pub struct StringMatch {
58    pub start: usize,
59    pub end: usize,
60    // The keyword that was used to match this rule. Optional, only some rules may set this value.
61    pub keyword: Option<String>,
62}
63
64pub trait MatchEmitter<T = ()> {
65    fn emit(&mut self, string_match: StringMatch) -> T;
66}
67
68// This implements MatchEmitter for mutable closures (so you can use a closure instead of a custom
69// struct that implements MatchEmitter)
70impl<F, T> MatchEmitter<T> for F
71where
72    F: FnMut(StringMatch) -> T,
73{
74    fn emit(&mut self, string_match: StringMatch) -> T {
75        // This just calls the closure (itself)
76        (self)(string_match)
77    }
78}
79
80/// The precedence of a rule. Catchall is the lowest precedence, Specific is the highest precedence.
81/// The default precedence is Specific.
82/// For rules that:
83/// - Have the same mutation priority
84/// - Match at the same index
85/// - Match the same number of characters
86///
87/// Then the rule with the highest precedence will be used.
88#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Default)]
89pub enum Precedence {
90    Catchall,
91    Generic,
92    #[default]
93    Specific,
94}
95
96#[serde_as]
97#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
98pub struct RootRuleConfig<T> {
99    #[serde(default)]
100    pub match_action: MatchAction,
101    #[serde(default)]
102    pub scope: Scope,
103    #[deprecated(note = "Use `third_party_active_checker` instead")]
104    match_validation_type: Option<MatchValidationType>,
105    third_party_active_checker: Option<MatchValidationType>,
106    suppressions: Option<Suppressions>,
107    #[serde(default)]
108    precedence: Precedence,
109    #[serde(default)]
110    pub is_supporting_rule: bool,
111    /// Raw `"key:value"` strings, matching the format used by standard rule definitions
112    /// (e.g. `sensitive_data:travis_ci_access_token`). Not parsed into a map since no
113    /// producer of these values does so either; consumers parse the entries they need.
114    #[serde(default)]
115    pub tags: Vec<String>,
116    #[serde(flatten)]
117    pub inner: T,
118}
119
120impl<T> RootRuleConfig<T>
121where
122    T: RuleConfig + 'static,
123{
124    pub fn new_dyn(inner: T) -> RootRuleConfig<Arc<dyn RuleConfig>> {
125        RootRuleConfig::new(Arc::new(inner) as Arc<dyn RuleConfig>)
126    }
127
128    pub fn into_dyn(self) -> RootRuleConfig<Arc<dyn RuleConfig>> {
129        self.map_inner(|x| Arc::new(x) as Arc<dyn RuleConfig>)
130    }
131}
132
133impl<T> RootRuleConfig<T> {
134    pub fn new(inner: T) -> Self {
135        #[allow(deprecated)]
136        Self {
137            match_action: MatchAction::None,
138            scope: Scope::all(),
139            match_validation_type: None,
140            third_party_active_checker: None,
141            suppressions: None,
142            precedence: Precedence::default(),
143            is_supporting_rule: false,
144            tags: Vec::new(),
145            inner,
146        }
147    }
148
149    pub fn map_inner<U>(self, func: impl FnOnce(T) -> U) -> RootRuleConfig<U> {
150        #[allow(deprecated)]
151        RootRuleConfig {
152            match_action: self.match_action,
153            scope: self.scope,
154            match_validation_type: self.match_validation_type,
155            third_party_active_checker: self.third_party_active_checker,
156            suppressions: self.suppressions,
157            precedence: self.precedence,
158            is_supporting_rule: self.is_supporting_rule,
159            tags: self.tags,
160            inner: func(self.inner),
161        }
162    }
163
164    pub fn match_action(mut self, action: MatchAction) -> Self {
165        self.match_action = action;
166        self
167    }
168
169    pub fn precedence(mut self, precedence: Precedence) -> Self {
170        self.precedence = precedence;
171        self
172    }
173
174    pub fn scope(mut self, scope: Scope) -> Self {
175        self.scope = scope;
176        self
177    }
178
179    pub fn third_party_active_checker(
180        mut self,
181        match_validation_type: MatchValidationType,
182    ) -> Self {
183        self.third_party_active_checker = Some(match_validation_type);
184        self
185    }
186
187    pub fn suppressions(mut self, suppressions: Suppressions) -> Self {
188        self.suppressions = Some(suppressions);
189        self
190    }
191
192    pub fn is_supporting_rule(mut self, value: bool) -> Self {
193        self.is_supporting_rule = value;
194        self
195    }
196
197    pub fn tags(mut self, tags: Vec<String>) -> Self {
198        self.tags = tags;
199        self
200    }
201
202    pub fn get_suppressions(&self) -> Option<&Suppressions> {
203        self.suppressions.as_ref()
204    }
205
206    fn get_third_party_active_checker(&self) -> Option<&MatchValidationType> {
207        #[allow(deprecated)]
208        self.third_party_active_checker
209            .as_ref()
210            .or(self.match_validation_type.as_ref())
211    }
212}
213
214impl<T> Deref for RootRuleConfig<T> {
215    type Target = T;
216
217    fn deref(&self) -> &Self::Target {
218        &self.inner
219    }
220}
221pub struct RootCompiledRule {
222    pub inner: Box<dyn CompiledRule>,
223    pub scope: Scope,
224    pub match_action: MatchAction,
225    pub match_validation_type: Option<MatchValidationType>,
226    pub suppressions: Option<CompiledSuppressions>,
227    pub precedence: Precedence,
228    pub is_supporting_rule: bool,
229    /// Precomputed `"{sensitive_data_category}/{sensitive_data}"` tag value derived from
230    /// the rule's `tags`, used to tag `scanning.match_count`. `None` when the rule has no
231    /// `sensitive_data` tag.
232    pub sds_rule_name: Option<String>,
233}
234
235impl RootCompiledRule {
236    pub fn internal_match_validation_type(&self) -> Option<InternalMatchValidationType> {
237        self.match_validation_type
238            .as_ref()
239            .map(|x| x.get_internal_match_validation_type())
240    }
241}
242
243impl Deref for RootCompiledRule {
244    type Target = dyn CompiledRule;
245
246    fn deref(&self) -> &Self::Target {
247        self.inner.as_ref()
248    }
249}
250
251pub struct StringMatchesCtx<'a> {
252    rule_index: usize,
253    pub regex_caches: &'a mut RegexCaches,
254    pub exclusion_check: &'a ExclusionCheck<'a>,
255    pub excluded_matches: &'a mut AHashMap<String, String>,
256    pub match_emitter: &'a mut dyn MatchEmitter,
257    pub wildcard_indices: Option<&'a Vec<(usize, usize)>>,
258    pub enable_debug_observability: bool,
259
260    // Shared Data
261    pub per_string_data: &'a mut SharedData,
262    pub per_scanner_data: &'a SharedData,
263    pub per_event_data: &'a mut SharedData,
264    pub event_id: Option<&'a str>,
265
266    // Per-scan metadata supplied by the caller via `ScanOptions::scan_metadata`.
267    pub scan_metadata: &'a AHashMap<String, String>,
268}
269
270impl StringMatchesCtx<'_> {
271    /// If a `get_string_matches` implementation needs to do any async processing (e.g. I/O),
272    /// this function can be used to return an "async job" to find matches. The return value
273    /// of `process_async` should be returned from the `get_string_matches` function. The future
274    /// passed into this function will be spawned and executed immediately without blocking
275    /// other `get_string_matches` calls. This means all the async jobs will run concurrently.
276    ///
277    /// The `ctx` available to async jobs is more restrictive than the normal `ctx` available in
278    /// `get_string_matches`. The only thing you can do is return matches. If other data is needed,
279    /// it should be accessed before `process_async` is called.
280    pub fn process_async(
281        &self,
282        func: impl for<'a> FnOnce(
283            &'a mut AsyncStringMatchesCtx,
284        )
285            -> Pin<Box<dyn Future<Output = Result<(), ScannerError>> + Send + 'a>>
286        + Send
287        + 'static,
288    ) -> RuleResult {
289        let rule_index = self.rule_index;
290
291        // The future is spawned onto the tokio runtime immediately so it starts running
292        // in the background
293        let fut = TOKIO_RUNTIME.spawn(async move {
294            let start = Instant::now();
295            let mut ctx = AsyncStringMatchesCtx {
296                rule_matches: vec![],
297            };
298            (func)(&mut ctx).await?;
299            let io_duration = start.elapsed();
300
301            Ok(AsyncRuleInfo {
302                rule_index,
303                rule_matches: ctx.rule_matches,
304                io_duration,
305            })
306        });
307
308        Ok(RuleStatus::Pending(fut))
309    }
310}
311
312pub struct AsyncStringMatchesCtx {
313    rule_matches: Vec<StringMatch>,
314}
315
316impl AsyncStringMatchesCtx {
317    pub fn emit_match(&mut self, string_match: StringMatch) {
318        self.rule_matches.push(string_match);
319    }
320}
321
322#[must_use]
323pub enum RuleStatus {
324    Done,
325    Pending(PendingRuleResult),
326}
327
328// pub type PendingRuleResult = BoxFuture<'static, Result<AsyncRuleInfo, ScannerError>>;
329pub type PendingRuleResult = JoinHandle<Result<AsyncRuleInfo, ScannerError>>;
330
331pub struct PendingRuleJob {
332    fut: PendingRuleResult,
333    path: Path<'static>,
334}
335
336pub struct AsyncRuleInfo {
337    rule_index: usize,
338    rule_matches: Vec<StringMatch>,
339    io_duration: Duration,
340}
341
342/// A rule result that cannot be async
343pub type RuleResult = Result<RuleStatus, ScannerError>;
344
345// This is the public trait that is used to define the behavior of a compiled rule.
346pub trait CompiledRule: Send + Sync {
347    fn init_per_scanner_data(&self, _per_scanner_data: &mut SharedData) {
348        // by default, no per-scanner data is initialized
349    }
350
351    fn init_per_string_data(&self, _labels: &Labels, _per_string_data: &mut SharedData) {
352        // by default, no per-string data is initialized
353    }
354
355    fn init_per_event_data(&self, _per_event_data: &mut SharedData) {
356        // by default, no per-event data is initialized
357    }
358
359    fn get_string_matches(
360        &self,
361        content: &str,
362        path: &Path,
363        ctx: &mut StringMatchesCtx<'_>,
364    ) -> RuleResult;
365
366    // Whether a match from this rule should be excluded (marked as a false-positive)
367    // if the content of this match was found in a match from an excluded scope
368    fn should_exclude_multipass_v0(&self) -> bool {
369        // default is to NOT use Multi-pass V0
370        false
371    }
372
373    fn on_excluded_match_multipass_v0(
374        &self,
375        _path: &Path,
376        _excluded_path: &str,
377        _enable_debug_observability: bool,
378    ) {
379        // default is to do nothing
380    }
381
382    fn as_regex_rule(&self) -> Option<&RegexCompiledRule> {
383        None
384    }
385
386    fn as_regex_rule_mut(&mut self) -> Option<&mut RegexCompiledRule> {
387        None
388    }
389
390    fn allow_scanner_to_exclude_namespace(&self) -> bool {
391        true
392    }
393}
394
395impl<T> RuleConfig for Box<T>
396where
397    T: RuleConfig + ?Sized,
398{
399    fn convert_to_compiled_rule(
400        &self,
401        rule_index: usize,
402        labels: Labels,
403    ) -> Result<Box<dyn CompiledRule>, CreateScannerError> {
404        self.as_ref().convert_to_compiled_rule(rule_index, labels)
405    }
406}
407
408#[derive(Debug, PartialEq, Clone)]
409struct ScannerFeatures {
410    pub add_implicit_index_wildcards: bool,
411    pub multipass_v0_enabled: bool,
412    pub return_matches: bool,
413    pub enable_debug_observability: bool,
414}
415
416impl Default for ScannerFeatures {
417    fn default() -> Self {
418        Self {
419            add_implicit_index_wildcards: false,
420            multipass_v0_enabled: true,
421            return_matches: false,
422            enable_debug_observability: false,
423        }
424    }
425}
426
427pub struct ScanOptions {
428    // The blocked_rules_idx parameter is a list of rule indices that should be skipped for this scan.
429    // this list shall be small (<10), so a linear search is acceptable otherwise performance will be impacted.
430    pub blocked_rules_idx: Vec<usize>,
431    // The wildcarded_indices parameter is a map containing a list of tuples of (start, end) indices that should be treated as wildcards (for the message key only) per path.
432    pub wildcarded_indices: AHashMap<Path<'static>, Vec<(usize, usize)>>,
433    // Whether to validate matches using third-party validators (e.g., checksum validation for credit cards).
434    // When enabled, the scanner automatically collects match content needed for validation.
435    pub validate_matches: bool,
436    // Arbitrary key-value metadata passed through to each rule's `get_string_matches` call via
437    // `StringMatchesCtx::scan_metadata`. Rules may use this to receive per-scan context (e.g.
438    // an org identifier) without requiring changes to the `CompiledRule` trait signature.
439    pub scan_metadata: AHashMap<String, String>,
440}
441
442impl Default for ScanOptions {
443    fn default() -> Self {
444        Self {
445            blocked_rules_idx: vec![],
446            wildcarded_indices: AHashMap::new(),
447            validate_matches: false,
448            scan_metadata: AHashMap::new(),
449        }
450    }
451}
452
453pub struct ScanOptionBuilder {
454    blocked_rules_idx: Vec<usize>,
455    wildcarded_indices: AHashMap<Path<'static>, Vec<(usize, usize)>>,
456    validate_matches: bool,
457    scan_metadata: AHashMap<String, String>,
458}
459
460impl ScanOptionBuilder {
461    pub fn new() -> Self {
462        Self {
463            blocked_rules_idx: vec![],
464            wildcarded_indices: AHashMap::new(),
465            validate_matches: false,
466            scan_metadata: AHashMap::new(),
467        }
468    }
469
470    pub fn with_blocked_rules_idx(mut self, blocked_rules_idx: Vec<usize>) -> Self {
471        self.blocked_rules_idx = blocked_rules_idx;
472        self
473    }
474
475    pub fn with_wildcarded_indices(
476        mut self,
477        wildcarded_indices: AHashMap<Path<'static>, Vec<(usize, usize)>>,
478    ) -> Self {
479        self.wildcarded_indices = wildcarded_indices;
480        self
481    }
482
483    pub fn with_validate_matching(mut self, validate_matches: bool) -> Self {
484        self.validate_matches = validate_matches;
485        self
486    }
487
488    pub fn with_scan_metadata(mut self, scan_metadata: AHashMap<String, String>) -> Self {
489        self.scan_metadata = scan_metadata;
490        self
491    }
492
493    pub fn build(self) -> ScanOptions {
494        ScanOptions {
495            blocked_rules_idx: self.blocked_rules_idx,
496            wildcarded_indices: self.wildcarded_indices,
497            validate_matches: self.validate_matches,
498            scan_metadata: self.scan_metadata,
499        }
500    }
501}
502
503pub struct Scanner {
504    rules: Vec<RootCompiledRule>,
505    scoped_ruleset: ScopedRuleSet,
506    scanner_features: ScannerFeatures,
507    metrics: ScannerMetrics,
508    labels: Labels,
509    // Without third-party active checkers this map is always empty and never read, but it is kept
510    // in the struct so the type stays identical across build configurations.
511    #[cfg_attr(not(feature = "third-party-active-checkers"), allow(dead_code))]
512    match_validators_per_type: AHashMap<InternalMatchValidationType, Box<dyn MatchValidator>>,
513    per_scanner_data: SharedData,
514    async_scan_timeout: Duration,
515}
516
517impl Scanner {
518    pub fn builder(rules: &[RootRuleConfig<Arc<dyn RuleConfig>>]) -> ScannerBuilder<'_> {
519        ScannerBuilder::new(rules)
520    }
521
522    // This function scans the given event with the rules configured in the scanner.
523    // The event parameter is a mutable reference to the event that should be scanned (implemented the Event trait).
524    // The return value is a list of RuleMatch objects, which contain information about the matches that were found.
525    // This version uses default scan options (no validation, no blocked rules, no wildcarded indices).
526    pub fn scan<E: Event>(&self, event: &mut E) -> Result<Vec<RuleMatch>, ScannerError> {
527        self.scan_with_options(event, ScanOptions::default())
528    }
529
530    // This function scans the given event with the rules configured in the scanner.
531    // The event parameter is a mutable reference to the event that should be scanned (implemented the Event trait).
532    // The options parameter allows customizing the scan behavior (validation, blocked rules, etc.).
533    // The return value is a list of RuleMatch objects, which contain information about the matches that were found.
534    pub fn scan_with_options<E: Event>(
535        &self,
536        event: &mut E,
537        options: ScanOptions,
538    ) -> Result<Vec<RuleMatch>, ScannerError> {
539        let start = Instant::now();
540        let validate = options.validate_matches;
541        // Collect matches inside block_on, then run finalize_matches (which uses rayon) outside of
542        // it to avoid re-entrancy between the futures LocalPool executor and RAYON_THREAD_POOL.
543        let result = block_on(self.internal_scan_collect(event, options));
544        match result {
545            Ok((mut rule_matches, io_duration)) => {
546                self.finalize_matches(&mut rule_matches, validate);
547                self.record_metrics(&rule_matches, start, Some(io_duration));
548                Ok(rule_matches)
549            }
550            Err(e) => {
551                self.record_metrics(&[], start, None);
552                Err(e)
553            }
554        }
555    }
556
557    // This function scans the given event with the rules configured in the scanner.
558    // The event parameter is a mutable reference to the event that should be scanned (implemented the Event trait).
559    // The return value is a list of RuleMatch objects, which contain information about the matches that were found.
560    pub async fn scan_async<E: Event>(
561        &self,
562        event: &mut E,
563    ) -> Result<Vec<RuleMatch>, ScannerError> {
564        self.scan_async_with_options(event, ScanOptions::default())
565            .await
566    }
567
568    pub async fn scan_async_with_options<E: Event>(
569        &self,
570        event: &mut E,
571        options: ScanOptions,
572    ) -> Result<Vec<RuleMatch>, ScannerError> {
573        let start = Instant::now();
574        let validate = options.validate_matches;
575        let fut = self.internal_scan_collect(event, options);
576
577        // The sleep from the timeout requires being in a tokio context
578        // The guard needs to be dropped before await since the guard is !Send
579        let timeout_result = {
580            let _tokio_guard = TOKIO_RUNTIME.enter();
581            timeout(self.async_scan_timeout, fut)
582        };
583
584        let result = timeout_result.await.unwrap_or(Err(ScannerError::Transient(
585            "Async scan timeout".to_string(),
586        )));
587
588        match result {
589            Ok((mut rule_matches, io_duration)) => {
590                self.finalize_matches(&mut rule_matches, validate);
591                self.record_metrics(&rule_matches, start, Some(io_duration));
592                Ok(rule_matches)
593            }
594            Err(e) => {
595                self.record_metrics(&[], start, None);
596                Err(e)
597            }
598        }
599    }
600
601    fn record_metrics(
602        &self,
603        output_rule_matches: &[RuleMatch],
604        start: Instant,
605        io_duration: Option<Duration>,
606    ) {
607        // Add number of scanned events
608        self.metrics.num_scanned_events.increment(1);
609        // Add number of matches, tagged per rule so `sds_rule_name` can break down match counts
610        // by the rule's sensitive_data_category/sensitive_data tags.
611        let mut match_counts_by_rule: AHashMap<usize, u64> = AHashMap::new();
612        for rule_match in output_rule_matches {
613            *match_counts_by_rule
614                .entry(rule_match.rule_index)
615                .or_default() += 1;
616        }
617        for (rule_index, count) in match_counts_by_rule {
618            match self.rules[rule_index].sds_rule_name.as_deref() {
619                Some(sds_rule_name) => {
620                    let labels = self.labels.clone_with_labels(Labels::new(&[(
621                        "sds_rule_name",
622                        sds_rule_name.to_string(),
623                    )]));
624                    counter!("scanning.match_count", labels).increment(count);
625                }
626                None => self.metrics.match_count.increment(count),
627            }
628        }
629
630        if let Some(io_duration) = io_duration {
631            let total_duration = start.elapsed();
632            let cpu_duration = total_duration.saturating_sub(io_duration);
633            self.metrics
634                .cpu_duration
635                .increment(cpu_duration.as_nanos() as u64);
636        }
637    }
638
639    fn process_rule_matches<E: Event>(
640        &self,
641        event: &mut E,
642        rule_matches: InternalRuleMatchSet<E::Encoding>,
643        excluded_matches: AHashMap<String, String>,
644        output_rule_matches: &mut Vec<RuleMatch>,
645        need_match_content: bool,
646    ) {
647        if rule_matches.is_empty() {
648            return;
649        }
650        access_regex_caches(|regex_caches| {
651            for (path, mut rule_matches) in rule_matches.into_iter() {
652                // All rule matches in each inner list are for a single path, so they can be processed independently.
653                event.visit_string_mut(&path, |content| {
654                    // calculate_indices requires that matches are sorted by start index
655                    rule_matches.sort_unstable_by_key(|rule_match| rule_match.utf8_start);
656
657                    <<E as Event>::Encoding>::calculate_indices(
658                        content,
659                        rule_matches.iter_mut().map(
660                            |rule_match: &mut InternalRuleMatch<E::Encoding>| EncodeIndices {
661                                utf8_start: rule_match.utf8_start,
662                                utf8_end: rule_match.utf8_end,
663                                custom_start: &mut rule_match.custom_start,
664                                custom_end: &mut rule_match.custom_end,
665                            },
666                        ),
667                    );
668
669                    if self.scanner_features.multipass_v0_enabled {
670                        // Now that the `excluded_matches` set is fully populated, filter out any matches
671                        // that are the same as excluded matches (also known as "Multi-pass V0")
672                        rule_matches.retain(|rule_match| {
673                            if self.rules[rule_match.rule_index]
674                                .inner
675                                .should_exclude_multipass_v0()
676                            {
677                                let match_content =
678                                    &content[rule_match.utf8_start..rule_match.utf8_end];
679                                let excluded_path = excluded_matches.get(match_content);
680                                if let Some(excluded_path) = excluded_path {
681                                    self.rules[rule_match.rule_index]
682                                        .on_excluded_match_multipass_v0(
683                                            &path,
684                                            excluded_path,
685                                            self.scanner_features.enable_debug_observability,
686                                        );
687                                }
688                                excluded_path.is_none()
689                            } else {
690                                true
691                            }
692                        });
693                    }
694
695                    self.suppress_matches::<E::Encoding>(&mut rule_matches, content, regex_caches);
696
697                    self.sort_and_remove_overlapping_rules::<E::Encoding>(&mut rule_matches);
698
699                    let will_mutate = rule_matches.iter().any(|rule_match| {
700                        self.rules[rule_match.rule_index].match_action.is_mutating()
701                    });
702
703                    self.apply_match_actions(
704                        content,
705                        &path,
706                        rule_matches,
707                        output_rule_matches,
708                        need_match_content,
709                    );
710
711                    will_mutate
712                });
713            }
714        });
715    }
716
717    async fn internal_scan_collect<E: Event>(
718        &self,
719        event: &mut E,
720        options: ScanOptions,
721    ) -> Result<(Vec<RuleMatch>, Duration), ScannerError> {
722        // If validation is requested, we need to collect match content even if the scanner
723        // wasn't originally configured to return matches
724        let need_match_content = self.scanner_features.return_matches || options.validate_matches;
725        // All matches, after some (but not all) false-positives have been removed.
726        let mut rule_matches = InternalRuleMatchSet::new();
727        let mut excluded_matches = AHashMap::new();
728        let mut async_jobs = vec![];
729
730        access_regex_caches(|regex_caches| {
731            self.scoped_ruleset.visit_string_rule_combinations(
732                event,
733                ScannerContentVisitor {
734                    scanner: self,
735                    regex_caches,
736                    rule_matches: &mut rule_matches,
737                    blocked_rules: &options.blocked_rules_idx,
738                    excluded_matches: &mut excluded_matches,
739                    per_event_data: SharedData::new(),
740                    wildcarded_indexes: &options.wildcarded_indices,
741                    async_jobs: &mut async_jobs,
742                    event_id: event.get_id().map(|s| s.to_string()),
743                    scan_metadata: &options.scan_metadata,
744                },
745            )
746        })?;
747
748        // The async jobs were already spawned on the tokio runtime, so the
749        // results just need to be collected
750        let mut total_io_duration = Duration::ZERO;
751        for job in async_jobs {
752            let rule_info = job.fut.await.unwrap()?;
753            total_io_duration += rule_info.io_duration;
754            rule_matches.push_async_matches(
755                &job.path,
756                rule_info
757                    .rule_matches
758                    .into_iter()
759                    .map(|x| InternalRuleMatch::new(rule_info.rule_index, x)),
760            );
761        }
762
763        let mut output_rule_matches = vec![];
764
765        self.process_rule_matches(
766            event,
767            rule_matches,
768            excluded_matches,
769            &mut output_rule_matches,
770            need_match_content,
771        );
772
773        Ok((output_rule_matches, total_io_duration))
774    }
775
776    pub fn suppress_matches<E: Encoding>(
777        &self,
778        rule_matches: &mut Vec<InternalRuleMatch<E>>,
779        content: &str,
780        regex_caches: &mut RegexCaches,
781    ) {
782        rule_matches.retain(|rule_match| {
783            if let Some(suppressions) = &self.rules[rule_match.rule_index].suppressions {
784                let match_should_be_suppressed = suppressions.should_match_be_suppressed(
785                    &content[rule_match.utf8_start..rule_match.utf8_end],
786                    regex_caches,
787                );
788
789                if match_should_be_suppressed {
790                    self.metrics.suppressed_match_count.increment(1);
791                }
792                !match_should_be_suppressed
793            } else {
794                true
795            }
796        });
797    }
798
799    #[cfg(feature = "third-party-active-checkers")]
800    pub fn validate_matches(&self, rule_matches: &mut Vec<RuleMatch>) {
801        // Create MatchValidatorRuleMatch per match_validator_type to pass it to each match_validator
802        let mut match_validator_rule_match_per_type = AHashMap::new();
803
804        let mut validated_rule_matches = vec![];
805
806        for mut rule_match in rule_matches.drain(..) {
807            let rule = &self.rules[rule_match.rule_index];
808            if let Some(match_validation_type) = rule.internal_match_validation_type() {
809                match_validator_rule_match_per_type
810                    .entry(match_validation_type)
811                    .or_insert_with(Vec::new)
812                    .push(rule_match)
813            } else {
814                // There is no match validator for this rule, so mark it as not available.
815                rule_match.match_status.merge(MatchStatus::NotAvailable);
816                validated_rule_matches.push(rule_match);
817            }
818        }
819
820        // Skip the pool hop when there's nothing to validate.
821        if !match_validator_rule_match_per_type.is_empty() {
822            let run_validation = || {
823                use rayon::prelude::*;
824
825                match_validator_rule_match_per_type.par_iter_mut().for_each(
826                    |(match_validation_type, matches_per_type)| {
827                        let match_validator =
828                            self.match_validators_per_type.get(match_validation_type);
829                        if let Some(match_validator) = match_validator {
830                            match_validator
831                                .as_ref()
832                                .validate(matches_per_type, &self.rules)
833                        }
834                    },
835                );
836            };
837
838            // TODO(SDSP-450): move validation onto the async TOKIO_RUNTIME. It is I/O-bound
839            // (blocking HTTP to third-party checkers), so a Rayon (CPU-bound) pool is a poor
840            // fit and forces the workaround below. Async validation would remove it entirely.
841            //
842            // If we're already on a Rayon worker (e.g. the caller scans from its own parallel
843            // iterator), calling `install` here blocks the worker, and Rayon keeps it busy by
844            // stealing more of the caller's jobs — which scan and validate and steal again,
845            // recursing until the stack overflows. Running `install` on a separate OS thread
846            // parks this worker instead, breaking the recursion; validation still runs in
847            // parallel on RAYON_THREAD_POOL.
848            if rayon::current_thread_index().is_some() {
849                std::thread::scope(|s| {
850                    s.spawn(|| RAYON_THREAD_POOL.install(run_validation));
851                });
852            } else {
853                RAYON_THREAD_POOL.install(run_validation);
854            }
855        }
856
857        // Refill the rule_matches with the validated matches
858        for (_, mut matches) in match_validator_rule_match_per_type {
859            validated_rule_matches.append(&mut matches);
860        }
861
862        // Sort rule_matches by start index
863        validated_rule_matches.sort_by_key(|rule_match| rule_match.start_index);
864        *rule_matches = validated_rule_matches;
865    }
866
867    // Runs optional validation and drops supporting-rule matches from the output.
868    // Must be called OUTSIDE of any futures executor (e.g. block_on) because validate_matches
869    // uses RAYON_THREAD_POOL internally; running rayon inside block_on causes an EnterError panic
870    // when the calling thread re-enters the LocalPool executor context.
871    fn finalize_matches(&self, rule_matches: &mut Vec<RuleMatch>, validate: bool) {
872        #[cfg(feature = "third-party-active-checkers")]
873        if validate {
874            self.validate_matches(rule_matches);
875        }
876        #[cfg(not(feature = "third-party-active-checkers"))]
877        let _ = validate;
878        // Supporting rules exist only to provide template variables to CustomHttpV2 validators of
879        // other rules. Their matches must not appear in the final output. They are retained until
880        // after validate_matches so that match pairing can reference their match values.
881        rule_matches.retain(|rule_match| !self.rules[rule_match.rule_index].is_supporting_rule);
882    }
883
884    /// Apply mutations from actions, and shift indices to match the mutated values.
885    /// This assumes the matches are all from the content given, and are sorted by start index.
886    fn apply_match_actions<E: Encoding>(
887        &self,
888        content: &mut String,
889        path: &Path<'static>,
890        rule_matches: Vec<InternalRuleMatch<E>>,
891        output_rule_matches: &mut Vec<RuleMatch>,
892        need_match_content: bool,
893    ) {
894        let mut utf8_byte_delta: isize = 0;
895        let mut custom_index_delta: <E>::IndexShift = <E>::zero_shift();
896
897        for rule_match in rule_matches {
898            output_rule_matches.push(self.apply_match_actions_for_string::<E>(
899                content,
900                path.clone(),
901                rule_match,
902                &mut utf8_byte_delta,
903                &mut custom_index_delta,
904                need_match_content,
905            ));
906        }
907    }
908
909    /// This will be called once for each match of a single string. The rules must be passed in in order of the start index. Mutating rules must not overlap.
910    fn apply_match_actions_for_string<E: Encoding>(
911        &self,
912        content: &mut String,
913        path: Path<'static>,
914        rule_match: InternalRuleMatch<E>,
915        // The current difference in length between the original and mutated string
916        utf8_byte_delta: &mut isize,
917
918        // The difference between the custom index on the original string and the mutated string
919        custom_index_delta: &mut <E>::IndexShift,
920        need_match_content: bool,
921    ) -> RuleMatch {
922        let rule = &self.rules[rule_match.rule_index];
923
924        let custom_start =
925            (<E>::get_index(&rule_match.custom_start, rule_match.utf8_start) as isize
926                + <E>::get_shift(custom_index_delta, *utf8_byte_delta)) as usize;
927
928        let mut matched_content_copy = None;
929
930        if need_match_content {
931            // This copies part of the is_mutating block but is seperate since can't mix compilation condition and code condition
932            let mutated_utf8_match_start =
933                (rule_match.utf8_start as isize + *utf8_byte_delta) as usize;
934            let mutated_utf8_match_end = (rule_match.utf8_end as isize + *utf8_byte_delta) as usize;
935
936            // Matches for mutating rules must have valid indices
937            debug_assert!(content.is_char_boundary(mutated_utf8_match_start));
938            debug_assert!(content.is_char_boundary(mutated_utf8_match_end));
939
940            let matched_content = &content[mutated_utf8_match_start..mutated_utf8_match_end];
941            matched_content_copy = Some(matched_content.to_string());
942        }
943
944        if rule.match_action.is_mutating() {
945            let mutated_utf8_match_start =
946                (rule_match.utf8_start as isize + *utf8_byte_delta) as usize;
947            let mutated_utf8_match_end = (rule_match.utf8_end as isize + *utf8_byte_delta) as usize;
948
949            // Matches for mutating rules must have valid indices
950            debug_assert!(content.is_char_boundary(mutated_utf8_match_start));
951            debug_assert!(content.is_char_boundary(mutated_utf8_match_end));
952
953            let matched_content = &content[mutated_utf8_match_start..mutated_utf8_match_end];
954            if let Some(replacement) = rule.match_action.get_replacement(matched_content) {
955                let before_replacement = &matched_content[replacement.start..replacement.end];
956
957                // update indices to match the new mutated content
958                <E>::adjust_shift(
959                    custom_index_delta,
960                    before_replacement,
961                    &replacement.replacement,
962                );
963                *utf8_byte_delta +=
964                    replacement.replacement.len() as isize - before_replacement.len() as isize;
965
966                let replacement_start = mutated_utf8_match_start + replacement.start;
967                let replacement_end = mutated_utf8_match_start + replacement.end;
968                content.replace_range(replacement_start..replacement_end, &replacement.replacement);
969            }
970        }
971
972        let shift_offset = <E>::get_shift(custom_index_delta, *utf8_byte_delta);
973        let custom_end = (<E>::get_index(&rule_match.custom_end, rule_match.utf8_end) as isize
974            + shift_offset) as usize;
975
976        let rule = &self.rules[rule_match.rule_index];
977
978        let match_status: MatchStatus = if rule.match_validation_type.is_some() {
979            MatchStatus::NotChecked
980        } else {
981            MatchStatus::NotAvailable
982        };
983
984        RuleMatch {
985            rule_index: rule_match.rule_index,
986            path,
987            replacement_type: rule.match_action.replacement_type(),
988            start_index: custom_start,
989            end_index_exclusive: custom_end,
990            shift_offset,
991            match_value: matched_content_copy,
992            match_status,
993            keyword: rule_match.keyword,
994        }
995    }
996
997    fn sort_and_remove_overlapping_rules<E: Encoding>(
998        &self,
999        rule_matches: &mut Vec<InternalRuleMatch<E>>,
1000    ) {
1001        // Some of the scanner code relies on the behavior here, such as the sort order and removal of overlapping mutating rules.
1002        // Be very careful if this function is modified.
1003
1004        rule_matches.sort_unstable_by(|a, b| {
1005            // Mutating rules are a higher priority (earlier in the list)
1006            let ord = self.rules[a.rule_index]
1007                .match_action
1008                .is_mutating()
1009                .cmp(&self.rules[b.rule_index].match_action.is_mutating())
1010                .reverse();
1011
1012            // Earlier start offset
1013            let ord = ord.then(a.utf8_start.cmp(&b.utf8_start));
1014
1015            // Longer matches
1016            let ord = ord.then(a.len().cmp(&b.len()).reverse());
1017
1018            // Matches with higher precedence come first
1019            let ord = ord.then(
1020                self.rules[a.rule_index]
1021                    .precedence
1022                    .cmp(&self.rules[b.rule_index].precedence)
1023                    .reverse(),
1024            );
1025
1026            // Matches from earlier rules
1027            let ord = ord.then(a.rule_index.cmp(&b.rule_index));
1028
1029            // swap the order of everything so matches can be efficiently popped off the back as they are processed
1030            ord.reverse()
1031        });
1032
1033        let mut retained_rules: Vec<InternalRuleMatch<E>> = vec![];
1034
1035        'rule_matches: while let Some(rule_match) = rule_matches.pop() {
1036            if self.rules[rule_match.rule_index].match_action.is_mutating() {
1037                // Mutating rules are kept only if they don't overlap with a previous rule.
1038                if let Some(last) = retained_rules.last()
1039                    && last.utf8_end > rule_match.utf8_start
1040                {
1041                    continue;
1042                }
1043            } else {
1044                // Only retain if it doesn't overlap with any other rule. Since mutating matches are sorted before non-mutated matches
1045                // this needs to check all retained matches (instead of just the last one)
1046                for retained_rule in &retained_rules {
1047                    if retained_rule.utf8_start < rule_match.utf8_end
1048                        && retained_rule.utf8_end > rule_match.utf8_start
1049                    {
1050                        continue 'rule_matches;
1051                    }
1052                }
1053            };
1054            retained_rules.push(rule_match);
1055        }
1056
1057        // ensure rules are sorted by start index (other parts of the library required this to function correctly)
1058        retained_rules.sort_unstable_by_key(|rule_match| rule_match.utf8_start);
1059
1060        *rule_matches = retained_rules;
1061    }
1062}
1063
1064impl Drop for Scanner {
1065    fn drop(&mut self) {
1066        let stats = &*GLOBAL_STATS;
1067        stats.scanner_deletions.increment(1);
1068        stats.decrement_total_scanners();
1069    }
1070}
1071
1072#[derive(Default)]
1073pub struct ScannerBuilder<'a> {
1074    rules: &'a [RootRuleConfig<Arc<dyn RuleConfig>>],
1075    labels: Labels,
1076    scanner_features: ScannerFeatures,
1077    async_scan_timeout: Duration,
1078}
1079
1080impl ScannerBuilder<'_> {
1081    pub fn new(rules: &[RootRuleConfig<Arc<dyn RuleConfig>>]) -> ScannerBuilder<'_> {
1082        ScannerBuilder {
1083            rules,
1084            labels: Labels::empty(),
1085            scanner_features: ScannerFeatures::default(),
1086            async_scan_timeout: Duration::from_secs(60 * 5),
1087        }
1088    }
1089
1090    pub fn labels(mut self, labels: Labels) -> Self {
1091        self.labels = labels;
1092        self
1093    }
1094
1095    pub fn with_async_scan_timeout(mut self, duration: Duration) -> Self {
1096        self.async_scan_timeout = duration;
1097        self
1098    }
1099
1100    pub fn with_implicit_wildcard_indexes_for_scopes(mut self, value: bool) -> Self {
1101        self.scanner_features.add_implicit_index_wildcards = value;
1102        self
1103    }
1104
1105    pub fn with_return_matches(mut self, value: bool) -> Self {
1106        self.scanner_features.return_matches = value;
1107        self
1108    }
1109
1110    /// Enables/Disables the Multipass V0 feature. This defaults to TRUE.
1111    /// Multipass V0 saves matches from excluded scopes, and marks any identical
1112    /// matches in included scopes as a false positive.
1113    pub fn with_multipass_v0(mut self, value: bool) -> Self {
1114        self.scanner_features.multipass_v0_enabled = value;
1115        self
1116    }
1117
1118    /// Enables/Disables debug observability features. This defaults to FALSE.
1119    /// When enabled, metrics will include additional tags (such as `sds_namespace`)
1120    /// to help debug the source of matches.
1121    pub fn with_debug_observability(mut self, value: bool) -> Self {
1122        self.scanner_features.enable_debug_observability = value;
1123        self
1124    }
1125
1126    pub fn build(self) -> Result<Scanner, CreateScannerError> {
1127        // When third-party active checkers are compiled out, no validators are ever
1128        // created; the map stays empty and `mut` would be unused.
1129        #[cfg_attr(not(feature = "third-party-active-checkers"), allow(unused_mut))]
1130        let mut match_validators_per_type = AHashMap::new();
1131
1132        #[cfg(feature = "third-party-active-checkers")]
1133        for rule in self.rules.iter() {
1134            if let Some(match_validation_type) = &rule.get_third_party_active_checker()
1135                && match_validation_type.can_create_match_validator()
1136            {
1137                let internal_type = match_validation_type.get_internal_match_validation_type();
1138                let match_validator = match_validation_type.into_match_validator();
1139                if let Ok(match_validator) = match_validator {
1140                    if !match_validators_per_type.contains_key(&internal_type) {
1141                        match_validators_per_type.insert(internal_type, match_validator);
1142                    }
1143                } else {
1144                    return Err(CreateScannerError::InvalidMatchValidator(
1145                        MatchValidatorCreationError::InternalError,
1146                    ));
1147                }
1148            }
1149        }
1150
1151        let compiled_rules = self
1152            .rules
1153            .iter()
1154            .enumerate()
1155            .map(|(rule_index, config)| {
1156                if config.is_supporting_rule && config.match_action != MatchAction::None {
1157                    return Err(CreateScannerError::SupportingRuleHasMatchAction);
1158                }
1159                let inner = config.convert_to_compiled_rule(rule_index, self.labels.clone())?;
1160                config.match_action.validate()?;
1161                let compiled_suppressions = match &config.suppressions {
1162                    Some(s) => s.compile()?,
1163                    None => None,
1164                };
1165                Ok(RootCompiledRule {
1166                    inner,
1167                    scope: config.scope.clone(),
1168                    match_action: config.match_action.clone(),
1169                    match_validation_type: config.get_third_party_active_checker().cloned(),
1170                    suppressions: compiled_suppressions,
1171                    precedence: config.precedence,
1172                    is_supporting_rule: config.is_supporting_rule,
1173                    sds_rule_name: metrics::compute_sds_rule_name(&config.tags),
1174                })
1175            })
1176            .collect::<Result<Vec<RootCompiledRule>, CreateScannerError>>()?;
1177
1178        let mut per_scanner_data = SharedData::new();
1179
1180        compiled_rules.iter().for_each(|rule| {
1181            rule.init_per_scanner_data(&mut per_scanner_data);
1182        });
1183
1184        let scoped_ruleset = ScopedRuleSet::new(
1185            &compiled_rules
1186                .iter()
1187                .map(|rule| rule.scope.clone())
1188                .collect::<Vec<_>>(),
1189        )
1190        .with_implicit_index_wildcards(self.scanner_features.add_implicit_index_wildcards);
1191
1192        {
1193            let stats = &*GLOBAL_STATS;
1194            stats.scanner_creations.increment(1);
1195            stats.increment_total_scanners();
1196        }
1197
1198        Ok(Scanner {
1199            rules: compiled_rules,
1200            scoped_ruleset,
1201            scanner_features: self.scanner_features,
1202            metrics: ScannerMetrics::new(&self.labels),
1203            match_validators_per_type,
1204            labels: self.labels,
1205            per_scanner_data,
1206            async_scan_timeout: self.async_scan_timeout,
1207        })
1208    }
1209}
1210
1211struct ScannerContentVisitor<'a, E: Encoding> {
1212    scanner: &'a Scanner,
1213    regex_caches: &'a mut RegexCaches,
1214    rule_matches: &'a mut InternalRuleMatchSet<E>,
1215    // Rules that shall be skipped for this scan
1216    // This list shall be small (<10), so a linear search is acceptable
1217    blocked_rules: &'a Vec<usize>,
1218    excluded_matches: &'a mut AHashMap<String, String>,
1219    per_event_data: SharedData,
1220    wildcarded_indexes: &'a AHashMap<Path<'static>, Vec<(usize, usize)>>,
1221    async_jobs: &'a mut Vec<PendingRuleJob>,
1222    event_id: Option<String>,
1223    scan_metadata: &'a AHashMap<String, String>,
1224}
1225
1226impl<'a, E: Encoding> ContentVisitor<'a> for ScannerContentVisitor<'a, E> {
1227    fn visit_content<'b>(
1228        &'b mut self,
1229        path: &Path<'a>,
1230        content: &str,
1231        mut rule_visitor: crate::scoped_ruleset::RuleIndexVisitor,
1232        exclusion_check: ExclusionCheck<'b>,
1233    ) -> Result<bool, ScannerError> {
1234        // matches for a single path
1235        let mut path_rules_matches = vec![];
1236
1237        // Create a map of per rule type data that can be shared between rules of the same type
1238        let mut per_string_data = SharedData::new();
1239        let wildcard_indices_per_path = self.wildcarded_indexes.get(path);
1240
1241        rule_visitor.visit_rule_indices(|rule_index| {
1242            if self.blocked_rules.contains(&rule_index) {
1243                return Ok(());
1244            }
1245            let rule = &self.scanner.rules[rule_index];
1246            {
1247                if rule.inner.allow_scanner_to_exclude_namespace() {
1248                    // check if the path is excluded
1249                    if exclusion_check.is_excluded(rule_index) {
1250                        return Ok(());
1251                    }
1252                }
1253                // creating the emitter is basically free, it will get mostly optimized away
1254                let mut emitter = |rule_match: StringMatch| {
1255                    // This should never happen, but to ensure no empty match is ever generated
1256                    // (which may cause an infinite loop), this will panic instead.
1257                    assert_ne!(
1258                        rule_match.start, rule_match.end,
1259                        "empty match detected on rule with index {rule_index}"
1260                    );
1261                    path_rules_matches.push(InternalRuleMatch::new(rule_index, rule_match));
1262                };
1263
1264                rule.init_per_string_data(&self.scanner.labels, &mut per_string_data);
1265
1266                // TODO: move this somewhere higher?
1267                rule.init_per_event_data(&mut self.per_event_data);
1268
1269                let mut ctx = StringMatchesCtx {
1270                    rule_index,
1271                    regex_caches: self.regex_caches,
1272                    exclusion_check: &exclusion_check,
1273                    excluded_matches: self.excluded_matches,
1274                    match_emitter: &mut emitter,
1275                    wildcard_indices: wildcard_indices_per_path,
1276                    enable_debug_observability: self
1277                        .scanner
1278                        .scanner_features
1279                        .enable_debug_observability,
1280                    per_string_data: &mut per_string_data,
1281                    per_scanner_data: &self.scanner.per_scanner_data,
1282                    per_event_data: &mut self.per_event_data,
1283                    event_id: self.event_id.as_deref(),
1284                    scan_metadata: self.scan_metadata,
1285                };
1286
1287                let async_status = rule.get_string_matches(content, path, &mut ctx)?;
1288
1289                match async_status {
1290                    RuleStatus::Done => {
1291                        // nothing to do
1292                    }
1293                    RuleStatus::Pending(fut) => {
1294                        self.async_jobs.push(PendingRuleJob {
1295                            fut,
1296                            path: path.into_static(),
1297                        });
1298                    }
1299                }
1300            }
1301            Ok(())
1302        })?;
1303
1304        // If there are any matches, the string will need to be accessed to check for false positives from
1305        // excluded matches, any to potentially mutate the string.
1306        // If there are any async jobs, this is also true since it's not known yet whether there
1307        // will be a match
1308        let needs_to_access_content = !path_rules_matches.is_empty() || !self.async_jobs.is_empty();
1309
1310        self.rule_matches
1311            .push_sync_matches(path, path_rules_matches);
1312
1313        Ok(needs_to_access_content)
1314    }
1315}
1316
1317// Calculates the next starting position for a regex match if a the previous match is a false positive
1318fn get_next_regex_start(content: &str, regex_match: (usize, usize)) -> Option<usize> {
1319    // The next valid UTF8 char after the start of the regex match is used
1320    if let Some((i, _)) = content[regex_match.0..].char_indices().nth(1) {
1321        Some(regex_match.0 + i)
1322    } else {
1323        // There are no more chars left in the string to scan
1324        None
1325    }
1326}
1327
1328fn is_false_positive_match(
1329    regex_match_range: (usize, usize),
1330    rule: &RegexCompiledRule,
1331    content: &str,
1332    check_excluded_keywords: bool,
1333) -> bool {
1334    if check_excluded_keywords
1335        && let Some(excluded_keywords) = &rule.excluded_keywords
1336        && excluded_keywords.is_false_positive_match(content, regex_match_range.0)
1337    {
1338        return true;
1339    }
1340
1341    if let Some(validator) = rule.validator.as_ref()
1342        && !validator.is_valid_match(&content[regex_match_range.0..regex_match_range.1])
1343    {
1344        return true;
1345    }
1346    false
1347}