1#![deny(missing_docs)]
28
29use serde_json::Value;
30
31pub const REPLACEMENT: &str = "[REDACTED]";
33
34const SENSITIVE_KEYS: &[&str] = &[
36 "api_key",
37 "apikey",
38 "token",
39 "access_token",
40 "refresh_token",
41 "id_token",
42 "authorization",
43 "password",
44 "secret",
45 "x-api-key",
46 "anthropic-api-key",
47 "openai-api-key",
48];
49
50pub fn redact(v: &mut Value) {
52 match v {
53 Value::Object(map) => {
54 let keys: Vec<String> = map.keys().cloned().collect();
55 for k in keys {
56 if is_sensitive_key(&k) {
57 if let Some(slot) = map.get_mut(&k) {
58 *slot = Value::String(REPLACEMENT.to_string());
59 }
60 continue;
61 }
62 if let Some(slot) = map.get_mut(&k) {
63 redact(slot);
64 }
65 }
66 }
67 Value::Array(items) => {
68 for item in items.iter_mut() {
69 redact(item);
70 }
71 }
72 Value::String(s) => {
73 if looks_sensitive(s) {
74 *v = Value::String(REPLACEMENT.to_string());
75 }
76 }
77 _ => {}
78 }
79}
80
81fn is_sensitive_key(k: &str) -> bool {
82 let lk = k.to_ascii_lowercase();
83 SENSITIVE_KEYS.iter().any(|s| *s == lk)
84}
85
86pub fn looks_sensitive(s: &str) -> bool {
89 is_api_keyish(s)
90 || s.starts_with("Bearer ")
91 || is_email(s)
92 || is_ssn(s)
93 || is_phone(s)
94}
95
96fn is_api_keyish(s: &str) -> bool {
97 let prefixes = ["sk-", "ghp_", "xoxb-", "sk_live_", "sk_test_", "rk_live_"];
99 if prefixes.iter().any(|p| s.starts_with(p)) {
100 let tail_len = s.split_once(|c: char| c == '-' || c == '_')
101 .map(|(_, t)| t.len())
102 .unwrap_or(0);
103 return tail_len >= 16;
104 }
105 false
106}
107
108fn is_email(s: &str) -> bool {
109 let parts: Vec<&str> = s.split('@').collect();
110 parts.len() == 2 && !parts[0].is_empty() && parts[1].contains('.')
111}
112
113fn is_ssn(s: &str) -> bool {
114 s.len() == 11
115 && s.chars().enumerate().all(|(i, c)| match i {
116 3 | 6 => c == '-',
117 _ => c.is_ascii_digit(),
118 })
119}
120
121fn is_phone(s: &str) -> bool {
122 let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
123 (10..=12).contains(&digits.len()) && s.chars().any(|c| c == '-' || c == '(' || c == ' ')
124}