use serde_json::Value;
const SUSPECT_SUBSTRINGS: &[&str] = &[
"token",
"secret",
"password",
"passwd",
"apikey",
"api_key",
"auth",
"credential",
"private",
"session",
"bearer",
"cookie",
];
pub fn key_looks_sensitive(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
SUSPECT_SUBSTRINGS.iter().any(|n| lower.contains(n))
}
pub fn mask(s: &str) -> String {
let visible = s.chars().take(4).collect::<String>();
if visible.is_empty() || visible.len() == s.len() {
"***".into()
} else {
format!("{visible}***")
}
}
pub fn mask_value(value: &mut Value) {
match value {
Value::String(s) => *s = mask(s),
Value::Array(arr) => arr.iter_mut().for_each(mask_value),
Value::Object(map) => map.values_mut().for_each(mask_value),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn mask_short_strings() {
assert_eq!(mask(""), "***");
assert_eq!(mask("abc"), "***");
assert_eq!(mask("abcd"), "***");
assert_eq!(mask("abcde"), "abcd***");
assert_eq!(mask("sk-very-long-token-here"), "sk-v***");
}
#[test]
fn key_looks_sensitive_examples() {
assert!(key_looks_sensitive("token"));
assert!(key_looks_sensitive("ANTHROPIC_API_KEY"));
assert!(key_looks_sensitive("github_token"));
assert!(key_looks_sensitive("password"));
assert!(!key_looks_sensitive("name"));
assert!(!key_looks_sensitive("path"));
}
#[test]
fn mask_value_masks_every_string_in_subtree() {
let mut v = json!({
"scalar": "sk-secret-12345",
"nested": { "GITHUB_TOKEN": "ghp_aaaaaaaa" },
"list": ["sk-one-aaaa", "sk-two-bbbb"],
"count": 7
});
mask_value(&mut v);
assert_eq!(v["scalar"], "sk-s***");
assert_eq!(v["nested"]["GITHUB_TOKEN"], "ghp_***");
assert_eq!(v["list"][0], "sk-o***");
assert_eq!(v["list"][1], "sk-t***");
assert_eq!(v["count"], 7);
}
#[test]
fn mask_value_masks_a_bare_string_array() {
let mut v = json!(["sk-real-aaaa", "sk-real-bbbb"]);
mask_value(&mut v);
assert_eq!(v[0], "sk-r***");
assert_eq!(v[1], "sk-r***");
}
}