use regex::Regex;
use crate::{
error::MuninError, index::BinlogIndex, reader::BinlogEvent, record_kind::BinaryLogRecordKind,
};
#[derive(Debug, Default)]
pub struct Redactor {
exact: Vec<ExactRule>,
regex: Vec<RegexRule>,
autodetect_username: bool,
common_patterns: bool,
}
#[derive(Debug)]
struct ExactRule {
needle: String,
replacement: String,
}
#[derive(Debug)]
struct RegexRule {
re: Regex,
replacement: String,
}
impl Redactor {
pub fn new() -> Self {
Self::default()
}
pub fn with_token(mut self, value: impl Into<String>) -> Self {
self.exact.push(ExactRule {
needle: value.into(),
replacement: "*****".to_string(),
});
self
}
pub fn with_regex(
mut self,
pattern: &str,
replacement: impl Into<String>,
) -> Result<Self, MuninError> {
let re = Regex::new(pattern)
.map_err(|e| MuninError::InvalidFormat(format!("invalid redaction regex: {e}")))?;
self.regex.push(RegexRule {
re,
replacement: replacement.into(),
});
Ok(self)
}
pub fn with_common_patterns(mut self) -> Self {
self.common_patterns = true;
self
}
pub fn with_autodetect_username(mut self) -> Self {
self.autodetect_username = true;
self
}
pub fn apply(&self, index: &mut BinlogIndex) {
let mut exact_owned: Vec<ExactRule> = Vec::new();
if self.autodetect_username {
exact_owned.extend(autodetect_username_rules(index));
}
let mut exact_order: Vec<&ExactRule> =
self.exact.iter().chain(exact_owned.iter()).collect();
exact_order.sort_by_key(|r| std::cmp::Reverse(r.needle.len()));
let common: Vec<RegexRule> = if self.common_patterns {
common_patterns()
} else {
Vec::new()
};
for entry in index.strings_mut().entries_mut() {
if entry.is_empty() {
continue;
}
for rule in &exact_order {
if rule.needle.is_empty() {
continue;
}
if entry.contains(&rule.needle) {
*entry = entry.replace(&rule.needle, &rule.replacement);
}
}
for rule in common.iter().chain(self.regex.iter()) {
let replaced = rule.re.replace_all(entry, rule.replacement.as_str());
if let std::borrow::Cow::Owned(s) = replaced {
*entry = s;
}
}
}
}
}
fn common_patterns() -> Vec<RegexRule> {
fn r(pat: &str, repl: &str) -> RegexRule {
RegexRule {
re: Regex::new(pat).expect("common-pattern regex must compile (see D-RDX-1)"),
replacement: repl.to_string(),
}
}
vec![
r(
r"(?P<scheme>[a-zA-Z][a-zA-Z0-9+.\-]*)://[^/\s:@]+:[^/\s:@]+@",
"$scheme://*****:*****@",
),
r(r"gh[pousr]_[A-Za-z0-9]{36,}", "gh*_*****"),
r(r"\b[a-z2-7]{52}\b", "*****"),
r(
r"(?i)(?P<prefix>bearer\s+)[A-Za-z0-9._\-]+",
"${prefix}*****",
),
r(
r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}",
"*****@*****",
),
]
}
fn autodetect_username_rules(index: &BinlogIndex) -> Vec<ExactRule> {
const REPLACEMENT: &str = "REDACTED-USER";
const KEYS: &[&str] = &["USERNAME", "USER", "USERPROFILE", "HOME"];
let mut out = Vec::new();
for i in index.indices_by_kind(BinaryLogRecordKind::BuildStarted) {
let Ok(Some(BinlogEvent::BuildStarted(ev))) = index.get(i) else {
continue;
};
let Some(env) = ev.environment.as_ref() else {
continue;
};
for (k, v) in env {
if v.is_empty() {
continue;
}
if !KEYS.iter().any(|w| k.eq_ignore_ascii_case(w)) {
continue;
}
let leaf = path_basename(v);
if leaf.is_empty() {
continue;
}
out.push(ExactRule {
needle: leaf.to_string(),
replacement: REPLACEMENT.to_string(),
});
for prefix in [
format!(r"C:\Users\{leaf}\"),
format!("/home/{leaf}/"),
format!("/Users/{leaf}/"),
] {
let scrubbed = prefix.replace(leaf, REPLACEMENT);
out.push(ExactRule {
needle: prefix,
replacement: scrubbed,
});
}
}
}
out.sort_by(|a, b| a.needle.cmp(&b.needle));
out.dedup_by(|a, b| a.needle == b.needle);
out
}
fn path_basename(s: &str) -> &str {
match s.rfind(['/', '\\']) {
Some(i) => &s[i + 1..],
None => s,
}
}
#[cfg(test)]
mod tests;