1use std::borrow::Cow;
4use std::collections::HashSet;
5
6use regex::Regex;
7
8use super::config::MaskingConfig;
9use super::masker::Masker;
10use crate::error::{self, LoggingResult};
11
12const DEFAULT_FIELD_NAMES: &[&str] = &[
14 "password",
15 "secret",
16 "token",
17 "api_key",
18 "apikey",
19 "api-key",
20 "authorization",
21 "auth_token",
22 "access_token",
23 "refresh_token",
24 "private_key",
25 "ssn",
26 "credit_card",
27 "card_number",
28 "cvv",
29 "pin",
30];
31
32struct MaskPattern {
34 regex: Regex,
35 kind: PatternKind,
36}
37
38enum PatternKind {
40 Replace(&'static str),
42 CreditCard,
44}
45
46const DEFAULT_VALUE_PATTERNS: &[(&str, u8)] = &[
51 (r"(?i)Bearer\s+[a-zA-Z0-9._~+/=-]+", 0),
53 (
55 r"eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]+",
56 1,
57 ),
58 (r"AKIA[0-9A-Z]{16}", 2),
60 (r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", 3),
62 (r"\b\d{3}-?\d{2}-?\d{4}\b", 4),
64 (r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", 5),
66 (r"\b[0-9a-fA-F]{32,}\b", 6),
68];
69
70fn tag_to_kind(tag: u8) -> PatternKind {
72 match tag {
73 0 => PatternKind::Replace("Bearer [REDACTED]"),
74 1 => PatternKind::Replace("[JWT_REDACTED]"),
75 2 => PatternKind::Replace("[AWS_KEY_REDACTED]"),
76 3 => PatternKind::CreditCard,
77 4 => PatternKind::Replace("***-**-****"),
78 5 => PatternKind::Replace("***@***.***"),
79 6 => PatternKind::Replace("[HEX_REDACTED]"),
80 _ => PatternKind::Replace("[REDACTED]"),
81 }
82}
83
84pub struct DefaultMasker {
107 enabled: bool,
109 field_names: HashSet<String>,
111 value_patterns: Vec<MaskPattern>,
113 extra_patterns: Vec<Regex>,
115 json_field_regex: Option<Regex>,
117 text_field_regex: Option<Regex>,
119 replacement: String,
121}
122
123impl DefaultMasker {
124 pub fn new(cfg: &MaskingConfig) -> LoggingResult<Self> {
132 let mut field_names: HashSet<String> = DEFAULT_FIELD_NAMES
133 .iter()
134 .map(|s| (*s).to_lowercase())
135 .collect();
136 for name in &cfg.field_names {
137 field_names.insert(name.to_lowercase());
138 }
139
140 let value_patterns = Self::default_value_patterns();
141
142 let mut extra_patterns = Vec::with_capacity(cfg.value_patterns.len());
143 for pattern in &cfg.value_patterns {
144 let regex =
145 Regex::new(pattern).map_err(|err| error::invalid_regex(pattern.clone(), err))?;
146 extra_patterns.push(regex);
147 }
148
149 let (json_field_regex, text_field_regex) = Self::build_field_regexes(&field_names);
150
151 Ok(Self {
152 enabled: cfg.enabled,
153 field_names,
154 value_patterns,
155 extra_patterns,
156 json_field_regex,
157 text_field_regex,
158 replacement: cfg.replacement.clone(),
159 })
160 }
161
162 fn default_value_patterns() -> Vec<MaskPattern> {
164 DEFAULT_VALUE_PATTERNS
165 .iter()
166 .filter_map(|&(src, tag)| {
167 Regex::new(src).ok().map(|regex| MaskPattern {
168 regex,
169 kind: tag_to_kind(tag),
170 })
171 })
172 .collect()
173 }
174
175 fn build_field_regexes(field_names: &HashSet<String>) -> (Option<Regex>, Option<Regex>) {
177 if field_names.is_empty() {
178 return (None, None);
179 }
180
181 let escaped: Vec<String> = field_names.iter().map(|s| regex::escape(s)).collect();
182 let alt = escaped.join("|");
183
184 let json_src = format!("(?i)\"({})\"\\s*:\\s*\"([^\"]*)\"", alt);
186 let text_src = format!("(?i)({})=([^\\s,}}\"]+)", alt);
188
189 (Regex::new(&json_src).ok(), Regex::new(&text_src).ok())
190 }
191
192 fn apply_value_pattern<'a>(&self, pattern: &MaskPattern, input: &'a str) -> Cow<'a, str> {
194 match pattern.kind {
195 PatternKind::Replace(replacement) => pattern.regex.replace_all(input, replacement),
196 PatternKind::CreditCard => {
197 pattern.regex.replace_all(input, |caps: ®ex::Captures| {
198 let matched = &caps[0];
199 let digits: String = matched.chars().filter(|c| c.is_ascii_digit()).collect();
200 if digits.len() >= 4 {
201 let last4 = &digits[digits.len() - 4..];
202 format!("****-****-****-{}", last4)
203 } else {
204 "[CARD_REDACTED]".to_string()
205 }
206 })
207 }
208 }
209 }
210
211 fn apply_all_value_patterns<'v>(&self, value: &'v str) -> Cow<'v, str> {
213 let mut result = Cow::Borrowed(value);
214
215 for pattern in &self.value_patterns {
216 if pattern.regex.is_match(&result) {
217 result = Cow::Owned(self.apply_value_pattern(pattern, &result).into_owned());
218 }
219 }
220
221 for extra in &self.extra_patterns {
222 if extra.is_match(&result) {
223 result = Cow::Owned(
224 extra
225 .replace_all(&result, self.replacement.as_str())
226 .into_owned(),
227 );
228 }
229 }
230
231 result
232 }
233}
234
235impl Default for DefaultMasker {
236 fn default() -> Self {
237 let cfg = MaskingConfig::default();
238 let mut field_names: HashSet<String> = DEFAULT_FIELD_NAMES
239 .iter()
240 .map(|s| (*s).to_lowercase())
241 .collect();
242 for name in &cfg.field_names {
243 field_names.insert(name.to_lowercase());
244 }
245 let (json_field_regex, text_field_regex) = Self::build_field_regexes(&field_names);
246 Self {
247 enabled: cfg.enabled,
248 field_names,
249 value_patterns: Self::default_value_patterns(),
250 extra_patterns: Vec::new(),
251 json_field_regex,
252 text_field_regex,
253 replacement: cfg.replacement,
254 }
255 }
256}
257
258impl Masker for DefaultMasker {
259 fn mask_value<'v>(&self, key: &str, value: &'v str) -> Cow<'v, str> {
260 if !self.enabled {
261 return Cow::Borrowed(value);
262 }
263
264 if !key.is_empty() && self.field_names.contains(&key.to_lowercase()) {
266 return Cow::Owned(self.replacement.clone());
267 }
268
269 self.apply_all_value_patterns(value)
271 }
272
273 fn mask_output<'v>(&self, line: &'v str) -> Cow<'v, str> {
274 if !self.enabled {
275 return Cow::Borrowed(line);
276 }
277
278 let mut result = Cow::Borrowed(line);
279
280 for pattern in &self.value_patterns {
282 if pattern.regex.is_match(&result) {
283 result = Cow::Owned(self.apply_value_pattern(pattern, &result).into_owned());
284 }
285 }
286
287 for extra in &self.extra_patterns {
288 if extra.is_match(&result) {
289 result = Cow::Owned(
290 extra
291 .replace_all(&result, self.replacement.as_str())
292 .into_owned(),
293 );
294 }
295 }
296
297 if let Some(ref re) = self.json_field_regex
299 && re.is_match(&result)
300 {
301 let replacement = &self.replacement;
302 let masked = re.replace_all(&result, |caps: ®ex::Captures| {
303 format!("\"{}\":\"{}\"", &caps[1], replacement)
304 });
305 result = Cow::Owned(masked.into_owned());
306 }
307
308 if let Some(ref re) = self.text_field_regex
310 && re.is_match(&result)
311 {
312 let replacement = &self.replacement;
313 let masked = re.replace_all(&result, |caps: ®ex::Captures| {
314 format!("{}={}", &caps[1], replacement)
315 });
316 result = Cow::Owned(masked.into_owned());
317 }
318
319 result
320 }
321}
322
323pub fn mask_value(key: &str, value: &str) -> String {
327 use std::sync::LazyLock;
328 static DEFAULT: LazyLock<DefaultMasker> = LazyLock::new(DefaultMasker::default);
329 DEFAULT.mask_value(key, value).into_owned()
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn all_default_value_patterns_compile() {
338 let patterns = DefaultMasker::default_value_patterns();
339 assert_eq!(
340 patterns.len(),
341 DEFAULT_VALUE_PATTERNS.len(),
342 "some default value patterns failed to compile"
343 );
344 }
345
346 #[test]
347 fn default_masker_masks_all_default_fields() {
348 let m = DefaultMasker::default();
349 for field in DEFAULT_FIELD_NAMES {
350 let result = m.mask_value(field, "test-value");
351 assert_eq!(
352 result.as_ref(),
353 "[REDACTED]",
354 "field '{}' not masked",
355 field
356 );
357 }
358 }
359}