#![expect(
clippy::expect_used,
reason = "static regex literals validated by tests"
)]
use std::sync::LazyLock;
use regex::Regex;
const PLACEHOLDER: &str = "***";
static SENSITIVE_KEY: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"\b([A-Za-z0-9_]*(?:[Tt][Oo][Kk][Ee][Nn]|[Kk][Ee][Yy]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Cc][Rr][Ee][Dd][Ee][Nn][Tt][Ii][Aa][Ll]|[Pp][Rr][Ii][Vv][Aa][Tt][Ee])[A-Za-z0-9_]*)\s*=\s*('[^']*'|\x22[^\x22]*\x22|[^\s;|&]+)",
)
.expect("static sensitive-key regex compiles")
});
static GH_TOKEN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{10,}\b").expect("gh token")
});
static GH_FINE_GRAINED_TOKEN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\bgithub_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}\b").expect("gh fine-grained pat")
});
static SLACK_TOKEN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\bxox[abprs]-[A-Za-z0-9-]{10,}\b").expect("slack token"));
static STRIPE_KEY: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\b(?:(?:sk|pk|rk)_(?:live|test)|whsec)_[A-Za-z0-9]{16,}\b").expect("stripe key")
});
static OPENAI_KEY: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\bsk-[A-Za-z0-9_-]{16,}\b").expect("openai key"));
static AWS_AKID: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\bAKIA[0-9A-Z]{16}\b").expect("aws akid"));
static JWT: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b").expect("jwt")
});
static JSON_SENSITIVE_KEY: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#""([A-Za-z0-9_]*(?:[Tt][Oo][Kk][Ee][Nn]|[Kk][Ee][Yy]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Cc][Rr][Ee][Dd][Ee][Nn][Tt][Ii][Aa][Ll]|[Pp][Rr][Ii][Vv][Aa][Tt][Ee])[A-Za-z0-9_]*)"\s*:\s*"((?:\\.|[^"\\])*)""#,
)
.expect("json sensitive key regex compiles")
});
static BASIC_AUTH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?P<scheme>[a-zA-Z][a-zA-Z0-9+.-]*://)(?P<user>[^:/@\s]+):(?P<pass>[^@\s]+)@")
.expect("basic auth")
});
static PEM_BLOB: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----")
.expect("pem blob")
});
pub fn redact_strict(input: &str) -> String {
let mut out = PEM_BLOB.replace_all(input, PLACEHOLDER).into_owned();
out = JSON_SENSITIVE_KEY
.replace_all(&out, |caps: ®ex::Captures| {
format!(r#""{}":"{}""#, &caps[1], PLACEHOLDER)
})
.into_owned();
out = SENSITIVE_KEY
.replace_all(&out, |caps: ®ex::Captures| {
format!("{}={}", &caps[1], PLACEHOLDER)
})
.into_owned();
out = BASIC_AUTH
.replace_all(&out, |caps: ®ex::Captures| {
format!("{}{}:{}@", &caps["scheme"], &caps["user"], PLACEHOLDER)
})
.into_owned();
out = GH_TOKEN.replace_all(&out, PLACEHOLDER).into_owned();
out = GH_FINE_GRAINED_TOKEN
.replace_all(&out, PLACEHOLDER)
.into_owned();
out = SLACK_TOKEN.replace_all(&out, PLACEHOLDER).into_owned();
out = STRIPE_KEY.replace_all(&out, PLACEHOLDER).into_owned();
out = OPENAI_KEY.replace_all(&out, PLACEHOLDER).into_owned();
out = AWS_AKID.replace_all(&out, PLACEHOLDER).into_owned();
out = JWT.replace_all(&out, PLACEHOLDER).into_owned();
out
}
#[cfg(test)]
mod tests {
use super::*;
const SLACK_PREFIXES: &[&str] = &["xoxa", "xoxb", "xoxp", "xoxr", "xoxs"];
const STRIPE_PREFIXES: &[&str] = &[
"sk_live", "sk_test", "pk_live", "pk_test", "rk_live", "rk_test", "whsec",
];
#[test]
fn passes_through_benign_command() {
let s = "ls -la /tmp";
assert_eq!(redact_strict(s), s);
}
#[test]
fn redacts_token_env_assignment() {
let s = "GITHUB_TOKEN=abcdef1234 cargo run";
let out = redact_strict(s);
assert_eq!(out, "GITHUB_TOKEN=*** cargo run");
}
#[test]
fn redacts_password_in_quoted_value() {
let s = "MY_PASSWORD='hunter2 with spaces' env";
let out = redact_strict(s);
assert!(out.contains("MY_PASSWORD=***"));
assert!(!out.contains("hunter2"));
}
#[test]
fn redacts_secret_with_double_quotes() {
let s = "AWS_SECRET=\"abc def\" cmd";
let out = redact_strict(s);
assert!(out.contains("AWS_SECRET=***"));
assert!(!out.contains("abc def"));
}
#[test]
fn redacts_private_key_assignment() {
let s = "MY_PRIVATE=foo cmd";
let out = redact_strict(s);
assert!(out.contains("MY_PRIVATE=***"));
}
#[test]
fn does_not_touch_non_sensitive_assignment() {
let s = "PATH=/usr/bin cmd";
assert_eq!(redact_strict(s), s);
}
#[test]
fn redacts_github_token() {
let s = "curl -H 'Authorization: token ghp_ABCDEFGHIJ1234567890' https://api.github.com";
let out = redact_strict(s);
assert!(out.contains("***"));
assert!(!out.contains("ghp_ABCDEFGHIJ"));
}
#[test]
fn redacts_openai_key() {
let s = "OPENAI=sk-AbCdEf0123456789xyz curl";
let out = redact_strict(s);
assert!(!out.contains("sk-AbCdEf0123456789xyz"));
}
#[test]
fn redacts_aws_access_key_id() {
let s = "echo AKIAIOSFODNN7EXAMPLE";
let out = redact_strict(s);
assert!(!out.contains("AKIAIOSFODNN7EXAMPLE"));
}
#[test]
fn redacts_jwt_blob() {
let s = "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NSJ9.SflKxwR";
let out = redact_strict(s);
assert!(!out.contains("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NSJ9.SflKxwR"));
assert!(out.contains("***"));
}
#[test]
fn redacts_basic_auth_password_only() {
let s = "git clone https://alice:hunter2@example.com/repo.git";
let out = redact_strict(s);
assert!(out.contains("alice:***@"));
assert!(!out.contains("hunter2"));
}
#[test]
fn redacts_pem_block() {
let s = "echo -----BEGIN RSA PRIVATE KEY-----\\nMIIEow...\\n-----END RSA PRIVATE KEY-----";
let out = redact_strict(s);
assert!(out.contains("***"));
assert!(!out.contains("MIIEow"));
}
#[test]
fn redacts_github_fine_grained_pat() {
let pat = format!("github_pat_{}_{}", "A".repeat(22), "B".repeat(59));
let s = format!("curl -H 'Authorization: token {pat}' https://api.github.com");
let out = redact_strict(&s);
assert!(!out.contains(&pat), "kept {pat:?} in {out:?}");
assert!(out.contains("***"));
}
#[test]
fn does_not_touch_short_github_pat_lookalike() {
let s = "github_pat_short value";
assert_eq!(redact_strict(s), s);
}
#[test]
fn redacts_slack_bot_token() {
for prefix in SLACK_PREFIXES {
let token = format!("{prefix}-1234567890-ABCDEFGHIJ");
let s = format!("curl -H 'X-Slack-Token: {token}' https://slack.com/api");
let out = redact_strict(&s);
assert!(!out.contains(&token), "kept {token} in {out}");
assert!(out.contains("***"));
}
}
#[test]
fn redacts_stripe_live_secret() {
let key = "sk_live_AbCdEf0123456789xyzABCD";
let s = format!("stripe charges create --api-key {key}");
let out = redact_strict(&s);
assert!(!out.contains(key), "kept {key} in {out}");
assert!(out.contains("***"));
}
#[test]
fn redacts_stripe_webhook_signing_secret() {
let key = "whsec_AbCdEf0123456789xyzABCD";
let s = format!("export WEBHOOK={key} && cmd");
let out = redact_strict(&s);
assert!(!out.contains(key), "kept {key} in {out}");
assert!(out.contains("***"));
}
#[test]
fn redacts_json_private_key_block() {
let s = r#"echo '{"type":"service_account","private_key":"-----BEGIN PRIVATE KEY-----\nMIIEow...\n-----END PRIVATE KEY-----","client_email":"x@y"}'"#;
let out = redact_strict(s);
assert!(!out.contains("MIIEow"));
assert!(out.contains(r#""private_key":"***""#));
}
#[test]
fn redacts_json_client_secret_without_pem() {
let s = r#"curl -d '{"client_secret":"abcdef0123456789"}'"#;
let out = redact_strict(s);
assert!(!out.contains("abcdef0123456789"));
assert!(out.contains(r#""client_secret":"***""#));
}
#[test]
fn redacts_json_refresh_token_with_spaces() {
let s = r#"{"refresh_token" : "1//abcDEF_xyz-123"}"#;
let out = redact_strict(s);
assert!(!out.contains("1//abcDEF_xyz-123"));
}
#[test]
fn does_not_touch_benign_json_field() {
let s = r#"{"name":"alice","age":30}"#;
assert_eq!(redact_strict(s), s);
}
#[test]
fn handles_multiple_categories_in_one_string() {
let s = "TOKEN=abc curl -u u:p@host ghp_ABCDEFGHIJ12345";
let out = redact_strict(s);
assert!(out.contains("TOKEN=***"));
assert!(!out.contains("ghp_"));
}
#[test]
fn long_input_does_not_break_replacement() {
let mut s = String::new();
for _ in 0..200 {
s.push_str("ls -la; ");
}
s.push_str("TOKEN=abc");
let out = redact_strict(&s);
assert!(out.ends_with("TOKEN=***"));
}
#[test]
fn placeholder_constant_is_stable() {
assert_eq!(PLACEHOLDER, "***");
}
use proptest::prelude::*;
proptest! {
#[test]
fn pbt_redact_is_idempotent(s in "[ -~]{0,80}") {
let once = redact_strict(&s);
let twice = redact_strict(&once);
prop_assert_eq!(once, twice);
}
#[test]
fn pbt_redact_never_panics(s in ".{0,80}") {
let _ = redact_strict(&s);
}
#[test]
fn pbt_redacts_github_tokens(suffix in "[A-Za-z0-9]{10,30}") {
for prefix in ["ghp", "gho", "ghu", "ghs", "ghr"] {
let needle = format!("{prefix}_{suffix}");
let s = format!("echo {needle} done");
let out = redact_strict(&s);
prop_assert!(!out.contains(&needle), "kept {needle:?} in {out:?}");
prop_assert!(out.contains("***"));
}
}
#[test]
fn pbt_redacts_openai_keys(suffix in "[A-Za-z0-9_-]{15,39}[A-Za-z0-9_]") {
let needle = format!("sk-{suffix}");
let s = format!("ENV={needle} cmd");
let out = redact_strict(&s);
prop_assert!(!out.contains(&needle));
}
#[test]
fn pbt_redacts_aws_akid(suffix in "[A-Z0-9]{16}") {
let needle = format!("AKIA{suffix}");
let s = format!("echo {needle}");
let out = redact_strict(&s);
prop_assert!(!out.contains(&needle));
}
#[test]
fn pbt_redacts_sensitive_env_assignments(value in "[A-Za-z0-9_]{1,30}") {
for k in ["GITHUB_TOKEN", "MY_SECRET", "DB_PASSWORD", "API_KEY"] {
let s = format!("{k}={value} cmd");
let out = redact_strict(&s);
let placeholder_form = format!("{k}=***");
let raw_form = format!("{k}={value}");
prop_assert!(out.contains(&placeholder_form));
prop_assert!(!out.contains(&raw_form));
}
}
#[test]
fn pbt_benign_passes_through(s in "[a-zA-Z0-9 /._-]{0,60}") {
prop_assume!(!s.contains("AKIA"));
prop_assume!(!s.contains("ghp_"));
prop_assume!(!s.contains("gho_"));
prop_assume!(!s.contains("ghu_"));
prop_assume!(!s.contains("ghs_"));
prop_assume!(!s.contains("ghr_"));
prop_assume!(!s.contains("github_pat_"));
prop_assume!(!s.contains("sk-"));
for prefix in STRIPE_PREFIXES {
let needle = format!("{prefix}_");
prop_assume!(!s.contains(&needle));
}
for prefix in SLACK_PREFIXES {
let needle = format!("{prefix}-");
prop_assume!(!s.contains(&needle));
}
prop_assume!(!s.contains("eyJ"));
let lower = s.to_lowercase();
for needle in ["token", "key", "secret", "password", "credential", "private"] {
prop_assume!(!lower.contains(needle));
}
prop_assert_eq!(redact_strict(&s), s);
}
#[test]
fn pbt_basic_auth_redaction_keeps_user(
scheme in "(https?|ssh|ftp)",
user in "[a-zA-Z][a-zA-Z0-9._-]{0,12}",
pass in "[A-Za-z0-9!#$%^*]{1,20}",
host in "[a-z][a-z0-9.-]{0,12}\\.com",
) {
let s = format!("{scheme}://{user}:{pass}@{host}/path");
let out = redact_strict(&s);
let user_mask = format!("{user}:***@");
let scheme_prefix = format!("{scheme}://");
prop_assert!(out.contains(&user_mask));
prop_assert!(out.contains(&scheme_prefix));
prop_assert!(out.contains(host.as_str()));
prop_assert!(out.contains(":***@"));
}
#[test]
fn pbt_pem_blob_redaction_replaces_block(
label in "(?:RSA |EC |DSA |OPENSSH |)",
body in "[A-Za-z0-9/+= \\n]{16,80}",
) {
let blob = format!(
"-----BEGIN {label}PRIVATE KEY-----\n{body}\n-----END {label}PRIVATE KEY-----"
);
let s = format!("echo '{blob}' && true");
let out = redact_strict(&s);
prop_assert!(out.contains("***"));
prop_assert!(!out.contains(&body), "leaked body in {out:?}");
}
#[test]
fn pbt_redacts_github_fine_grained_pat(
head in "[A-Za-z0-9]{22}",
tail in "[A-Za-z0-9]{59}",
) {
let token = format!("github_pat_{head}_{tail}");
let s = format!("curl -H 'Authorization: token {token}' api");
let out = redact_strict(&s);
prop_assert!(!out.contains(&token), "kept {token:?} in {out:?}");
prop_assert!(out.contains("***"));
}
#[test]
fn pbt_redacts_slack_tokens(
prefix in proptest::sample::select(SLACK_PREFIXES),
suffix in "[A-Za-z0-9-]{9,29}[A-Za-z0-9]",
) {
let needle = format!("{prefix}-{suffix}");
let s = format!("ENV={needle} cmd");
let out = redact_strict(&s);
prop_assert!(!out.contains(&needle), "kept {needle:?} in {out:?}");
}
#[test]
fn pbt_redacts_stripe_keys(
prefix in proptest::sample::select(STRIPE_PREFIXES),
suffix in "[A-Za-z0-9]{16,40}",
) {
let needle = format!("{prefix}_{suffix}");
let s = format!("echo {needle}");
let out = redact_strict(&s);
prop_assert!(!out.contains(&needle), "kept {needle:?} in {out:?}");
}
#[test]
fn pbt_redacts_json_sensitive_values(value in "[A-Za-z0-9/+=.-]{1,40}") {
for k in [
"token",
"secret",
"password",
"private_key",
"api_key",
"client_credential",
] {
let s = format!(r#"{{"{k}":"{value}"}}"#);
let out = redact_strict(&s);
let masked = format!(r#""{k}":"***""#);
prop_assert!(out.contains(&masked), "missing mask for {k} in {out:?}");
}
}
#[test]
fn pbt_idempotent_does_not_grow_placeholders(s in "[ -~]{0,80}") {
let once = redact_strict(&s);
let twice = redact_strict(&once);
let count_once = once.matches(PLACEHOLDER).count();
let count_twice = twice.matches(PLACEHOLDER).count();
prop_assert_eq!(count_once, count_twice);
}
#[test]
fn pbt_redaction_does_not_explode_length(s in "[ -~]{0,200}") {
let out = redact_strict(&s);
prop_assert!(out.len() <= s.len() * 3 + 16);
}
#[test]
fn pbt_multi_token_stuffing(
ghp in "ghp_[A-Za-z0-9]{12,20}",
akia in "AKIA[A-Z0-9]{16}",
jwt_a in "[A-Za-z0-9_-]{4,8}",
jwt_b in "[A-Za-z0-9_-]{4,8}",
jwt_c in "[A-Za-z0-9_-]{3,7}[A-Za-z0-9_]",
) {
let jwt = format!("eyJ{jwt_a}.{jwt_b}.{jwt_c}");
let s = format!("X={ghp} Y={akia} Z={jwt}");
let out = redact_strict(&s);
prop_assert!(!out.contains(&ghp));
prop_assert!(!out.contains(&akia));
prop_assert!(!out.contains(&jwt));
}
#[test]
fn pbt_redaction_of_known_tokens_shrinks_or_equals(
ghp in "ghp_[A-Za-z0-9]{12,30}",
) {
let s = format!("echo {ghp}");
let out = redact_strict(&s);
prop_assert!(out.len() <= s.len());
}
}
}