pub const SECRET_PREFIXES: &[&str] = &[
"sk-",
"sk_live_",
"sk_test_",
"AKIA",
"ghp_",
"gho_",
"-----BEGIN",
"xoxb-",
"xoxp-",
"AIza",
"ya29.",
"glpat-",
"hf_",
"npm_",
"dckr_pat_",
];
pub const PATH_PREFIXES: &[&str] = &["/home/", "/Users/", "/root/", "/tmp/", "/var/"];
pub const BEARER_TOKEN_PATTERN: &str = r"(?i)(Authorization:\s*Bearer\s+)\S+";
pub const JWT_PATTERN: &str = r"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*";
pub const PEM_PRIVATE_KEY_PATTERN: &str = r"(?s)(?:-----BEGIN (?:[A-Z]+ )?PRIVATE KEY(?: BLOCK)?-----|---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----).{0,8192}?(?:-----END (?:[A-Z]+ )?PRIVATE KEY(?: BLOCK)?-----|---- END SSH2 ENCRYPTED PRIVATE KEY ----)";
pub const PEM_BODY_CAP: usize = 8192;
pub const PEM_PRIVATE_KEY_UNTERMINATED_PATTERN: &str = r"(?:-----BEGIN (?:[A-Z]+ )?PRIVATE KEY(?: BLOCK)?-----|---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----)[A-Za-z0-9+/=\s]{0,8192}";
pub const AWS_SECRET_KEY_PATTERN: &str = r#"(?i)((?:aws[_\-. ]?secret[_\-. ]?access[_\-. ]?key|aws[_\-. ]?secret[_\-. ]?key|secret[_\-. ]?access[_\-. ]?key|aws[_\-. ]?session[_\-. ]?token|session[_\-. ]?token)['"]?\s*[:=]\s*['"]?)[A-Za-z0-9+/]{40,}={0,2}(['"]?)"#;
#[cfg(test)]
mod tests {
use regex::Regex;
use super::*;
#[test]
fn bearer_pattern_compiles_and_matches() {
let re = Regex::new(BEARER_TOKEN_PATTERN).unwrap();
assert!(re.is_match("Authorization: Bearer abc.def.ghi"));
}
#[test]
fn jwt_pattern_compiles_and_matches() {
let re = Regex::new(JWT_PATTERN).unwrap();
assert!(re.is_match("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.sig"));
}
#[test]
fn jwt_pattern_matches_alg_none_empty_signature() {
let re = Regex::new(JWT_PATTERN).unwrap();
assert!(re.is_match("eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1c2VyIn0."));
}
#[test]
fn secret_prefixes_build_valid_escaped_regex_alternation() {
let pattern = SECRET_PREFIXES
.iter()
.map(|p| regex::escape(p))
.collect::<Vec<_>>()
.join("|");
let full = format!("(?:{pattern})[^\\s]*");
let re = Regex::new(&full).expect("alternation built from SECRET_PREFIXES must compile");
assert!(re.is_match("sk-abc123"));
assert!(re.is_match("ya29.a0AfH6"));
}
#[test]
fn path_prefixes_build_valid_regex_alternation() {
let pattern = PATH_PREFIXES.join("|");
let full = format!("(?:{pattern})[^\\s]*");
let re = Regex::new(&full).expect("alternation built from PATH_PREFIXES must compile");
assert!(re.is_match("/home/user/file"));
}
#[test]
fn pem_pattern_compiles_and_matches_plain_private_key() {
let re = Regex::new(PEM_PRIVATE_KEY_PATTERN).unwrap();
let pem =
"-----BEGIN PRIVATE KEY-----\nMIIBVQIBADANBgkqhkiG9w0B\n-----END PRIVATE KEY-----";
assert!(re.is_match(pem));
}
#[test]
fn pem_pattern_matches_common_label_variants() {
let re = Regex::new(PEM_PRIVATE_KEY_PATTERN).unwrap();
for label in ["RSA", "EC", "DSA", "OPENSSH", "ENCRYPTED"] {
let pem = format!(
"-----BEGIN {label} PRIVATE KEY-----\nbase64body\n-----END {label} PRIVATE KEY-----"
);
assert!(re.is_match(&pem), "failed for label: {label}");
}
}
#[test]
fn pem_pattern_matches_pgp_block_and_ssh2_rfc4716_variants() {
let re = Regex::new(PEM_PRIVATE_KEY_PATTERN).unwrap();
let pgp = "-----BEGIN PGP PRIVATE KEY BLOCK-----\nbase64body\n-----END PGP PRIVATE KEY BLOCK-----";
assert!(re.is_match(pgp), "failed for PGP PRIVATE KEY BLOCK");
let ssh2 = "---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\nbase64body\n---- END SSH2 ENCRYPTED PRIVATE KEY ----";
assert!(re.is_match(ssh2), "failed for RFC 4716 SSH2 marker");
}
#[test]
fn pem_pattern_matches_mismatched_header_footer_labels() {
let re = Regex::new(PEM_PRIVATE_KEY_PATTERN).unwrap();
let mismatched =
"-----BEGIN RSA PRIVATE KEY-----\nbase64body\n-----END EC PRIVATE KEY-----";
assert!(
re.is_match(mismatched),
"mismatched header/footer labels must still match (accepted over-match tradeoff)"
);
}
#[test]
fn pem_pattern_does_not_match_partial_markers() {
let re = Regex::new(PEM_PRIVATE_KEY_PATTERN).unwrap();
assert!(!re.is_match("this text mentions BEGIN and PRIVATE but no PEM markers"));
assert!(!re.is_match("-----BEGIN PRIVATE KEY----- with no matching end marker"));
}
#[test]
fn pem_unterminated_pattern_matches_footerless_header() {
let re = Regex::new(PEM_PRIVATE_KEY_UNTERMINATED_PATTERN).unwrap();
assert!(re.is_match("-----BEGIN RSA PRIVATE KEY-----\nbase64body with no end marker"));
assert!(!re.is_match("this text mentions BEGIN and PRIVATE but no PEM markers"));
}
#[test]
fn pem_unterminated_pattern_body_is_bounded() {
let re = Regex::new(PEM_PRIVATE_KEY_UNTERMINATED_PATTERN).unwrap();
let huge_body = "A".repeat(200_000);
let text = format!("-----BEGIN RSA PRIVATE KEY-----\n{huge_body}");
let m = re
.find(&text)
.expect("unterminated header must still match");
let header_len = "-----BEGIN RSA PRIVATE KEY-----".len();
assert!(
m.len() <= header_len + PEM_BODY_CAP,
"match length {} exceeds header + {PEM_BODY_CAP}-char body cap",
m.len()
);
}
#[test]
fn pem_unterminated_pattern_handles_repeated_begin_with_no_end() {
let re = Regex::new(PEM_PRIVATE_KEY_UNTERMINATED_PATTERN).unwrap();
let text = "-----BEGIN RSA PRIVATE KEY-----\nfirst\n-----BEGIN EC PRIVATE KEY-----\nsecond";
assert!(re.is_match(text));
}
#[test]
fn pem_unterminated_pattern_stops_at_first_non_pem_character() {
let re = Regex::new(PEM_PRIVATE_KEY_UNTERMINATED_PATTERN).unwrap();
let text =
"Found -----BEGIN RSA PRIVATE KEY----- in file /etc/ssl/key.pem and the deploy failed";
let m = re.find(text).expect("header must still match");
assert!(
!m.as_str().contains("and the deploy failed"),
"match must stop well before swallowing unrelated trailing prose: {:?}",
m.as_str()
);
assert!(
text[m.end()..].contains("and the deploy failed"),
"trailing prose must remain outside the match, available for the caller to keep: {:?}",
&text[m.end()..]
);
}
#[test]
fn aws_secret_pattern_compiles_and_matches_marker_anchored_value() {
let re = Regex::new(AWS_SECRET_KEY_PATTERN).unwrap();
assert!(re.is_match("aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"));
}
#[test]
fn aws_secret_pattern_matches_camelcase_json_key_form() {
let re = Regex::new(AWS_SECRET_KEY_PATTERN).unwrap();
assert!(re.is_match(r#""SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY""#));
assert!(re.is_match(r#""SessionToken": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY""#));
}
#[test]
fn aws_secret_pattern_matches_alternate_separator_forms() {
let re = Regex::new(AWS_SECRET_KEY_PATTERN).unwrap();
assert!(re.is_match("aws-secret-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"));
assert!(re.is_match("aws.secret.key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"));
assert!(re.is_match("aws secret key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"));
assert!(re.is_match("aws_session_token=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"));
}
#[test]
fn aws_secret_pattern_redacts_full_longer_than_standard_value() {
let re = Regex::new(AWS_SECRET_KEY_PATTERN).unwrap();
let long_value = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEYEXTRA1234"; let text = format!("aws_secret_access_key={long_value}");
let m = re.find(&text).expect("must match");
assert!(
m.as_str().ends_with(long_value),
"match must cover the full value, not just the first 40 chars: {}",
m.as_str()
);
}
#[test]
fn aws_secret_pattern_ignores_unanchored_high_entropy_string() {
let re = Regex::new(AWS_SECRET_KEY_PATTERN).unwrap();
assert!(!re.is_match("commit sha or hash: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"));
}
#[test]
fn aws_secret_pattern_ignores_marker_like_identifier_suffix() {
let re = Regex::new(AWS_SECRET_KEY_PATTERN).unwrap();
assert!(!re.is_match("let aws_secret_access_key_length = 40"));
}
}