use regex::Regex;
pub struct Redactor {
patterns: Vec<Regex>,
high_entropy_pattern: Regex,
}
const REDACTED: &str = "[REDACTED]";
impl Redactor {
pub fn new() -> Self {
let patterns = [
r"AKIA[0-9A-Z]{16}",
r"gh[ps]_[A-Za-z0-9]{36,}",
r#"(?i)(api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}"#,
r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+",
r"-----BEGIN (?:RSA )?PRIVATE KEY-----",
r"xox[bpras]-[0-9A-Za-z\-]+",
r"(?i)bearer\s+[A-Za-z0-9\-._~+/]+=*",
];
Self {
patterns: patterns.iter().map(|p| Regex::new(p).unwrap()).collect(),
high_entropy_pattern: Regex::new(r"[A-Za-z0-9/+_=\-]{16,}").unwrap(),
}
}
pub fn redact_string(&self, input: &str) -> String {
let mut result = input.to_string();
for pattern in &self.patterns {
result = pattern.replace_all(&result, REDACTED).to_string();
}
let entropy_re = &self.high_entropy_pattern;
result = entropy_re
.replace_all(&result, |caps: ®ex::Captures| {
let matched = caps.get(0).unwrap().as_str();
if shannon_entropy(matched) > 4.5 {
REDACTED.to_string()
} else {
matched.to_string()
}
})
.to_string();
result
}
}
impl Default for Redactor {
fn default() -> Self {
Self::new()
}
}
fn shannon_entropy(s: &str) -> f64 {
if s.is_empty() {
return 0.0;
}
let mut freq = [0u32; 256];
for b in s.bytes() {
freq[b as usize] += 1;
}
let len = s.len() as f64;
freq.iter()
.filter(|&&c| c > 0)
.map(|&c| {
let p = c as f64 / len;
-p * p.log2()
})
.sum()
}