ccs_proxy/capture/
redact.rs1use serde_json::Value;
4use std::collections::BTreeMap;
5
6const SECRET_HEADER_NAMES: &[&str] = &[
7 "authorization",
8 "x-api-key",
9 "anthropic-api-key",
10 "cookie",
11 "set-cookie",
12];
13
14const SECRET_BODY_KEYS: &[&str] = &[
15 "api_key",
16 "apikey",
17 "auth_token",
18 "authtoken",
19 "access_token",
20 "accesstoken",
21 "token",
22 "secret",
23 "password",
24];
25
26const PLACEHOLDER: &str = "<redacted>";
27
28pub fn redact_headers(headers: &mut BTreeMap<String, String>) {
29 for (k, v) in headers.iter_mut() {
30 if SECRET_HEADER_NAMES
31 .iter()
32 .any(|name| k.eq_ignore_ascii_case(name))
33 {
34 *v = PLACEHOLDER.to_string();
35 }
36 }
37}
38
39pub fn redact_body(body: &mut Value) {
40 if let Value::Object(map) = body {
41 for (k, v) in map.iter_mut() {
42 if SECRET_BODY_KEYS
43 .iter()
44 .any(|secret| k.eq_ignore_ascii_case(secret))
45 {
46 *v = Value::String(PLACEHOLDER.to_string());
47 }
48 }
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn redacts_known_headers_case_insensitive() {
58 let mut h: BTreeMap<String, String> = [
59 ("Authorization".into(), "Bearer sk-abc".into()),
60 ("X-Api-Key".into(), "sk-xyz".into()),
61 ("content-type".into(), "application/json".into()),
62 ]
63 .into();
64 redact_headers(&mut h);
65 assert_eq!(h["Authorization"], "<redacted>");
66 assert_eq!(h["X-Api-Key"], "<redacted>");
67 assert_eq!(h["content-type"], "application/json");
68 }
69
70 #[test]
71 fn redacts_top_level_body_keys() {
72 let mut body: Value = serde_json::json!({
73 "api_key": "secret",
74 "model": "claude-sonnet-4-6",
75 "nested": { "api_key": "should_not_be_touched" }
76 });
77 redact_body(&mut body);
78 assert_eq!(body["api_key"], "<redacted>");
79 assert_eq!(body["model"], "claude-sonnet-4-6");
80 assert_eq!(body["nested"]["api_key"], "should_not_be_touched");
81 }
82}