Skip to main content

tracevault_core/
redact.rs

1use 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            // AWS Access Key
14            r"AKIA[0-9A-Z]{16}",
15            // GitHub token
16            r"gh[ps]_[A-Za-z0-9]{36,}",
17            // Generic API key patterns
18            r#"(?i)(api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}"#,
19            // JWT
20            r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+",
21            // RSA private key header
22            r"-----BEGIN (?:RSA )?PRIVATE KEY-----",
23            // Slack token
24            r"xox[bpras]-[0-9A-Za-z\-]+",
25            // Generic bearer token
26            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        // Pattern-based redaction first
42        for pattern in &self.patterns {
43            result = pattern.replace_all(&result, REDACTED).to_string();
44        }
45
46        // Entropy-based redaction
47        let entropy_re = &self.high_entropy_pattern;
48        result = entropy_re
49            .replace_all(&result, |caps: &regex::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}