syncable_cli/analyzer/security/
patterns.rs

1//! # Security Pattern Management
2//! 
3//! Centralized management of security patterns for different tools and services.
4
5use std::collections::HashMap;
6use regex::Regex;
7
8use super::{SecuritySeverity, SecurityCategory};
9
10/// Manager for organizing security patterns by tool/service
11pub struct SecretPatternManager {
12    patterns_by_tool: HashMap<String, Vec<ToolPattern>>,
13    generic_patterns: Vec<GenericPattern>,
14}
15
16/// Tool-specific pattern (e.g., Firebase, Stripe, etc.)
17#[derive(Debug, Clone)]
18pub struct ToolPattern {
19    pub tool_name: String,
20    pub pattern_type: String, // e.g., "api_key", "config_object", "token"
21    pub pattern: Regex,
22    pub severity: SecuritySeverity,
23    pub description: String,
24    pub public_safe: bool, // Whether this type of key is safe to expose publicly
25    pub context_keywords: Vec<String>, // Keywords that increase confidence
26    pub false_positive_keywords: Vec<String>, // Keywords that suggest false positive
27}
28
29/// Generic patterns that apply across tools
30#[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    /// Initialize patterns for specific tools/services
52    fn initialize_tool_patterns() -> Result<HashMap<String, Vec<ToolPattern>>, regex::Error> {
53        let mut patterns = HashMap::new();
54        
55        // Firebase patterns
56        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, // Firebase API keys are safe to expose
62                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        // Stripe patterns
80        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, // Publishable keys are meant to be public
86                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        // Supabase patterns
114        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, // Anon keys are meant for client-side
120                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        // Clerk patterns
138        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        // Auth0 patterns
162        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        // AWS patterns
196        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        // OpenAI patterns
220        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        // Vercel patterns
234        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        // Netlify patterns
248        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    /// Initialize generic patterns that apply across tools
265    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    /// Get patterns for a specific tool
313    pub fn get_tool_patterns(&self, tool: &str) -> Option<&Vec<ToolPattern>> {
314        self.patterns_by_tool.get(tool)
315    }
316    
317    /// Get all generic patterns
318    pub fn get_generic_patterns(&self) -> &Vec<GenericPattern> {
319        &self.generic_patterns
320    }
321    
322    /// Get all supported tools
323    pub fn get_supported_tools(&self) -> Vec<String> {
324        self.patterns_by_tool.keys().cloned().collect()
325    }
326    
327    /// Get patterns for JavaScript/TypeScript frameworks
328    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    /// Check if this pattern should be treated as a high-confidence match given the context
345    pub fn assess_confidence(&self, file_content: &str, line_content: &str) -> f32 {
346        let mut confidence: f32 = 0.5; // Base confidence
347        
348        // Increase confidence for context keywords
349        for keyword in &self.context_keywords {
350            if file_content.to_lowercase().contains(&keyword.to_lowercase()) {
351                confidence += 0.2;
352            }
353        }
354        
355        // Decrease confidence for false positive indicators
356        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    /// Get severity adjusted for public safety
366    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}