cuenv_events/
redaction.rs1use std::collections::HashSet;
8use std::sync::{LazyLock, RwLock};
9
10pub const MIN_SECRET_LENGTH: usize = 4;
12
13pub const REDACTED_PLACEHOLDER: &str = "*_*";
15
16static SECRET_REGISTRY: LazyLock<RwLock<HashSet<String>>> =
18 LazyLock::new(|| RwLock::new(HashSet::new()));
19
20pub fn register_secret(secret: impl Into<String>) {
34 let secret = secret.into();
35 if secret.len() >= MIN_SECRET_LENGTH
36 && let Ok(mut registry) = SECRET_REGISTRY.write()
37 {
38 registry.insert(secret);
39 }
40}
41
42pub fn register_secrets(secrets: impl IntoIterator<Item = impl Into<String>>) {
54 if let Ok(mut registry) = SECRET_REGISTRY.write() {
55 for secret in secrets {
56 let s = secret.into();
57 if s.len() >= MIN_SECRET_LENGTH {
58 registry.insert(s);
59 }
60 }
61 }
62}
63
64#[must_use]
80pub fn redact(input: &str) -> String {
81 let secrets = match SECRET_REGISTRY.read() {
82 Ok(registry) => registry.clone(),
83 Err(_) => return input.to_string(),
84 };
85
86 if secrets.is_empty() {
87 return input.to_string();
88 }
89
90 let mut sorted: Vec<_> = secrets.into_iter().collect();
92 sorted.sort_by_key(|s| std::cmp::Reverse(s.len()));
93
94 let mut result = input.to_string();
95 for secret in &sorted {
96 result = result.replace(secret, REDACTED_PLACEHOLDER);
97 }
98 result
99}
100
101#[must_use]
103pub fn has_secrets() -> bool {
104 SECRET_REGISTRY
105 .read()
106 .map(|r| !r.is_empty())
107 .unwrap_or(false)
108}
109
110#[must_use]
112pub fn secret_count() -> usize {
113 SECRET_REGISTRY.read().map(|r| r.len()).unwrap_or(0)
114}
115
116#[cfg(test)]
120pub fn clear_secrets() {
121 if let Ok(mut registry) = SECRET_REGISTRY.write() {
122 registry.clear();
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use std::sync::Mutex;
130
131 static TEST_LOCK: Mutex<()> = Mutex::new(());
133
134 fn with_clean_registry<F, R>(f: F) -> R
135 where
136 F: FnOnce() -> R,
137 {
138 let _guard = TEST_LOCK.lock().unwrap();
139 clear_secrets();
140 let result = f();
141 clear_secrets();
142 result
143 }
144
145 #[test]
146 fn test_simple_redaction() {
147 with_clean_registry(|| {
148 register_secret("secret123");
149 let result = redact("The password is secret123, don't share it");
150 assert_eq!(result, "The password is *_*, don't share it");
151 });
152 }
153
154 #[test]
155 fn test_multiple_secrets() {
156 with_clean_registry(|| {
157 register_secrets(["password123", "api_key_xyz"]);
158 let result = redact("password123 and api_key_xyz are both secrets");
159 assert_eq!(result, "*_* and *_* are both secrets");
160 });
161 }
162
163 #[test]
164 fn test_repeated_secret() {
165 with_clean_registry(|| {
166 register_secret("secret");
167 let result = redact("secret appears twice: secret");
168 assert_eq!(result, "*_* appears twice: *_*");
169 });
170 }
171
172 #[test]
173 fn test_short_secret_ignored() {
174 with_clean_registry(|| {
175 register_secret("ab"); register_secret("abc"); register_secret("abcd"); assert_eq!(secret_count(), 1);
180
181 let result = redact("ab abc abcd");
182 assert_eq!(result, "ab abc *_*");
183 });
184 }
185
186 #[test]
187 fn test_empty_input() {
188 with_clean_registry(|| {
189 register_secret("secret");
190 let result = redact("");
191 assert_eq!(result, "");
192 });
193 }
194
195 #[test]
196 fn test_no_secrets_registered() {
197 with_clean_registry(|| {
198 assert!(!has_secrets());
199 let result = redact("nothing to redact here");
200 assert_eq!(result, "nothing to redact here");
201 });
202 }
203
204 #[test]
205 fn test_greedy_matching() {
206 with_clean_registry(|| {
207 register_secrets(["pass", "password"]);
209 let result = redact("the password is set");
210 assert_eq!(result, "the *_* is set");
212 });
213 }
214
215 #[test]
216 fn test_secret_at_boundaries() {
217 with_clean_registry(|| {
218 register_secret("secret");
219
220 let result = redact("secret is here");
222 assert_eq!(result, "*_* is here");
223
224 let result = redact("here is secret");
226 assert_eq!(result, "here is *_*");
227 });
228 }
229
230 #[test]
231 fn test_special_characters() {
232 with_clean_registry(|| {
233 register_secret("pass$word!@#");
234 let result = redact("the pass$word!@# is special");
235 assert_eq!(result, "the *_* is special");
236 });
237 }
238
239 #[test]
240 fn test_multiline_content() {
241 with_clean_registry(|| {
242 register_secret("secretkey");
243 let input = "line1\nsecretkey\nline3";
244 let result = redact(input);
245 assert_eq!(result, "line1\n*_*\nline3");
246 });
247 }
248
249 #[test]
250 fn test_has_secrets() {
251 with_clean_registry(|| {
252 assert!(!has_secrets());
253 register_secret("test_secret");
254 assert!(has_secrets());
255 });
256 }
257
258 #[test]
259 fn test_secret_count() {
260 with_clean_registry(|| {
261 assert_eq!(secret_count(), 0);
262 register_secret("secret1");
263 assert_eq!(secret_count(), 1);
264 register_secret("secret2");
265 assert_eq!(secret_count(), 2);
266 register_secret("secret1");
268 assert_eq!(secret_count(), 2);
269 });
270 }
271}