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#"AKIA[0-9A-Z]{16}"#)?,
201 severity: SecuritySeverity::Critical,
202 description: "AWS access key ID (CRITICAL)".to_string(),
203 public_safe: false,
204 context_keywords: vec!["aws".to_string(), "access".to_string(), "key".to_string()],
205 false_positive_keywords: vec![],
206 },
207 ToolPattern {
208 tool_name: "AWS".to_string(),
209 pattern_type: "secret_key".to_string(),
210 pattern: Regex::new(r#"(?i)(?:aws[_-]?secret|secret[_-]?access[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']"#)?,
211 severity: SecuritySeverity::Critical,
212 description: "AWS secret access key (CRITICAL)".to_string(),
213 public_safe: false,
214 context_keywords: vec!["aws".to_string(), "secret".to_string()],
215 false_positive_keywords: vec![],
216 },
217 ]);
218
219 patterns.insert("openai".to_string(), vec![
221 ToolPattern {
222 tool_name: "OpenAI".to_string(),
223 pattern_type: "api_key".to_string(),
224 pattern: Regex::new(r#"sk-[A-Za-z0-9]{48}"#)?,
225 severity: SecuritySeverity::High,
226 description: "OpenAI API key".to_string(),
227 public_safe: false,
228 context_keywords: vec!["openai".to_string(), "gpt".to_string(), "api".to_string()],
229 false_positive_keywords: vec![],
230 },
231 ]);
232
233 patterns.insert("vercel".to_string(), vec![
235 ToolPattern {
236 tool_name: "Vercel".to_string(),
237 pattern_type: "token".to_string(),
238 pattern: Regex::new(r#"(?i)vercel.*token.*["\'][a-zA-Z0-9]{24,}["\']"#)?,
239 severity: SecuritySeverity::High,
240 description: "Vercel deployment token".to_string(),
241 public_safe: false,
242 context_keywords: vec!["vercel".to_string(), "deploy".to_string()],
243 false_positive_keywords: vec![],
244 },
245 ]);
246
247 patterns.insert("netlify".to_string(), vec![
249 ToolPattern {
250 tool_name: "Netlify".to_string(),
251 pattern_type: "access_token".to_string(),
252 pattern: Regex::new(r#"(?i)netlify.*token.*["\'][a-zA-Z0-9_-]{40,}["\']"#)?,
253 severity: SecuritySeverity::High,
254 description: "Netlify access token".to_string(),
255 public_safe: false,
256 context_keywords: vec!["netlify".to_string(), "deploy".to_string()],
257 false_positive_keywords: vec![],
258 },
259 ]);
260
261 Ok(patterns)
262 }
263
264 fn initialize_generic_patterns() -> Result<Vec<GenericPattern>, regex::Error> {
266 let patterns = vec![
267 GenericPattern {
268 id: "bearer-token".to_string(),
269 name: "Bearer Token".to_string(),
270 pattern: Regex::new(r#"(?i)(?:authorization|bearer)\s*[:=]\s*["'](?:bearer\s+)?([A-Za-z0-9_-]{20,})["']"#)?,
271 severity: SecuritySeverity::Critical,
272 category: SecurityCategory::SecretsExposure,
273 description: "Bearer token in authorization header".to_string(),
274 },
275 GenericPattern {
276 id: "jwt-token".to_string(),
277 name: "JWT Token".to_string(),
278 pattern: Regex::new(r#"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+"#)?,
279 severity: SecuritySeverity::Medium,
280 category: SecurityCategory::SecretsExposure,
281 description: "JSON Web Token detected".to_string(),
282 },
283 GenericPattern {
284 id: "database-url".to_string(),
285 name: "Database Connection URL".to_string(),
286 pattern: Regex::new(r#"(?i)(?:mongodb|postgres|mysql)://[^"'\s]+:[^"'\s]+@[^"'\s]+"#)?,
287 severity: SecuritySeverity::Critical,
288 category: SecurityCategory::SecretsExposure,
289 description: "Database connection string with credentials".to_string(),
290 },
291 GenericPattern {
292 id: "private-key".to_string(),
293 name: "Private Key".to_string(),
294 pattern: Regex::new(r#"-----BEGIN (?:RSA |OPENSSH |PGP )?PRIVATE KEY-----"#)?,
295 severity: SecuritySeverity::Critical,
296 category: SecurityCategory::SecretsExposure,
297 description: "Private key detected".to_string(),
298 },
299 GenericPattern {
300 id: "generic-api-key".to_string(),
301 name: "Generic API Key".to_string(),
302 pattern: Regex::new(r#"(?i)(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']"#)?,
303 severity: SecuritySeverity::High,
304 category: SecurityCategory::SecretsExposure,
305 description: "Generic API key pattern".to_string(),
306 },
307 ];
308
309 Ok(patterns)
310 }
311
312 pub fn get_tool_patterns(&self, tool: &str) -> Option<&Vec<ToolPattern>> {
314 self.patterns_by_tool.get(tool)
315 }
316
317 pub fn get_generic_patterns(&self) -> &Vec<GenericPattern> {
319 &self.generic_patterns
320 }
321
322 pub fn get_supported_tools(&self) -> Vec<String> {
324 self.patterns_by_tool.keys().cloned().collect()
325 }
326
327 pub fn get_js_framework_patterns(&self) -> Vec<&ToolPattern> {
329 let js_tools = ["firebase", "stripe", "supabase", "clerk", "auth0", "vercel", "netlify"];
330 js_tools.iter()
331 .filter_map(|tool| self.patterns_by_tool.get(*tool))
332 .flat_map(|patterns| patterns.iter())
333 .collect()
334 }
335}
336
337impl Default for SecretPatternManager {
338 fn default() -> Self {
339 Self::new().expect("Failed to initialize security patterns")
340 }
341}
342
343impl ToolPattern {
344 pub fn assess_confidence(&self, file_content: &str, line_content: &str) -> f32 {
346 let mut confidence: f32 = 0.5; for keyword in &self.context_keywords {
350 if file_content.to_lowercase().contains(&keyword.to_lowercase()) {
351 confidence += 0.2;
352 }
353 }
354
355 for indicator in &self.false_positive_keywords {
357 if line_content.to_lowercase().contains(&indicator.to_lowercase()) {
358 confidence -= 0.3;
359 }
360 }
361
362 confidence.clamp(0.0, 1.0)
363 }
364
365 pub fn effective_severity(&self) -> SecuritySeverity {
367 if self.public_safe {
368 match &self.severity {
369 SecuritySeverity::Critical => SecuritySeverity::Medium,
370 SecuritySeverity::High => SecuritySeverity::Low,
371 other => other.clone(),
372 }
373 } else {
374 self.severity.clone()
375 }
376 }
377}