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