use std::sync::OnceLock;
use regex::Regex;
pub(crate) const MAX_LEN: usize = 1024;
pub fn scrub_for_log(input: &str) -> String {
let mut out = input.to_owned();
for (re, replacement) in patterns() {
out = re.replace_all(&out, *replacement).into_owned();
}
truncate(out)
}
fn truncate(mut s: String) -> String {
if s.len() <= MAX_LEN {
return s;
}
let mut cut = MAX_LEN;
while cut > 0 && !s.is_char_boundary(cut) {
cut -= 1;
}
let dropped = s.len() - cut;
s.truncate(cut);
s.push_str(&format!("…(truncated, {dropped} more bytes)"));
s
}
fn patterns() -> &'static [(Regex, &'static str)] {
static PATTERNS: OnceLock<Vec<(Regex, &'static str)>> = OnceLock::new();
PATTERNS.get_or_init(|| {
vec![
(
Regex::new(r"(?P<scheme>[a-zA-Z][a-zA-Z0-9+.\-]*://)[^\s/@]+@").unwrap(),
"$scheme***@",
),
(
Regex::new(r"(?i)bearer\s+[A-Za-z0-9._\-+/=]+").unwrap(),
"Bearer ***",
),
(Regex::new(r"sk-[A-Za-z0-9_\-]{16,}").unwrap(), "sk-***"),
(Regex::new(r"ghp_[A-Za-z0-9]{20,}").unwrap(), "ghp_***"),
(Regex::new(r"ghs_[A-Za-z0-9]{20,}").unwrap(), "ghs_***"),
(Regex::new(r"gho_[A-Za-z0-9]{20,}").unwrap(), "gho_***"),
(
Regex::new(r"xox[abprs]-[A-Za-z0-9\-]{8,}").unwrap(),
"xox-***",
),
(
Regex::new(
r#"(?i)(?P<k>api[_\-]?key|password|passwd|pwd|secret|token|auth)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,}\)\]]+)"#,
)
.unwrap(),
"$k=***",
),
(
Regex::new(r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b").unwrap(),
"***@***",
),
]
})
}
#[cfg(test)]
#[path = "redact_tests.rs"]
mod tests;