systemprompt_security/policy/secrets/
mod.rs1mod entropy;
17mod patterns;
18
19use std::sync::LazyLock;
20
21use regex::Regex;
22
23use super::governed::GovernedInput;
24pub use entropy::{DEFAULT_MIN_LEN, DEFAULT_THRESHOLD, EntropyConfig, find_high_entropy_token};
25use patterns::HIGH_ENTROPY_PATTERN;
26pub use patterns::{SECRET_PATTERNS, SecretPattern};
27
28static DEFAULT_ENTROPY: LazyLock<EntropyConfig> = LazyLock::new(EntropyConfig::default);
29
30static COMPILED: LazyLock<Vec<(usize, Regex)>> = LazyLock::new(|| {
31 SECRET_PATTERNS
32 .iter()
33 .enumerate()
34 .filter_map(|(i, p)| match Regex::new(p.expr) {
35 Ok(re) => Some((i, re)),
36 Err(e) => {
37 tracing::error!(pattern_id = %p.id, error = %e, "secret pattern disabled: regex failed to compile");
38 None
39 },
40 })
41 .collect()
42});
43
44#[must_use]
48pub fn compiled_pattern_count() -> usize {
49 COMPILED.len()
50}
51
52fn redacted_snippet(s: &str, match_start: usize) -> String {
53 let mut snippet_end = (match_start + 12).min(s.len());
54 while !s.is_char_boundary(snippet_end) {
55 snippet_end -= 1;
56 }
57 format!("{}...[REDACTED]", &s[match_start..snippet_end])
58}
59
60fn scan_str(s: &str, entropy: &EntropyConfig) -> Option<(&'static SecretPattern, String)> {
61 for (i, re) in COMPILED.iter() {
62 if let Some(m) = re.find(s) {
63 return Some((&SECRET_PATTERNS[*i], redacted_snippet(s, m.start())));
64 }
65 }
66 find_high_entropy_token(s, entropy).map(|token| {
67 let start = token.as_ptr() as usize - s.as_ptr() as usize;
68 (&HIGH_ENTROPY_PATTERN, redacted_snippet(s, start))
69 })
70}
71
72#[must_use]
75pub fn scan_str_for_secret(text: &str) -> Option<String> {
76 scan_str(text, &DEFAULT_ENTROPY).map(|(_, redacted)| redacted)
77}
78
79#[derive(Debug)]
83pub struct SecretHit {
84 pub pattern: &'static SecretPattern,
85 pub path: String,
86 pub redacted: String,
87}
88
89#[must_use]
90pub fn detect_secrets(input: &GovernedInput) -> Option<SecretHit> {
91 detect_secrets_with(input, &DEFAULT_ENTROPY)
92}
93
94#[must_use]
97pub fn detect_secrets_with(input: &GovernedInput, entropy: &EntropyConfig) -> Option<SecretHit> {
98 input.strings().into_iter().find_map(|s| {
99 scan_str(s.value, entropy).map(|(pattern, redacted)| SecretHit {
100 pattern,
101 path: s.path,
102 redacted,
103 })
104 })
105}