1use regex::Regex;
2
3pub struct Rule {
4 pub id: String,
5 pub description: String,
6 pub pattern: Regex,
7}
8
9pub fn built_in_rules() -> Vec<Rule> {
10 let rules_data: Vec<(&str, &str, &str)> = vec![
11 ("aws-access-key", "AWS Access Key ID", r"AKIA[0-9A-Z]{16}"),
13 (
14 "aws-secret-key",
15 "AWS Secret Access Key",
16 r"(?i)aws_secret_access_key\s*=\s*\S+",
17 ),
18 (
19 "aws-session-token",
20 "AWS Session Token",
21 r"(?i)aws_session_token\s*=\s*\S+",
22 ),
23 (
25 "anthropic-api-key",
26 "Anthropic API Key",
27 r"sk-ant-[a-zA-Z0-9_-]{20,}",
28 ),
29 ("openai-api-key", "OpenAI API Key", r"sk-[a-zA-Z0-9]{20,}"),
30 (
31 "openai-project-key",
32 "OpenAI Project Key",
33 r"sk-proj-[a-zA-Z0-9_-]{20,}",
34 ),
35 (
37 "github-pat",
38 "GitHub Personal Access Token",
39 r"ghp_[a-zA-Z0-9]{36}",
40 ),
41 ("github-oauth", "GitHub OAuth Token", r"gho_[a-zA-Z0-9]{36}"),
42 (
43 "github-fine-grained",
44 "GitHub Fine-Grained Token",
45 r"github_pat_[a-zA-Z0-9_]{22,}",
46 ),
47 (
48 "gitlab-pat",
49 "GitLab Personal Access Token",
50 r"glpat-[a-zA-Z0-9_-]{20,}",
51 ),
52 (
54 "stripe-secret",
55 "Stripe Secret Key",
56 r"sk_live_[a-zA-Z0-9]{24,}",
57 ),
58 (
59 "stripe-restricted",
60 "Stripe Restricted Key",
61 r"rk_live_[a-zA-Z0-9]{24,}",
62 ),
63 (
65 "jwt-token",
66 "JWT Token",
67 r"eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]+",
68 ),
69 (
70 "bearer-token",
71 "Bearer Token",
72 r"(?i)bearer\s+[a-zA-Z0-9_\-.]{20,}",
73 ),
74 (
76 "rsa-private-key",
77 "RSA Private Key",
78 r"-----BEGIN RSA PRIVATE KEY-----",
79 ),
80 (
81 "ec-private-key",
82 "EC Private Key",
83 r"-----BEGIN EC PRIVATE KEY-----",
84 ),
85 (
86 "pkcs8-private-key",
87 "PKCS8 Private Key",
88 r"-----BEGIN PRIVATE KEY-----",
89 ),
90 (
91 "openssh-private-key",
92 "OpenSSH Private Key",
93 r"-----BEGIN OPENSSH PRIVATE KEY-----",
94 ),
95 (
97 "postgres-uri",
98 "PostgreSQL Connection String",
99 r"postgres(?:ql)?://[^\s]+:[^\s]+@[^\s]+",
100 ),
101 (
102 "mysql-uri",
103 "MySQL Connection String",
104 r"mysql://[^\s]+:[^\s]+@[^\s]+",
105 ),
106 (
107 "mongodb-uri",
108 "MongoDB Connection String",
109 r"mongodb(?:\+srv)?://[^\s]+:[^\s]+@[^\s]+",
110 ),
111 (
112 "redis-uri",
113 "Redis Connection String",
114 r"redis://[^\s]*:[^\s]+@[^\s]+",
115 ),
116 (
118 "slack-token",
119 "Slack Token",
120 r"xox[baprs]-[a-zA-Z0-9-]{10,}",
121 ),
122 (
123 "slack-webhook",
124 "Slack Webhook URL",
125 r"https://hooks\.slack\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+",
126 ),
127 (
128 "discord-bot-token",
129 "Discord Bot Token",
130 r"[MN][a-zA-Z0-9_-]{23,}\.[a-zA-Z0-9_-]{6}\.[a-zA-Z0-9_-]{27,}",
131 ),
132 (
134 "sendgrid-key",
135 "SendGrid API Key",
136 r"SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}",
137 ),
138 (
139 "vault-token",
140 "HashiCorp Vault Token",
141 r"hvs\.[a-zA-Z0-9_-]{24,}",
142 ),
143 (
145 "generic-password-assign",
146 "Password Assignment",
147 r#"(?i)(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']{8,}"#,
148 ),
149 ];
150
151 rules_data
152 .into_iter()
153 .map(|(id, desc, pat)| Rule {
154 id: id.to_string(),
155 description: desc.to_string(),
156 pattern: Regex::new(pat).unwrap_or_else(|e| panic!("Bad regex for rule '{id}': {e}")),
157 })
158 .collect()
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn all_rules_compile() {
167 let rules = built_in_rules();
168 assert!(rules.len() >= 27, "Expected 27+ rules, got {}", rules.len());
169 }
170
171 #[test]
172 fn aws_access_key() {
173 let rules = built_in_rules();
174 let rule = rules.iter().find(|r| r.id == "aws-access-key").unwrap();
175 assert!(rule.pattern.is_match("AKIAIOSFODNN7EXAMPLE"));
176 assert!(!rule.pattern.is_match("not-an-aws-key"));
177 }
178
179 #[test]
180 fn anthropic_api_key() {
181 let rules = built_in_rules();
182 let rule = rules.iter().find(|r| r.id == "anthropic-api-key").unwrap();
183 assert!(rule.pattern.is_match("sk-ant-api03-abcdefghijklmnopqrst"));
184 assert!(!rule.pattern.is_match("sk-ant-short"));
185 }
186
187 #[test]
188 fn openai_api_key() {
189 let rules = built_in_rules();
190 let rule = rules.iter().find(|r| r.id == "openai-api-key").unwrap();
191 assert!(rule.pattern.is_match("sk-abcdefghijklmnopqrstuvwx"));
192 assert!(!rule.pattern.is_match("sk-short"));
193 }
194
195 #[test]
196 fn github_pat() {
197 let rules = built_in_rules();
198 let rule = rules.iter().find(|r| r.id == "github-pat").unwrap();
199 assert!(rule
200 .pattern
201 .is_match("ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij"));
202 assert!(!rule.pattern.is_match("ghp_short"));
203 }
204
205 #[test]
206 fn github_fine_grained() {
207 let rules = built_in_rules();
208 let rule = rules
209 .iter()
210 .find(|r| r.id == "github-fine-grained")
211 .unwrap();
212 assert!(rule.pattern.is_match("github_pat_11ABCDEFGH0123456789AB"));
213 assert!(!rule.pattern.is_match("github_pat_short"));
214 }
215
216 #[test]
217 fn gitlab_pat() {
218 let rules = built_in_rules();
219 let rule = rules.iter().find(|r| r.id == "gitlab-pat").unwrap();
220 assert!(rule.pattern.is_match("glpat-ABCDEFghijklmnopqrstu"));
221 assert!(!rule.pattern.is_match("glpat-short"));
222 }
223
224 #[test]
225 fn stripe_secret_key() {
226 let rules = built_in_rules();
227 let rule = rules.iter().find(|r| r.id == "stripe-secret").unwrap();
228 assert!(rule.pattern.is_match("sk_live_abcdefghijklmnopqrstuvwx"));
229 assert!(!rule.pattern.is_match("sk_test_abcdefghijklmnopqrstuvwx"));
230 }
231
232 #[test]
233 fn jwt_token() {
234 let rules = built_in_rules();
235 let rule = rules.iter().find(|r| r.id == "jwt-token").unwrap();
236 assert!(rule
237 .pattern
238 .is_match("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123def456ghi789"));
239 assert!(!rule.pattern.is_match("not.a.jwt"));
240 }
241
242 #[test]
243 fn rsa_private_key() {
244 let rules = built_in_rules();
245 let rule = rules.iter().find(|r| r.id == "rsa-private-key").unwrap();
246 assert!(rule.pattern.is_match("-----BEGIN RSA PRIVATE KEY-----"));
247 assert!(!rule.pattern.is_match("-----BEGIN PUBLIC KEY-----"));
248 }
249
250 #[test]
251 fn postgres_uri() {
252 let rules = built_in_rules();
253 let rule = rules.iter().find(|r| r.id == "postgres-uri").unwrap();
254 assert!(rule
255 .pattern
256 .is_match("postgresql://user:password@localhost:5432/db"));
257 assert!(rule
258 .pattern
259 .is_match("postgres://admin:secret@prod.db.com/mydb"));
260 assert!(!rule.pattern.is_match("postgres://localhost/db"));
261 }
262
263 #[test]
264 fn mongodb_uri() {
265 let rules = built_in_rules();
266 let rule = rules.iter().find(|r| r.id == "mongodb-uri").unwrap();
267 assert!(rule
268 .pattern
269 .is_match("mongodb+srv://user:pass@cluster.mongodb.net/db"));
270 assert!(!rule.pattern.is_match("mongodb://localhost/db"));
271 }
272
273 #[test]
274 fn slack_token() {
275 let rules = built_in_rules();
276 let rule = rules.iter().find(|r| r.id == "slack-token").unwrap();
277 assert!(rule.pattern.is_match("xoxb-123456789-abcdefghij"));
278 assert!(rule.pattern.is_match("xoxp-123456789-abcdefghij"));
279 assert!(!rule.pattern.is_match("xoxb-short"));
280 }
281
282 #[test]
283 fn sendgrid_key() {
284 let rules = built_in_rules();
285 let rule = rules.iter().find(|r| r.id == "sendgrid-key").unwrap();
286 assert!(rule
287 .pattern
288 .is_match("SG.abcdefghijklmnopqrstuv.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst"));
289 }
290
291 #[test]
292 fn vault_token() {
293 let rules = built_in_rules();
294 let rule = rules.iter().find(|r| r.id == "vault-token").unwrap();
295 assert!(rule.pattern.is_match("hvs.ABCDEFghijklmnopqrstuvwx"));
296 assert!(!rule.pattern.is_match("hvs.short"));
297 }
298
299 #[test]
300 fn generic_password() {
301 let rules = built_in_rules();
302 let rule = rules
303 .iter()
304 .find(|r| r.id == "generic-password-assign")
305 .unwrap();
306 assert!(rule.pattern.is_match("password=mysecretpassword"));
307 assert!(rule.pattern.is_match("PASSWORD: 'longpassword123'"));
308 assert!(!rule.pattern.is_match("password=short"));
309 }
310
311 #[test]
312 fn bearer_token() {
313 let rules = built_in_rules();
314 let rule = rules.iter().find(|r| r.id == "bearer-token").unwrap();
315 assert!(rule.pattern.is_match("Bearer abcdefghijklmnopqrstuvwx"));
316 assert!(rule.pattern.is_match("bearer abcdefghijklmnopqrstuvwx"));
317 assert!(!rule.pattern.is_match("Bearer short"));
318 }
319
320 #[test]
321 fn no_false_positive_on_normal_text() {
322 let rules = built_in_rules();
323 let normal_texts = [
324 "hello world",
325 "npm install express",
326 "const x = 42;",
327 "PATH=/usr/bin",
328 "HOME=/Users/dev",
329 "NODE_ENV=production",
330 ];
331 for text in &normal_texts {
332 for rule in &rules {
333 assert!(
334 !rule.pattern.is_match(text),
335 "Rule '{}' false-positive on: {}",
336 rule.id,
337 text
338 );
339 }
340 }
341 }
342}