Skip to main content

soma_codemode/
redact.rs

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}