use std::sync::OnceLock;
use regex::Regex;
struct Pattern {
name: &'static str,
re: Regex,
}
fn patterns() -> &'static [Pattern] {
static P: OnceLock<Vec<Pattern>> = OnceLock::new();
P.get_or_init(|| {
let mk = |name: &'static str, re: &str| Pattern {
name,
re: Regex::new(re).expect("valid regex"),
};
vec![
mk("private key", r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY( BLOCK)?-----"),
mk("AWS access key", r"\b(AKIA|ASIA)[0-9A-Z]{16}\b"),
mk("GitHub token", r"\b(gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{22,})\b"),
mk("OpenAI or Anthropic key", r"\bsk-(ant-)?[A-Za-z0-9_-]{20,}\b"),
mk("Slack token", r"\bxox[abprs]-[A-Za-z0-9-]{10,}\b"),
mk("Slack app token", r"\bxapp-[A-Za-z0-9-]{10,}\b"),
mk("Google API key", r"\bAIza[0-9A-Za-z_-]{35}\b"),
mk("Stripe key", r"\b[sr]k_(live|test)_[0-9a-zA-Z]{24,}\b"),
mk("JWT", r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"),
mk("Diavlos key", r"ed25519:[0-9a-f]{64}\b(?:[^\S]|$)*\bsecret"),
mk(
"password or key set in plain text",
r#"(?i)\b(api[_-]?key|secret[_-]?key|access[_-]?token|auth[_-]?token|password)\b['"]?\s*[:=]\s*['"]?[A-Za-z0-9_\-/+=]{20,}"#,
),
]
})
}
pub fn find_secret(text: &str) -> Option<&'static str> {
patterns()
.iter()
.find(|p| p.re.is_match(text))
.map(|p| p.name)
}
pub fn scan_message(
text: &str,
data: &serde_json::Value,
action: Option<&crate::Action>,
trace: Option<&str>,
) -> Option<&'static str> {
if let Some(hit) = find_secret(text) {
return Some(hit);
}
if !data.is_null() {
if let Some(hit) = find_secret(&data.to_string()) {
return Some(hit);
}
}
if let Some(action) = action {
let flat = serde_json::to_string(action).unwrap_or_default();
if let Some(hit) = find_secret(&flat) {
return Some(hit);
}
}
trace.and_then(find_secret)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catches_common_keys() {
assert_eq!(
find_secret("here AKIAIOSFODNN7EXAMPLE ok"),
Some("AWS access key")
);
assert_eq!(
find_secret("token ghp_abcdefghijklmnopqrstuvwxyz0123456789ABCD"),
Some("GitHub token")
);
assert_eq!(
find_secret("use sk-ant-api03-abcdefghijklmnopqrstuvwxyz"),
Some("OpenAI or Anthropic key")
);
assert_eq!(
find_secret("-----BEGIN OPENSSH PRIVATE KEY-----\nabc"),
Some("private key")
);
assert_eq!(
find_secret("API_KEY=abcdefghijklmnopqrstuvwxyz1234"),
Some("password or key set in plain text")
);
assert_eq!(
find_secret("xoxb-1234567890-abcdefghij"),
Some("Slack token")
);
}
#[test]
fn leaves_normal_text_alone() {
for t in [
"found a bug in auth",
"deploy api-service v1.2 to prod?",
"the password field is empty",
"seq 42 prev sha256:9f3a",
"please set the API key in the vault, not here",
] {
assert_eq!(find_secret(t), None, "{t}");
}
}
#[test]
fn scans_data_too() {
let data = serde_json::json!({"env": {"AWS": "AKIAIOSFODNN7EXAMPLE"}});
assert_eq!(
scan_message("fine", &data, None, None),
Some("AWS access key")
);
assert_eq!(
scan_message("fine", &serde_json::Value::Null, None, None),
None
);
}
fn action(verb: &str, target: &str, params: serde_json::Value) -> crate::Action {
crate::Action {
verb: verb.into(),
target: target.into(),
params,
}
}
#[test]
fn scans_every_part_of_an_action() {
let null = serde_json::Value::Null;
let key = "AKIAIOSFODNN7EXAMPLE";
let in_params = action("deploy", "prod", serde_json::json!({"aws_key": key}));
assert_eq!(
scan_message("fine", &null, Some(&in_params), None),
Some("AWS access key")
);
let in_target = action("deploy", key, serde_json::Value::Null);
assert_eq!(
scan_message("fine", &null, Some(&in_target), None),
Some("AWS access key")
);
let clean = action(
"deploy",
"api-service",
serde_json::json!({"version": "1.2"}),
);
assert_eq!(scan_message("fine", &null, Some(&clean), None), None);
}
#[test]
fn scans_the_trace() {
let null = serde_json::Value::Null;
assert!(scan_message(
"fine",
&null,
None,
Some("ghp_abcdefghijklmnopqrstuvwxyz0123456789ABCD")
)
.is_some());
assert_eq!(scan_message("fine", &null, None, Some("TICKET-42")), None);
}
#[test]
fn catches_a_password_written_as_json() {
let null = serde_json::Value::Null;
let data = serde_json::json!({"password": "hunter2hunter2hunter2hunter2"});
assert!(scan_message("fine", &data, None, None).is_some());
let params = action(
"login",
"db",
serde_json::json!({"api_key": "abcdefghijklmnopqrstuvwxyz012345"}),
);
assert!(scan_message("fine", &null, Some(¶ms), None).is_some());
let talk = serde_json::json!({"note": "reset the password tomorrow"});
assert_eq!(scan_message("fine", &talk, None, None), None);
}
}