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                // More specific - must be in assignment context
201                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        // OpenAI patterns
221        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        // Vercel patterns
235        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        // Netlify patterns
249        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    /// Initialize generic patterns that apply across tools
266    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                // More specific - exclude template literals and ensure it's a real assignment
272                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                // More specific JWT pattern - must be properly formatted and in assignment context
281                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                // More specific - require longer keys and exclude common false positives
306                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    /// Get patterns for a specific tool
317    pub fn get_tool_patterns(&self, tool: &str) -> Option<&Vec<ToolPattern>> {
318        self.patterns_by_tool.get(tool)
319    }
320    
321    /// Get all generic patterns
322    pub fn get_generic_patterns(&self) -> &Vec<GenericPattern> {
323        &self.generic_patterns
324    }
325    
326    /// Get all supported tools
327    pub fn get_supported_tools(&self) -> Vec<String> {
328        self.patterns_by_tool.keys().cloned().collect()
329    }
330    
331    /// Get patterns for JavaScript/TypeScript frameworks
332    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    /// Check if this pattern should be treated as a high-confidence match given the context
349    pub fn assess_confidence(&self, file_content: &str, line_content: &str) -> f32 {
350        let mut confidence: f32 = 0.5; // Base confidence
351        
352        // Increase confidence for context keywords
353        for keyword in &self.context_keywords {
354            if file_content.to_lowercase().contains(&keyword.to_lowercase()) {
355                confidence += 0.2;
356            }
357        }
358        
359        // Decrease confidence for false positive indicators
360        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    /// Get severity adjusted for public safety
370    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}