mur_common/skill/scan/
secrets.rs1use regex_lite::Regex;
2use std::sync::OnceLock;
3
4fn patterns() -> &'static [(Regex, &'static str)] {
5 static P: OnceLock<Vec<(Regex, &'static str)>> = OnceLock::new();
6 P.get_or_init(|| {
7 vec![
8 (Regex::new(r"\bsk-[a-zA-Z0-9]{20,}\b").unwrap(), "openai_key"),
9 (Regex::new(r"\bsk-ant-[a-zA-Z0-9-]{20,}\b").unwrap(), "anthropic_key"),
10 (Regex::new(r"\bAKIA[0-9A-Z]{16}\b").unwrap(), "aws_access_key"),
11 (
12 Regex::new(r"\baws_secret_access_key\s*[:=]\s*[A-Za-z0-9/+=]{40}\b").unwrap(),
13 "aws_secret_key",
14 ),
15 (Regex::new(r"\bghp_[A-Za-z0-9]{36}\b").unwrap(), "github_pat"),
16 (Regex::new(r"\bghs_[A-Za-z0-9]{36}\b").unwrap(), "github_app_token"),
17 (Regex::new(r"\bAIza[0-9A-Za-z_-]{35}\b").unwrap(), "gcp_api_key"),
18 (
19 Regex::new(r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b").unwrap(),
20 "jwt",
21 ),
22 (
23 Regex::new(r"-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----").unwrap(),
24 "pem_private_key",
25 ),
26 (
27 Regex::new(r"\bhooks\.slack\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+\b").unwrap(),
28 "slack_webhook",
29 ),
30 (
31 Regex::new(
32 r"(?i)\b(api_key|api_secret|secret_key|access_token|password|token)\s*[:=]\s*[A-Za-z0-9_\-./+=]{20,}\b",
33 )
34 .unwrap(),
35 "env_assignment",
36 ),
37 ]
38 })
39}
40
41#[derive(Debug, PartialEq, Eq)]
42pub struct SecretFinding {
43 pub label: &'static str,
44 pub matched: String,
45}
46
47pub fn scan_secrets(body: &str) -> Vec<SecretFinding> {
48 let mut out = Vec::new();
49 for (rx, label) in patterns() {
50 for m in rx.find_iter(body) {
51 out.push(SecretFinding {
52 label,
53 matched: m.as_str().to_string(),
54 });
55 }
56 }
57 out
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn detects_openai_key() {
66 let f = scan_secrets("here is my key: sk-abcd1234567890efghij1234");
67 assert!(f.iter().any(|x| x.label == "openai_key"));
68 }
69
70 #[test]
71 fn detects_anthropic_key() {
72 let f = scan_secrets("sk-ant-abcdefghijklmnopqrst-1234");
73 assert!(f.iter().any(|x| x.label == "anthropic_key"));
74 }
75
76 #[test]
77 fn detects_aws_access_key() {
78 let f = scan_secrets("AKIAIOSFODNN7EXAMPLE");
79 assert!(f.iter().any(|x| x.label == "aws_access_key"));
80 }
81
82 #[test]
83 fn detects_github_pat() {
84 let f = scan_secrets("ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
85 assert!(f.iter().any(|x| x.label == "github_pat"));
86 }
87
88 #[test]
89 fn detects_jwt() {
90 let jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.SflKxwRJSMeKKF2QT4fwpMeJf36";
91 let f = scan_secrets(jwt);
92 assert!(f.iter().any(|x| x.label == "jwt"));
93 }
94
95 #[test]
96 fn clean_body_returns_empty() {
97 assert!(scan_secrets("nothing to see").is_empty());
98 }
99}