mermaid_cli/utils/
redact.rs1use std::sync::LazyLock;
16
17use regex::Regex;
18
19static SECRET_PATTERNS: LazyLock<Vec<(Regex, &'static str)>> = LazyLock::new(|| {
23 let p = |re: &str| Regex::new(re).expect("static redaction regex must compile");
24 vec![
25 (
31 p(
32 r#"(?i)\b([A-Z0-9_]*(?:API[_-]?KEY|APIKEY|SECRET|TOKEN|PASSWORD|PASSWD|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|CREDENTIALS?)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"']{6,}["']?"#,
33 ),
34 "${1}=[REDACTED]",
35 ),
36 (
38 p(r#"(?i)\b(Bearer)\s+[A-Za-z0-9._\-]{8,}"#),
39 "${1} [REDACTED]",
40 ),
41 (p(r#"\bsk-(?:ant-)?[A-Za-z0-9._\-]{12,}"#), "[REDACTED]"), (p(r#"\bAKIA[0-9A-Z]{16}\b"#), "[REDACTED]"), (p(r#"\bgh[pousr]_[A-Za-z0-9]{20,}\b"#), "[REDACTED]"), (p(r#"\bxox[baprs]-[A-Za-z0-9-]{10,}\b"#), "[REDACTED]"), (p(r#"\bAIza[0-9A-Za-z._\-]{20,}\b"#), "[REDACTED]"), ]
48});
49
50pub fn redact_secrets(input: &str) -> String {
52 let mut out = std::borrow::Cow::Borrowed(input);
53 for (re, repl) in SECRET_PATTERNS.iter() {
54 if re.is_match(&out) {
55 out = std::borrow::Cow::Owned(re.replace_all(&out, *repl).into_owned());
56 }
57 }
58 out.into_owned()
59}
60
61pub fn redact_json(value: &mut serde_json::Value) {
64 match value {
65 serde_json::Value::String(s) => {
66 let red = redact_secrets(s);
67 if &red != s {
68 *s = red;
69 }
70 },
71 serde_json::Value::Array(items) => items.iter_mut().for_each(redact_json),
72 serde_json::Value::Object(map) => map.values_mut().for_each(redact_json),
73 _ => {},
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn redacts_named_secrets() {
83 assert_eq!(
84 redact_secrets("OPENAI_API_KEY=sk-abcdefghijklmnop1234"),
85 "OPENAI_API_KEY=[REDACTED]"
86 );
87 assert_eq!(
88 redact_secrets("export AWS_SECRET_ACCESS_KEY: 'wJalrXUtnFEMI/K7MDENG'"),
89 "export AWS_SECRET_ACCESS_KEY=[REDACTED]"
90 );
91 assert_eq!(redact_secrets("password = hunter2"), "password=[REDACTED]");
92 assert_eq!(
94 redact_secrets("--api-key=sk-abcdefghijklmnop1234"),
95 "--api-key=[REDACTED]"
96 );
97 }
98
99 #[test]
100 fn redacts_known_prefixes_and_bearer() {
101 assert_eq!(
102 redact_secrets("Authorization: Bearer abcdef123456ghijkl"),
103 "Authorization: Bearer [REDACTED]"
104 );
105 assert_eq!(
106 redact_secrets("key sk-ant-api03-abcdefghijklmnop"),
107 "key [REDACTED]"
108 );
109 assert_eq!(
110 redact_secrets("AKIAIOSFODNN7EXAMPLE here"),
111 "[REDACTED] here"
112 );
113 assert_eq!(
114 redact_secrets("token ghp_0123456789abcdefghijABCDEFGHIJ012345"),
115 "token [REDACTED]"
116 );
117 }
118
119 #[test]
120 fn leaves_ordinary_text_untouched() {
121 for ok in [
123 "the quick brown fox",
124 "let token_count = 42;",
125 "commit a614aa9f deploys the fix",
126 "see src/providers/tool/exec.rs:855",
127 ] {
128 assert_eq!(redact_secrets(ok), ok, "should not redact: {ok}");
129 }
130 }
131
132 #[test]
133 fn redact_json_scrubs_string_leaves() {
134 let mut v = serde_json::json!({
135 "text": "OPENAI_API_KEY=sk-abcdefghijklmnop1234",
136 "count": 7,
137 "nested": ["safe", "Bearer abcdef123456ghijkl"],
138 });
139 redact_json(&mut v);
140 assert_eq!(v["text"], "OPENAI_API_KEY=[REDACTED]");
141 assert_eq!(v["count"], 7);
142 assert_eq!(v["nested"][0], "safe");
143 assert_eq!(v["nested"][1], "Bearer [REDACTED]");
144 }
145}