use crate::proximity_keywords::{
CompiledExcludedProximityKeywords, CompiledIncludedProximityKeywords, contains_keyword_in_path,
get_prefix_start, is_index_within_prefix,
};
use crate::scanner::metrics::RuleMetrics;
use crate::scanner::regex_rule::regex_cache_store::RegexCacheValue;
use crate::scanner::regex_rule::regex_store::SharedRegex;
use crate::scanner::{
RuleResult, RuleStatus, StringMatchesCtx, get_next_regex_start, is_false_positive_match,
};
use crate::secondary_validation::Validator;
use crate::{CompiledRule, ExclusionCheck, Labels, Path, StringMatch};
use ahash::AHashMap;
use metrics::counter;
use regex_automata::Input;
use regex_automata::meta::Cache;
use regex_automata::util::captures::Captures;
use std::sync::Arc;
#[derive(Clone)]
pub struct RegexCompiledRule {
pub rule_index: usize,
pub regex: SharedRegex,
pub included_keywords: Option<CompiledIncludedProximityKeywords>,
pub excluded_keywords: Option<CompiledExcludedProximityKeywords>,
pub validator: Option<Arc<dyn Validator>>,
pub metrics: RuleMetrics,
pub pattern_capture_groups: Option<Vec<String>>,
}
impl CompiledRule for RegexCompiledRule {
fn allow_scanner_to_exclude_namespace(&self) -> bool {
false
}
fn get_string_matches(
&self,
content: &str,
path: &Path,
ctx: &mut StringMatchesCtx,
) -> RuleResult {
match self.included_keywords {
Some(ref included_keywords) => {
self.get_string_matches_with_included_keywords(
content,
path,
ctx,
included_keywords,
);
}
None => {
let enable_debug = ctx.enable_debug_observability;
let cache_value = ctx.regex_caches.get(&self.regex);
let true_positive_search = self.true_positive_matches(
content,
0,
cache_value,
true,
ctx.exclusion_check,
ctx.excluded_matches,
path,
enable_debug,
);
for string_match in true_positive_search {
ctx.match_emitter.emit(string_match);
}
}
}
Ok(RuleStatus::Done)
}
fn should_exclude_multipass_v0(&self) -> bool {
true
}
fn on_excluded_match_multipass_v0(
&self,
path: &Path,
excluded_path: &str,
enable_debug_observability: bool,
) {
if enable_debug_observability {
let labels = self.metrics.base_labels.clone_with_labels(Labels::new(&[
("sds_namespace", path.to_string()),
("sds_excluded_namespace", excluded_path.to_string()),
]));
counter!("false_positive.multipass.excluded_match", labels).increment(1);
} else {
self.metrics.false_positive_excluded_attributes.increment(1);
}
}
fn as_regex_rule(&self) -> Option<&RegexCompiledRule> {
Some(self)
}
fn as_regex_rule_mut(&mut self) -> Option<&mut RegexCompiledRule> {
Some(self)
}
}
impl RegexCompiledRule {
#[allow(clippy::too_many_arguments)]
fn get_string_matches_with_included_keywords(
&self,
content: &str,
path: &Path,
ctx: &mut StringMatchesCtx,
included_keywords: &CompiledIncludedProximityKeywords,
) {
let enable_debug = ctx.enable_debug_observability;
let mut included_keyword_matches = included_keywords.keyword_matches(content);
'included_keyword_search: while let Some(included_keyword_match) =
included_keyword_matches.next(ctx.regex_caches)
{
let cache_value = ctx.regex_caches.get(&self.regex);
let true_positive_search = self.true_positive_matches(
content,
included_keyword_match.end,
cache_value,
false,
ctx.exclusion_check,
ctx.excluded_matches,
path,
enable_debug,
);
for true_positive_match in true_positive_search {
if is_index_within_prefix(
content,
included_keyword_match.start,
true_positive_match.start,
included_keywords.look_ahead_character_count,
) {
included_keyword_matches.skip_to(true_positive_match.start);
ctx.match_emitter.emit(true_positive_match);
} else {
let new_start = get_prefix_start(
true_positive_match.start,
included_keywords.look_ahead_character_count,
content,
)
.start;
included_keyword_matches.skip_to(new_start);
continue 'included_keyword_search;
}
}
break;
}
let mut has_verified_kws_in_path: Option<bool> = None;
{
let input = Input::new(content);
let cache_value = ctx.regex_caches.get(&self.regex);
if self
.regex
.search_half_with(&mut cache_value.cache, &input)
.is_some()
{
has_verified_kws_in_path = Some(contains_keyword_in_path(
&path.sanitize(),
&included_keywords.keywords_pattern,
))
}
};
if has_verified_kws_in_path.is_none() || has_verified_kws_in_path.is_some_and(|x| !x) {
return;
}
let cache_value = ctx.regex_caches.get(&self.regex);
let true_positive_search = self.true_positive_matches(
content,
0,
cache_value,
false,
ctx.exclusion_check,
ctx.excluded_matches,
path,
enable_debug,
);
for string_match in true_positive_search {
ctx.match_emitter.emit(string_match);
}
}
#[allow(clippy::too_many_arguments)]
fn true_positive_matches<'a>(
&'a self,
content: &'a str,
start: usize,
cache: &'a mut RegexCacheValue,
check_excluded_keywords: bool,
exclusion_check: &'a ExclusionCheck<'a>,
excluded_matches: &'a mut AHashMap<String, String>,
scan_path: &'a Path<'a>,
enable_debug_observability: bool,
) -> TruePositiveSearch<'a> {
TruePositiveSearch {
rule: self,
content,
start,
cache: &mut cache.cache,
captures: &mut cache.captures,
check_excluded_keywords,
exclusion_check,
excluded_matches,
scan_path,
enable_debug_observability,
}
}
}
pub struct TruePositiveSearch<'a> {
rule: &'a RegexCompiledRule,
content: &'a str,
start: usize,
cache: &'a mut Cache,
check_excluded_keywords: bool,
exclusion_check: &'a ExclusionCheck<'a>,
excluded_matches: &'a mut AHashMap<String, String>,
captures: &'a mut Captures,
scan_path: &'a Path<'a>,
enable_debug_observability: bool,
}
impl TruePositiveSearch<'_> {
fn perform_regex_scan(&mut self, input: &Input) -> Option<(usize, usize)> {
match &self.rule.pattern_capture_groups {
Some(capture_group) => {
self.captures.clear();
self.rule
.regex
.search_captures_with(self.cache, input, self.captures);
self.captures
.get_group_by_name(
capture_group
.first()
.expect("pattern_capture_group is empty"),
)
.map(|span| (span.start, span.end))
}
None => self
.rule
.regex
.search_with(self.cache, input)
.map(|re_match| (re_match.start(), re_match.end())),
}
}
}
impl Iterator for TruePositiveSearch<'_> {
type Item = StringMatch;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.start > self.content.len() {
return None;
}
let input = Input::new(self.content).range(self.start..);
self.rule.regex.search_half_with(self.cache, &input)?;
let regex_match_range = self.perform_regex_scan(&input)?;
let is_false_positive_match = is_false_positive_match(
regex_match_range,
self.rule,
self.content,
self.check_excluded_keywords,
);
if is_false_positive_match {
if let Some(next) = get_next_regex_start(self.content, regex_match_range) {
self.start = next;
} else {
return None;
}
} else {
self.start = regex_match_range.1;
if self.exclusion_check.is_excluded(self.rule.rule_index) {
let match_str = &self.content[regex_match_range.0..regex_match_range.1];
if !self.excluded_matches.contains_key(match_str) {
let path = if self.enable_debug_observability {
self.scan_path.to_string()
} else {
String::new()
};
self.excluded_matches.insert(match_str.to_string(), path);
}
} else {
return Some(StringMatch {
start: regex_match_range.0,
end: regex_match_range.1,
keyword: None,
});
}
}
}
}
}