systemprompt_security/policy/secrets/
recovery.rs1use std::ops::Range;
14
15use systemprompt_identifiers::SecretPatternId;
16
17use super::super::GovernedInput;
18use super::patterns::HIGH_ENTROPY_PATTERN;
19use super::{COMPILED, EntropyConfig, SECRET_PATTERNS, SignatureExemptions};
20
21pub const REDACTION_MARKER: &str = "[REDACTED_BY_GOVERNANCE]";
22pub const MAX_RECOVERY_FINDINGS: usize = 4096;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct SecretSource {
27 pub part_index: usize,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct SecretFinding {
34 pub source: SecretSource,
35 pub span: Range<usize>,
36 pub pattern_id: SecretPatternId,
37}
38
39#[must_use]
40pub fn secret_findings(input: &GovernedInput, entropy: &EntropyConfig) -> Vec<SecretFinding> {
41 let strings = input.strings();
42 let exemptions = SignatureExemptions::from_strings(&strings);
43 strings
44 .iter()
45 .enumerate()
46 .flat_map(|(part_index, found)| {
47 let source = SecretSource { part_index };
48 let value = found.value;
49 let patterns = COMPILED.iter().flat_map(move |(index, regex)| {
50 let pattern = &SECRET_PATTERNS[*index];
51 regex.find_iter(value).map(move |hit| SecretFinding {
52 source,
53 span: if pattern.redact_whole_value {
54 0..value.len()
55 } else {
56 hit.range()
57 },
58 pattern_id: SecretPatternId::new(pattern.id),
59 })
60 });
61 let tokens = (!exemptions.exempts_entropy(&found.path))
62 .then(|| {
63 super::entropy::high_entropy_spans(value, entropy).map(move |(span, _)| {
64 SecretFinding {
65 source,
66 span,
67 pattern_id: SecretPatternId::new(HIGH_ENTROPY_PATTERN.id),
68 }
69 })
70 })
71 .into_iter()
72 .flatten();
73 patterns.chain(tokens)
74 })
75 .take(MAX_RECOVERY_FINDINGS + 1)
76 .collect()
77}
78
79#[must_use]
80pub fn redact_spans(value: &str, spans: impl IntoIterator<Item = Range<usize>>) -> Option<String> {
81 let mut spans: Vec<_> = spans.into_iter().collect();
82 spans.sort_unstable_by_key(|span| (span.start, span.end));
83 let mut out = String::new();
84 let mut cursor = 0;
85 for span in spans {
86 if span.start >= span.end || value.get(span.clone()).is_none() {
87 return None;
88 }
89 if span.end <= cursor {
90 continue;
91 }
92 if span.start >= cursor {
93 out.push_str(value.get(cursor..span.start)?);
94 out.push_str(REDACTION_MARKER);
95 }
96 cursor = span.end;
97 }
98 out.push_str(value.get(cursor..)?);
99 Some(out)
100}