use regex::{Captures, Regex};
use std::sync::OnceLock;
const MASK: &str = "[redacted]";
fn simple_rules() -> &'static Vec<(Regex, String)> {
static RULES: OnceLock<Vec<(Regex, String)>> = OnceLock::new();
RULES.get_or_init(|| {
[
(
r#"(?i)\b(authorization\s*:\s*)[^"'\\]+"#,
format!("${{1}}{MASK}"),
),
(
r#"(?i)\b(x-(?:api-key|auth-token|access-token)\s*:\s*)[^"'\\]+"#,
format!("${{1}}{MASK}"),
),
(
r"(?i)\b(bearer\s+)[A-Za-z0-9._~+/=-]+",
format!("${{1}}{MASK}"),
),
(
r#"(?i)(--?(?:password|passwd|token|api[-_]?key|secret|access[-_]?key)[=\s]+)("[^"]*"|'[^']*'|\S+)"#,
format!("${{1}}{MASK}"),
),
(
r#"\b([A-Za-z_][A-Za-z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|API_?KEY|CREDENTIALS?)[A-Za-z0-9_]*)=("[^"]*"|'[^']*'|\S+)"#,
format!("${{1}}={MASK}"),
),
]
.into_iter()
.map(|(p, r)| (Regex::new(p).expect("static pattern"), r))
.collect()
})
}
fn basic_auth_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"(?i)(^|\s)(-u|--user)(\s+)(\S+)").expect("static pattern"))
}
fn url_userinfo_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r"([a-zA-Z][a-zA-Z0-9+.\-]*://[^/\s:@]+):([^/\s@]+)@").expect("static pattern")
})
}
fn all_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
pub fn redact(cmd: &str) -> String {
let mut out = cmd.to_string();
out = url_userinfo_re()
.replace_all(&out, |c: &Captures| format!("{}:{MASK}@", &c[1]))
.into_owned();
out = basic_auth_re()
.replace_all(&out, |c: &Captures| {
let (lead, flag, gap, val) = (&c[1], &c[2], &c[3], &c[4]);
match val.split_once(':') {
Some((user, pass)) if !(all_digits(user) && all_digits(pass)) => {
format!("{lead}{flag}{gap}{user}:{MASK}")
}
_ => format!("{lead}{flag}{gap}{val}"),
}
})
.into_owned();
for (re, replacement) in simple_rules() {
out = re.replace_all(&out, replacement.as_str()).into_owned();
}
out
}