1pub fn is_sensitive_key(key: &str) -> bool {
2 let normalized = key.to_ascii_lowercase().replace('-', "_");
3 if matches!(
4 normalized.as_str(),
5 "sort_key" | "cache_key" | "idempotency_key" | "partition_key" | "primary_key"
6 ) {
7 return false;
8 }
9 matches!(
10 normalized.as_str(),
11 "token"
12 | "access_token"
13 | "id_token"
14 | "refresh_token"
15 | "apikey"
16 | "api_key"
17 | "password"
18 | "passwd"
19 | "secret"
20 | "client_secret"
21 | "authorization"
22 | "bearer"
23 | "session"
24 | "session_id"
25 | "cookie"
26 | "code"
27 | "cwd"
28 | "terminal_id"
29 ) || normalized.ends_with("_token")
30 || normalized.ends_with("_secret")
31 || normalized.ends_with("_password")
32 || normalized.ends_with("_key")
33}
34
35pub fn redact_url(url: &str) -> String {
36 match url::Url::parse(url) {
37 Ok(mut parsed) => {
38 let _ = parsed.set_username("");
39 let _ = parsed.set_password(None);
40 parsed.set_query(parsed.query().map(redact_query_pairs).as_deref());
41 parsed.set_fragment(None);
42 parsed.to_string()
43 }
44 Err(_) => "[invalid-url-redacted]".to_string(),
45 }
46}
47
48pub fn redact_stdio_value(value: &str) -> String {
49 if let Some((key, _)) = value.split_once('=') {
50 if is_sensitive_key(key) {
51 return format!("{key}=[redacted]");
52 }
53 }
54 if let Some(flag) = value.strip_prefix("--") {
55 let (key, _) = flag
56 .split_once('=')
57 .map_or((flag, ""), |(key, value)| (key, value));
58 if is_sensitive_key(key) {
59 return format!("--{key}=[redacted]");
60 }
61 }
62 value.to_string()
63}
64
65pub fn redact_stdio_args(args: &[String]) -> Vec<String> {
66 let mut redacted = Vec::with_capacity(args.len());
67 let mut redact_next = false;
68 for arg in args {
69 if redact_next {
70 redacted.push("[redacted]".to_string());
71 redact_next = false;
72 continue;
73 }
74 let sensitive_flag = arg
75 .strip_prefix("--")
76 .map(|flag| flag.split_once('=').map_or(flag, |(key, _)| key))
77 .is_some_and(is_sensitive_key);
78 if sensitive_flag && !arg.contains('=') {
79 redacted.push(arg.clone());
80 redact_next = true;
81 } else {
82 redacted.push(redact_stdio_value(arg));
83 }
84 }
85 redacted
86}
87
88fn redact_query_pairs(query: &str) -> String {
89 query
90 .split('&')
91 .filter(|pair| !pair.is_empty())
92 .map(|pair| {
93 let (key, value) = pair
94 .split_once('=')
95 .map_or((pair, ""), |(key, value)| (key, value));
96 if is_sensitive_key(key) {
97 format!("{key}=[redacted]")
98 } else if value.is_empty() {
99 key.to_string()
100 } else {
101 format!("{key}={value}")
102 }
103 })
104 .collect::<Vec<_>>()
105 .join("&")
106}