mod entropy;
mod patterns;
use std::sync::LazyLock;
use regex::Regex;
use super::governed::GovernedInput;
pub use entropy::{DEFAULT_MIN_LEN, DEFAULT_THRESHOLD, EntropyConfig, find_high_entropy_token};
use patterns::HIGH_ENTROPY_PATTERN;
pub use patterns::{SECRET_PATTERNS, SecretPattern};
static DEFAULT_ENTROPY: LazyLock<EntropyConfig> = LazyLock::new(EntropyConfig::default);
static COMPILED: LazyLock<Vec<(usize, Regex)>> = LazyLock::new(|| {
SECRET_PATTERNS
.iter()
.enumerate()
.filter_map(|(i, p)| match Regex::new(p.expr) {
Ok(re) => Some((i, re)),
Err(e) => {
tracing::error!(pattern_id = %p.id, error = %e, "secret pattern disabled: regex failed to compile");
None
},
})
.collect()
});
#[must_use]
pub fn compiled_pattern_count() -> usize {
COMPILED.len()
}
fn redacted_snippet(s: &str, match_start: usize) -> String {
let mut snippet_end = (match_start + 12).min(s.len());
while !s.is_char_boundary(snippet_end) {
snippet_end -= 1;
}
format!("{}...[REDACTED]", &s[match_start..snippet_end])
}
fn scan_str(s: &str, entropy: &EntropyConfig) -> Option<(&'static SecretPattern, String)> {
for (i, re) in COMPILED.iter() {
if let Some(m) = re.find(s) {
return Some((&SECRET_PATTERNS[*i], redacted_snippet(s, m.start())));
}
}
find_high_entropy_token(s, entropy).map(|token| {
let start = token.as_ptr() as usize - s.as_ptr() as usize;
(&HIGH_ENTROPY_PATTERN, redacted_snippet(s, start))
})
}
#[must_use]
pub fn scan_str_for_secret(text: &str) -> Option<String> {
scan_str(text, &DEFAULT_ENTROPY).map(|(_, redacted)| redacted)
}
#[derive(Debug)]
pub struct SecretHit {
pub pattern: &'static SecretPattern,
pub path: String,
pub redacted: String,
}
#[must_use]
pub fn detect_secrets(input: &GovernedInput) -> Option<SecretHit> {
detect_secrets_with(input, &DEFAULT_ENTROPY)
}
#[must_use]
pub fn detect_secrets_with(input: &GovernedInput, entropy: &EntropyConfig) -> Option<SecretHit> {
input.strings().into_iter().find_map(|s| {
scan_str(s.value, entropy).map(|(pattern, redacted)| SecretHit {
pattern,
path: s.path,
redacted,
})
})
}