tracevault_core/
redact.rs1use regex::Regex;
2
3pub struct Redactor {
4 patterns: Vec<Regex>,
5 high_entropy_pattern: Regex,
6}
7
8const REDACTED: &str = "[REDACTED]";
9
10impl Redactor {
11 pub fn new() -> Self {
12 let patterns = vec![
13 r"AKIA[0-9A-Z]{16}",
15 r"gh[ps]_[A-Za-z0-9]{36,}",
17 r#"(?i)(api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}"#,
19 r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+",
21 r"-----BEGIN (?:RSA )?PRIVATE KEY-----",
23 r"xox[bpras]-[0-9A-Za-z\-]+",
25 r"(?i)bearer\s+[A-Za-z0-9\-._~+/]+=*",
27 ];
28
29 Self {
30 patterns: patterns
31 .iter()
32 .map(|p| Regex::new(p).unwrap())
33 .collect(),
34 high_entropy_pattern: Regex::new(r"[A-Za-z0-9/+_=\-]{16,}").unwrap(),
35 }
36 }
37
38 pub fn redact_string(&self, input: &str) -> String {
39 let mut result = input.to_string();
40
41 for pattern in &self.patterns {
43 result = pattern.replace_all(&result, REDACTED).to_string();
44 }
45
46 let entropy_re = &self.high_entropy_pattern;
48 result = entropy_re
49 .replace_all(&result, |caps: ®ex::Captures| {
50 let matched = caps.get(0).unwrap().as_str();
51 if shannon_entropy(matched) > 4.5 {
52 REDACTED.to_string()
53 } else {
54 matched.to_string()
55 }
56 })
57 .to_string();
58
59 result
60 }
61}
62
63impl Default for Redactor {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69fn shannon_entropy(s: &str) -> f64 {
70 if s.is_empty() {
71 return 0.0;
72 }
73 let mut freq = [0u32; 256];
74 for b in s.bytes() {
75 freq[b as usize] += 1;
76 }
77 let len = s.len() as f64;
78 freq.iter()
79 .filter(|&&c| c > 0)
80 .map(|&c| {
81 let p = c as f64 / len;
82 -p * p.log2()
83 })
84 .sum()
85}