use std::sync::OnceLock;
use regex::Regex;
pub fn redact_in_place(line: &mut String) {
let original = std::mem::take(line);
let scrubbed = redact_str(&original);
*line = scrubbed;
}
fn redact_str(input: &str) -> String {
let mut out = input.to_string();
for r in rules() {
out = r.regex.replace_all(&out, r.replacement).into_owned();
}
out
}
struct Rule {
regex: Regex,
replacement: &'static str,
}
fn rules() -> &'static [Rule] {
static RULES: OnceLock<Vec<Rule>> = OnceLock::new();
RULES.get_or_init(build_rules)
}
fn build_rules() -> Vec<Rule> {
let entries: &[(&str, &str)] = &[
(
r#"(?i)(authorization\s*[:=]\s*"?\s*(?:bearer|basic|token)\s+)[A-Za-z0-9._\-+/=]{8,}"#,
"[REDACTED:auth-header]",
),
(
r#"(?i)\b(password|passwd|pwd|api[_-]?key|secret|token)\s*[:=]\s*"?[^\s",&}]{4,}"#,
"[REDACTED:secret-kv]",
),
(
r"\beyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\b",
"[REDACTED:jwt]",
),
(r"\bsk-[A-Za-z0-9]{16,}\b", "[REDACTED:api-key]"),
(r"\bxox[baps]-[A-Za-z0-9-]{10,}\b", "[REDACTED:api-key]"),
(r"\bgh[posu]_[A-Za-z0-9]{20,}\b", "[REDACTED:api-key]"),
(r"\bpat-[A-Za-z0-9]{16,}\b", "[REDACTED:api-key]"),
(r"\bthingspat_[A-Za-z0-9]{16,}\b", "[REDACTED:api-key]"),
(r"\bAKIA[0-9A-Z]{16}\b", "[REDACTED:aws-access]"),
(r"\bASIA[0-9A-Z]{16}\b", "[REDACTED:aws-session]"),
];
entries
.iter()
.map(|(pat, repl)| Rule {
regex: Regex::new(pat)
.unwrap_or_else(|e| panic!("redact rule {pat:?} failed to compile: {e}")),
replacement: repl,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn r(s: &str) -> String {
let mut s = s.to_string();
redact_in_place(&mut s);
s
}
#[test]
fn redacts_jwt() {
let input =
"context eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.abc-DEF_xyz tail";
let out = r(input);
assert!(!out.contains("eyJhbGciOi"));
assert!(out.contains("[REDACTED:jwt]"));
}
#[test]
fn redacts_openai_sk_key() {
let fixture = format!("{}-{}", "sk", "1234567890abcdefghij");
let input = format!(r#"{{"key":"{fixture}","ok":true}}"#);
let out = r(&input);
assert!(!out.contains(&fixture), "out: {out}");
assert!(out.contains("[REDACTED"));
}
#[test]
fn redacts_authorization_bearer() {
let value = "abcdefghij1234567890XYZ";
let input = format!("req: Authorization: Bearer {value}");
let out = r(&input);
assert!(!out.contains(value), "out: {out}");
assert!(out.contains("[REDACTED:auth-header]"));
}
#[test]
fn redacts_password_kv() {
let value = "sup3rh4rd!";
let input = format!("db_url=postgres://user:supersecret123@host/db password={value}");
let out = r(&input);
assert!(!out.contains(value), "out: {out}");
}
#[test]
fn redacts_aws_access_key() {
let fixture = format!("{}{}", "AKIA", "IOSFODNN7EXAMPLE");
let input = format!("boot {fixture} bye");
let out = r(&input);
assert!(!out.contains(&fixture), "out: {out}");
assert!(out.contains("[REDACTED:aws-access]"));
}
#[test]
fn redacts_github_token() {
let fixture = format!("{}_{}", "ghp", "abcdefghijklmnopqrstuvwxyz12");
let input = format!("tok={fixture}");
let out = r(&input);
assert!(!out.contains(&fixture), "out: {out}");
}
#[test]
fn redacts_slack_xoxb() {
let fixture = format!("{}-{}", "xoxb", "12345-67890-abcdefghi");
let input = format!("header {fixture} tail");
let out = r(&input);
assert!(!out.contains(&fixture), "out: {out}");
}
#[test]
fn passes_through_safe_text() {
let inputs = [
"the quick brown fox",
r#"{"msg":"hello world","level":"info"}"#,
"version=0.1.0-alpha.0",
"bytes=42",
];
for s in inputs {
let out = r(s);
assert_eq!(out, s, "should not redact: {s}");
}
}
#[test]
fn handles_multiple_secrets_in_one_line() {
let sk = format!("{}-{}", "sk", "1234567890abcdefghij");
let aws = format!("{}{}", "AKIA", "IOSFODNN7EXAMPLE");
let input = format!("{sk} and {aws}");
let out = r(&input);
assert!(!out.contains(&sk));
assert!(!out.contains(&aws));
}
}