use crate::security::patterns::SENSITIVE_KEYWORDS;
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> {
SENSITIVE_KEYWORDS.iter().map(|s| s.to_string()).collect()
}
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();
let char_count = s.chars().count();
if char_count <= *visible_chars {
"*".repeat(char_count)
} else {
let prefix: String = s.chars().take(*visible_chars).collect();
format!("{}***", prefix)
}
} 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..];
let local_chars = local_part.chars().count();
if local_chars <= 2 {
"***".to_string() + domain
} else {
let prefix: String = local_part.chars().take(2).collect();
format!("{}**{}", prefix, 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);
match level {
LogLevel::Error | LogLevel::Warn => eprintln!("{log_entry}"),
LogLevel::Info | LogLevel::Debug => eprintln!("{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"));
}
_ => unreachable!("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);
}
#[test]
fn test_api_key_masking() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.sanitize("API Key: sk-1234567890abcdef"); assert!(!result.contains("sk-1234567890abcdef"));
assert!(result.contains("***"));
let result = sanitizer.sanitize("API Key: abcd"); assert!(!result.contains("abcd"));
assert!(result.contains("****"));
let result = sanitizer.sanitize("api key=secret12345"); assert!(!result.contains("secret12345"));
}
#[test]
fn test_password_and_token_masking() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.sanitize("password: mySecretPass"); assert!(!result.contains("mySecretPass"));
assert!(result.contains("***"));
let result = sanitizer.sanitize("access token: abcdefghijk"); assert!(!result.contains("abcdefghijk"));
assert!(result.contains("***"));
let result = sanitizer.sanitize("refresh token: tok1234567890"); assert!(!result.contains("tok1234567890"));
let result = sanitizer.sanitize("auth token: authval123456"); assert!(!result.contains("authval123456"));
}
#[test]
fn test_secret_and_private_key_masking() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.sanitize("secret key: ABCDEFGHIJ123456"); assert!(!result.contains("ABCDEFGHIJ123456"));
assert!(result.contains("***"));
let result = sanitizer.sanitize("private key: PRIVKEYVALUE123"); assert!(!result.contains("PRIVKEYVALUE123"));
}
#[test]
fn test_connection_string_masking() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.sanitize("mongodb://user:pass@host:27017/db"); assert!(!result.contains("user:pass@host:27017"));
assert!(result.contains("CONNECTION_STRING"));
let result = sanitizer.sanitize("mongodb+ssl://user:pass@host/db"); assert!(result.contains("CONNECTION_STRING"));
}
#[test]
fn test_bearer_and_basic_auth_masking() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.sanitize("Bearer eyJhbGciOiJIUzI1NiJ9.signature"); assert!(!result.contains("eyJhbGciOiJIUzI1NiJ9"));
assert!(result.contains("***"));
let result = sanitizer.sanitize("Basic dXNlcjpwYXNz"); assert!(!result.contains("dXNlcjpwYXNz"));
assert!(result.contains("***"));
}
#[test]
fn test_email_masking_variants() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.sanitize("Contact: aliceuser@example.com");
assert!(!result.contains("aliceuser@example.com"));
assert!(result.contains("@example.com"));
let result = sanitizer.sanitize("ab@x.co");
assert!(result.contains("***@x.co"));
assert!(!result.contains("ab@"));
}
#[test]
fn test_ip_address_masking() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.sanitize("Connecting to 192.168.1.1 failed");
assert!(!result.contains("192.168.1.1"));
assert!(result.contains("IP_ADDRESS"));
let result = sanitizer.sanitize("from 10.0.0.1");
assert!(result.contains("IP_ADDRESS"));
}
#[test]
fn test_aws_key_like_value_detected_and_safe() {
let sanitizer = ErrorSanitizer::new();
let msg = "access_key: AKIAIOSFODNN7EXAMPLE"; assert!(sanitizer.contains_sensitive(msg));
let safe = sanitizer.safe_message(msg, "AWS");
assert!(!safe.contains("AKIAIOSFODNN7EXAMPLE"));
assert!(safe.contains("Sensitive data detected"));
assert!(safe.contains("[AWS]"));
}
#[test]
fn test_add_rule_valid_and_invalid() {
let sanitizer = ErrorSanitizer::new();
assert!(sanitizer.add_rule(r"session_\d+", "[SESSION]").is_ok());
let result = sanitizer.sanitize("error in session_12345 failed");
assert!(result.contains("[SESSION]"));
assert!(!result.contains("session_12345"));
let err = sanitizer.add_rule(r"(", "").unwrap_err();
assert_eq!(err, Error::InvalidPattern);
}
#[test]
fn test_add_sensitive_keyword_strict() {
let sanitizer = ErrorSanitizer::new().with_strict_mode();
sanitizer.add_sensitive_keyword("proprietary");
let result = sanitizer.sanitize("the proprietary value leaked");
assert!(!result.contains("proprietary"));
assert!(result.contains("***"));
}
#[test]
fn test_contains_sensitive_and_sanitize_all() {
let sanitizer = ErrorSanitizer::new();
assert!(sanitizer.contains_sensitive("API Key: sk-abc123"));
assert!(sanitizer.contains_sensitive("the password was set"));
assert!(!sanitizer.contains_sensitive("normal log message"));
let results = sanitizer.sanitize_all(&["API Key: sk-abcdef", "clean message"]);
assert!(!results[0].contains("sk-abcdef"));
assert_eq!(results[1], "clean message");
}
#[test]
fn test_sanitize_with_indicator() {
let sanitizer = ErrorSanitizer::new();
let (sanitized, flagged) = sanitizer.sanitize_with_indicator("API Key: sk-abcdef");
assert!(flagged);
assert!(sanitized.contains("***"));
let (sanitized, flagged) = sanitizer.sanitize_with_indicator("nothing sensitive");
assert!(!flagged);
assert_eq!(sanitized, "nothing sensitive");
}
#[test]
fn test_safe_message_branches() {
let sanitizer = ErrorSanitizer::new();
let result = sanitizer.safe_message("API Key: sk-abcdef", "CTX");
assert!(result.contains("Sensitive data detected"));
assert!(result.contains("CTX"));
let result = sanitizer.safe_message("all good here", "CTX");
assert_eq!(result, "all good here");
}
#[test]
fn test_error_sanitizer_default() {
let sanitizer = ErrorSanitizer::default();
let result = sanitizer.sanitize("Password: secret123"); assert!(!result.contains("secret123"));
}
#[test]
fn test_error_display_variants() {
assert_eq!(
format!("{}", Error::InvalidPattern),
"Invalid regex pattern"
);
assert_eq!(format!("{}", Error::PoisonedLock), "Lock poisoned");
}
#[test]
fn test_log_level_as_str() {
assert_eq!(LogLevel::Debug.as_str(), "DEBUG");
assert_eq!(LogLevel::Info.as_str(), "INFO");
assert_eq!(LogLevel::Warn.as_str(), "WARN");
assert_eq!(LogLevel::Error.as_str(), "ERROR");
}
#[test]
fn test_secure_logger_levels_and_context() {
let logger = SecureLogger::new().with_min_level(LogLevel::Warn);
logger.debug("skipped debug");
logger.info("skipped info");
logger.warn("warn with API Key: sk-abcdef"); logger.error("error msg");
logger.error_with_context("DB", "failed with password: secret123"); logger.error_with_context("DB", "plain failure");
let sanitized = logger.sanitizer().sanitize("Password: p"); assert!(!sanitized.contains("Password: p"));
}
#[test]
fn test_secure_logger_default() {
let logger = SecureLogger::default();
logger.debug("d");
logger.info("i");
logger.warn("w");
logger.error("e");
}
#[test]
fn test_safe_result_ok_and_err() {
let ok: SafeResult<i32> = SafeResult::ok(42);
assert!(ok.is_ok());
assert!(!ok.is_err());
assert_eq!(ok.value(), Some(&42));
assert!(!ok.contained_sensitive());
assert_eq!(ok.unwrap(), 42);
assert_eq!(SafeResult::ok(7).unwrap_or(0), 7);
let err: SafeResult<i32> = SafeResult::err("Error password: secret123"); assert!(!err.is_ok());
assert!(err.is_err());
assert!(err.error_message().is_some());
assert!(err.contained_sensitive());
assert_eq!(err.unwrap_or(0), 0);
let err: SafeResult<i32> = SafeResult::err("plain failure");
assert!(!err.contained_sensitive());
assert_eq!(err.error_message(), Some("plain failure"));
}
#[test]
#[should_panic(expected = "SafeResult::unwrap()")]
fn test_safe_result_unwrap_panic_on_err() {
let err: SafeResult<i32> = SafeResult::err("failure");
let _ = err.unwrap();
}
#[test]
fn test_sensitive_data_filter_allowed_blocked_sanitized() {
let mut filter = SensitiveDataFilter::new();
assert!(filter.add_allowed_pattern(r"^ok").is_ok());
assert!(filter.add_blocked_pattern(r".*password.*").is_ok());
let r = filter.filter("normal message");
assert!(r.is_allowed());
assert!(!r.is_blocked());
assert_eq!(r.message(), Some("normal message"));
let r = filter.filter("contains password here");
assert!(r.is_blocked());
assert!(!r.is_allowed());
assert_eq!(r.message(), None);
let r = filter.filter("API Key: sk-abcdef");
match r {
FilterResult::Sanitized(msg) => {
assert!(!msg.contains("sk-abcdef"));
assert!(msg.contains("***"));
assert!(FilterResult::Sanitized(msg.clone()).message().is_some());
}
_ => unreachable!("expected Sanitized"),
}
}
#[test]
fn test_sensitive_data_filter_invalid_pattern() {
let mut filter = SensitiveDataFilter::new();
assert_eq!(
filter.add_allowed_pattern(r"(").unwrap_err(),
Error::InvalidPattern
);
assert_eq!(
filter.add_blocked_pattern(r"[").unwrap_err(),
Error::InvalidPattern
);
}
#[test]
fn test_sensitive_data_filter_default_and_all() {
let filter = SensitiveDataFilter::default();
let results = filter.filter_all(&["normal", "API Key: sk-abcdef"]);
assert_eq!(results.len(), 2);
assert!(results[0].1.is_allowed());
assert!(!results[1].1.is_allowed());
}
#[test]
fn test_filter_result_message_all_variants() {
assert_eq!(FilterResult::Allowed("a".to_string()).message(), Some("a"));
assert_eq!(
FilterResult::Sanitized("b".to_string()).message(),
Some("b")
);
assert_eq!(
FilterResult::Blocked {
reason: "r".to_string()
}
.message(),
None
);
}
}