ricecoder_providers/
redaction.rs1use regex::Regex;
7use std::sync::OnceLock;
8
9pub struct RedactionFilter {
11 patterns: Vec<RedactionPattern>,
13}
14
15struct RedactionPattern {
17 regex: Regex,
19 replacement: String,
21}
22
23impl RedactionFilter {
24 pub fn new() -> Self {
26 Self {
27 patterns: vec![
28 RedactionPattern {
30 regex: Regex::new(r"sk-[A-Za-z0-9]{20,}").unwrap(),
31 replacement: "[REDACTED_OPENAI_KEY]".to_string(),
32 },
33 RedactionPattern {
35 regex: Regex::new(r"sk-ant-[A-Za-z0-9]{20,}").unwrap(),
36 replacement: "[REDACTED_ANTHROPIC_KEY]".to_string(),
37 },
38 RedactionPattern {
40 regex: Regex::new(r"(?i)(api[_-]?key|token|secret|password)\s*=\s*[^\s,;]+")
41 .unwrap(),
42 replacement: "$1=[REDACTED]".to_string(),
43 },
44 RedactionPattern {
46 regex: Regex::new(r"(?i)bearer\s+[A-Za-z0-9._\-/+=]+").unwrap(),
47 replacement: "Bearer [REDACTED]".to_string(),
48 },
49 RedactionPattern {
51 regex: Regex::new(r"(?i)authorization:\s*[^\s,;]+").unwrap(),
52 replacement: "Authorization: [REDACTED]".to_string(),
53 },
54 RedactionPattern {
56 regex: Regex::new(
57 r"(?i)(OPENAI|ANTHROPIC|GOOGLE|GROQ|MISTRAL)_API_KEY\s*=\s*[^\s,;]+",
58 )
59 .unwrap(),
60 replacement: "$1_API_KEY=[REDACTED]".to_string(),
61 },
62 ],
63 }
64 }
65
66 pub fn add_pattern(&mut self, pattern: &str, replacement: &str) -> Result<(), String> {
68 let regex = Regex::new(pattern).map_err(|e| e.to_string())?;
69 self.patterns.push(RedactionPattern {
70 regex,
71 replacement: replacement.to_string(),
72 });
73 Ok(())
74 }
75
76 pub fn redact(&self, input: &str) -> String {
78 let mut result = input.to_string();
79 for pattern in &self.patterns {
80 result = pattern
81 .regex
82 .replace_all(&result, &pattern.replacement)
83 .to_string();
84 }
85 result
86 }
87
88 pub fn contains_sensitive_info(&self, input: &str) -> bool {
90 self.patterns.iter().any(|p| p.regex.is_match(input))
91 }
92}
93
94impl Default for RedactionFilter {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100pub fn get_redaction_filter() -> &'static RedactionFilter {
102 static FILTER: OnceLock<RedactionFilter> = OnceLock::new();
103 FILTER.get_or_init(RedactionFilter::new)
104}
105
106pub fn redact(input: &str) -> String {
108 get_redaction_filter().redact(input)
109}
110
111pub fn contains_sensitive_info(input: &str) -> bool {
113 get_redaction_filter().contains_sensitive_info(input)
114}
115
116pub struct Redacted<T: AsRef<str>>(pub T);
118
119impl<T: AsRef<str>> std::fmt::Debug for Redacted<T> {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 write!(f, "{}", redact(self.0.as_ref()))
122 }
123}
124
125impl<T: AsRef<str>> std::fmt::Display for Redacted<T> {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 write!(f, "{}", redact(self.0.as_ref()))
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_redact_openai_key() {
137 let filter = RedactionFilter::new();
138 let input = "My API key is sk-1234567890abcdefghij";
139 let redacted = filter.redact(input);
140 assert!(!redacted.contains("sk-1234567890abcdefghij"));
141 assert!(redacted.contains("[REDACTED_OPENAI_KEY]"));
142 }
143
144 #[test]
145 fn test_redact_anthropic_key() {
146 let filter = RedactionFilter::new();
147 let input = "My API key is sk-ant-1234567890abcdefghij";
148 let redacted = filter.redact(input);
149 assert!(!redacted.contains("sk-ant-1234567890abcdefghij"));
150 assert!(redacted.contains("[REDACTED_ANTHROPIC_KEY]"));
151 }
152
153 #[test]
154 fn test_redact_api_key_equals() {
155 let filter = RedactionFilter::new();
156 let input = "api_key=secret123456789";
157 let redacted = filter.redact(input);
158 assert!(!redacted.contains("secret123456789"));
159 assert!(redacted.contains("[REDACTED]"));
160 }
161
162 #[test]
163 fn test_redact_bearer_token() {
164 let filter = RedactionFilter::new();
165 let input = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
166 let redacted = filter.redact(input);
167 assert!(!redacted.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"));
168 assert!(redacted.contains("[REDACTED]"));
170 }
171
172 #[test]
173 fn test_redact_env_var() {
174 let filter = RedactionFilter::new();
175 let input = "OPENAI_API_KEY=sk-1234567890abcdefghij";
176 let redacted = filter.redact(input);
177 assert!(!redacted.contains("sk-1234567890abcdefghij"));
178 assert!(redacted.contains("[REDACTED]"));
179 }
180
181 #[test]
182 fn test_contains_sensitive_info_true() {
183 let filter = RedactionFilter::new();
184 assert!(filter.contains_sensitive_info("My key is sk-1234567890abcdefghij"));
185 assert!(filter.contains_sensitive_info("api_key=secret123"));
186 assert!(filter.contains_sensitive_info("Bearer token123"));
187 }
188
189 #[test]
190 fn test_contains_sensitive_info_false() {
191 let filter = RedactionFilter::new();
192 assert!(!filter.contains_sensitive_info("This is a normal message"));
193 assert!(!filter.contains_sensitive_info("No secrets here"));
194 }
195
196 #[test]
197 fn test_add_custom_pattern() {
198 let mut filter = RedactionFilter::new();
199 filter
200 .add_pattern(r"custom_secret_\d+", "[CUSTOM_REDACTED]")
201 .unwrap();
202
203 let input = "Found custom_secret_12345";
204 let redacted = filter.redact(input);
205 assert!(redacted.contains("[CUSTOM_REDACTED]"));
206 }
207
208 #[test]
209 fn test_redacted_debug() {
210 let secret = "sk-1234567890abcdefghij";
211 let redacted = Redacted(secret);
212 let debug_str = format!("{:?}", redacted);
213 assert!(!debug_str.contains("sk-1234567890abcdefghij"));
214 assert!(debug_str.contains("[REDACTED_OPENAI_KEY]"));
215 }
216
217 #[test]
218 fn test_redacted_display() {
219 let secret = "api_key=secret123";
220 let redacted = Redacted(secret);
221 let display_str = format!("{}", redacted);
222 assert!(!display_str.contains("secret123"));
223 assert!(display_str.contains("[REDACTED]"));
224 }
225
226 #[test]
227 fn test_global_redaction_filter() {
228 let filter = get_redaction_filter();
229 let input = "My key is sk-1234567890abcdefghij";
230 let redacted = filter.redact(input);
231 assert!(redacted.contains("[REDACTED_OPENAI_KEY]"));
232 }
233
234 #[test]
235 fn test_global_redact_function() {
236 let input = "My key is sk-1234567890abcdefghij";
237 let redacted = redact(input);
238 assert!(redacted.contains("[REDACTED_OPENAI_KEY]"));
239 }
240
241 #[test]
242 fn test_multiple_keys_in_string() {
243 let filter = RedactionFilter::new();
244 let input = "openai: sk-1234567890abcdefghij, anthropic: sk-ant-1234567890abcdefghij";
245 let redacted = filter.redact(input);
246 assert!(!redacted.contains("sk-1234567890abcdefghij"));
247 assert!(!redacted.contains("sk-ant-1234567890abcdefghij"));
248 assert!(redacted.contains("[REDACTED_OPENAI_KEY]"));
249 assert!(redacted.contains("[REDACTED_ANTHROPIC_KEY]"));
250 }
251
252 #[test]
253 fn test_case_insensitive_redaction() {
254 let filter = RedactionFilter::new();
255 let input = "API_KEY=secret123 and ApiKey=secret456";
256 let redacted = filter.redact(input);
257 assert!(!redacted.contains("secret123"));
258 assert!(!redacted.contains("secret456"));
259 }
260}