agentzero_core/security/
redaction.rs1use regex::Regex;
2use std::error::Error;
3
4fn apply_regex(input: String, pattern: &str, replacement: &str) -> String {
5 let regex = Regex::new(pattern).expect("redaction regex must compile");
6 regex.replace_all(&input, replacement).to_string()
7}
8
9pub fn redact_text(input: &str) -> String {
10 let mut out = input.to_string();
11
12 out = apply_regex(
13 out,
14 r#"(?i)\b(OPENAI_API_KEY|TURSO_AUTH_TOKEN)\s*=\s*([^\s,;]+)"#,
15 "$1=[REDACTED]",
16 );
17 out = apply_regex(
18 out,
19 r#"(?i)"(api[_-]?key|auth[_-]?token)"\s*:\s*"[^"]*""#,
20 "\"$1\":\"[REDACTED]\"",
21 );
22 out = apply_regex(
23 out,
24 r#"(?i)\b(authorization:\s*bearer)\s+[^\s,;]+"#,
25 "$1 [REDACTED]",
26 );
27 out = apply_regex(out, r#"(?i)\b(bearer)\s+[^\s,;]+"#, "$1 [REDACTED]");
28 out = apply_regex(out, r#"\bsk-[A-Za-z0-9_-]{10,}\b"#, "sk-[REDACTED]");
29
30 out
31}
32
33pub fn redact_error_chain(err: &(dyn Error + 'static)) -> String {
34 let mut parts = vec![redact_text(&err.to_string())];
35 let mut source = err.source();
36
37 while let Some(cause) = source {
38 parts.push(redact_text(&cause.to_string()));
39 source = cause.source();
40 }
41
42 parts.join(": ")
43}
44
45pub fn install_redacting_panic_hook() {
46 std::panic::set_hook(Box::new(|panic_info| {
47 let message = if let Some(text) = panic_info.payload().downcast_ref::<&str>() {
48 (*text).to_string()
49 } else if let Some(text) = panic_info.payload().downcast_ref::<String>() {
50 text.clone()
51 } else {
52 "panic occurred".to_string()
53 };
54
55 eprintln!("panic: {}", redact_text(&message));
56 if let Some(location) = panic_info.location() {
57 eprintln!(
58 "at {}:{}:{}",
59 location.file(),
60 location.line(),
61 location.column()
62 );
63 }
64 }));
65}
66
67#[cfg(test)]
68mod tests {
69 use super::{redact_error_chain, redact_text};
70 use std::error::Error;
71 use std::fmt::{Display, Formatter};
72
73 #[test]
74 fn redacts_common_secret_formats() {
75 let input = "OPENAI_API_KEY=sk-supersecret12345 Authorization: Bearer token123 {\"auth_token\":\"abc\"}";
76 let out = redact_text(input);
77
78 assert!(!out.contains("sk-supersecret12345"));
79 assert!(!out.contains("token123"));
80 assert!(!out.contains("\"abc\""));
81 assert!(out.contains("OPENAI_API_KEY=[REDACTED]"));
82 }
83
84 #[test]
85 fn leaves_non_secret_text_unchanged() {
86 let input = "status ok; model=gpt-4o-mini";
87 assert_eq!(redact_text(input), input);
88 }
89
90 #[test]
91 fn redacts_error_chain_output() {
92 #[derive(Debug)]
93 struct Root;
94 impl Display for Root {
95 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
96 write!(f, "root OPENAI_API_KEY=sk-exposed012345")
97 }
98 }
99 impl Error for Root {}
100
101 #[derive(Debug)]
102 struct Wrapped(Root);
103 impl Display for Wrapped {
104 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105 write!(f, "wrapper failed")
106 }
107 }
108 impl Error for Wrapped {
109 fn source(&self) -> Option<&(dyn Error + 'static)> {
110 Some(&self.0)
111 }
112 }
113
114 let wrapped = Wrapped(Root);
115 let text = redact_error_chain(&wrapped);
116 assert!(text.contains("OPENAI_API_KEY=[REDACTED]"));
117 assert!(!text.contains("sk-exposed012345"));
118 }
119}