use regex::Regex;
use std::collections::HashSet;
use std::sync::{Arc, OnceLock, RwLock};
static SENSITIVE_PATTERNS: OnceLock<Vec<(Regex, Replacement)>> = OnceLock::new();
fn init_sensitive_patterns() -> Vec<(Regex, Replacement)> {
vec![
(
Regex::new(r"(?i)api[ ]?key[\s]*[:=][\s]*([a-zA-Z0-9_\-]+)").unwrap(),
Replacement::MaskedGroup(1, 8),
),
(
Regex::new(r"(?i)password[\s]*[:=][\s]*([^;\s]+)").unwrap(),
Replacement::MaskedGroup(1, 4),
),
(
Regex::new(r"(?i)access[ ]?token[\s]*[:=][\s]*([a-zA-Z0-9_\-\.]+)").unwrap(),
Replacement::MaskedGroup(1, 6),
),
(
Regex::new(r"(?i)refresh[ ]?token[\s]*[:=][\s]*([a-zA-Z0-9_\-\.]+)").unwrap(),
Replacement::MaskedGroup(1, 6),
),
(
Regex::new(r"(?i)auth[ ]?token[\s]*[:=][\s]*([a-zA-Z0-9_\-\.]+)").unwrap(),
Replacement::MaskedGroup(1, 6),
),
(
Regex::new(r"(?i)secret[ ]?key[\s]*[:=][\s]*([a-zA-Z0-9_\-\/+=]*)").unwrap(),
Replacement::MaskedGroup(1, 8),
),
(
Regex::new(r"(?i)private[ ]?key[\s]*[:=][\s]*([a-zA-Z0-9_\-\/+=]*)").unwrap(),
Replacement::MaskedGroup(1, 8),
),
(
Regex::new(r"(?i)(database[ ]?url|connection[ ]?string|mongodb(\+ssl)?://)[^\s]+")
.unwrap(),
Replacement::Value("***CONNECTION_STRING***".to_string()),
),
(
Regex::new(r"(?i)Bearer\s+([a-zA-Z0-9_\-\.]+)").unwrap(),
Replacement::MaskedGroup(1, 6),
),
(
Regex::new(r"(?i)Basic\s+([a-zA-Z0-9+/=]+)").unwrap(),
Replacement::MaskedGroup(1, 6),
),
(
Regex::new(r"([a-zA-Z0-9._%+-]+)@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap(),
Replacement::EmailMask,
),
(
Regex::new(r"\b(?:\d{1,3}\.){3}\d{1,3}\b").unwrap(),
Replacement::Value("***IP_ADDRESS***".to_string()),
),
]
}
fn get_sensitive_patterns() -> &'static Vec<(Regex, Replacement)> {
SENSITIVE_PATTERNS.get_or_init(init_sensitive_patterns)
}
#[derive(Debug, Clone)]
enum Replacement {
MaskedGroup(usize, usize),
Value(String),
EmailMask,
}
#[derive(Debug, Clone)]
pub struct ErrorSanitizer {
custom_rules: Arc<RwLock<Vec<(Regex, String)>>>,
sensitive_keywords: Arc<RwLock<HashSet<String>>>,
strict_mode: bool,
}
impl Default for ErrorSanitizer {
fn default() -> Self {
Self::new()
}
}
impl ErrorSanitizer {
pub fn new() -> Self {
Self {
custom_rules: Arc::new(RwLock::new(Vec::new())),
sensitive_keywords: Arc::new(RwLock::new(Self::default_keywords())),
strict_mode: false,
}
}
fn default_keywords() -> HashSet<String> {
let mut set = HashSet::new();
set.insert("password".to_string());
set.insert("secret".to_string());
set.insert("token".to_string());
set.insert("key".to_string());
set.insert("credential".to_string());
set.insert("auth".to_string());
set.insert("private".to_string());
set.insert("encryption".to_string());
set
}
pub fn with_strict_mode(mut self) -> Self {
self.strict_mode = true;
self
}
pub fn add_rule(&self, pattern: &str, replacement: &str) -> Result<(), Error> {
let regex = Regex::new(pattern).map_err(|_| Error::InvalidPattern)?;
let mut rules = self.custom_rules.write().map_err(|_| Error::PoisonedLock)?;
rules.push((regex, replacement.to_string()));
Ok(())
}
pub fn add_sensitive_keyword(&self, keyword: &str) {
let mut keywords = self.sensitive_keywords.write().unwrap();
keywords.insert(keyword.to_lowercase());
}
pub fn sanitize(&self, message: &str) -> String {
let mut result = message.to_string();
for (ref pattern, ref replacement) in get_sensitive_patterns().iter() {
result = apply_replacement(&result, pattern, replacement);
}
let custom_rules = self.custom_rules.read().unwrap();
for (ref pattern, ref replacement) in custom_rules.iter() {
result = pattern
.replace_all(&result, replacement.as_str())
.to_string();
}
if self.strict_mode {
let keywords = self.sensitive_keywords.read().unwrap();
for keyword in keywords.iter() {
let pattern = Regex::new(&format!(r"(?i)\b{}\b", regex::escape(keyword))).unwrap();
result = pattern.replace_all(&result, "***").to_string();
}
}
result
}
pub fn sanitize_with_indicator(&self, message: &str) -> (String, bool) {
let sanitized = self.sanitize(message);
let contains_sensitive = sanitized.contains("***");
(sanitized, contains_sensitive)
}
pub fn contains_sensitive(&self, message: &str) -> bool {
for (ref pattern, _) in get_sensitive_patterns().iter() {
if pattern.is_match(message) {
return true;
}
}
let keywords = self.sensitive_keywords.read().unwrap();
let message_lower = message.to_lowercase();
keywords
.iter()
.any(|keyword| message_lower.contains(keyword))
}
pub fn sanitize_all(&self, messages: &[&str]) -> Vec<String> {
messages.iter().map(|m| self.sanitize(m)).collect()
}
pub fn safe_message(&self, message: &str, context: &str) -> String {
if self.contains_sensitive(message) {
format!(
"[{}] Sensitive data detected in error message - sanitized for security",
context
)
} else {
self.sanitize(message)
}
}
}
fn apply_replacement(input: &str, pattern: &Regex, replacement: &Replacement) -> String {
pattern
.replace_all(input, |caps: ®ex::Captures| match replacement {
Replacement::MaskedGroup(group_idx, visible_chars) => {
if let Some(matched) = caps.get(*group_idx) {
let s = matched.as_str();
if s.len() <= *visible_chars {
"*".repeat(s.len())
} else {
format!("{}***", &s[..*visible_chars])
}
} else {
"***".to_string()
}
}
Replacement::Value(repl) => repl.clone(),
Replacement::EmailMask => {
if let Some(email) = caps.get(0) {
let s = email.as_str();
if let Some(at_pos) = s.find('@') {
let local_part = &s[..at_pos];
let domain = &s[at_pos..];
if local_part.len() <= 2 {
"***".to_string() + domain
} else {
format!("{}**{}", &local_part[..2], domain)
}
} else {
"***".to_string()
}
} else {
"***".to_string()
}
}
})
.to_string()
}
#[derive(Debug, Clone)]
pub struct SecureLogger {
sanitizer: ErrorSanitizer,
min_level: LogLevel,
}
impl Default for SecureLogger {
fn default() -> Self {
Self::new()
}
}
impl SecureLogger {
pub fn new() -> Self {
Self {
sanitizer: ErrorSanitizer::new(),
min_level: LogLevel::Debug,
}
}
pub fn with_min_level(mut self, level: LogLevel) -> Self {
self.min_level = level;
self
}
pub fn debug(&self, message: &str) {
self.log(LogLevel::Debug, message);
}
pub fn info(&self, message: &str) {
self.log(LogLevel::Info, message);
}
pub fn warn(&self, message: &str) {
self.log(LogLevel::Warn, message);
}
pub fn error(&self, message: &str) {
self.log(LogLevel::Error, message);
}
pub fn error_with_context(&self, context: &str, error: &str) {
let safe_message = self.sanitizer.safe_message(error, context);
self.log(LogLevel::Error, &safe_message);
}
fn log(&self, level: LogLevel, message: &str) {
if level < self.min_level {
return;
}
let sanitized = self.sanitizer.sanitize(message);
let _log_entry = format!("[{}] {}", level.as_str(), sanitized);
#[cfg(feature = "tracing")]
match level {
LogLevel::Error => tracing::error!("{}", _log_entry),
LogLevel::Warn => tracing::warn!("{}", _log_entry),
LogLevel::Info => tracing::info!("{}", _log_entry),
LogLevel::Debug => tracing::debug!("{}", _log_entry),
}
}
pub fn sanitizer(&self) -> &ErrorSanitizer {
&self.sanitizer
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Error {
InvalidPattern,
PoisonedLock,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidPattern => write!(f, "Invalid regex pattern"),
Error::PoisonedLock => write!(f, "Lock poisoned"),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone)]
pub struct SafeResult<T> {
value: Option<T>,
error_message: Option<String>,
contained_sensitive: bool,
}
impl<T> SafeResult<T> {
pub fn ok(value: T) -> Self {
Self {
value: Some(value),
error_message: None,
contained_sensitive: false,
}
}
pub fn err(message: &str) -> Self {
let sanitizer = ErrorSanitizer::new();
let (sanitized, contained_sensitive) = sanitizer.sanitize_with_indicator(message);
Self {
value: None,
error_message: Some(sanitized),
contained_sensitive,
}
}
pub fn is_ok(&self) -> bool {
self.value.is_some()
}
pub fn is_err(&self) -> bool {
self.value.is_none()
}
pub fn value(&self) -> Option<&T> {
self.value.as_ref()
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn contained_sensitive(&self) -> bool {
self.contained_sensitive
}
pub fn unwrap(self) -> T {
self.value.expect("SafeResult::unwrap() on None")
}
pub fn unwrap_or(self, default: T) -> T {
self.value.unwrap_or(default)
}
}
#[derive(Debug, Clone)]
pub struct SensitiveDataFilter {
sanitizer: ErrorSanitizer,
allowed_patterns: Vec<Regex>,
blocked_patterns: Vec<Regex>,
}
impl Default for SensitiveDataFilter {
fn default() -> Self {
Self::new()
}
}
impl SensitiveDataFilter {
pub fn new() -> Self {
Self {
sanitizer: ErrorSanitizer::new(),
allowed_patterns: Vec::new(),
blocked_patterns: Vec::new(),
}
}
pub fn add_allowed_pattern(&mut self, pattern: &str) -> Result<(), Error> {
let regex = Regex::new(pattern).map_err(|_| Error::InvalidPattern)?;
self.allowed_patterns.push(regex);
Ok(())
}
pub fn add_blocked_pattern(&mut self, pattern: &str) -> Result<(), Error> {
let regex = Regex::new(pattern).map_err(|_| Error::InvalidPattern)?;
self.blocked_patterns.push(regex);
Ok(())
}
pub fn filter(&self, message: &str) -> FilterResult {
if !self.sanitizer.contains_sensitive(message) {
return FilterResult::Allowed(message.to_string());
}
for pattern in &self.blocked_patterns {
if pattern.is_match(message) {
return FilterResult::Blocked {
reason: "message matches blocked pattern".to_string(),
};
}
}
let sanitized = self.sanitizer.sanitize(message);
FilterResult::Sanitized(sanitized)
}
pub fn filter_all<'a>(&self, messages: &'a [&str]) -> Vec<(&'a str, FilterResult)> {
messages.iter().map(|m| (*m, self.filter(m))).collect()
}
}
#[derive(Debug, Clone)]
pub enum FilterResult {
Allowed(String),
Sanitized(String),
Blocked { reason: String },
}
impl FilterResult {
pub fn is_allowed(&self) -> bool {
matches!(self, FilterResult::Allowed(_))
}
pub fn is_blocked(&self) -> bool {
matches!(self, FilterResult::Blocked { .. })
}
pub fn message(&self) -> Option<&str> {
match self {
FilterResult::Allowed(msg) => Some(msg),
FilterResult::Sanitized(msg) => Some(msg),
FilterResult::Blocked { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strict_mode() {
let sanitizer = ErrorSanitizer::new().with_strict_mode();
let result = sanitizer.sanitize("The password is secret and token is key123");
assert!(!result.contains("password"));
assert!(!result.contains("secret"));
assert!(!result.contains("token"));
}
#[test]
fn test_safe_message() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.safe_message("Database connection failed", "DB");
assert_eq!(result, "Database connection failed");
let result = sanitizer.safe_message("API Key: sk-12345", "API");
assert!(result.contains("sanitized"));
}
#[test]
fn test_secure_logger() {
let logger = SecureLogger::new().with_min_level(LogLevel::Debug);
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warning message");
logger.error("Error with API Key: sk-12345");
}
#[test]
fn test_safe_result() {
let success = SafeResult::ok("value");
assert!(success.is_ok());
assert!(!success.is_err());
assert_eq!(success.value(), Some(&"value"));
let failure: SafeResult<()> = SafeResult::err("Error with password: secret");
assert!(!failure.is_ok());
assert!(failure.is_err());
assert!(failure.error_message().is_some());
assert!(failure.contained_sensitive());
}
#[test]
fn test_sensitive_data_filter() {
let mut filter = SensitiveDataFilter::new();
filter.add_blocked_pattern(r".*password.*").unwrap();
let result = filter.filter("Contains password secret");
assert!(result.is_blocked());
let result = filter.filter("Normal message");
assert!(result.is_allowed());
let result = filter.filter("API Key: sk-12345");
match result {
FilterResult::Sanitized(msg) => {
assert!(!msg.contains("sk-12345"));
}
_ => panic!("Expected sanitized result"),
}
}
#[test]
fn test_error_sanitization() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.sanitize("API Key: secret123");
println!("API Key result: '{}'", result);
assert!(
!result.contains("secret123"),
"API key not masked: {}",
result
);
assert!(result.contains("***"), "No masking: {}", result);
let result = sanitizer.sanitize("Password: mySecretPassword123");
assert!(!result.contains("mySecretPassword123"));
assert!(result.contains("***"));
let result = sanitizer.sanitize("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
assert!(!result.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"));
let result = sanitizer.sanitize("Contact: user@example.com");
assert!(!result.contains("user@example.com"));
assert!(result.contains("**"), "Email not masked: {}", result);
}
}