Skip to main content

apollo/
redaction.rs

1use regex::Regex;
2use serde_json::Value;
3use std::sync::OnceLock;
4
5fn patterns() -> &'static [Regex] {
6    static PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();
7    PATTERNS.get_or_init(|| {
8        [
9            r"(?i)\b(?:sk|sk-ant|ghp_|gho_|github_pat_|xoxb-|xapp-)[A-Za-z0-9_\-]{16,}\b",
10            r"(?i)(?:authorization|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\s*[:=]\s*(?:bearer\s+)?[^\s,;]+",
11            r"(?i)https?://[^\s/@:]+:[^\s/@]+@[^\s]+",
12            r"(?im)\b[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIALS?)[A-Z0-9_]*\s*=\s*[^\s]+",
13            r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
14        ]
15        .into_iter()
16        .map(|pattern| Regex::new(pattern).expect("valid redaction regex"))
17        .collect()
18    })
19}
20
21pub fn redact_text(input: &str) -> String {
22    let mut output = input.to_string();
23    for pattern in patterns() {
24        output = pattern.replace_all(&output, "[REDACTED]").into_owned();
25    }
26    for name in [
27        "ANTHROPIC_API_KEY",
28        "OPENAI_API_KEY",
29        "GEMINI_API_KEY",
30        "DISCORD_TOKEN",
31        "TELEGRAM_BOT_TOKEN",
32        "SLACK_BOT_TOKEN",
33        "SLACK_APP_TOKEN",
34        "GITHUB_TOKEN",
35    ] {
36        if let Ok(secret) = std::env::var(name) {
37            if secret.len() >= 8 {
38                output = output.replace(&secret, "[REDACTED]");
39            }
40        }
41    }
42    output
43}
44
45pub fn redact_tool_payload(tool: &str, input: &str) -> String {
46    if tool != "praefectus" {
47        return redact_text(input);
48    }
49    let Ok(mut value) = serde_json::from_str::<Value>(input) else {
50        return "[REDACTED]".to_string();
51    };
52    redact_praefectus_value(&mut value);
53    serde_json::to_string(&value).unwrap_or_else(|_| "[REDACTED]".to_string())
54}
55
56fn redact_praefectus_value(value: &mut Value) {
57    match value {
58        Value::Array(values) => values.iter_mut().for_each(redact_praefectus_value),
59        Value::Object(values) => {
60            for key in [
61                "after",
62                "authority",
63                "before",
64                "evidence",
65                "fallback_chain",
66                "name",
67                "signature",
68                "target",
69                "text",
70                "value",
71                "warnings",
72            ] {
73                if values.contains_key(key) {
74                    values.insert(key.to_string(), Value::String("[REDACTED]".to_string()));
75                }
76            }
77            values.values_mut().for_each(redact_praefectus_value);
78        }
79        _ => {}
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn redacts_common_tokens() {
89        let output = redact_text(
90            "token sk-ant-abcdefghijklmnopqrstuvwxyz and ghp_abcdefghijklmnopqrstuvwxyz1234567890",
91        );
92        assert!(!output.contains("abcdefghijklmnopqrstuvwxyz"));
93        assert!(output.contains("[REDACTED]"));
94    }
95
96    #[test]
97    fn redacts_environment_assignments() {
98        let output = redact_text("AWS_SECRET_ACCESS_KEY=top-secret\nCUSTOM_TOKEN=value");
99        assert!(!output.contains("top-secret"));
100        assert!(!output.contains("CUSTOM_TOKEN=value"));
101    }
102
103    #[test]
104    fn redacts_headers_and_private_keys() {
105        let output = redact_text("Authorization: Bearer secret-token\n-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----");
106        assert!(!output.contains("secret-token"));
107        assert!(!output.contains("abc"));
108    }
109
110    #[test]
111    fn redacts_praefectus_targets_and_values_from_persistent_payloads() {
112        let output = redact_tool_payload(
113            "praefectus",
114            r#"{"desktop_action":{"kind":"set_value","value":"private"},"target":{"element_id":"selector"}}"#,
115        );
116        assert!(!output.contains("private"));
117        assert!(!output.contains("selector"));
118        assert!(output.contains("[REDACTED]"));
119    }
120}