use std::borrow::Cow;
use std::sync::OnceLock;
use regex::Regex;
fn rules() -> &'static [(&'static str, Regex)] {
static RULES: OnceLock<Vec<(&'static str, Regex)>> = OnceLock::new();
RULES.get_or_init(|| {
let specs: &[(&str, &str)] = &[
(
"private-key",
r"(?s)-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?-----END [A-Z0-9 ]*PRIVATE KEY-----",
),
("anthropic-key", r"sk-ant-[A-Za-z0-9_-]{20,200}"),
("openai-key", r"sk-(?:proj-)?[A-Za-z0-9_-]{20,200}"),
("github-pat", r"github_pat_[A-Za-z0-9_]{22,90}"),
("github-token", r"gh[pousr]_[A-Za-z0-9]{36,255}\b"),
("slack-token", r"xox[baprs]-[A-Za-z0-9-]{10,}"),
("google-api-key", r"AIza[0-9A-Za-z_-]{35}"),
("aws-access-key", r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b"),
("jwt", r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}"),
("url-credentials", r"[a-zA-Z][a-zA-Z0-9+.\-]*://[^:@/\s]+:[^@/\s]+@"),
("bearer-token", r"(?i)bearer\s+[A-Za-z0-9_-][A-Za-z0-9._~+/-]{15,}={0,2}"),
];
#[allow(clippy::expect_used)]
specs
.iter()
.map(|(name, pat)| {
let re = Regex::new(pat).expect("redaction rule pattern must compile");
(*name, re)
})
.collect()
})
}
fn disabled() -> bool {
crate::config::env_flag("WINX_NO_REDACT")
}
pub fn redact(text: &str) -> Cow<'_, str> {
if disabled() || text.is_empty() {
return Cow::Borrowed(text);
}
let mut owned: Option<String> = None;
for (name, re) in rules() {
let current: &str = owned.as_deref().unwrap_or(text);
if let Cow::Owned(replaced) =
re.replace_all(current, |_: ®ex::Captures| format!("[REDACTED:{name}]"))
{
owned = Some(replaced);
}
}
owned.map_or(Cow::Borrowed(text), Cow::Owned)
}
pub fn redact_json(value: &mut serde_json::Value) {
match value {
serde_json::Value::String(s) => {
if let Cow::Owned(r) = redact(s) {
*s = r;
}
}
serde_json::Value::Array(items) => items.iter_mut().for_each(redact_json),
serde_json::Value::Object(map) => map.values_mut().for_each(redact_json),
_ => {}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn isolated_secret_never_survives(
before in "[ \t=:;,(]{1,8}",
after in "[ \t=:;,)\n]{1,8}",
which in 0usize..2,
) {
let secret = [
"AKIAIOSFODNN7EXAMPLE",
"ghp_0123456789012345678901234567890123456",
][which];
let input = format!("{before}{secret}{after}");
let out = redact(&input);
prop_assert!(!out.contains(secret), "secret leaked: {out:?}");
prop_assert!(out.contains("[REDACTED:"), "no marker emitted: {out:?}");
}
#[test]
fn redaction_is_idempotent(s in "[ -~]{0,80}") {
let once = redact(&s).into_owned();
let twice = redact(&once).into_owned();
prop_assert_eq!(once, twice);
}
}
#[test]
fn redacts_known_credentials() {
let cases = [
"AKIAIOSFODNN7EXAMPLE",
"ghp_0123456789012345678901234567890123456",
"sk-ant-api03-abcdefghijklmnopqrstuvwxyz",
"AIzaSyA1234567890123456789012345678901234",
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abcDEF123456",
"postgres://user:hunter2@db.local/app",
];
for c in cases {
let red = redact(c);
assert!(red.contains("[REDACTED:"), "did not redact: {c} -> {red}");
assert!(!red.contains("hunter2"));
}
}
#[test]
fn redacts_pem_private_key_block() {
let pem = "before\n-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAKj34Gkx...\n-----END RSA PRIVATE KEY-----\nafter";
let red = redact(pem);
assert!(red.contains("before") && red.contains("after"));
assert!(red.contains("[REDACTED:private-key]"));
assert!(!red.contains("MIIBOgIBAAJBAKj"));
}
#[test]
fn leaves_ordinary_code_untouched() {
let code = "pub fn parse_config(path: &str) -> Result<Config> { Ok(Config::default()) }";
assert!(matches!(redact(code), Cow::Borrowed(_)));
}
#[test]
fn bearer_does_not_eat_a_path() {
let line = "run: bearer ./../etc/passwd && cat file";
assert!(matches!(redact(line), Cow::Borrowed(_)), "redacted a path as a bearer token");
let auth = "Authorization: Bearer abcdef0123456789ABCDEF";
assert!(redact(auth).contains("[REDACTED:bearer-token]"));
}
#[test]
fn redacts_inside_surrounding_text() {
let line = "export GITHUB_TOKEN=ghp_0123456789012345678901234567890123456 # ci";
let red = redact(line);
assert!(red.contains("export GITHUB_TOKEN="));
assert!(red.contains("[REDACTED:github-token]"));
assert!(red.contains("# ci"));
}
#[test]
fn redact_rules_compile() {
let names: Vec<&str> = rules().iter().map(|(n, _)| *n).collect();
assert!(names.contains(&"private-key"));
assert!(names.contains(&"github-token"));
assert_eq!(names.len(), 11, "rule count changed — update this assertion");
}
#[test]
fn redacts_long_github_token() {
let tok = format!("ghs_{}", "a".repeat(120));
let line = format!("token={tok} done");
let red = redact(&line);
assert!(red.contains("[REDACTED:github-token]"), "not redacted: {red}");
assert!(!red.contains("aaaaaaaaaa"), "token tail leaked: {red}");
}
#[test]
fn redacts_json_strings() {
let mut v = serde_json::json!({"out": "key=AKIAIOSFODNN7EXAMPLE", "n": 5, "ok": true});
redact_json(&mut v);
assert!(v["out"].as_str().unwrap().contains("[REDACTED:"));
assert_eq!(v["n"], 5);
}
}