Skip to main content

mur_common/skill/scan/
secrets.rs

1use 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}