1use regex::Regex;
6use std::collections::HashMap;
7
8use super::{SecurityCategory, SecuritySeverity};
9
10pub struct SecretPatternManager {
12 patterns_by_tool: HashMap<String, Vec<ToolPattern>>,
13 generic_patterns: Vec<GenericPattern>,
14}
15
16#[derive(Debug, Clone)]
18pub struct ToolPattern {
19 pub tool_name: String,
20 pub pattern_type: String, pub pattern: Regex,
22 pub severity: SecuritySeverity,
23 pub description: String,
24 pub public_safe: bool, pub context_keywords: Vec<String>, pub false_positive_keywords: Vec<String>, }
28
29#[derive(Debug, Clone)]
31pub struct GenericPattern {
32 pub id: String,
33 pub name: String,
34 pub pattern: Regex,
35 pub severity: SecuritySeverity,
36 pub category: SecurityCategory,
37 pub description: String,
38}
39
40impl SecretPatternManager {
41 pub fn new() -> Result<Self, regex::Error> {
42 let patterns_by_tool = Self::initialize_tool_patterns()?;
43 let generic_patterns = Self::initialize_generic_patterns()?;
44
45 Ok(Self {
46 patterns_by_tool,
47 generic_patterns,
48 })
49 }
50
51 fn initialize_tool_patterns() -> Result<HashMap<String, Vec<ToolPattern>>, regex::Error> {
53 let mut patterns = HashMap::new();
54
55 patterns.insert(
57 "firebase".to_string(),
58 vec![
59 ToolPattern {
60 tool_name: "Firebase".to_string(),
61 pattern_type: "api_key".to_string(),
62 pattern: Regex::new(
63 r#"(?i)(?:firebase.*)?apiKey\s*[:=]\s*["']([A-Za-z0-9_-]{39})["']"#,
64 )?,
65 severity: SecuritySeverity::Medium, description: "Firebase API key (safe to expose publicly)".to_string(),
67 public_safe: true,
68 context_keywords: vec![
69 "firebase".to_string(),
70 "initializeApp".to_string(),
71 "getApps".to_string(),
72 ],
73 false_positive_keywords: vec![
74 "example".to_string(),
75 "placeholder".to_string(),
76 "your-api-key".to_string(),
77 ],
78 },
79 ToolPattern {
80 tool_name: "Firebase".to_string(),
81 pattern_type: "service_account".to_string(),
82 pattern: Regex::new(
83 r#"(?i)(?:type|client_email|private_key).*firebase.*service_account"#,
84 )?,
85 severity: SecuritySeverity::Critical,
86 description: "Firebase service account credentials (CRITICAL - never expose)"
87 .to_string(),
88 public_safe: false,
89 context_keywords: vec![
90 "service_account".to_string(),
91 "private_key".to_string(),
92 "client_email".to_string(),
93 ],
94 false_positive_keywords: vec![],
95 },
96 ],
97 );
98
99 patterns.insert(
101 "stripe".to_string(),
102 vec![
103 ToolPattern {
104 tool_name: "Stripe".to_string(),
105 pattern_type: "publishable_key".to_string(),
106 pattern: Regex::new(r#"pk_(?:test_|live_)[a-zA-Z0-9]{24,}"#)?,
107 severity: SecuritySeverity::Low, description: "Stripe publishable key (safe for client-side use)".to_string(),
109 public_safe: true,
110 context_keywords: vec!["stripe".to_string(), "publishable".to_string()],
111 false_positive_keywords: vec![],
112 },
113 ToolPattern {
114 tool_name: "Stripe".to_string(),
115 pattern_type: "secret_key".to_string(),
116 pattern: Regex::new(r#"sk_(?:test_|live_)[a-zA-Z0-9]{24,}"#)?,
117 severity: SecuritySeverity::Critical,
118 description: "Stripe secret key (CRITICAL - server-side only)".to_string(),
119 public_safe: false,
120 context_keywords: vec!["stripe".to_string(), "secret".to_string()],
121 false_positive_keywords: vec![],
122 },
123 ToolPattern {
124 tool_name: "Stripe".to_string(),
125 pattern_type: "webhook_secret".to_string(),
126 pattern: Regex::new(r#"whsec_[a-zA-Z0-9]{32,}"#)?,
127 severity: SecuritySeverity::High,
128 description: "Stripe webhook endpoint secret".to_string(),
129 public_safe: false,
130 context_keywords: vec!["webhook".to_string(), "endpoint".to_string()],
131 false_positive_keywords: vec![],
132 },
133 ],
134 );
135
136 patterns.insert("supabase".to_string(), vec![
138 ToolPattern {
139 tool_name: "Supabase".to_string(),
140 pattern_type: "anon_key".to_string(),
141 pattern: Regex::new(r#"(?i)supabase.*anon.*["\']eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+["\']"#)?,
142 severity: SecuritySeverity::Medium, description: "Supabase anonymous key (safe for client-side use with RLS)".to_string(),
144 public_safe: true,
145 context_keywords: vec!["supabase".to_string(), "anon".to_string(), "createClient".to_string()],
146 false_positive_keywords: vec!["example".to_string(), "placeholder".to_string()],
147 },
148 ToolPattern {
149 tool_name: "Supabase".to_string(),
150 pattern_type: "service_role_key".to_string(),
151 pattern: Regex::new(r#"(?i)supabase.*service.*role.*["\']eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+["\']"#)?,
152 severity: SecuritySeverity::Critical,
153 description: "Supabase service role key (CRITICAL - server-side only)".to_string(),
154 public_safe: false,
155 context_keywords: vec!["service".to_string(), "role".to_string(), "bypass".to_string()],
156 false_positive_keywords: vec![],
157 },
158 ]);
159
160 patterns.insert(
162 "clerk".to_string(),
163 vec![
164 ToolPattern {
165 tool_name: "Clerk".to_string(),
166 pattern_type: "publishable_key".to_string(),
167 pattern: Regex::new(
168 r#"pk_test_[a-zA-Z0-9_-]{60,}|pk_live_[a-zA-Z0-9_-]{60,}"#,
169 )?,
170 severity: SecuritySeverity::Low,
171 description: "Clerk publishable key (safe for client-side use)".to_string(),
172 public_safe: true,
173 context_keywords: vec!["clerk".to_string(), "publishable".to_string()],
174 false_positive_keywords: vec![],
175 },
176 ToolPattern {
177 tool_name: "Clerk".to_string(),
178 pattern_type: "secret_key".to_string(),
179 pattern: Regex::new(
180 r#"sk_test_[a-zA-Z0-9_-]{60,}|sk_live_[a-zA-Z0-9_-]{60,}"#,
181 )?,
182 severity: SecuritySeverity::Critical,
183 description: "Clerk secret key (CRITICAL - server-side only)".to_string(),
184 public_safe: false,
185 context_keywords: vec!["clerk".to_string(), "secret".to_string()],
186 false_positive_keywords: vec![],
187 },
188 ],
189 );
190
191 patterns.insert("auth0".to_string(), vec![
193 ToolPattern {
194 tool_name: "Auth0".to_string(),
195 pattern_type: "domain".to_string(),
196 pattern: Regex::new(r#"[a-zA-Z0-9-]+\.auth0\.com"#)?,
197 severity: SecuritySeverity::Low,
198 description: "Auth0 domain (safe to expose)".to_string(),
199 public_safe: true,
200 context_keywords: vec!["auth0".to_string(), "domain".to_string()],
201 false_positive_keywords: vec!["example".to_string(), "your-domain".to_string()],
202 },
203 ToolPattern {
204 tool_name: "Auth0".to_string(),
205 pattern_type: "client_id".to_string(),
206 pattern: Regex::new(r#"(?i)(?:client_?id|clientId)\s*[:=]\s*["']([a-zA-Z0-9]{32})["']"#)?,
207 severity: SecuritySeverity::Low,
208 description: "Auth0 client ID (safe for client-side use)".to_string(),
209 public_safe: true,
210 context_keywords: vec!["auth0".to_string(), "client".to_string()],
211 false_positive_keywords: vec![],
212 },
213 ToolPattern {
214 tool_name: "Auth0".to_string(),
215 pattern_type: "client_secret".to_string(),
216 pattern: Regex::new(r#"(?i)(?:client_?secret|clientSecret)\s*[:=]\s*["']([a-zA-Z0-9_-]{64})["']"#)?,
217 severity: SecuritySeverity::Critical,
218 description: "Auth0 client secret (CRITICAL - server-side only)".to_string(),
219 public_safe: false,
220 context_keywords: vec!["auth0".to_string(), "secret".to_string()],
221 false_positive_keywords: vec![],
222 },
223 ]);
224
225 patterns.insert("aws".to_string(), vec![
227 ToolPattern {
228 tool_name: "AWS".to_string(),
229 pattern_type: "access_key".to_string(),
230 pattern: Regex::new(r#"(?i)(?:aws[_-]?access[_-]?key|access[_-]?key[_-]?id)\s*[:=]\s*["']?(AKIA[0-9A-Z]{16})["']?"#)?,
232 severity: SecuritySeverity::Critical,
233 description: "AWS access key ID in assignment (CRITICAL)".to_string(),
234 public_safe: false,
235 context_keywords: vec!["aws".to_string(), "access".to_string(), "key".to_string()],
236 false_positive_keywords: vec!["example".to_string(), "AKIAEXAMPLE".to_string()],
237 },
238 ToolPattern {
239 tool_name: "AWS".to_string(),
240 pattern_type: "secret_key".to_string(),
241 pattern: Regex::new(r#"(?i)(?:aws[_-]?secret|secret[_-]?access[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']"#)?,
242 severity: SecuritySeverity::Critical,
243 description: "AWS secret access key (CRITICAL)".to_string(),
244 public_safe: false,
245 context_keywords: vec!["aws".to_string(), "secret".to_string()],
246 false_positive_keywords: vec!["example".to_string(), "your_secret".to_string(), "placeholder".to_string()],
247 },
248 ]);
249
250 patterns.insert(
252 "openai".to_string(),
253 vec![ToolPattern {
254 tool_name: "OpenAI".to_string(),
255 pattern_type: "api_key".to_string(),
256 pattern: Regex::new(r#"sk-[A-Za-z0-9]{48}"#)?,
257 severity: SecuritySeverity::High,
258 description: "OpenAI API key".to_string(),
259 public_safe: false,
260 context_keywords: vec!["openai".to_string(), "gpt".to_string(), "api".to_string()],
261 false_positive_keywords: vec![],
262 }],
263 );
264
265 patterns.insert(
267 "vercel".to_string(),
268 vec![ToolPattern {
269 tool_name: "Vercel".to_string(),
270 pattern_type: "token".to_string(),
271 pattern: Regex::new(r#"(?i)vercel.*token.*["\'][a-zA-Z0-9]{24,}["\']"#)?,
272 severity: SecuritySeverity::High,
273 description: "Vercel deployment token".to_string(),
274 public_safe: false,
275 context_keywords: vec!["vercel".to_string(), "deploy".to_string()],
276 false_positive_keywords: vec![],
277 }],
278 );
279
280 patterns.insert(
282 "netlify".to_string(),
283 vec![ToolPattern {
284 tool_name: "Netlify".to_string(),
285 pattern_type: "access_token".to_string(),
286 pattern: Regex::new(r#"(?i)netlify.*token.*["\'][a-zA-Z0-9_-]{40,}["\']"#)?,
287 severity: SecuritySeverity::High,
288 description: "Netlify access token".to_string(),
289 public_safe: false,
290 context_keywords: vec!["netlify".to_string(), "deploy".to_string()],
291 false_positive_keywords: vec![],
292 }],
293 );
294
295 Ok(patterns)
296 }
297
298 fn initialize_generic_patterns() -> Result<Vec<GenericPattern>, regex::Error> {
300 let patterns = vec![
301 GenericPattern {
302 id: "bearer-token".to_string(),
303 name: "Bearer Token".to_string(),
304 pattern: Regex::new(
306 r#"(?i)(?:authorization|bearer)\s*[:=]\s*["'](?:bearer\s+)?([A-Za-z0-9_-]{32,})["']"#,
307 )?,
308 severity: SecuritySeverity::Critical,
309 category: SecurityCategory::SecretsExposure,
310 description: "Bearer token in authorization header".to_string(),
311 },
312 GenericPattern {
313 id: "jwt-token".to_string(),
314 name: "JWT Token".to_string(),
315 pattern: Regex::new(
317 r#"(?i)(?:token|jwt|authorization|bearer)\s*[:=]\s*["']?eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}["']?"#,
318 )?,
319 severity: SecuritySeverity::Medium,
320 category: SecurityCategory::SecretsExposure,
321 description: "JSON Web Token detected in assignment".to_string(),
322 },
323 GenericPattern {
324 id: "database-url".to_string(),
325 name: "Database Connection URL".to_string(),
326 pattern: Regex::new(
327 r#"(?i)(?:mongodb|postgres|mysql)://[^"'\s]+:[^"'\s]+@[^"'\s]+"#,
328 )?,
329 severity: SecuritySeverity::Critical,
330 category: SecurityCategory::SecretsExposure,
331 description: "Database connection string with credentials".to_string(),
332 },
333 GenericPattern {
334 id: "private-key".to_string(),
335 name: "Private Key".to_string(),
336 pattern: Regex::new(r#"-----BEGIN (?:RSA |OPENSSH |PGP )?PRIVATE KEY-----"#)?,
337 severity: SecuritySeverity::Critical,
338 category: SecurityCategory::SecretsExposure,
339 description: "Private key detected".to_string(),
340 },
341 GenericPattern {
342 id: "generic-api-key".to_string(),
343 name: "Generic API Key".to_string(),
344 pattern: Regex::new(
346 r#"(?i)(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{32,})["']"#,
347 )?,
348 severity: SecuritySeverity::High,
349 category: SecurityCategory::SecretsExposure,
350 description: "Generic API key pattern (32+ characters)".to_string(),
351 },
352 ];
353
354 Ok(patterns)
355 }
356
357 pub fn get_tool_patterns(&self, tool: &str) -> Option<&Vec<ToolPattern>> {
359 self.patterns_by_tool.get(tool)
360 }
361
362 pub fn get_generic_patterns(&self) -> &Vec<GenericPattern> {
364 &self.generic_patterns
365 }
366
367 pub fn get_supported_tools(&self) -> Vec<String> {
369 self.patterns_by_tool.keys().cloned().collect()
370 }
371
372 pub fn get_js_framework_patterns(&self) -> Vec<&ToolPattern> {
374 let js_tools = [
375 "firebase", "stripe", "supabase", "clerk", "auth0", "vercel", "netlify",
376 ];
377 js_tools
378 .iter()
379 .filter_map(|tool| self.patterns_by_tool.get(*tool))
380 .flat_map(|patterns| patterns.iter())
381 .collect()
382 }
383}
384
385impl Default for SecretPatternManager {
386 fn default() -> Self {
387 Self::new().expect("Failed to initialize security patterns")
388 }
389}
390
391impl ToolPattern {
392 pub fn assess_confidence(&self, file_content: &str, line_content: &str) -> f32 {
394 let mut confidence: f32 = 0.5; for keyword in &self.context_keywords {
398 if file_content
399 .to_lowercase()
400 .contains(&keyword.to_lowercase())
401 {
402 confidence += 0.2;
403 }
404 }
405
406 for indicator in &self.false_positive_keywords {
408 if line_content
409 .to_lowercase()
410 .contains(&indicator.to_lowercase())
411 {
412 confidence -= 0.3;
413 }
414 }
415
416 confidence.clamp(0.0, 1.0)
417 }
418
419 pub fn effective_severity(&self) -> SecuritySeverity {
421 if self.public_safe {
422 match &self.severity {
423 SecuritySeverity::Critical => SecuritySeverity::Medium,
424 SecuritySeverity::High => SecuritySeverity::Low,
425 other => other.clone(),
426 }
427 } else {
428 self.severity.clone()
429 }
430 }
431}