use anyhow::{Context, Result};
use log::{debug, trace};
use regex::{Regex, RegexBuilder};
use strip_ansi_escapes::strip as strip_ansi_escapes_fn;
use crate::config::{self, RedactionSummaryItem};
pub struct SanitizationRule {
pub name: String,
compiled_regex: Regex,
replace_with: String,
original_pattern: String,
description: Option<String>,
dot_matches_new_line: bool,
}
impl std::fmt::Debug for SanitizationRule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SanitizationRule")
.field("name", &self.name)
.field("pattern", &self.original_pattern)
.field("replace_with", &self.replace_with)
.field("description", &self.description)
.field("dot_matches_new_line", &self.dot_matches_new_line)
.finish()
}
}
pub fn compile_rules(rules_config: config::RulesConfig) -> Result<Vec<SanitizationRule>> {
let mut compiled_rules = Vec::with_capacity(rules_config.rules.len());
for rule in rules_config.rules {
debug!("Attempting to compile rule: {}", rule.name);
trace!("Pattern: '{}'", rule.pattern);
let mut builder = RegexBuilder::new(&rule.pattern);
builder.multi_line(rule.multiline);
if rule.dot_matches_new_line {
builder.dot_matches_new_line(true);
}
let compiled_regex = builder
.build()
.with_context(|| {
format!(
"Failed to compile regex for rule '{}' with pattern: '{}'",
rule.name, rule.pattern
)
})?;
compiled_rules.push(SanitizationRule {
name: rule.name,
compiled_regex,
replace_with: rule.replace_with,
original_pattern: rule.pattern,
description: rule.description,
dot_matches_new_line: rule.dot_matches_new_line,
});
debug!("Successfully compiled rule: {}", compiled_rules.last().unwrap().name);
}
Ok(compiled_rules)
}
pub fn sanitize_content(
content: &str,
rules: &[SanitizationRule],
) -> (String, Vec<RedactionSummaryItem>) {
debug!("Starting content sanitization.");
let stripped_bytes = strip_ansi_escapes_fn(content);
let mut sanitized_content = String::from_utf8_lossy(&stripped_bytes).to_string();
let mut all_redactions: Vec<RedactionSummaryItem> = Vec::new();
for rule in rules {
trace!(
"Applying rule '{}' with pattern '{}'",
rule.name,
rule.original_pattern
);
let matches: Vec<regex::Match> = rule.compiled_regex.find_iter(&sanitized_content).collect();
if !matches.is_empty() {
debug!(
"Found {} matches for rule '{}'.",
matches.len(),
rule.name
);
let mut current_rule_redactions = Vec::new();
for m in &matches {
let original = m.as_str().to_string();
let sanitized = rule
.compiled_regex
.replace(&original, rule.replace_with.as_str())
.to_string();
current_rule_redactions.push(RedactionSummaryItem {
rule_name: rule.name.clone(),
original_text: original,
sanitized_text: sanitized,
occurrences: 1,
});
}
all_redactions.extend(current_rule_redactions);
sanitized_content = rule
.compiled_regex
.replace_all(&sanitized_content, rule.replace_with.as_str())
.to_string();
} else {
trace!("No matches found for rule '{}'.", rule.name);
}
}
debug!(
"Sanitization complete. {} individual redactions recorded.",
all_redactions.len()
);
(sanitized_content, summarize_redaction_items(all_redactions))
}
fn summarize_redaction_items(items: Vec<RedactionSummaryItem>) -> Vec<RedactionSummaryItem> {
use std::collections::HashMap;
let mut summary_map: HashMap<String, RedactionSummaryItem> = HashMap::new();
for item in items {
summary_map
.entry(item.rule_name.clone())
.and_modify(|existing| {
existing.occurrences += 1;
existing.original_text = item.original_text.clone();
existing.sanitized_text = item.sanitized_text.clone();
})
.or_insert(item);
}
let mut summarized: Vec<RedactionSummaryItem> = summary_map.into_values().collect();
summarized.sort_by(|a, b| a.rule_name.cmp(&b.rule_name));
summarized
}