use serde_json::{Map, Value};
pub(crate) const MARKER: &str = "[REDACTED]";
const SENSITIVE_SUBSTRINGS: [&str; 11] = [
"token",
"password",
"passwd",
"secret",
"key",
"keyfile",
"credential",
"passphrase",
"sas",
"sig",
"signature",
];
pub(crate) fn is_sensitive_key(key: &str) -> bool {
let lower = key.to_ascii_lowercase();
SENSITIVE_SUBSTRINGS
.iter()
.any(|needle| lower.contains(needle))
}
pub(crate) fn parameters(params: &Map<String, Value>) -> Map<String, Value> {
let mut out = Map::new();
for (key, value) in params {
if is_sensitive_key(key) {
out.insert(key.clone(), Value::String(MARKER.to_string()));
} else {
out.insert(key.clone(), redact_value(value));
}
}
out
}
pub(crate) fn redact_body(body: &Value) -> Value {
redact_value(body)
}
fn redact_value(value: &Value) -> Value {
match value {
Value::Object(map) => {
let mut out = Map::new();
for (key, v) in map {
if is_sensitive_key(key) {
out.insert(key.clone(), Value::String(MARKER.to_string()));
} else {
out.insert(key.clone(), redact_value(v));
}
}
Value::Object(out)
}
Value::Array(items) => Value::Array(items.iter().map(redact_value).collect()),
other => other.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn sensitive_key_matches_substrings_case_insensitively() {
assert!(is_sensitive_key("secretaccesskey"));
assert!(is_sensitive_key("Password"));
assert!(is_sensitive_key("accountKey"));
assert!(is_sensitive_key("SAS"));
assert!(is_sensitive_key("token"));
assert!(!is_sensitive_key("bucket"));
assert!(!is_sensitive_key("host"));
assert!(!is_sensitive_key("projectid"));
}
#[test]
fn parameters_masks_only_sensitive_leaves() {
let p = json!({"bucket": "b", "token": "PARAMSECRET"})
.as_object()
.unwrap()
.clone();
let red = parameters(&p);
assert_eq!(red["bucket"], json!("b"));
assert_eq!(red["token"], json!(MARKER));
}
#[test]
fn redact_body_walks_nested_and_dotted_keys() {
let body = json!({
"message": "Validation failed",
"errors": {"input.0.credentials.secretaccesskey": "SUPERSECRET123"}
});
let red = redact_body(&body);
let s = red.to_string();
assert!(!s.contains("SUPERSECRET123"));
assert!(s.contains(MARKER));
assert_eq!(red["message"], json!("Validation failed"));
}
}