systemprompt_security/policy/secrets/
mod.rs1mod patterns;
17
18use std::sync::LazyLock;
19
20use regex::Regex;
21
22use super::governed::GovernedInput;
23use patterns::HIGH_ENTROPY_PATTERN;
24pub use patterns::{SECRET_PATTERNS, SecretPattern};
25
26static COMPILED: LazyLock<Vec<(usize, Regex)>> = LazyLock::new(|| {
27 SECRET_PATTERNS
28 .iter()
29 .enumerate()
30 .filter_map(|(i, p)| match Regex::new(p.expr) {
31 Ok(re) => Some((i, re)),
32 Err(e) => {
35 tracing::error!(pattern_id = %p.id, error = %e, "secret pattern disabled: regex failed to compile");
36 None
37 },
38 })
39 .collect()
40});
41
42#[must_use]
46pub fn compiled_pattern_count() -> usize {
47 COMPILED.len()
48}
49
50const ENTROPY_MIN_LEN: usize = 32;
51
52const ENTROPY_THRESHOLD: f64 = 4.0;
55
56fn shannon_entropy(s: &str) -> f64 {
57 let mut counts = [0u32; 256];
58 let bytes = s.as_bytes();
59 for &b in bytes {
60 counts[usize::from(b)] += 1;
61 }
62 let len = bytes.len() as f64;
63 counts
64 .iter()
65 .filter(|&&c| c > 0)
66 .map(|&c| {
67 let p = f64::from(c) / len;
68 -p * p.log2()
69 })
70 .sum()
71}
72
73#[must_use]
77pub fn find_high_entropy_token(text: &str) -> Option<&str> {
78 text.split(|c: char| c.is_whitespace() || "\"'`()[]{}<>,;:".contains(c))
79 .find(|token| {
80 token.len() >= ENTROPY_MIN_LEN
81 && token
82 .chars()
83 .all(|c| c.is_ascii_alphanumeric() || "+/=_-".contains(c))
84 && token.chars().any(|c| c.is_ascii_uppercase())
85 && token.chars().any(|c| c.is_ascii_lowercase())
86 && token.chars().any(|c| c.is_ascii_digit())
87 && shannon_entropy(token) >= ENTROPY_THRESHOLD
88 })
89}
90
91fn redacted_snippet(s: &str, match_start: usize) -> String {
92 let mut snippet_end = (match_start + 12).min(s.len());
93 while !s.is_char_boundary(snippet_end) {
94 snippet_end -= 1;
95 }
96 format!("{}...[REDACTED]", &s[match_start..snippet_end])
97}
98
99fn scan_str(s: &str) -> Option<(&'static SecretPattern, String)> {
100 for (i, re) in COMPILED.iter() {
101 if let Some(m) = re.find(s) {
102 return Some((&SECRET_PATTERNS[*i], redacted_snippet(s, m.start())));
103 }
104 }
105 find_high_entropy_token(s).map(|token| {
106 let start = token.as_ptr() as usize - s.as_ptr() as usize;
107 (&HIGH_ENTROPY_PATTERN, redacted_snippet(s, start))
108 })
109}
110
111#[must_use]
114pub fn scan_str_for_secret(text: &str) -> Option<String> {
115 scan_str(text).map(|(_, redacted)| redacted)
116}
117
118#[derive(Debug)]
122pub struct SecretHit {
123 pub pattern: &'static SecretPattern,
124 pub path: String,
125 pub redacted: String,
126}
127
128#[must_use]
129pub fn detect_secrets(input: &GovernedInput) -> Option<SecretHit> {
130 input.strings().into_iter().find_map(|s| {
131 scan_str(s.value).map(|(pattern, redacted)| SecretHit {
132 pattern,
133 path: s.path,
134 redacted,
135 })
136 })
137}