const SCAN_WINDOW: usize = 8192;
pub struct SecretHit {
pub description: &'static str,
}
pub fn scan(data: &[u8]) -> Vec<SecretHit> {
let window = &data[..data.len().min(SCAN_WINDOW)];
let mut hits = Vec::new();
if contains(window, b"-----BEGIN") && contains(window, b"PRIVATE KEY-----") {
hits.push(SecretHit {
description: "a PEM private key",
});
}
if has_prefixed_run(window, b"AKIA", |n| n == 16, is_upper_or_digit)
|| has_prefixed_run(window, b"ASIA", |n| n == 16, is_upper_or_digit)
{
hits.push(SecretHit {
description: "an AWS access key ID",
});
}
for prefix in [&b"ghp_"[..], b"gho_", b"ghu_", b"ghs_", b"ghr_"] {
if has_prefixed_run(window, prefix, |n| n >= 36, |b| b.is_ascii_alphanumeric()) {
hits.push(SecretHit {
description: "a GitHub access token",
});
break;
}
}
if contains(window, b"sk-ant-") {
hits.push(SecretHit {
description: "an Anthropic API key",
});
}
for prefix in [&b"xoxa-"[..], b"xoxb-", b"xoxp-", b"xoxr-", b"xoxs-"] {
if contains(window, prefix) {
hits.push(SecretHit {
description: "a Slack token",
});
break;
}
}
hits
}
fn is_upper_or_digit(b: u8) -> bool {
b.is_ascii_uppercase() || b.is_ascii_digit()
}
fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || needle.len() > hay.len() {
return None;
}
hay.windows(needle.len()).position(|w| w == needle)
}
fn contains(hay: &[u8], needle: &[u8]) -> bool {
find(hay, needle).is_some()
}
fn has_prefixed_run(
window: &[u8],
prefix: &[u8],
len_ok: impl Fn(usize) -> bool,
run_char: impl Fn(u8) -> bool,
) -> bool {
let mut start = 0;
while let Some(rel) = find(&window[start..], prefix) {
let pos = start + rel;
let run_start = pos + prefix.len();
let mut run_len = 0;
while run_start + run_len < window.len() && run_char(window[run_start + run_len]) {
run_len += 1;
}
if len_ok(run_len) {
return true;
}
start = pos + 1;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn descriptions(data: &[u8]) -> Vec<&'static str> {
scan(data).into_iter().map(|h| h.description).collect()
}
#[test]
fn detects_pem_private_key() {
let key = b"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQ==\n-----END RSA PRIVATE KEY-----\n";
assert_eq!(descriptions(key), vec!["a PEM private key"]);
}
#[test]
fn detects_pem_private_key_with_no_type_name() {
assert_eq!(
descriptions(b"-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----"),
vec!["a PEM private key"]
);
}
#[test]
fn detects_aws_access_key() {
assert_eq!(descriptions(b"AKIAIOSFODNN7EXAMPLE"), vec!["an AWS access key ID"]);
assert_eq!(
descriptions(b"key=ASIAABCDEFGHIJ123456 rest"),
vec!["an AWS access key ID"]
);
}
#[test]
fn detects_github_token() {
let tok = "ghp_".to_string() + &"a".repeat(36);
assert_eq!(descriptions(tok.as_bytes()), vec!["a GitHub access token"]);
let long_tok = "gho_".to_string() + &"B3x9".repeat(20); assert_eq!(descriptions(long_tok.as_bytes()), vec!["a GitHub access token"]);
}
#[test]
fn detects_anthropic_key() {
assert_eq!(
descriptions(b"ANTHROPIC_API_KEY=sk-ant-api03-abc123xyz"),
vec!["an Anthropic API key"]
);
}
#[test]
fn detects_slack_token() {
assert_eq!(descriptions(b"xoxb-1234567890-abcdefg"), vec!["a Slack token"]);
assert_eq!(descriptions(b"xoxp-0000000000-1111111111"), vec!["a Slack token"]);
}
#[test]
fn multiple_distinct_hits_are_all_reported() {
let mixed = format!(
"AKIAIOSFODNN7EXAMPLE and sk-ant-abcdef and ghp_{}",
"x".repeat(36)
);
let mut ds = descriptions(mixed.as_bytes());
ds.sort_unstable();
assert_eq!(
ds,
vec!["a GitHub access token", "an AWS access key ID", "an Anthropic API key"]
);
}
#[test]
fn ordinary_prose_is_clean() {
let text = b"Dear team, please review the AKIA presentation deck and \
the sk-ant guidelines before Friday's standup. Thanks!";
assert!(descriptions(text).is_empty(), "{:?}", descriptions(text));
}
#[test]
fn short_prefix_without_a_real_body_is_clean() {
assert!(descriptions(b"AKIA is a term you might see in logs").is_empty());
assert!(descriptions(b"AKIAABC123").is_empty());
assert!(descriptions(b"ghp_short").is_empty());
assert!(descriptions(b"gho_only20charslongxx").is_empty());
}
#[test]
fn aws_run_longer_than_sixteen_does_not_match() {
assert!(descriptions(b"AKIAAAAAAAAAAAAAAAAAAAAA").is_empty());
}
#[test]
fn ordinary_uppercase_identifiers_are_clean() {
assert!(descriptions(b"CONST_MAX_RETRY_COUNT_1234567890AB = 5").is_empty());
}
#[test]
fn json_config_without_real_secrets_is_clean() {
let cfg = br#"{"region":"us-east-1","role":"admin","xox_flag":false}"#;
assert!(descriptions(cfg).is_empty());
}
#[test]
fn plain_hex_and_base64_blobs_are_clean() {
let hex = b"deadbeefcafebabe0123456789abcdef0123456789abcdef0123456789abcd";
assert!(descriptions(hex).is_empty());
}
#[test]
fn empty_input_is_clean() {
assert!(descriptions(b"").is_empty());
}
#[test]
fn scan_window_is_bounded_to_8kb() {
let mut beyond = vec![b'x'; SCAN_WINDOW];
beyond.extend_from_slice(b"AKIAIOSFODNN7EXAMPLE");
assert!(descriptions(&beyond).is_empty());
let mut fits = vec![b'x'; SCAN_WINDOW - 20];
fits.extend_from_slice(b"AKIAIOSFODNN7EXAMPLE");
assert_eq!(fits.len(), SCAN_WINDOW);
assert_eq!(descriptions(&fits), vec!["an AWS access key ID"]);
let mut truncated = vec![b'x'; SCAN_WINDOW - 5];
truncated.extend_from_slice(b"AKIAIOSFODNN7EXAMPLE");
assert!(descriptions(&truncated).is_empty());
}
}