use rustlavel_core::Json;
use std::collections::BTreeMap;
pub const REDACTED: &str = "[redacted]";
const SENSITIVE: &[&str] = &[
"password",
"passwd",
"token",
"secret",
"authorization",
"api_key",
"apikey",
"api-key",
"credential",
"private_key",
];
pub fn is_sensitive(key: &str) -> bool {
let lowered = key.to_ascii_lowercase();
SENSITIVE.iter().any(|marker| lowered.contains(marker))
}
pub fn fields(fields: BTreeMap<String, Json>) -> BTreeMap<String, Json> {
fields.into_iter().map(|(key, value)| (key.clone(), value_for(&key, value))).collect()
}
fn value_for(key: &str, value: Json) -> Json {
if is_sensitive(key) {
return Json::String(REDACTED.to_string());
}
match value {
Json::Array(items) => Json::Array(items.into_iter().map(|item| value_for(key, item)).collect()),
Json::Object(map) => {
Json::Object(map.into_iter().map(|(k, v)| (k.clone(), value_for(&k, v))).collect())
}
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keys_that_look_like_credentials_are_recognised() {
assert!(is_sensitive("password"));
assert!(is_sensitive("DB_PASSWORD"));
assert!(is_sensitive("Authorization"));
assert!(is_sensitive("refreshToken"));
assert!(is_sensitive("x-api-key"));
assert!(!is_sensitive("path"));
assert!(!is_sensitive("status"));
}
#[test]
fn sensitive_values_are_replaced_but_their_neighbours_survive() {
let redacted = fields(BTreeMap::from([
("path".to_string(), Json::from("/login")),
("password".to_string(), Json::from("hunter2")),
]));
assert_eq!(redacted["path"].as_str(), Some("/login"));
assert_eq!(redacted["password"].as_str(), Some(REDACTED));
}
#[test]
fn nested_objects_are_scrubbed_too() {
let nested = Json::object([
("user", Json::from("ada")),
("api_key", Json::from("sk-live-123")),
]);
let redacted = fields(BTreeMap::from([("payload".to_string(), nested)]));
let rendered = redacted["payload"].to_string();
assert!(!rendered.contains("sk-live-123"));
assert!(rendered.contains("ada"));
}
#[test]
fn a_sensitive_key_hides_its_entire_subtree() {
let redacted = fields(BTreeMap::from([(
"authorization".to_string(),
Json::object([("scheme", Json::from("Bearer")), ("value", Json::from("abc"))]),
)]));
assert_eq!(redacted["authorization"].as_str(), Some(REDACTED));
}
}