#![cfg_attr(doctest, allow(unused_imports))]
use crate::security::redaction::redact_cache_key;
#[macro_export]
macro_rules! secure_info {
($($arg:tt)*) => {{
use $crate::security::redaction::redact_connection_string;
tracing::info!("{}", format!($($arg)*)
.replace(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == ':' || c == '/' || c == '@' || c == '.', |c| {
if c == '@' || (c.is_ascii_digit() && false) {
c
} else {
c
}
})
.split_inclusive("://")
.map(|part| {
if part.contains("password") || part.contains("secret") || part.contains("token") {
redact_connection_string(part)
} else {
part.to_string()
}
})
.collect::<String>()
);
}}
}
#[macro_export]
macro_rules! secure_debug {
($($arg:tt)*) => {{
use $crate::security::redaction::redact_connection_string;
tracing::debug!("{}", format!($($arg)*)
.split("://")
.map(|part| {
if part.contains("password") || part.contains("secret") || part.contains("token") {
redact_connection_string(&format!("://{}", part))
} else {
part.to_string()
}
})
.collect::<String>()
);
}}
}
pub fn log_cache_key(level: &str, message: &str, key: &str) {
let redacted = redact_cache_key(key);
match level {
"info" => tracing::info!("{}: {}", message, redacted),
"debug" => tracing::debug!("{}: {}", message, redacted),
"warn" => tracing::warn!("{}: {}", message, redacted),
"error" => tracing::error!("{}: {}", message, redacted),
_ => tracing::info!("{}: {}", message, redacted),
}
}
pub fn sanitize_message(message: &str) -> String {
let mut result = message.to_string();
if result.contains("://") {
if let Some(start) = result.find("://") {
let protocol = &result[..start];
let after_protocol = &result[start + 3..];
if let Some(at_pos) = after_protocol.find('@') {
let user_part = &after_protocol[..at_pos];
let host_part = &after_protocol[at_pos..];
let sanitized_user: String = user_part
.chars()
.take_while(|c| *c != ':')
.chain(std::iter::once('*').chain(std::iter::once('*')).take(2))
.collect();
result = format!("{}://{}{}", protocol, sanitized_user, host_part);
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::security::redaction::{redact_cache_key, redact_connection_string};
#[test]
fn test_log_connection_string() {
let conn_str = "redis://user:password123@localhost:6379";
let redacted = redact_connection_string(conn_str);
assert!(!redacted.contains("password123"));
assert!(redacted.contains("user:****"));
}
#[test]
fn test_log_cache_key() {
let key = "user_token_abc123";
let redacted = redact_cache_key(key);
assert!(!redacted.contains("token"));
assert!(redacted.starts_with("****"));
}
#[test]
fn test_sanitize_message() {
let msg = "Connection: redis://user:secret123@localhost:6379";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("secret123"));
assert!(sanitized.contains("**"));
}
#[test]
fn test_log_cache_key_info_level() {
log_cache_key("info", "Cache hit", "user_token_abc123");
}
#[test]
fn test_log_cache_key_debug_level() {
log_cache_key("debug", "Cache debug", "session_xyz");
}
#[test]
fn test_log_cache_key_warn_level() {
log_cache_key("warn", "Cache warning", "password_123");
}
#[test]
fn test_log_cache_key_error_level() {
log_cache_key("error", "Cache error", "api_key_test");
}
#[test]
fn test_log_cache_key_default_level() {
log_cache_key("trace", "Cache trace", "normal_key");
log_cache_key("unknown_level", "Cache unknown", "another_key");
}
#[test]
fn test_log_cache_key_non_sensitive_key() {
log_cache_key("info", "Cache access", "user_profile_123");
}
#[test]
fn test_log_cache_key_empty_key() {
log_cache_key("info", "Empty key", "");
}
#[test]
fn test_log_cache_key_empty_message() {
log_cache_key("info", "", "some_key");
}
#[test]
fn test_sanitize_message_no_connection_string() {
let msg = "This is a normal message without connection string";
let sanitized = sanitize_message(msg);
assert_eq!(sanitized, msg);
}
#[test]
fn test_sanitize_message_empty() {
let sanitized = sanitize_message("");
assert_eq!(sanitized, "");
}
#[test]
fn test_sanitize_message_no_at_symbol() {
let msg = "redis://localhost:6379";
let sanitized = sanitize_message(msg);
assert_eq!(sanitized, msg);
}
#[test]
fn test_sanitize_message_with_password() {
let msg = "redis://user:password123@host:6379";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("password123"));
}
#[test]
fn test_sanitize_message_multiple_protocols() {
let msg = "redis://user:pass1@host1:6379 and redis://user:pass2@host2:6380";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("pass1"));
}
}