llm_kernel/safety/
sanitize.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6static SECRET_RE: LazyLock<Regex> = LazyLock::new(|| {
12 Regex::new(
13 r"(?i:bearer |basic )(\S+)|((?:password|token|key|secret|api_key|apikey|access_token|private_key)=)(\S+)|(sk-\S*)|(AKIA[A-Za-z0-9]+)|(gh[posu]_[A-Za-z0-9]+)"
14 ).expect("SECRET_RE is valid")
15});
16
17pub fn mask_secrets(input: &str) -> String {
24 SECRET_RE
25 .replace_all(input, |caps: ®ex::Captures| -> String {
26 if caps.get(1).is_some() {
27 let full = caps.get(0).unwrap().as_str();
29 let val = caps.get(1).unwrap().as_str();
30 let prefix = &full[..full.len() - val.len()];
31 format!("{prefix}****")
32 } else if let Some(key_prefix) = caps.get(2) {
33 format!("{}****", key_prefix.as_str())
35 } else {
36 "****".to_string()
38 }
39 })
40 .into_owned()
41}
42
43pub fn strip_ansi(input: &str) -> String {
45 let mut result = String::with_capacity(input.len());
46 let mut chars = input.chars().peekable();
47 while let Some(ch) = chars.next() {
48 if ch == '\x1B' {
49 if chars.peek() == Some(&'[') {
51 chars.next(); while let Some(&next) = chars.peek() {
54 let cp = next as u32;
55 if (0x30..=0x3F).contains(&cp) || (0x20..=0x2F).contains(&cp) {
56 chars.next();
57 } else if (0x40..=0x7E).contains(&cp) {
58 chars.next(); break;
60 } else {
61 break;
62 }
63 }
64 }
65 continue;
66 }
67 result.push(ch);
68 }
69 result
70}
71
72pub fn sanitize_output(input: &str) -> String {
80 input
81 .chars()
82 .filter(|c| {
83 let cp = *c as u32;
84 if cp == 0 {
86 return false;
87 }
88 if (0x80..=0x9F).contains(&cp) {
90 return false;
91 }
92 if (0x202A..=0x202E).contains(&cp) {
94 return false;
95 }
96 if cp == 0x2028 || cp == 0x2029 {
98 return false;
99 }
100 if (0xE0000..=0xE007F).contains(&cp) {
102 return false;
103 }
104 true
105 })
106 .collect()
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn mask_bearer_token() {
115 let input = "Authorization: Bearer sk-longtoken123456";
116 let masked = mask_secrets(input);
117 assert!(masked.contains("****"));
118 }
119
120 #[test]
121 fn mask_password_value() {
122 let input = "password=supersecret other=value";
123 let masked = mask_secrets(input);
124 assert!(masked.contains("password=****"));
125 assert!(masked.contains("other=value"));
126 }
127
128 #[test]
129 fn mask_token_value() {
130 let masked = mask_secrets("token=abc123");
131 assert!(masked.contains("token=****"));
132 }
133
134 #[test]
135 fn mask_key_value() {
136 let masked = mask_secrets("key=my-api-key-here");
137 assert!(masked.contains("key=****"));
138 }
139
140 #[test]
141 fn mask_secret_value() {
142 let masked = mask_secrets("secret=data rest=ok");
143 assert!(masked.contains("secret=****"));
144 assert!(masked.contains("rest=ok"));
145 }
146
147 #[test]
148 fn no_secrets_unchanged() {
149 let input = "hello world 123";
150 assert_eq!(mask_secrets(input), input);
151 }
152
153 #[test]
154 fn mask_multiple_passwords() {
155 let masked = mask_secrets("password=a password=b");
156 assert!(masked.contains("password=****"), "got: {masked}");
157 }
158
159 #[test]
160 fn mask_multiple_bearer() {
161 let masked = mask_secrets("Bearer token1 and Bearer token2");
162 assert_eq!(masked, "Bearer **** and Bearer ****");
163 }
164
165 #[test]
166 fn mask_standalone_sk_key() {
167 let masked = mask_secrets("key is sk-proj-abc123 here");
168 assert!(masked.contains("****"), "got: {masked}");
169 assert!(!masked.contains("sk-proj"), "got: {masked}");
170 }
171
172 #[test]
173 fn mask_api_key_value() {
174 let masked = mask_secrets("api_key=my-secret-key other=ok");
175 assert!(masked.contains("api_key=****"));
176 assert!(masked.contains("other=ok"));
177 }
178
179 #[test]
180 fn mask_apikey_nounderscore() {
181 let masked = mask_secrets("apikey=abc123");
182 assert!(masked.contains("apikey=****"));
183 }
184
185 #[test]
186 fn mask_access_token_value() {
187 let masked = mask_secrets("access_token=eyJhbGciOiJI");
188 assert!(masked.contains("access_token=****"));
189 }
190
191 #[test]
192 fn mask_private_key_value() {
193 let masked = mask_secrets("private_key=-----BEGIN RSA");
194 assert!(masked.contains("private_key=****"));
195 }
196
197 #[test]
198 fn mask_basic_auth() {
199 let masked = mask_secrets("Authorization: Basic dXNlcjpwYXNz");
200 assert!(masked.contains("Basic ****"));
201 }
202
203 #[test]
204 fn mask_bearer_case_insensitive() {
205 let masked = mask_secrets("auth: bearer token123 here");
206 assert!(masked.contains("bearer ****"));
207 }
208
209 #[test]
210 fn mask_aws_access_key() {
211 let masked = mask_secrets("key=AKIAIOSFODNN7EXAMPLE end");
212 assert!(masked.contains("key=****"));
213 }
214
215 #[test]
216 fn mask_standalone_aws_key() {
217 let masked = mask_secrets("found AKIAIOSFODNN7EXAMPLE in config");
218 assert!(!masked.contains("AKIAIOSFODNN7EXAMPLE"), "got: {masked}");
219 assert!(masked.contains("****"));
220 }
221
222 #[test]
223 fn mask_github_pat() {
224 let masked = mask_secrets("token ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh end");
225 assert!(!masked.contains("ghp_"), "got: {masked}");
226 assert!(masked.contains("****"));
227 }
228
229 #[test]
230 fn mask_github_oauth() {
231 let masked = mask_secrets("using gho_ABCDEF1234567890 here");
232 assert!(!masked.contains("gho_"), "got: {masked}");
233 }
234
235 #[test]
236 fn sanitize_removes_bidi() {
237 let input = "hello\u{202E}world";
238 let clean = sanitize_output(input);
239 assert_eq!(clean, "helloworld");
240 }
241
242 #[test]
243 fn sanitize_removes_plane14() {
244 let input = "text\u{E0001}hidden";
245 let clean = sanitize_output(input);
246 assert_eq!(clean, "texthidden");
247 }
248
249 #[test]
250 fn sanitize_removes_null() {
251 let clean = sanitize_output("a\u{0000}b");
252 assert_eq!(clean, "ab");
253 }
254
255 #[test]
256 fn sanitize_removes_line_sep() {
257 let clean = sanitize_output("a\u{2028}b");
258 assert_eq!(clean, "ab");
259 }
260
261 #[test]
262 fn sanitize_preserves_normal() {
263 let input = "Hello, 世界! 🎉";
264 assert_eq!(sanitize_output(input), input);
265 }
266
267 #[test]
268 fn sanitize_removes_c1_controls() {
269 let clean = sanitize_output("a\u{0080}b\u{009F}c");
270 assert_eq!(clean, "abc");
271 }
272
273 #[test]
274 fn strip_ansi_removes_color_codes() {
275 let input = "\x1B[31mHello\x1B[0m \x1B[1;32mWorld\x1B[0m";
276 let clean = strip_ansi(input);
277 assert_eq!(clean, "Hello World");
278 }
279
280 #[test]
281 fn strip_ansi_preserves_plain_text() {
282 let input = "Hello, 世界! 🎉";
283 assert_eq!(strip_ansi(input), input);
284 }
285
286 #[test]
287 fn strip_ansi_handles_complex_sequence() {
288 let input = "\x1B[38;5;196mRed\x1B[0m \x1B[38;2;0;255;0mGreen\x1B[0m";
290 let clean = strip_ansi(input);
291 assert_eq!(clean, "Red Green");
292 }
293}