use std::borrow::Cow;
use std::collections::HashSet;
use regex::Regex;
use super::config::MaskingConfig;
use super::masker::Masker;
use crate::error::{self, LoggingResult};
const DEFAULT_FIELD_NAMES: &[&str] = &[
"password",
"secret",
"token",
"api_key",
"apikey",
"api-key",
"authorization",
"auth_token",
"access_token",
"refresh_token",
"private_key",
"ssn",
"credit_card",
"card_number",
"cvv",
"pin",
];
struct MaskPattern {
regex: Regex,
kind: PatternKind,
}
enum PatternKind {
Replace(&'static str),
CreditCard,
}
const DEFAULT_VALUE_PATTERNS: &[(&str, u8)] = &[
(r"(?i)Bearer\s+[a-zA-Z0-9._~+/=-]+", 0),
(
r"eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]+",
1,
),
(r"AKIA[0-9A-Z]{16}", 2),
(r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", 3),
(r"\b\d{3}-?\d{2}-?\d{4}\b", 4),
(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", 5),
(r"\b[0-9a-fA-F]{32,}\b", 6),
];
fn tag_to_kind(tag: u8) -> PatternKind {
match tag {
0 => PatternKind::Replace("Bearer [REDACTED]"),
1 => PatternKind::Replace("[JWT_REDACTED]"),
2 => PatternKind::Replace("[AWS_KEY_REDACTED]"),
3 => PatternKind::CreditCard,
4 => PatternKind::Replace("***-**-****"),
5 => PatternKind::Replace("***@***.***"),
6 => PatternKind::Replace("[HEX_REDACTED]"),
_ => PatternKind::Replace("[REDACTED]"),
}
}
pub struct DefaultMasker {
enabled: bool,
field_names: HashSet<String>,
value_patterns: Vec<MaskPattern>,
extra_patterns: Vec<Regex>,
json_field_regex: Option<Regex>,
text_field_regex: Option<Regex>,
replacement: String,
}
impl DefaultMasker {
pub fn new(cfg: &MaskingConfig) -> LoggingResult<Self> {
let mut field_names: HashSet<String> = DEFAULT_FIELD_NAMES
.iter()
.map(|s| (*s).to_lowercase())
.collect();
for name in &cfg.field_names {
field_names.insert(name.to_lowercase());
}
let value_patterns = Self::default_value_patterns();
let mut extra_patterns = Vec::with_capacity(cfg.value_patterns.len());
for pattern in &cfg.value_patterns {
let regex =
Regex::new(pattern).map_err(|err| error::invalid_regex(pattern.clone(), err))?;
extra_patterns.push(regex);
}
let (json_field_regex, text_field_regex) = Self::build_field_regexes(&field_names);
Ok(Self {
enabled: cfg.enabled,
field_names,
value_patterns,
extra_patterns,
json_field_regex,
text_field_regex,
replacement: cfg.replacement.clone(),
})
}
fn default_value_patterns() -> Vec<MaskPattern> {
DEFAULT_VALUE_PATTERNS
.iter()
.filter_map(|&(src, tag)| {
Regex::new(src).ok().map(|regex| MaskPattern {
regex,
kind: tag_to_kind(tag),
})
})
.collect()
}
fn build_field_regexes(field_names: &HashSet<String>) -> (Option<Regex>, Option<Regex>) {
if field_names.is_empty() {
return (None, None);
}
let escaped: Vec<String> = field_names.iter().map(|s| regex::escape(s)).collect();
let alt = escaped.join("|");
let json_src = format!("(?i)\"({})\"\\s*:\\s*\"([^\"]*)\"", alt);
let text_src = format!("(?i)({})=([^\\s,}}\"]+)", alt);
(Regex::new(&json_src).ok(), Regex::new(&text_src).ok())
}
fn apply_value_pattern<'a>(&self, pattern: &MaskPattern, input: &'a str) -> Cow<'a, str> {
match pattern.kind {
PatternKind::Replace(replacement) => pattern.regex.replace_all(input, replacement),
PatternKind::CreditCard => {
pattern.regex.replace_all(input, |caps: ®ex::Captures| {
let matched = &caps[0];
let digits: String = matched.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() >= 4 {
let last4 = &digits[digits.len() - 4..];
format!("****-****-****-{}", last4)
} else {
"[CARD_REDACTED]".to_string()
}
})
}
}
}
fn apply_all_value_patterns<'v>(&self, value: &'v str) -> Cow<'v, str> {
let mut result = Cow::Borrowed(value);
for pattern in &self.value_patterns {
if pattern.regex.is_match(&result) {
result = Cow::Owned(self.apply_value_pattern(pattern, &result).into_owned());
}
}
for extra in &self.extra_patterns {
if extra.is_match(&result) {
result = Cow::Owned(
extra
.replace_all(&result, self.replacement.as_str())
.into_owned(),
);
}
}
result
}
}
impl Default for DefaultMasker {
fn default() -> Self {
let cfg = MaskingConfig::default();
let mut field_names: HashSet<String> = DEFAULT_FIELD_NAMES
.iter()
.map(|s| (*s).to_lowercase())
.collect();
for name in &cfg.field_names {
field_names.insert(name.to_lowercase());
}
let (json_field_regex, text_field_regex) = Self::build_field_regexes(&field_names);
Self {
enabled: cfg.enabled,
field_names,
value_patterns: Self::default_value_patterns(),
extra_patterns: Vec::new(),
json_field_regex,
text_field_regex,
replacement: cfg.replacement,
}
}
}
impl Masker for DefaultMasker {
fn mask_value<'v>(&self, key: &str, value: &'v str) -> Cow<'v, str> {
if !self.enabled {
return Cow::Borrowed(value);
}
if !key.is_empty() && self.field_names.contains(&key.to_lowercase()) {
return Cow::Owned(self.replacement.clone());
}
self.apply_all_value_patterns(value)
}
fn mask_output<'v>(&self, line: &'v str) -> Cow<'v, str> {
if !self.enabled {
return Cow::Borrowed(line);
}
let mut result = Cow::Borrowed(line);
for pattern in &self.value_patterns {
if pattern.regex.is_match(&result) {
result = Cow::Owned(self.apply_value_pattern(pattern, &result).into_owned());
}
}
for extra in &self.extra_patterns {
if extra.is_match(&result) {
result = Cow::Owned(
extra
.replace_all(&result, self.replacement.as_str())
.into_owned(),
);
}
}
if let Some(ref re) = self.json_field_regex
&& re.is_match(&result)
{
let replacement = &self.replacement;
let masked = re.replace_all(&result, |caps: ®ex::Captures| {
format!("\"{}\":\"{}\"", &caps[1], replacement)
});
result = Cow::Owned(masked.into_owned());
}
if let Some(ref re) = self.text_field_regex
&& re.is_match(&result)
{
let replacement = &self.replacement;
let masked = re.replace_all(&result, |caps: ®ex::Captures| {
format!("{}={}", &caps[1], replacement)
});
result = Cow::Owned(masked.into_owned());
}
result
}
}
pub fn mask_value(key: &str, value: &str) -> String {
use std::sync::LazyLock;
static DEFAULT: LazyLock<DefaultMasker> = LazyLock::new(DefaultMasker::default);
DEFAULT.mask_value(key, value).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_default_value_patterns_compile() {
let patterns = DefaultMasker::default_value_patterns();
assert_eq!(
patterns.len(),
DEFAULT_VALUE_PATTERNS.len(),
"some default value patterns failed to compile"
);
}
#[test]
fn default_masker_masks_all_default_fields() {
let m = DefaultMasker::default();
for field in DEFAULT_FIELD_NAMES {
let result = m.mask_value(field, "test-value");
assert_eq!(
result.as_ref(),
"[REDACTED]",
"field '{}' not masked",
field
);
}
}
}