1use crate::json::JsonValue;
15use crate::sha256;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Redaction {
20 Mask,
22 Hash,
24 Remove,
26}
27
28pub fn redact(packet: &JsonValue, fields: &[&str], how: Redaction) -> JsonValue {
33 let mut result = packet.clone();
34 if let JsonValue::Object(members) = &mut result {
35 for (k, v) in members.iter_mut() {
36 if k == "fields" {
37 *v = redact_fields(v, fields, how);
38 }
39 }
40 }
41 result
42}
43
44fn redact_fields(node: &JsonValue, names: &[&str], how: Redaction) -> JsonValue {
45 let members = match node {
46 JsonValue::Object(m) => m,
47 _ => return node.clone(),
48 };
49 let mut out: Vec<(String, JsonValue)> = Vec::with_capacity(members.len());
50 for (key, val) in members {
51 let is_group = matches!(val, JsonValue::Object(_)) && val.get("fields").is_some();
53 let target = names.iter().any(|n| n == key);
54
55 if target {
56 match how {
57 Redaction::Remove => continue, Redaction::Mask | Redaction::Hash => {
59 out.push((key.clone(), redact_member(val, how)));
60 }
61 }
62 continue;
63 }
64
65 if is_group {
66 if let JsonValue::Object(gm) = val {
68 let mut new_gm = Vec::with_capacity(gm.len());
69 for (gk, gv) in gm {
70 if gk == "fields" {
71 new_gm.push((gk.clone(), redact_fields(gv, names, how)));
72 } else {
73 new_gm.push((gk.clone(), gv.clone()));
74 }
75 }
76 out.push((key.clone(), JsonValue::Object(new_gm)));
77 continue;
78 }
79 } else if let JsonValue::Object(inner) = val {
80 if val.get("value").is_none() && val.get("raw_hex").is_none() {
83 let _ = inner; out.push((key.clone(), redact_fields(val, names, how)));
85 continue;
86 }
87 }
88 out.push((key.clone(), val.clone()));
89 }
90 JsonValue::Object(out)
91}
92
93fn redact_member(val: &JsonValue, how: Redaction) -> JsonValue {
96 if let JsonValue::Object(members) = val {
97 if val.get("value").is_some() || val.get("raw_hex").is_some() {
98 let mut out = Vec::with_capacity(members.len());
99 for (k, v) in members {
100 match k.as_str() {
101 "value" => out.push((k.clone(), apply(v, how))),
102 "raw_hex" => out.push((k.clone(), JsonValue::str("REDACTED"))),
103 _ => out.push((k.clone(), v.clone())),
104 }
105 }
106 return JsonValue::Object(out);
107 }
108 }
109 apply(val, how)
110}
111
112fn apply(v: &JsonValue, how: Redaction) -> JsonValue {
113 match how {
114 Redaction::Mask => JsonValue::str("****"),
115 Redaction::Hash => {
116 let bytes = match v {
117 JsonValue::String(s) => s.clone().into_bytes(),
118 JsonValue::Number(n) => n.clone().into_bytes(),
119 other => crate::json::to_string(other).into_bytes(),
120 };
121 JsonValue::str(format!("sha256:{}", sha256::hex_digest(&bytes)))
122 }
123 Redaction::Remove => JsonValue::Null, }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::json::to_string;
131
132 fn compact() -> JsonValue {
133 JsonValue::Object(vec![
134 ("record".into(), JsonValue::str("CUST")),
135 (
136 "fields".into(),
137 JsonValue::Object(vec![
138 ("NAME".into(), JsonValue::str("JOHN")),
139 ("SSN".into(), JsonValue::str("123456789")),
140 ]),
141 ),
142 ])
143 }
144
145 #[test]
146 fn mask_replaces_value() {
147 let r = redact(&compact(), &["SSN"], Redaction::Mask);
148 assert_eq!(r.get("fields").unwrap().get("SSN").unwrap().as_str(), Some("****"));
149 assert_eq!(r.get("fields").unwrap().get("NAME").unwrap().as_str(), Some("JOHN"));
151 }
152
153 #[test]
154 fn hash_is_stable_token() {
155 let r = redact(&compact(), &["SSN"], Redaction::Hash);
156 let t = r.get("fields").unwrap().get("SSN").unwrap().as_str().unwrap();
157 assert!(t.starts_with("sha256:"));
158 let r2 = redact(&compact(), &["SSN"], Redaction::Hash);
160 assert_eq!(to_string(&r), to_string(&r2));
161 }
162
163 #[test]
164 fn remove_drops_member() {
165 let r = redact(&compact(), &["SSN"], Redaction::Remove);
166 assert!(r.get("fields").unwrap().get("SSN").is_none());
167 assert!(r.get("fields").unwrap().get("NAME").is_some());
168 }
169
170 #[test]
171 fn redacts_audit_detail_value_and_raw_hex() {
172 let packet = JsonValue::Object(vec![
173 ("record".into(), JsonValue::str("R")),
174 (
175 "fields".into(),
176 JsonValue::Object(vec![(
177 "SSN".into(),
178 JsonValue::Object(vec![
179 ("value".into(), JsonValue::str("123456789")),
180 ("raw_hex".into(), JsonValue::str("313233")),
181 ]),
182 )]),
183 ),
184 ]);
185 let r = redact(&packet, &["SSN"], Redaction::Mask);
186 let ssn = r.get("fields").unwrap().get("SSN").unwrap();
187 assert_eq!(ssn.get("value").unwrap().as_str(), Some("****"));
188 assert_eq!(ssn.get("raw_hex").unwrap().as_str(), Some("REDACTED"));
189 }
190}