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