Skip to main content

auditaur_core/
redaction.rs

1use serde_json::Value;
2use std::collections::HashSet;
3
4pub const DEFAULT_REDACTION_KEYS: &[&str] = &[
5    "password",
6    "passwd",
7    "pwd",
8    "secret",
9    "token",
10    "access_token",
11    "refresh_token",
12    "id_token",
13    "authorization",
14    "api_key",
15    "apikey",
16    "key",
17    "cookie",
18    "set-cookie",
19    "session",
20    "credential",
21    "connection_string",
22];
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct RedactionResult {
26    pub value: Value,
27    pub redacted: bool,
28}
29
30pub fn redact_json(value: &Value, extra_keys: &[String]) -> RedactionResult {
31    redact_json_with_options(value, true, extra_keys)
32}
33
34pub fn redact_json_with_options(
35    value: &Value,
36    redact_defaults: bool,
37    extra_keys: &[String],
38) -> RedactionResult {
39    let keys = redaction_key_set(extra_keys);
40    let keys = if redact_defaults {
41        keys
42    } else {
43        extra_keys
44            .iter()
45            .map(|key| key.to_ascii_lowercase())
46            .collect()
47    };
48    let mut redacted = false;
49    let value = redact_value(value, &keys, &mut redacted);
50    RedactionResult { value, redacted }
51}
52
53fn redaction_key_set(extra_keys: &[String]) -> HashSet<String> {
54    DEFAULT_REDACTION_KEYS
55        .iter()
56        .map(|key| key.to_ascii_lowercase())
57        .chain(extra_keys.iter().map(|key| key.to_ascii_lowercase()))
58        .collect()
59}
60
61fn redact_value(value: &Value, keys: &HashSet<String>, redacted: &mut bool) -> Value {
62    match value {
63        Value::Object(map) => Value::Object(
64            map.iter()
65                .map(|(key, value)| {
66                    if keys.contains(&key.to_ascii_lowercase()) {
67                        *redacted = true;
68                        (key.clone(), Value::String("[REDACTED]".to_string()))
69                    } else {
70                        (key.clone(), redact_value(value, keys, redacted))
71                    }
72                })
73                .collect(),
74        ),
75        Value::Array(items) => Value::Array(
76            items
77                .iter()
78                .map(|item| redact_value(item, keys, redacted))
79                .collect(),
80        ),
81        _ => value.clone(),
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::redact_json;
88    use serde_json::json;
89
90    #[test]
91    fn redacts_matching_keys_recursively() {
92        let input = json!({
93            "name": "auditaur",
94            "nested": {
95                "token": "abc",
96                "items": [{ "api_key": "def" }]
97            }
98        });
99
100        let result = redact_json(&input, &[]);
101
102        assert!(result.redacted);
103        assert_eq!(result.value["name"], "auditaur");
104        assert_eq!(result.value["nested"]["token"], "[REDACTED]");
105        assert_eq!(result.value["nested"]["items"][0]["api_key"], "[REDACTED]");
106    }
107
108    #[test]
109    fn can_use_extra_keys_without_default_keys() {
110        let input = json!({
111            "token": "kept",
112            "custom_secret": "hidden"
113        });
114
115        let result = super::redact_json_with_options(&input, false, &["custom_secret".to_string()]);
116
117        assert!(result.redacted);
118        assert_eq!(result.value["token"], "kept");
119        assert_eq!(result.value["custom_secret"], "[REDACTED]");
120    }
121}