Skip to main content

agentzero_core/security/
redaction.rs

1use 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}