use regex::Regex;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use crate::error::InklogError;
static SENSITIVE_FIELD_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
vec![
Regex::new(r"(?i)\b(password|passwd|pwd)\b").unwrap(),
Regex::new(r"(?i)(?:[^a-zA-Z0-9_])?(token|bearer|auth)\b").unwrap(),
Regex::new(r"(?i)\b(secret|credential)\b").unwrap(),
Regex::new(r"(?i)\b(api[_-]?key|apikey|api[_-]?secret)\b").unwrap(),
Regex::new(r"(?i)\b(access[_-]?key|access[_-]?key[_-]?id)\b").unwrap(),
Regex::new(r"(?i)\b(secret[_-]?key|private[_-]?key|public[_-]?key)\b").unwrap(),
Regex::new(r"(?i)\b(encryption[_-]?key|decryption[_-]?key|master[_-]?key)\b").unwrap(),
Regex::new(r"(?i)\b(session[_-]?key|session[_-]?id|session[_-]?token)\b").unwrap(),
Regex::new(r"(?i)\b(oauth|oauth[_-]?token|oauth[_-]?secret)\b").unwrap(),
Regex::new(r"(?i)\b(jwt(_[a-zA-Z0-9]+)?|bearer[_-]?token)\b").unwrap(),
Regex::new(r"(?i)\b(aws[_-]?secret|aws[_-]?key|aws[_-]?token|aws[_-]?credentials)\b").unwrap(),
Regex::new(r"(?i)\b(database[_-]?url|db[_-]?password|db[_-]?user|connection[_-]?string)\b").unwrap(),
Regex::new(r"(?i)\b(credit[_-]?card|card[_-]?number|cvv|ssn|social[_-]?security)\b").unwrap(),
Regex::new(r"(?i)\b(client[_-]?secret|client[_-]?id)\b").unwrap(),
Regex::new(r"(?i)\b(refresh[_-]?token|pin|pin[_-]?code|two[_-]?factor|totp|backup[_-]?code|recovery[_-]?code)\b").unwrap(),
]
});
#[derive(Debug, Clone, Default)]
pub struct DataMasker {
rules: Vec<MaskRule>,
#[cfg(feature = "fast-masking")]
ac_masker: Option<super::masking_ac::AcMasker>,
}
type ApplyFn = Arc<dyn Fn(&Regex, &str, &str) -> String + Send + Sync>;
#[derive(Clone)]
pub struct MaskRule {
name: String,
pattern: Regex,
replacement: String,
priority: i32,
enabled: bool,
apply_fn: ApplyFn,
is_literal: bool,
}
impl std::fmt::Debug for MaskRule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MaskRule")
.field("name", &self.name)
.field("pattern", &self.pattern.as_str())
.field("replacement", &self.replacement)
.field("priority", &self.priority)
.field("enabled", &self.enabled)
.field("apply_fn", &"<fn>")
.field("is_literal", &self.is_literal)
.finish()
}
}
impl DataMasker {
pub fn new() -> Self {
let mut rules = vec![
MaskRule::new_email_rule(),
MaskRule::new_phone_rule(),
MaskRule::new_id_card_rule(),
MaskRule::new_bank_card_rule(),
MaskRule::new_api_key_rule(),
MaskRule::new_aws_key_rule(),
MaskRule::new_jwt_rule(),
MaskRule::new_generic_secret_rule(),
MaskRule::new_international_phone_rule(),
MaskRule::new_credit_card_rule(),
MaskRule::new_ipv4_rule(),
MaskRule::new_ipv6_rule(),
MaskRule::new_mac_address_rule(),
MaskRule::new_passport_rule(),
MaskRule::new_ssn_rule(),
MaskRule::new_db_connection_rule(),
MaskRule::new_github_token_rule(),
MaskRule::new_slack_token_rule(),
MaskRule::new_stripe_key_rule(),
MaskRule::new_google_api_key_rule(),
MaskRule::new_private_key_rule(),
];
rules.sort_by_key(|r| r.priority());
Self {
rules,
#[cfg(feature = "fast-masking")]
ac_masker: None,
}
}
pub fn is_sensitive_field(field_name: &str) -> bool {
SENSITIVE_FIELD_PATTERNS
.iter()
.any(|pattern| pattern.is_match(field_name))
}
pub fn mask(&self, text: &str) -> String {
#[cfg(feature = "fast-masking")]
let mut result = {
if let Some(ref ac) = self.ac_masker {
ac.mask_fast(text)
} else {
text.to_string()
}
};
#[cfg(not(feature = "fast-masking"))]
let mut result = text.to_string();
for rule in &self.rules {
if rule.is_enabled() {
result = rule.apply(&result);
}
}
result
}
pub fn mask_value(&self, value: &mut Value) {
match value {
Value::String(s) => {
*s = self.mask(s);
}
Value::Array(arr) => {
for item in arr {
self.mask_value(item);
}
}
Value::Object(map) => {
for (k, v) in map.iter_mut() {
if Self::is_sensitive_field(k) {
*v = Value::String("***MASKED***".to_string());
} else {
self.mask_value(v);
}
}
}
_ => {}
}
}
pub fn mask_hashmap(&self, map: &mut HashMap<String, Value>) {
for (k, v) in map.iter_mut() {
if Self::is_sensitive_field(k) {
*v = Value::String("***MASKED***".to_string());
} else {
self.mask_value(v);
}
}
}
pub fn into_rules(self) -> Vec<MaskRule> {
self.rules
}
pub fn builder() -> DataMaskerBuilder {
DataMaskerBuilder::new()
}
}
pub struct DataMaskerBuilder {
extra_rules: Vec<MaskRule>,
disabled_builtins: Vec<String>,
use_builtins: bool,
custom_registry: Option<super::masking_registry::MaskRuleRegistry>,
}
impl DataMaskerBuilder {
fn new() -> Self {
Self {
extra_rules: Vec::new(),
disabled_builtins: Vec::new(),
use_builtins: true,
custom_registry: None,
}
}
pub fn add_rule(mut self, rule: MaskRule) -> Self {
self.extra_rules.push(rule);
self
}
pub fn with_registry(mut self, registry: super::masking_registry::MaskRuleRegistry) -> Self {
self.custom_registry = Some(registry);
self.use_builtins = false;
self
}
pub fn disable_builtin(mut self, name: &str) -> Self {
self.disabled_builtins.push(name.to_string());
self
}
pub fn build(self) -> DataMasker {
let mut rules = if let Some(registry) = self.custom_registry {
registry.active_rules().into_iter().cloned().collect()
} else if self.use_builtins {
DataMasker::new().into_rules()
} else {
Vec::new()
};
for name in &self.disabled_builtins {
rules.retain(|r| r.name() != name.as_str());
}
rules.extend(self.extra_rules);
rules.sort_by_key(|r| r.priority());
#[cfg(feature = "fast-masking")]
{
let (literal_rules, regex_rules): (Vec<_>, Vec<_>) = rules
.into_iter()
.partition(|r| r.is_literal() && r.is_enabled());
let ac_masker = if !literal_rules.is_empty() {
let patterns: Vec<String> = literal_rules
.iter()
.map(|r| r.pattern.as_str().to_string())
.collect();
let replacements: Vec<String> = literal_rules
.iter()
.map(|r| r.replacement.clone())
.collect();
super::masking_ac::AcMasker::new(patterns, replacements)
} else {
None
};
DataMasker {
rules: regex_rules,
ac_masker,
}
}
#[cfg(not(feature = "fast-masking"))]
DataMasker { rules }
}
}
use std::sync::LazyLock;
static EMAIL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+").expect("Invalid email regex"));
static PHONE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\b1[3-9]\d{9}\b").expect("Invalid phone regex"));
static ID_CARD_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\b(\d{6})(\d{8})(\d{3}[\dX])\b").expect("Invalid ID card regex"));
static BANK_CARD_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(\d{4})(\d{5,11})(\d{4})").expect("Invalid bank card regex"));
static API_KEY_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(api[_-]?key[^\s:=]*\s*[=:]\s*[a-zA-Z0-9_-]{20,})")
.expect("Invalid API key regex")
});
static AWS_KEY_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}").expect("Invalid AWS key regex")
});
static JWT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*")
.expect("Invalid JWT regex")
});
static GENERIC_SECRET_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)([^\s:=]*(?:token|secret|key|password|passwd|pwd|credential)s?[^\s:=]*\s*[=:]\s*)([a-zA-Z0-9_\-\+]{16,})")
.expect("Invalid generic secret regex")
});
static INTERNATIONAL_PHONE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\+(\d{1,3})[\s.-]?(\(?\d{1,4}\)?[\s.-]?\d{2,4}[\s.-]?)(\d{2,4})")
.expect("Invalid international phone regex")
});
static CREDIT_CARD_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12}|35[0-9]{14})\b")
.expect("Invalid credit card regex")
});
static IPV4_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b")
.expect("Invalid IPv4 regex")
});
static IPV6_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b").expect("Invalid IPv6 regex")
});
static MAC_ADDRESS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b").expect("Invalid MAC address regex")
});
static PASSPORT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\b[EeGg][A-Za-z0-9]{8}\b").expect("Invalid passport regex"));
static SSN_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\b\d{3}-\d{2}-\d{4}\b").expect("Invalid SSN regex"));
static DB_CONNECTION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)((?:postgres|mysql|mongodb|redis|amqp)://[^:\s]+:)([^@]+)(@\S+)")
.expect("Invalid DB connection regex")
});
static GITHUB_TOKEN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\b(?:ghp|github_pat)_[A-Za-z0-9_]{36,}\b").expect("Invalid GitHub token regex")
});
static SLACK_TOKEN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"xox[bpas]-[0-9]{10,13}-[0-9a-zA-Z-]+").expect("Invalid Slack token regex")
});
static STRIPE_KEY_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?:sk|pk)_(?:live|test)_[0-9a-zA-Z]{24,}").expect("Invalid Stripe key regex")
});
static GOOGLE_API_KEY_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"AIza[0-9A-Za-z_-]{35}").expect("Invalid Google API key regex"));
static PRIVATE_KEY_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----")
.expect("Invalid private key regex")
});
impl MaskRule {
fn new_email_rule() -> Self {
Self::build_from_regex("email", EMAIL_REGEX.clone(), "**@**.***", 100, None)
}
fn new_phone_rule() -> Self {
Self::build_from_regex("phone", PHONE_REGEX.clone(), "***-****-****", 100, None)
}
fn new_id_card_rule() -> Self {
Self::build_from_regex(
"id_card",
ID_CARD_REGEX.clone(),
"MASK_ID_CARD",
100,
Some(Arc::new(|regex: &Regex, text: &str, _replacement: &str| {
regex.replace(text, "******$3").to_string()
})),
)
}
fn new_bank_card_rule() -> Self {
Self::build_from_regex(
"bank_card",
BANK_CARD_REGEX.clone(),
"MASK_BANK_CARD",
100,
Some(Arc::new(|regex: &Regex, text: &str, _replacement: &str| {
regex
.replace_all(text, |caps: ®ex::Captures| {
let matched = caps.get(0).unwrap().as_str();
if matched.len() >= 12 {
let last_four = &matched[matched.len() - 4..];
format!("****-****-****-{}", last_four)
} else {
matched.to_string()
}
})
.to_string()
})),
)
}
fn new_api_key_rule() -> Self {
Self::build_from_regex(
"api_key",
API_KEY_REGEX.clone(),
"${1}***REDACTED***",
100,
None,
)
}
fn new_aws_key_rule() -> Self {
Self::build_from_regex(
"aws_key",
AWS_KEY_REGEX.clone(),
"***REDACTED***",
100,
None,
)
}
fn new_jwt_rule() -> Self {
Self::build_from_regex("jwt", JWT_REGEX.clone(), "***REDACTED_JWT***", 100, None)
}
fn new_generic_secret_rule() -> Self {
Self::build_from_regex(
"generic_secret",
GENERIC_SECRET_REGEX.clone(),
"${1}***REDACTED***",
100,
None,
)
}
fn build_from_regex(
name: &str,
regex: Regex,
replacement: &str,
priority: i32,
apply_fn: Option<ApplyFn>,
) -> Self {
MaskRule {
name: name.to_string(),
pattern: regex,
replacement: replacement.to_string(),
priority,
enabled: true,
apply_fn: apply_fn.unwrap_or_else(|| {
Arc::new(|regex: &Regex, text: &str, replacement: &str| {
regex.replace(text, replacement).to_string()
})
}),
is_literal: false,
}
}
fn new_international_phone_rule() -> Self {
Self::build_from_regex(
"international_phone",
INTERNATIONAL_PHONE_REGEX.clone(),
"+${1}-***-***-${3}",
10,
None,
)
}
fn new_credit_card_rule() -> Self {
Self::build_from_regex(
"credit_card",
CREDIT_CARD_REGEX.clone(),
"***REDACTED_CC***",
15,
Some(Arc::new(|regex: &Regex, text: &str, _replacement: &str| {
regex
.replace_all(text, |caps: ®ex::Captures| {
let number = caps.get(0).unwrap().as_str();
let digits: Vec<u32> =
number.chars().filter_map(|c| c.to_digit(10)).collect();
let mut sum = 0u32;
let mut alternate = false;
for &d in digits.iter().rev() {
if alternate {
let doubled = d * 2;
sum += if doubled > 9 { doubled - 9 } else { doubled };
} else {
sum += d;
}
alternate = !alternate;
}
if !sum.is_multiple_of(10) {
return number.to_string();
}
let last4 = &number[number.len() - 4..];
if number.starts_with('3') {
format!("****-******-{}", last4)
} else {
format!("****-****-****-{}", last4)
}
})
.to_string()
})),
)
}
fn new_ipv4_rule() -> Self {
Self::build_from_regex(
"ipv4",
IPV4_REGEX.clone(),
"***.***.***.XXX",
20,
Some(Arc::new(|regex: &Regex, text: &str, _replacement: &str| {
regex
.replace_all(text, |caps: ®ex::Captures| {
let ip = caps.get(0).unwrap().as_str();
if let Some(pos) = ip.rfind('.') {
format!("***.***.***.{}", &ip[pos + 1..])
} else {
"***.***.***.***".to_string()
}
})
.to_string()
})),
)
}
fn new_ipv6_rule() -> Self {
Self::build_from_regex(
"ipv6",
IPV6_REGEX.clone(),
"****:****:****:XXXX",
21,
Some(Arc::new(|regex: &Regex, text: &str, _replacement: &str| {
regex
.replace_all(text, |caps: ®ex::Captures| {
let ip = caps.get(0).unwrap().as_str();
if let Some(pos) = ip.rfind(':') {
let last_group = &ip[pos + 1..];
let prefix_count = ip.matches(':').count();
let mut result = "****".to_string();
for _ in 1..prefix_count {
result.push_str(":****");
}
result.push(':');
result.push_str(last_group);
result
} else {
ip.to_string()
}
})
.to_string()
})),
)
}
fn new_mac_address_rule() -> Self {
Self::build_from_regex(
"mac_address",
MAC_ADDRESS_REGEX.clone(),
"XX:**:**:**:**:XX",
19,
Some(Arc::new(|regex: &Regex, text: &str, _replacement: &str| {
regex
.replace_all(text, |caps: ®ex::Captures| {
let mac = caps.get(0).unwrap().as_str();
let sep = if mac.contains(':') { ':' } else { '-' };
let parts: Vec<&str> = mac.split(sep).collect();
if parts.len() == 6 {
format!(
"{}{}{}{}{}{}{}{}{}{}{}",
parts[0], sep, "**", sep, "**", sep, "**", sep, "**", sep, parts[5]
)
} else {
mac.to_string()
}
})
.to_string()
})),
)
}
fn new_passport_rule() -> Self {
Self::build_from_regex(
"passport",
PASSPORT_REGEX.clone(),
"******XX",
30,
Some(Arc::new(|regex: &Regex, text: &str, _replacement: &str| {
regex
.replace_all(text, |caps: ®ex::Captures| {
let passport = caps.get(0).unwrap().as_str();
let first = &passport[..1];
let last2 = &passport[passport.len() - 2..];
format!("{}******{}", first, last2)
})
.to_string()
})),
)
}
fn new_ssn_rule() -> Self {
Self::build_from_regex(
"ssn",
SSN_REGEX.clone(),
"***-**-XXXX",
35,
Some(Arc::new(|regex: &Regex, text: &str, _replacement: &str| {
regex
.replace_all(text, |caps: ®ex::Captures| {
let ssn = caps.get(0).unwrap().as_str();
let last4 = &ssn[ssn.len() - 4..];
format!("***-**-{}", last4)
})
.to_string()
})),
)
}
fn new_db_connection_rule() -> Self {
Self::build_from_regex(
"db_connection",
DB_CONNECTION_REGEX.clone(),
"${1}***${3}",
40,
None,
)
}
fn new_github_token_rule() -> Self {
Self::build_from_regex(
"github_token",
GITHUB_TOKEN_REGEX.clone(),
"***REDACTED_GITHUB***",
50,
None,
)
}
fn new_slack_token_rule() -> Self {
Self::build_from_regex(
"slack_token",
SLACK_TOKEN_REGEX.clone(),
"***REDACTED_SLACK***",
51,
None,
)
}
fn new_stripe_key_rule() -> Self {
Self::build_from_regex(
"stripe_key",
STRIPE_KEY_REGEX.clone(),
"***REDACTED_STRIPE***",
52,
None,
)
}
fn new_google_api_key_rule() -> Self {
Self::build_from_regex(
"google_api_key",
GOOGLE_API_KEY_REGEX.clone(),
"***REDACTED_GOOGLE***",
53,
None,
)
}
fn new_private_key_rule() -> Self {
Self::build_from_regex(
"private_key",
PRIVATE_KEY_REGEX.clone(),
"***REDACTED_PRIVATE_KEY***",
54,
None,
)
}
fn apply(&self, text: &str) -> String {
(self.apply_fn)(&self.pattern, text, &self.replacement)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn priority(&self) -> i32 {
self.priority
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn is_literal(&self) -> bool {
self.is_literal
}
pub fn builder(name: &str) -> MaskRuleBuilder {
MaskRuleBuilder::new(name)
}
}
pub struct MaskRuleBuilder {
name: String,
pattern: Option<String>,
replacement: String,
priority: i32,
enabled: bool,
apply_fn: Option<ApplyFn>,
is_literal: bool,
}
impl MaskRuleBuilder {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
pattern: None,
replacement: String::new(),
priority: 100,
enabled: true,
apply_fn: None,
is_literal: false,
}
}
pub fn pattern(mut self, regex: &str) -> Self {
self.pattern = Some(regex.to_string());
self
}
pub fn replacement(mut self, replacement: &str) -> Self {
self.replacement = replacement.to_string();
self
}
pub fn priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn apply_fn(mut self, f: ApplyFn) -> Self {
self.apply_fn = Some(f);
self
}
pub fn literal(mut self, is_literal: bool) -> Self {
self.is_literal = is_literal;
self
}
pub fn build(self) -> Result<MaskRule, InklogError> {
let pattern_str = self.pattern.ok_or_else(|| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("name", &self.name);
InklogError::ConfigError(crate::i18n::tr_args(
"config-mask_rule_requires_pattern",
args,
))
})?;
let regex = Regex::new(&pattern_str).map_err(|e| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("name", &self.name);
args.set("err", e.to_string());
InklogError::ConfigError(crate::i18n::tr_args("config-invalid_regex_in_rule", args))
})?;
Ok(MaskRule {
name: self.name,
pattern: regex,
replacement: self.replacement,
priority: self.priority,
enabled: self.enabled,
apply_fn: self.apply_fn.unwrap_or_else(|| {
Arc::new(|regex: &Regex, text: &str, replacement: &str| {
regex.replace(text, replacement).to_string()
})
}),
is_literal: self.is_literal,
})
}
}
#[cfg(test)]
mod builder_tests {
use super::*;
#[test]
fn test_builder_defaults() {
let rule = MaskRule::builder("test")
.pattern(r"\d+")
.replacement("***")
.build()
.unwrap();
assert_eq!(rule.name(), "test");
assert_eq!(rule.priority(), 100);
assert!(rule.is_enabled());
assert_eq!(rule.apply("abc123def"), "abc***def");
}
#[test]
fn test_builder_custom_values() {
let rule = MaskRule::builder("custom")
.pattern(r"\d+")
.replacement("###")
.priority(50)
.enabled(false)
.build()
.unwrap();
assert_eq!(rule.priority(), 50);
assert!(!rule.is_enabled());
}
#[test]
fn test_builder_custom_apply_fn() {
let rule = MaskRule::builder("reverse")
.pattern(r"\w+")
.replacement("")
.apply_fn(Arc::new(|_re: &Regex, text: &str, _rep: &str| {
text.chars().rev().collect()
}))
.build()
.unwrap();
assert_eq!(rule.apply("hello"), "olleh");
}
#[test]
fn test_builder_missing_pattern() {
let result = MaskRule::builder("no_pattern").replacement("***").build();
assert!(result.is_err());
}
#[test]
fn test_builder_invalid_regex() {
let result = MaskRule::builder("bad_regex").pattern(r"[invalid").build();
assert!(result.is_err());
}
}
pub fn mask_email(email: &str) -> String {
EMAIL_REGEX.replace(email, "**@**.***").to_string()
}
pub fn mask_phone(phone: &str) -> String {
PHONE_REGEX.replace(phone, "***-****-****").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mask_email() {
let test_cases = vec![
("test@example.com", "**@**.***"),
("user.name@company.co.uk", "**@**.***"),
("admin@localhost", "**@**.***"),
];
for (input, expected) in test_cases {
let result = mask_email(input);
assert_eq!(result, expected, "Failed for: {}", input);
}
}
#[test]
fn test_mask_phone() {
let test_cases = vec![
("13812345678", "***-****-****"),
("15987654321", "***-****-****"),
("Contact: 18655556666 now", "Contact: ***-****-**** now"),
];
for (input, expected) in test_cases {
let result = mask_phone(input);
assert_eq!(result, expected, "Failed for: {}", input);
}
}
#[test]
fn test_data_masker() {
let masker = DataMasker::new();
let test_email = "user@example.com";
assert_eq!(masker.mask(test_email), "**@**.***");
let test_phone = "13912345678";
assert_eq!(masker.mask(test_phone), "***-****-****");
let mixed = "Contact user at test@example.com, phone: 13812345678";
let result = masker.mask(mixed);
assert!(!result.contains("test@example.com"));
assert!(!result.contains("13812345678"));
}
#[test]
fn test_mask_value() {
let masker = DataMasker::new();
let mut value = serde_json::json!({
"email": "user@example.com",
"phone": "13712345678",
"name": "John"
});
masker.mask_value(&mut value);
assert_eq!(value["email"], "**@**.***");
assert_eq!(value["phone"], "***-****-****");
assert_eq!(value["name"], "John");
}
#[test]
fn test_mask_nested_value() {
let masker = DataMasker::new();
let mut value = serde_json::json!({
"user": {
"email": "admin@company.org",
"contacts": ["test@email.com", "13811112222"]
}
});
masker.mask_value(&mut value);
let user = &value["user"];
assert_eq!(user["email"], "**@**.***");
let contacts = user["contacts"]
.as_array()
.expect("contacts should be an array");
assert_eq!(contacts[0], "**@**.***");
assert_eq!(contacts[1], "***-****-****");
}
#[test]
fn test_is_sensitive_field_password() {
assert!(DataMasker::is_sensitive_field("password"));
assert!(DataMasker::is_sensitive_field("PASSWORD"));
assert!(DataMasker::is_sensitive_field("Password"));
}
#[test]
fn test_is_sensitive_field_api_key() {
assert!(DataMasker::is_sensitive_field("api_key"));
assert!(DataMasker::is_sensitive_field("apiKey"));
assert!(DataMasker::is_sensitive_field("API_KEY"));
assert!(DataMasker::is_sensitive_field("api-secret"));
}
#[test]
fn test_is_sensitive_field_jwt() {
assert!(DataMasker::is_sensitive_field("jwt"));
assert!(DataMasker::is_sensitive_field("jwt_token"));
assert!(DataMasker::is_sensitive_field("bearer_token"));
}
#[test]
fn test_is_sensitive_field_aws() {
assert!(DataMasker::is_sensitive_field("aws_secret"));
assert!(DataMasker::is_sensitive_field("aws_key"));
assert!(DataMasker::is_sensitive_field("aws_credentials"));
}
#[test]
fn test_is_sensitive_field_credit_card() {
assert!(DataMasker::is_sensitive_field("credit_card"));
assert!(DataMasker::is_sensitive_field("card_number"));
assert!(DataMasker::is_sensitive_field("cvv"));
}
#[test]
fn test_is_not_sensitive_field() {
assert!(!DataMasker::is_sensitive_field("username"));
assert!(!DataMasker::is_sensitive_field("message"));
assert!(!DataMasker::is_sensitive_field("content"));
assert!(!DataMasker::is_sensitive_field("title"));
}
#[test]
fn test_mask_email_variations() {
let test_cases = vec![
("test@example.com", "**@**.***"),
("user.name@company.co.uk", "**@**.***"),
("admin@localhost", "**@**.***"),
("user+tag@example.org", "**@**.***"),
("user_name@test.io", "**@**.***"),
];
for (input, expected) in test_cases {
let result = mask_email(input);
assert_eq!(result, expected, "Failed for: {}", input);
}
}
#[test]
fn test_mask_phone_variations() {
let test_cases = vec![
("13812345678", "***-****-****"),
("15987654321", "***-****-****"),
("Contact: 18655556666 now", "Contact: ***-****-**** now"),
];
for (input, expected) in test_cases {
let result = mask_phone(input);
assert_eq!(result, expected, "Failed for: {}", input);
}
}
#[test]
fn test_mask_jwt_token() {
let masker = DataMasker::new();
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
let result = masker.mask(jwt);
assert!(result.contains("***REDACTED_JWT***"));
assert!(!result.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"));
}
#[test]
fn test_mask_aws_key() {
let masker = DataMasker::new();
let aws_key = "AKIAIOSFODNN7EXAMPLE";
let result = masker.mask(aws_key);
assert!(result.contains("***REDACTED***"));
}
#[test]
fn test_mask_api_key_value() {
let masker = DataMasker::new();
let message = "api_key=sk-1234567890abcdefghijABCDEFGH";
let result = masker.mask(message);
assert!(result.contains("***REDACTED***"));
assert!(!result.contains("sk-1234567890abcdefghijABCDEFGH"));
}
#[test]
fn test_mask_password_value() {
let masker = DataMasker::new();
let message = "mypassword=abcdefghijklmnopqrst";
let result = masker.mask(message);
assert!(result.contains("REDACTED") || !result.contains("abcdefghijklmnopqrst"));
}
#[test]
fn test_mask_database_url() {
let masker = DataMasker::new();
let message = "db_url=postgres://user:password123@localhost:5432/mydb";
let result = masker.mask(message);
assert!(result.contains("REDACTED") || !result.contains("password123"));
}
#[test]
fn test_mask_oauth_token() {
let masker = DataMasker::new();
let message = "oauth_token=ya29_token_value_here";
let result = masker.mask(message);
assert!(result.contains("REDACTED") || !result.contains("token_value"));
}
#[test]
fn test_mask_empty_string() {
let masker = DataMasker::new();
let result = masker.mask("");
assert_eq!(result, "");
}
#[test]
fn test_mask_no_sensitive_data() {
let masker = DataMasker::new();
let message = "This is a normal log message without any sensitive data";
let result = masker.mask(message);
assert_eq!(result, message);
}
#[test]
fn test_mask_multiple_sensitive_items() {
let masker = DataMasker::new();
let message = "Email: test@example.com, Phone: 13812345678";
let result = masker.mask(message);
assert!(!result.contains("test@example.com"));
}
#[test]
fn test_mask_hashmap() {
let masker = DataMasker::new();
let mut map: HashMap<String, Value> = HashMap::new();
map.insert(
"email".to_string(),
Value::String("user@example.com".to_string()),
);
map.insert(
"password".to_string(),
Value::String("secret123".to_string()),
);
map.insert("name".to_string(), Value::String("John".to_string()));
masker.mask_hashmap(&mut map);
assert_eq!(map["email"], "**@**.***");
assert_eq!(map["name"], "John");
}
#[test]
fn test_mask_array_of_objects() {
let masker = DataMasker::new();
let mut value = serde_json::json!([
{"email": "a@b.com", "name": "A"},
{"email": "c@d.com", "name": "B"}
]);
masker.mask_value(&mut value);
let arr = value.as_array().unwrap();
assert_eq!(arr[0]["email"], "**@**.***");
assert_eq!(arr[1]["email"], "**@**.***");
}
#[test]
fn test_api_key_rule_does_not_panic() {
let masker = DataMasker::new();
let input = "api_key=abcdefghijklmnopqrstuvwxyz1234";
let result = masker.mask(input);
assert!(result.contains("***REDACTED***"));
}
#[test]
fn test_generic_secret_rule_does_not_panic() {
let masker = DataMasker::new();
let input = "my_token=abcdefghijklmnop1234";
let result = masker.mask(input);
assert!(result.contains("***REDACTED***"));
}
#[test]
fn test_mask_skips_disabled_rules() {
let masker = DataMasker::builder().disable_builtin("email").build();
let input = "user@example.com";
let result = masker.mask(input);
assert_eq!(result, "user@example.com");
}
#[test]
fn test_mask_value_masks_sensitive_keys() {
let masker = DataMasker::new();
let mut value = serde_json::json!({
"password": "secret123",
"name": "Alice"
});
masker.mask_value(&mut value);
assert_eq!(value["password"], "***MASKED***");
assert_eq!(value["name"], "Alice");
}
#[test]
fn test_mask_hashmap_masks_sensitive_keys() {
let masker = DataMasker::new();
let mut map = HashMap::new();
map.insert(
"api_key".to_string(),
Value::String("supersecret".to_string()),
);
map.insert("user".to_string(), Value::String("bob".to_string()));
masker.mask_hashmap(&mut map);
assert_eq!(map["api_key"], Value::String("***MASKED***".to_string()));
assert_eq!(map["user"], Value::String("bob".to_string()));
}
#[test]
fn test_mask_rule_debug() {
let rule = MaskRule::builder("test_debug")
.pattern(r"\d+")
.replacement("***")
.build()
.unwrap();
let debug_str = format!("{:?}", rule);
assert!(debug_str.contains("MaskRule"));
assert!(debug_str.contains("test_debug"));
assert!(debug_str.contains("<fn>"));
}
#[test]
fn test_builder_add_rule() {
let custom_rule = MaskRule::builder("custom_upper")
.pattern(r"[a-z]+")
.replacement("REPLACED")
.priority(1)
.build()
.unwrap();
let masker = DataMasker::builder().add_rule(custom_rule).build();
let result = masker.mask("hello");
assert!(result.contains("REPLACED"));
}
#[test]
fn test_builder_with_registry() {
let mut registry =
crate::support::processing::masking_registry::MaskRuleRegistry::with_builtins();
registry.set_enabled("email", false);
let masker = DataMasker::builder().with_registry(registry).build();
let result = masker.mask("user@example.com");
assert_eq!(result, "user@example.com");
}
#[test]
fn test_mask_credit_card_visa() {
let masker = DataMasker::new();
let result = masker.mask("Card: 4111111111111111");
assert!(!result.contains("4111111111111111"));
assert!(result.contains("****-****-****-1111"));
}
#[test]
fn test_mask_credit_card_amex() {
let masker = DataMasker::new();
let result = masker.mask("Card: 378282246310005");
assert!(!result.contains("378282246310005"));
assert!(result.contains("****-******-0005"));
}
#[test]
fn test_mask_ipv4() {
let masker = DataMasker::new();
let result = masker.mask("Server IP: 192.168.1.100");
assert!(!result.contains("192.168.1.100"));
assert!(result.contains("***.***.***.100"));
}
#[test]
fn test_mask_ipv6() {
let masker = DataMasker::new();
let result = masker.mask("IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334");
assert!(!result.contains("2001:0db8:85a3:0000:0000:8a2e:0370:7334"));
assert!(result.contains("7334"));
}
#[test]
fn test_mask_mac_address_colon() {
let masker = DataMasker::new();
let result = masker.mask("MAC: AA:BB:CC:DD:EE:FF");
assert!(!result.contains("AA:BB:CC:DD:EE:FF"));
assert!(result.contains("AA:**:**:**:**:FF"));
}
#[test]
fn test_mask_mac_address_dash() {
let masker = DataMasker::new();
let result = masker.mask("MAC: 00-1A-2B-3C-4D-5E");
assert!(!result.contains("00-1A-2B-3C-4D-5E"));
assert!(result.contains("00-**-**-**-**-5E"));
}
#[test]
fn test_mask_passport() {
let masker = DataMasker::new();
let result = masker.mask("Passport: E12345678");
assert!(!result.contains("E12345678"));
assert!(result.contains("E******78"));
}
#[test]
fn test_mask_ssn() {
let masker = DataMasker::new();
let result = masker.mask("SSN: 123-45-6789");
assert!(!result.contains("123-45-6789"));
assert!(result.contains("***-**-6789"));
}
#[test]
fn test_mask_credit_card_luhn_failure_still_masked_by_bank_card() {
let masker = DataMasker::new();
let result = masker.mask("Card: 4111111111111112");
assert!(!result.contains("4111111111111112"));
}
#[test]
fn test_mask_builder_no_builtins_no_custom() {
let registry = crate::support::processing::masking_registry::MaskRuleRegistry::default();
let masker = DataMasker::builder().with_registry(registry).build();
let result = masker.mask("user@example.com 4111111111111111");
assert_eq!(result, "user@example.com 4111111111111111");
}
}