use std::borrow::Cow;
use std::sync::LazyLock;
#[must_use]
pub fn scrub_content(text: &str) -> Cow<'_, str> {
let after_secrets = match redact_secrets(text) {
Cow::Borrowed(_) => {
return match sanitize_paths(text) {
Cow::Owned(s) => Cow::Owned(s),
Cow::Borrowed(_) => Cow::Borrowed(text),
};
}
Cow::Owned(s) => s,
};
match sanitize_paths(&after_secrets) {
Cow::Owned(s) => Cow::Owned(s),
Cow::Borrowed(_) => Cow::Owned(after_secrets),
}
}
use regex::Regex;
const SECRET_PREFIXES: &[&str] = &[
"sk-",
"sk_live_",
"sk_test_",
"AKIA",
"ghp_",
"gho_",
"-----BEGIN",
"xoxb-",
"xoxp-",
"AIza",
"ya29\\.",
"glpat-",
"hf_",
"npm_",
"dckr_pat_",
];
static SECRET_REGEX: LazyLock<Regex> = LazyLock::new(|| {
let pattern = SECRET_PREFIXES.join("|");
let full = format!("(?:{pattern})[^\\s\"'`,;{{}}\\[\\]]*");
Regex::new(&full).expect("secret redaction regex is valid")
});
static PATH_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"(?:/home/|/Users/|/root/|/tmp/|/var/)[^\s"'`,;{}\[\]]*"#)
.expect("path redaction regex is valid")
});
#[must_use]
pub fn redact_secrets(text: &str) -> Cow<'_, str> {
let raw_prefixes = &[
"sk-",
"sk_live_",
"sk_test_",
"AKIA",
"ghp_",
"gho_",
"-----BEGIN",
"xoxb-",
"xoxp-",
"AIza",
"ya29.",
"glpat-",
"hf_",
"npm_",
"dckr_pat_",
];
if !raw_prefixes.iter().any(|p| text.contains(p)) {
return Cow::Borrowed(text);
}
let result = SECRET_REGEX.replace_all(text, "[REDACTED]");
match result {
Cow::Borrowed(_) => Cow::Borrowed(text),
Cow::Owned(s) => Cow::Owned(s),
}
}
#[must_use]
pub fn sanitize_paths(text: &str) -> Cow<'_, str> {
const PATH_PREFIXES: &[&str] = &["/home/", "/Users/", "/root/", "/tmp/", "/var/"];
if !PATH_PREFIXES.iter().any(|p| text.contains(p)) {
return Cow::Borrowed(text);
}
let result = PATH_REGEX.replace_all(text, "[PATH]");
match result {
Cow::Borrowed(_) => Cow::Borrowed(text),
Cow::Owned(s) => Cow::Owned(s),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn redacts_openai_key() {
let text = "Use key sk-abc123def456 for API calls";
let result = redact_secrets(text);
assert_eq!(result, "Use key [REDACTED] for API calls");
}
#[test]
fn redacts_stripe_live_key() {
let text = "Stripe key: sk_live_abcdef123456";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("sk_live_"));
}
#[test]
fn redacts_stripe_test_key() {
let text = "Test key sk_test_abc123";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
}
#[test]
fn redacts_aws_key() {
let text = "AWS access key: AKIAIOSFODNN7EXAMPLE";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("AKIA"));
}
#[test]
fn redacts_github_pat() {
let text = "Token: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("ghp_"));
}
#[test]
fn redacts_github_oauth() {
let text = "OAuth: gho_xxxxxxxxxxxx";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
}
#[test]
fn redacts_private_key_header() {
let text = "Found -----BEGIN RSA PRIVATE KEY----- in file";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("-----BEGIN"));
}
#[test]
fn redacts_slack_tokens() {
let text = "Bot token xoxb-123-456 and user xoxp-789";
let result = redact_secrets(text);
assert_eq!(result, "Bot token [REDACTED] and user [REDACTED]");
}
#[test]
fn preserves_normal_text() {
let text = "This is a normal response with no secrets";
let result = redact_secrets(text);
assert_eq!(result, text);
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn handles_empty_string() {
assert_eq!(redact_secrets(""), "");
}
#[test]
fn multiple_secrets_redacted() {
let text = "Keys: sk-abc123 AKIAIOSFODNN7 ghp_xxxxx";
let result = redact_secrets(text);
assert_eq!(result, "Keys: [REDACTED] [REDACTED] [REDACTED]");
}
#[test]
fn preserves_multiline_whitespace() {
let text = "Line one\n indented line\n\ttabbed line\nsk-secret here";
let result = redact_secrets(text);
assert_eq!(
result,
"Line one\n indented line\n\ttabbed line\n[REDACTED] here"
);
}
#[test]
fn preserves_code_block_formatting() {
let text = "```rust\nfn main() {\n let key = \"sk-abc123\";\n println!(\"{}\", key);\n}\n```";
let result = redact_secrets(text);
assert!(result.contains("```rust\nfn"));
assert!(result.contains(" let"));
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("sk-abc123"));
}
#[test]
fn preserves_multiple_spaces() {
let text = "word1 word2 word3";
let result = redact_secrets(text);
assert_eq!(result, text);
}
#[test]
fn no_allocation_without_secrets() {
let text = "safe text without any secrets";
let result = redact_secrets(text);
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn all_secret_prefixes_tested() {
for prefix in &[
"sk-",
"sk_live_",
"sk_test_",
"AKIA",
"ghp_",
"gho_",
"-----BEGIN",
"xoxb-",
"xoxp-",
"AIza",
"ya29.",
"glpat-",
"hf_",
"npm_",
"dckr_pat_",
] {
let text = format!("token: {prefix}abc123");
let result = redact_secrets(&text);
assert!(result.contains("[REDACTED]"), "Failed for prefix: {prefix}");
assert!(!result.contains(*prefix), "Prefix not redacted: {prefix}");
}
}
#[test]
fn redacts_google_api_key() {
let text = "Google key: AIzaSyA1234567890abcdefghijklmnop";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("AIza"));
}
#[test]
fn redacts_google_oauth_token() {
let text = "OAuth token ya29.a0AfH6SMBx1234567890";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("ya29."));
}
#[test]
fn redacts_gitlab_pat() {
let text = "GitLab token: glpat-xxxxxxxxxxxxxxxxxxxx";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("glpat-"));
}
#[test]
fn only_whitespace() {
assert_eq!(redact_secrets(" \n\t "), " \n\t ");
}
#[test]
fn secret_at_end_of_line() {
let text = "token: sk-abc123";
let result = redact_secrets(text);
assert_eq!(result, "token: [REDACTED]");
}
#[test]
fn redacts_secret_in_url() {
let text = "https://api.example.com?key=sk-abc123xyz";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("sk-abc123xyz"));
}
#[test]
fn redacts_secret_in_json() {
let text = r#"{"api_key":"sk-abc123def456"}"#;
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("sk-abc123def456"));
}
#[test]
fn sanitize_home_path() {
let text = "error at /home/user/project/src/main.rs:42";
let result = sanitize_paths(text);
assert_eq!(result, "error at [PATH]");
}
#[test]
fn sanitize_users_path() {
let text = "failed: /Users/dev/code/lib.rs not found";
let result = sanitize_paths(text);
assert!(result.contains("[PATH]"));
assert!(!result.contains("/Users/"));
}
#[test]
fn sanitize_no_paths() {
let text = "normal error message";
let result = sanitize_paths(text);
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn redacts_huggingface_token() {
let text = "HuggingFace token: hf_abcdefghijklmnopqrstuvwxyz";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("hf_"));
}
#[test]
fn redacts_npm_token() {
let text = "NPM token npm_abc123XYZ";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("npm_abc"));
}
#[test]
fn redacts_docker_pat() {
let text = "Docker token: dckr_pat_xxxxxxxxxxxx";
let result = redact_secrets(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("dckr_pat_"));
}
use proptest::prelude::*;
#[test]
fn scrub_no_match_passthrough() {
let text = "hello world, nothing sensitive here";
let result = scrub_content(text);
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result.as_ref(), text);
}
#[test]
fn scrub_only_secrets() {
let text = "key: sk-abc123def";
let result = scrub_content(text);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("sk-abc123"));
assert!(!result.contains("/home/"));
}
#[test]
fn scrub_only_paths() {
let text = "error at /Users/dev/project/src/main.rs:42";
let result = scrub_content(text);
assert!(result.contains("[PATH]"));
assert!(!result.contains("/Users/dev/"));
}
#[test]
fn scrub_secrets_and_paths_combined() {
let text = "token sk-abc123 found at /home/user/config.toml";
let result = scrub_content(text);
assert!(result.contains("[REDACTED]"));
assert!(result.contains("[PATH]"));
assert!(!result.contains("sk-abc123"));
assert!(!result.contains("/home/user/"));
}
#[test]
fn scrub_secrets_no_paths() {
let text = "use sk-abc123 for auth";
let result = scrub_content(text);
assert!(
matches!(result, Cow::Owned(_)),
"must return Cow::Owned when secret was found"
);
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("[PATH]"));
}
#[test]
fn sanitize_paths_all_prefixes() {
let cases = [
("/root/secrets.toml", "/root/"),
("/tmp/tmpfile.lock", "/tmp/"),
("/var/log/app.log", "/var/"),
];
for (text, prefix) in cases {
let result = sanitize_paths(text);
assert!(result.contains("[PATH]"), "{prefix} must be sanitized");
assert!(
!result.contains(prefix),
"{prefix} must be removed from output"
);
}
}
proptest! {
#[test]
fn redact_secrets_never_panics(s in ".*") {
let _ = redact_secrets(&s);
}
#[test]
fn sanitize_paths_never_panics(s in ".*") {
let _ = sanitize_paths(&s);
}
#[test]
fn redact_preserves_non_secret_text(s in "[a-zA-Z0-9 .,!?]{1,200}") {
let secret_prefixes = [
"sk-", "sk_live_", "sk_test_", "AKIA", "ghp_", "gho_",
"-----BEGIN", "xoxb-", "xoxp-", "AIza", "ya29.", "glpat-",
"hf_", "npm_", "dckr_pat_",
];
if !secret_prefixes.iter().any(|p| s.contains(p)) {
let result = redact_secrets(&s);
assert_eq!(result.as_ref(), s.as_str());
}
}
#[test]
fn scrub_content_never_panics(s in ".*") {
let _ = scrub_content(&s);
}
#[test]
fn scrub_content_result_never_contains_raw_secret(s in ".*") {
let result = scrub_content(&s);
let secret_prefixes = [
"sk-", "sk_live_", "sk_test_", "AKIA", "ghp_", "gho_",
"xoxb-", "xoxp-", "AIza", "glpat-", "dckr_pat_",
];
for prefix in secret_prefixes {
assert!(
!result.contains(prefix),
"scrub_content must redact prefix: {prefix}"
);
}
}
}
}