assay/lua/policy/
redact.rs1use serde_json::Value;
2
3pub fn redact_json_text(text: &str, keys: &[String]) -> Option<String> {
4 if keys.is_empty() {
5 return None;
6 }
7 let mut value: Value = serde_json::from_str(text).ok()?;
8 let lowered: Vec<String> = keys.iter().map(|k| k.to_ascii_lowercase()).collect();
9 strip(&mut value, &lowered);
10 serde_json::to_string(&value).ok()
11}
12
13pub fn is_redacted_header(name: &str, keys: &[String]) -> bool {
14 let name = name.to_ascii_lowercase();
15 keys.iter().any(|k| k.to_ascii_lowercase() == name)
16}
17
18fn strip(value: &mut Value, keys: &[String]) {
19 match value {
20 Value::Object(map) => {
21 for (_, v) in map.iter_mut() {
22 strip(v, keys);
23 }
24 for key in map.keys().cloned().collect::<Vec<_>>() {
25 if keys.contains(&key.to_ascii_lowercase()) {
26 map.insert(key, Value::String("[redacted]".to_string()));
27 }
28 }
29 }
30 Value::Array(items) => {
31 for item in items.iter_mut() {
32 strip(item, keys);
33 }
34 }
35 _ => {}
36 }
37}
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42
43 fn keys() -> Vec<String> {
44 vec!["password".into(), "token".into()]
45 }
46
47 #[test]
48 fn nested_and_arrayed_secrets_are_replaced_not_removed() {
49 let out = redact_json_text(
50 r#"{"user":{"name":"a","password":"hunter2"},"items":[{"token":"t"}]}"#,
51 &keys(),
52 )
53 .expect("redacted");
54 let parsed: Value = serde_json::from_str(&out).expect("valid json");
55 assert_eq!(
56 parsed["user"]["password"],
57 Value::String("[redacted]".into())
58 );
59 assert_eq!(
60 parsed["items"][0]["token"],
61 Value::String("[redacted]".into())
62 );
63 assert_eq!(parsed["user"]["name"], Value::String("a".into()));
64 }
65
66 #[test]
67 fn matching_is_case_insensitive() {
68 let out = redact_json_text(r#"{"Password":"hunter2"}"#, &keys()).expect("redacted");
69 assert!(!out.contains("hunter2"));
70 }
71
72 #[test]
73 fn non_json_and_empty_keys_pass_through_untouched() {
74 assert!(redact_json_text("not json at all", &keys()).is_none());
75 assert!(redact_json_text(r#"{"password":"x"}"#, &[]).is_none());
76 }
77
78 #[test]
79 fn header_names_match_case_insensitively() {
80 assert!(is_redacted_header("X-Auth-Token", &["x-auth-token".into()]));
81 assert!(!is_redacted_header("content-type", &keys()));
82 }
83}