#[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);
}
return format!("{}***{}", &local_part[..1], domain);
}
redact_basic(Some(value))
}
#[cfg(feature = "log-redaction")]
use regex::Regex;
#[cfg(feature = "log-redaction")]
use std::sync::Mutex as SyncMutex;
#[cfg(feature = "log-redaction")]
static SENSITIVE_PATTERNS: SyncMutex<Vec<(&str, Regex)>> = SyncMutex::new(Vec::new());
#[cfg(feature = "log-redaction")]
fn initialize_patterns() {
let mut patterns = SENSITIVE_PATTERNS.lock().unwrap();
if patterns.is_empty() {
patterns.push((
"password",
Regex::new(r"(?i)(password[\s]*[:=][\s]*)([^\s,\}]+)").unwrap(),
));
patterns.push((
"api_key",
Regex::new(r"(?i)(api[_-]?key[\s]*[:=][\s]*)([^\s,\}]+)").unwrap(),
));
patterns.push((
"token",
Regex::new(r"(?i)(token[\s]*[:=][\s]*)([^\s,\}]+)").unwrap(),
));
patterns.push((
"secret",
Regex::new(r"(?i)(secret[\s]*[:=][\s]*)([^\s,\}]+)").unwrap(),
));
patterns.push((
"credential",
Regex::new(r"(?i)(credential[\s]*[:=][\s]*)([^\s,\}]+)").unwrap(),
));
patterns.push((
"authorization",
Regex::new(r"(?i)(authorization[\s]*[:=][\s]*)([^\s,\}]+)").unwrap(),
));
patterns.push((
"email",
Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap(),
));
patterns.push(("phone", Regex::new(r"1[3-9]\d{9}").unwrap()));
patterns.push(("id_card", Regex::new(r"\d{17}[\dXx]").unwrap()));
patterns.push((
"credit_card",
Regex::new(r"\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}").unwrap(),
));
}
}
#[cfg(feature = "log-redaction")]
#[inline]
pub fn redact_enhanced(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 {
let lower_name = name.to_lowercase();
if lower_name.contains("password")
|| lower_name.contains("secret")
|| lower_name.contains("token")
|| lower_name.contains("key")
|| lower_name.contains("credential")
|| lower_name.contains("authorization")
{
return "***".to_string();
}
}
let mut result = value.to_string();
initialize_patterns();
for (pattern_name, regex) in SENSITIVE_PATTERNS.lock().unwrap().iter() {
if *pattern_name == "email"
|| *pattern_name == "phone"
|| *pattern_name == "id_card"
|| *pattern_name == "credit_card"
{
result = regex.replace_all(&result, "***").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)
}
#[cfg(feature = "log-redaction")]
#[inline]
pub fn contains_sensitive_info(value: &str) -> bool {
initialize_patterns();
for (_, regex) in SENSITIVE_PATTERNS.lock().unwrap().iter() {
if regex.is_match(value) {
return true;
}
}
let lower_value = value.to_lowercase();
lower_value.contains("password")
|| lower_value.contains("secret")
|| lower_value.contains("token")
|| lower_value.contains("api_key")
|| lower_value.contains("credential")
|| lower_value.contains("authorization")
}
#[cfg(feature = "log-redaction")]
#[inline]
pub fn redact_http_content(content: &str) -> String {
let mut result = content.to_string();
initialize_patterns();
for (_, regex) in SENSITIVE_PATTERNS.lock().unwrap().iter() {
result = regex.replace_all(&result, "***").to_string();
}
result
}
#[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_enhanced(Some(&value), Some(field_name))
} else {
value
};
parts.push(format!("{}={}", field_name, redacted_value));
}
}
format!("{{{}}}", parts.join(", "))
}
}
#[cfg(feature = "log-redaction")]
impl Default for RedactionConfig<'_> {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[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_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_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");
}
#[cfg(feature = "log-redaction")]
mod log_redaction_tests {
use super::*;
#[test]
fn test_redact_enhanced() {
assert_eq!(redact_enhanced(Some("secret123"), Some("password")), "***");
assert_eq!(redact_enhanced(Some("token123"), Some("api_key")), "***");
assert_eq!(
redact_enhanced(Some("user123"), Some("username")),
"us***23"
);
}
#[test]
fn test_contains_sensitive_info() {
assert!(contains_sensitive_info("password=secret123"));
assert!(contains_sensitive_info("api_key=abc123xyz"));
assert!(contains_sensitive_info(
"token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
));
assert!(!contains_sensitive_info("username=user123"));
}
#[test]
fn test_redact_http_content() {
let content = r#"password=secret123, username=user123"#;
let redacted = redact_http_content(content);
assert!(!redacted.contains("secret123"));
assert!(redacted.contains("user123"));
}
#[test]
fn test_redaction_config() {
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("user123".to_string()),
_ => None,
});
assert_eq!(result, "{password=***, username=user123}");
}
}
}