#[inline]
pub fn redact_basic(value: Option<&str>) -> String {
let Some(value) = value else {
return "unknown".to_string();
};
let value = value.trim();
if value.is_empty() {
return "unknown".to_string();
}
if value.len() < 4 {
return "***".to_string();
}
let prefix = &value[..2.min(value.len())];
let suffix_len = 2.min(value.len().saturating_sub(2));
let suffix = &value[value.len().saturating_sub(suffix_len)..];
format!("{}***{}", prefix, suffix)
}
#[inline]
pub fn redact_user_id(value: Option<&str>) -> String {
redact_basic(value)
}
#[inline]
pub fn redact_ip(value: Option<&str>) -> String {
let Some(value) = value else {
return "unknown".to_string();
};
let value = value.trim();
if value.is_empty() {
return "unknown".to_string();
}
let parts: Vec<&str> = value.split('.').collect();
if parts.len() == 4 {
return format!("{}.{}.***.***", parts[0], parts[1]);
}
if value.contains(':') {
let parts: Vec<&str> = value.split(':').collect();
if parts.len() >= 2 {
return format!("{}:***:***", parts[0]);
}
}
redact_basic(Some(value))
}
#[inline]
pub fn redact_email(value: Option<&str>) -> String {
let Some(value) = value else {
return "unknown".to_string();
};
let value = value.trim();
if value.is_empty() {
return "unknown".to_string();
}
if let Some(at_pos) = value.find('@') {
let local_part = &value[..at_pos];
let domain = &value[at_pos..];
if local_part.len() <= 2 {
return format!("***{}", domain);
}
let prefix_len = if local_part.len() <= 4 { 1 } else { 2 };
return format!("{}***{}", &local_part[..prefix_len], domain);
}
redact_basic(Some(value))
}
#[cfg(feature = "log-redaction")]
use inklog::{DataMasker, MaskRule, MaskRuleRegistry};
#[cfg(feature = "log-redaction")]
use std::sync::LazyLock;
#[cfg(feature = "log-redaction")]
static DATA_MASKER: LazyLock<DataMasker> = LazyLock::new(build_data_masker);
#[cfg(feature = "log-redaction")]
const KV_MASK_RULES: [(&str, &str); 7] = [
(
"kv_password",
r#"(?i)("?password"?\s*[:=]\s*"?\s*)([^"\s,\}]+)"#,
),
(
"kv_api_key",
r#"(?i)("?api[_-]?key"?\s*[:=]\s*"?\s*)([^"\s,\}]+)"#,
),
("kv_token", r#"(?i)("?token"?\s*[:=]\s*"?\s*)([^"\s,\}]+)"#),
(
"kv_secret",
r#"(?i)("?secret"?\s*[:=]\s*"?\s*)([^"\s,\}]+)"#,
),
(
"kv_credential",
r#"(?i)("?credential"?\s*[:=]\s*"?\s*)([^"\s,\}]+)"#,
),
(
"kv_authorization",
r#"(?i)("?authorization"?\s*[:=]\s*"?\s*)([^"\s,\}]+)"#,
),
(
"credit_card_separated",
r"(?<![0-9])\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}(?![0-9])",
),
];
#[cfg(feature = "log-redaction")]
fn build_data_masker() -> DataMasker {
let mut registry = MaskRuleRegistry::with_builtins();
for (name, pattern) in KV_MASK_RULES {
let rule = MaskRule::builder(name)
.pattern(pattern)
.replacement("***")
.priority(90)
.build()
.unwrap_or_else(|e| panic!("invalid limiteron masking rule {name}: {e}"));
registry
.register(rule)
.expect("limiteron kv rule name collision with builtins");
}
DataMasker::builder().with_registry(registry).build()
}
#[cfg(feature = "log-redaction")]
fn is_sensitive_field_name(field_name: &str) -> bool {
const SENSITIVE_PREFIXES: [&str; 3] = ["key", "secret", "token"];
const SENSITIVE_WORDS: [&str; 7] = [
"password",
"passwd",
"secret",
"token",
"key",
"credential",
"authorization",
];
split_field_tokens(field_name).any(|token| {
let exact = SENSITIVE_WORDS
.iter()
.any(|&word| token.eq_ignore_ascii_case(word));
let prefixed = SENSITIVE_PREFIXES.iter().any(|&prefix| {
token.len() >= prefix.len() && token[..prefix.len()].eq_ignore_ascii_case(prefix)
});
exact || prefixed
})
}
#[cfg(feature = "log-redaction")]
fn split_field_tokens(name: &str) -> impl Iterator<Item = &str> {
let mut indices = name.char_indices().peekable();
let mut start: Option<usize> = None;
let mut prev_was_lowercase = false;
let mut done = false;
std::iter::from_fn(move || {
if done {
return None;
}
loop {
let (idx, ch) = match indices.next() {
Some(pair) => pair,
None => {
done = true;
let s = start.take()?;
return Some(&name[s..]);
}
};
let is_sep = !ch.is_alphanumeric();
let camel_break = start.is_some() && ch.is_uppercase() && prev_was_lowercase;
if is_sep {
if let Some(s) = start.take() {
return Some(&name[s..idx]);
}
prev_was_lowercase = false;
continue;
}
if camel_break {
let s = start.take().unwrap();
start = Some(idx);
prev_was_lowercase = ch.is_lowercase();
return Some(&name[s..idx]);
}
if start.is_none() {
start = Some(idx);
}
prev_was_lowercase = ch.is_lowercase();
}
})
}
#[cfg(feature = "log-redaction")]
#[inline]
pub fn redact_advanced(value: Option<&str>, field_name: Option<&str>) -> String {
let Some(value) = value else {
return "unknown".to_string();
};
let value = value.trim();
if value.is_empty() {
return "unknown".to_string();
}
if let Some(name) = field_name
&& is_sensitive_field_name(name)
{
return "***".to_string();
}
if value.len() <= 4 {
return "***".to_string();
}
DATA_MASKER.mask(value)
}
#[cfg(feature = "log-redaction")]
#[inline]
pub fn contains_sensitive_info(value: &str) -> bool {
!DATA_MASKER.detect(value).is_empty()
}
#[cfg(feature = "log-redaction")]
#[inline]
pub fn redact_http_content(content: &str) -> String {
DATA_MASKER.mask(content)
}
#[cfg(feature = "log-redaction")]
pub struct RedactionConfig<'a> {
pub fields: Vec<(&'a str, bool)>, }
#[cfg(feature = "log-redaction")]
impl<'a> RedactionConfig<'a> {
#[inline]
pub fn new() -> Self {
Self { fields: Vec::new() }
}
#[inline]
pub fn add_field(mut self, field_name: &'a str, is_sensitive: bool) -> Self {
self.fields.push((field_name, is_sensitive));
self
}
#[inline]
pub fn format<F>(&self, get_field: F) -> String
where
F: Fn(&str) -> Option<String>,
{
let mut parts = Vec::new();
for (field_name, is_sensitive) in &self.fields {
if let Some(value) = get_field(field_name) {
let redacted_value = if *is_sensitive {
redact_advanced(Some(&value), Some(field_name))
} else {
value
};
parts.push(format!("{}={}", field_name, redacted_value));
}
}
format!("{{{}}}", parts.join(", "))
}
}
#[cfg(feature = "log-redaction")]
impl<'a> Default for RedactionConfig<'a> {
fn default() -> Self {
Self::new()
}
}
#[inline]
#[cfg(feature = "ban-manager")]
pub fn redact_ban_target(target: &crate::storage::BanTarget) -> String {
match target {
crate::storage::BanTarget::Ip(ip) => redact_ip(Some(ip)),
crate::storage::BanTarget::UserId(user_id) => redact_user_id(Some(user_id)),
crate::storage::BanTarget::Mac(mac) => redact_basic(Some(mac)),
crate::storage::BanTarget::Geo { country_code } => {
format!("Geo({})", redact_basic(Some(country_code)))
}
crate::storage::BanTarget::Cidr(cidr) => format!("Cidr({})", redact_basic(Some(cidr))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact_basic() {
assert_eq!(redact_basic(None), "unknown");
assert_eq!(redact_basic(Some("")), "unknown");
assert_eq!(redact_basic(Some(" ")), "unknown");
assert_eq!(redact_basic(Some("abc")), "***");
assert_eq!(redact_basic(Some("user123")), "us***23");
assert_eq!(redact_basic(Some("192.168.1.1")), "19***.1");
}
#[test]
fn test_redact_basic_boundaries() {
assert_eq!(redact_basic(Some("abcd")), "ab***cd");
assert_eq!(redact_basic(Some("abcde")), "ab***de");
assert_eq!(redact_basic(Some("ab")), "***");
assert_eq!(redact_basic(Some("a")), "***");
}
#[test]
fn test_redact_user_id() {
assert_eq!(redact_user_id(None), "unknown");
assert_eq!(redact_user_id(Some("")), "unknown");
assert_eq!(redact_user_id(Some("user123")), "us***23");
assert_eq!(redact_user_id(Some("abc")), "***");
}
#[test]
fn test_redact_ip() {
assert_eq!(redact_ip(None), "unknown");
assert_eq!(redact_ip(Some("192.168.1.1")), "192.168.***.***");
assert_eq!(redact_ip(Some("::1")), ":***:***");
}
#[test]
fn test_redact_ip_more() {
assert_eq!(redact_ip(Some("")), "unknown");
assert_eq!(redact_ip(Some(" ")), "unknown");
assert_eq!(redact_ip(Some("2001:db8::1")), "2001:***:***");
assert_eq!(redact_ip(Some("not_an_ip")), "no***ip");
assert_eq!(redact_ip(Some("ab")), "***");
}
#[test]
fn test_redact_email() {
assert_eq!(redact_email(None), "unknown");
assert_eq!(redact_email(Some("test@example.com")), "t***@example.com");
assert_eq!(redact_email(Some("ab@example.com")), "***@example.com");
}
#[test]
fn test_redact_email_more() {
assert_eq!(redact_email(Some("")), "unknown");
assert_eq!(redact_email(Some(" ")), "unknown");
assert_eq!(redact_email(Some("abc@example.com")), "a***@example.com");
assert_eq!(redact_email(Some("abcde@example.com")), "ab***@example.com");
assert_eq!(redact_email(Some("noatsign")), "no***gn");
}
#[test]
fn test_redact_email_local_part_boundaries() {
assert_eq!(redact_email(Some("a@b.co")), "***@b.co");
assert_eq!(redact_email(Some("ab@b.co")), "***@b.co");
assert_eq!(redact_email(Some("abc@b.co")), "a***@b.co");
assert_eq!(redact_email(Some("abcd@b.co")), "a***@b.co");
assert_eq!(redact_email(Some("abcde@b.co")), "ab***@b.co");
assert_eq!(redact_email(Some("abcdef@b.co")), "ab***@b.co");
}
#[test]
fn test_redact_ip_extra_segments() {
assert_eq!(redact_ip(Some("1.2.3.4.5")), "1.***.5");
assert_eq!(redact_ip(Some("1.2.3")), "1.***.3");
assert_eq!(redact_ip(Some("1.2.3.4.5.6")), "1.***.6");
}
#[test]
fn test_redact_ip_single_colon() {
assert_eq!(redact_ip(Some(":")), ":***:***");
assert_eq!(redact_ip(Some("a:")), "a:***:***");
assert_eq!(redact_ip(Some(":b")), ":***:***");
}
#[test]
fn test_redact_ip_double_colon() {
assert_eq!(redact_ip(Some("::")), ":***:***");
assert_eq!(redact_ip(Some(":::")), ":***:***");
}
#[test]
fn test_redact_email_at_start() {
assert_eq!(redact_email(Some("@example.com")), "***@example.com");
}
#[test]
fn test_redact_email_multiple_at() {
assert_eq!(redact_email(Some("a@b@c.com")), "***@b@c.com");
assert_eq!(redact_email(Some("@a@b.com")), "***@a@b.com");
}
#[test]
fn test_redact_email_dots_in_local() {
assert_eq!(
redact_email(Some("first.last@example.com")),
"fi***@example.com"
);
}
#[test]
fn test_redact_basic_very_long() {
let long = "abcdefghijklmnopqrstuvwxyz1234567890";
assert_eq!(redact_basic(Some(long)), "ab***90");
assert_eq!(redact_basic(Some("a")), "***");
assert_eq!(redact_basic(Some("xy")), "***");
}
#[test]
fn test_redact_basic_whitespace_mixed() {
assert_eq!(redact_basic(Some(" a ")), "***");
assert_eq!(redact_basic(Some(" ab ")), "***");
assert_eq!(redact_basic(Some(" abc ")), "***");
assert_eq!(redact_basic(Some(" abcd ")), "ab***cd");
}
#[test]
fn test_redact_user_id_delegates() {
assert_eq!(redact_user_id(Some("long_user_id_123")), "lo***23");
assert_eq!(redact_user_id(Some("xy")), "***");
}
#[cfg(feature = "ban-manager")]
#[test]
fn test_redact_ban_target() {
use crate::storage::BanTarget;
let ip_target = BanTarget::Ip("192.168.1.1".to_string());
assert_eq!(redact_ban_target(&ip_target), "192.168.***.***");
let user_target = BanTarget::UserId("user123".to_string());
assert_eq!(redact_ban_target(&user_target), "us***23");
let mac_target = BanTarget::Mac("00:1a:2b:3c:4d:5e".to_string());
assert_eq!(redact_ban_target(&mac_target), "00***5e");
let geo_target = BanTarget::Geo {
country_code: "CN".to_string(),
};
assert_eq!(redact_ban_target(&geo_target), "Geo(***)");
}
#[cfg(feature = "ban-manager")]
#[test]
fn test_redact_ban_target_short_values() {
use crate::storage::BanTarget;
let ip_target = BanTarget::Ip("1.2.3.4".to_string());
assert_eq!(redact_ban_target(&ip_target), "1.2.***.***");
let user_target = BanTarget::UserId("ab".to_string());
assert_eq!(redact_ban_target(&user_target), "***");
let mac_target = BanTarget::Mac("ab".to_string());
assert_eq!(redact_ban_target(&mac_target), "***");
}
#[cfg(feature = "ban-manager")]
#[test]
fn test_redact_ban_target_empty_values() {
use crate::storage::BanTarget;
assert_eq!(redact_ban_target(&BanTarget::Ip("".to_string())), "unknown");
assert_eq!(
redact_ban_target(&BanTarget::UserId("".to_string())),
"unknown"
);
assert_eq!(
redact_ban_target(&BanTarget::Mac("".to_string())),
"unknown"
);
assert_eq!(
redact_ban_target(&BanTarget::Geo {
country_code: "".to_string()
}),
"Geo(unknown)"
);
}
#[cfg(feature = "log-redaction")]
mod log_redaction_tests {
use super::*;
#[test]
fn test_redact_advanced_field_name_gate() {
assert_eq!(redact_advanced(None, None), "unknown");
assert_eq!(redact_advanced(Some(""), None), "unknown");
assert_eq!(redact_advanced(Some(" "), None), "unknown");
assert_eq!(redact_advanced(Some("val"), Some("password")), "***");
assert_eq!(redact_advanced(Some("val"), Some("my_secret")), "***");
assert_eq!(redact_advanced(Some("val"), Some("auth_token")), "***");
assert_eq!(redact_advanced(Some("val"), Some("api_key")), "***");
assert_eq!(redact_advanced(Some("val"), Some("credential")), "***");
assert_eq!(redact_advanced(Some("val"), Some("authorization")), "***");
assert_eq!(redact_advanced(Some("val"), Some("Password")), "***");
assert_eq!(redact_advanced(Some("val"), Some("SECRET_KEY")), "***");
assert_eq!(redact_advanced(Some("val"), Some("keychain")), "***");
assert_eq!(redact_advanced(Some("val"), Some("tokenizer")), "***");
assert_eq!(redact_advanced(Some("x"), Some("PASSWORD")), "***");
assert_eq!(redact_advanced(Some("x"), Some("SECRET")), "***");
assert_eq!(redact_advanced(Some("x"), Some("TOKEN")), "***");
assert_eq!(redact_advanced(Some("x"), Some("KEY")), "***");
assert_eq!(redact_advanced(Some("x"), Some("CREDENTIAL")), "***");
assert_eq!(redact_advanced(Some("x"), Some("AUTHORIZATION")), "***");
}
#[test]
fn test_redact_advanced_short_value_fully_masked() {
assert_eq!(redact_advanced(Some("abc"), None), "***");
assert_eq!(redact_advanced(Some("abcd"), None), "***");
assert_eq!(redact_advanced(Some("hi"), None), "***");
assert_eq!(redact_advanced(Some("bye"), Some("tag")), "***");
assert_eq!(redact_advanced(Some("test"), None), "***");
}
#[test]
fn test_redact_advanced_monkey_not_over_masked() {
let masked = redact_advanced(Some("value"), Some("monkey"));
assert_ne!(masked, "***", "monkey must not be fully masked");
}
#[test]
fn test_is_sensitive_field_name_boundaries() {
assert!(is_sensitive_field_name("keychain"));
assert!(is_sensitive_field_name("tokenizer"));
assert!(is_sensitive_field_name("db_password"));
assert!(is_sensitive_field_name("authToken"));
assert!(is_sensitive_field_name("my_token_123"));
assert!(is_sensitive_field_name("myToken"));
assert!(is_sensitive_field_name("SECRET_KEY"));
assert!(!is_sensitive_field_name("monkey"));
assert!(!is_sensitive_field_name("username"));
assert!(!is_sensitive_field_name("message"));
assert!(!is_sensitive_field_name("role"));
}
#[test]
fn test_redact_http_content_key_value_forms() {
for key in [
"password",
"token",
"authorization",
"api_key",
"secret",
"credential",
] {
let url = format!("GET /api?{}=s3cr3tV4lue user=bob", key);
let redacted = redact_http_content(&url);
assert!(
!redacted.contains("s3cr3tV4lue"),
"{key} URL form is not redacted: {redacted}"
);
assert!(
redacted.contains("user=bob"),
"{key} URL form harms an adjacent parameter: {redacted}"
);
let yaml = format!("{}: s3cr3tV4lue\nusername: bob", key);
let redacted = redact_http_content(&yaml);
assert!(
!redacted.contains("s3cr3tV4lue"),
"{key} YAML form is not redacted: {redacted}"
);
assert!(
redacted.contains("username: bob"),
"{key} YAML form harms an adjacent key: {redacted}"
);
let json = format!(r#"{{"{}": "s3cr3tV4lue", "username": "bob"}}"#, key);
let redacted = redact_http_content(&json);
assert!(
!redacted.contains("s3cr3tV4lue"),
"{key} JSON form is not redacted: {redacted}"
);
assert!(
redacted.contains("bob"),
"{key} JSON form harms an adjacent key: {redacted}"
);
}
}
#[test]
fn test_redact_http_content_value_patterns() {
let cases = [
("email=test@example.com", "test@example.com"),
("phone=13800138000", "13800138000"),
("id_card=110101199001011234", "110101199001011234"),
("credit_card=1234-5678-9012-3456", "1234-5678-9012-3456"),
("card 1234567890123456", "1234567890123456"),
];
for (input, secret) in cases {
let redacted = redact_http_content(input);
assert!(
!redacted.contains(secret),
"the value pattern is not redacted: {input} -> {redacted}"
);
}
}
#[test]
fn test_redact_http_content_non_sensitive_unchanged() {
assert_eq!(redact_http_content(""), "");
assert_eq!(
redact_http_content("username=user123, role=admin"),
"username=user123, role=admin"
);
let content = "line1\nusername=user123\nline2";
assert_eq!(redact_http_content(content), content);
}
#[test]
fn test_redact_http_content_multiple_sensitive() {
let content = "password=abc123 token=xyz789";
assert_eq!(redact_http_content(content), "*** ***");
}
#[test]
fn test_redact_http_content_mixed_newlines() {
let content = "line1\npassword=secret123\nline2\ntoken=abc";
let redacted = redact_http_content(content);
assert!(redacted.contains("line1"));
assert!(redacted.contains("line2"));
assert!(!redacted.contains("secret123"));
assert!(!redacted.contains("token=abc"));
}
#[test]
fn test_redact_http_content_special_chars_in_value() {
let content = r#"password=abc123!@#$%"#;
let redacted = redact_http_content(content);
assert!(!redacted.contains("abc123!@#$%"));
}
#[test]
fn test_contains_sensitive_info_detects() {
assert!(contains_sensitive_info("password=secret123"));
assert!(contains_sensitive_info("api_key=abc123xyz"));
assert!(contains_sensitive_info(
"token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
));
assert!(contains_sensitive_info("secret=mysecret"));
assert!(contains_sensitive_info("credential=mycred"));
assert!(contains_sensitive_info("authorization=bearer token"));
assert!(contains_sensitive_info("password: secret123"));
assert!(contains_sensitive_info("token : abcdef"));
assert!(contains_sensitive_info("api_key=value123"));
assert!(contains_sensitive_info("SECRET:myvalue"));
assert!(contains_sensitive_info("user=admin, password=secret"));
assert!(contains_sensitive_info(
"header contains authorization: bearer xyz"
));
assert!(contains_sensitive_info(
"email: test@example.com, password: secret"
));
assert!(contains_sensitive_info("email me at test@example.com"));
assert!(contains_sensitive_info("call 13800138000"));
assert!(contains_sensitive_info("my id is 110101199001011234"));
assert!(contains_sensitive_info("card 1234567890123456"));
assert!(!contains_sensitive_info("username=user123"));
assert!(!contains_sensitive_info("hello world"));
assert!(!contains_sensitive_info("user=admin, role=editor"));
assert!(!contains_sensitive_info(""));
assert!(!contains_sensitive_info(" "));
assert!(!contains_sensitive_info("\t"));
assert!(!contains_sensitive_info("a"));
assert!(!contains_sensitive_info("ab"));
}
#[test]
fn test_redaction_config_field_name_gate() {
let config = RedactionConfig::new()
.add_field("password", true)
.add_field("username", false);
let result = config.format(|field| match field {
"password" => Some("secret123".to_string()),
"username" => Some("alice".to_string()),
_ => None,
});
assert_eq!(result, "{password=***, username=alice}");
let config = RedactionConfig::new()
.add_field("pwd", true)
.add_field("tok", true);
let result = config.format(|field| match field {
"pwd" => Some("ab".to_string()),
"tok" => Some("abc".to_string()),
_ => None,
});
assert_eq!(result, "{pwd=***, tok=***}");
let config = RedactionConfig::new()
.add_field("password", true)
.add_field("secret_token", true)
.add_field("role", false);
let result = config.format(|field| match field {
"password" => Some("secret123".to_string()),
"secret_token" => Some("myvalue".to_string()),
"role" => Some("admin".to_string()),
_ => None,
});
assert_eq!(result, "{password=***, secret_token=***, role=admin}");
}
#[test]
fn test_redaction_config_default_and_empty() {
let config: RedactionConfig = Default::default();
assert_eq!(config.fields.len(), 0);
let config = RedactionConfig::new();
assert_eq!(config.format(|_| None), "{}");
}
#[test]
fn test_redact_advanced_non_sensitive_value_unchanged() {
assert_eq!(redact_advanced(Some("user123"), None), "user123");
assert_eq!(
redact_advanced(Some("user123"), Some("username")),
"user123"
);
assert_eq!(
redact_advanced(Some("plain text value"), None),
"plain text value"
);
}
#[test]
fn test_redact_advanced_masks_value_pattern_in_place() {
let masked = redact_advanced(Some("contact me at test@example.com"), None);
assert!(!masked.contains("test@example.com"));
assert!(masked.contains("contact me at"));
}
#[test]
fn test_contains_sensitive_info_bare_field_name_not_sensitive() {
assert!(!contains_sensitive_info("password"));
assert!(!contains_sensitive_info("token"));
assert!(!contains_sensitive_info("secret"));
assert!(!contains_sensitive_info("api_key"));
assert!(!contains_sensitive_info("credential"));
assert!(!contains_sensitive_info("authorization"));
}
#[test]
fn test_new_engine_extra_categories_masked() {
let masked = redact_http_content("aws key AKIAIOSFODNN7EXAMPLE rotated");
assert!(!masked.contains("AKIAIOSFODNN7EXAMPLE"));
let masked = redact_http_content(
"jwt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N",
);
assert!(!masked.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"));
let masked = redact_http_content("client ip 10.0.0.5 disconnected");
assert!(!masked.contains("10.0.0.5"));
let masked = redact_http_content("mac 00:1a:2b:3c:4d:5e seen");
assert!(!masked.contains("00:1a:2b:3c:4d:5e"));
assert!(contains_sensitive_info("AKIAIOSFODNN7EXAMPLE"));
assert!(contains_sensitive_info("client 192.168.1.100 connected"));
assert!(contains_sensitive_info("mac 00:1a:2b:3c:4d:5e seen"));
}
}
}