syncable_cli/analyzer/security/
patterns.rs

1//! # Security Pattern Management
2//!
3//! Centralized management of security patterns for different tools and services.
4
5use regex::Regex;
6use std::collections::HashMap;
7
8use super::{SecurityCategory, SecuritySeverity};
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(
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, // Firebase API keys are safe to expose
66                    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        // Stripe patterns
100        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, // Publishable keys are meant to be public
108                    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        // Supabase patterns
137        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, // Anon keys are meant for client-side
143                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        // Clerk patterns
161        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        // Auth0 patterns
192        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        // AWS patterns
226        patterns.insert("aws".to_string(), vec![
227            ToolPattern {
228                tool_name: "AWS".to_string(),
229                pattern_type: "access_key".to_string(),
230                // More specific - must be in assignment context
231                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        // OpenAI patterns
251        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        // Vercel patterns
266        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        // Netlify patterns
281        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    /// Initialize generic patterns that apply across tools
299    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                // More specific - ensure it's a real assignment with a token value
305                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                // More specific JWT pattern - must be properly formatted and in assignment context
316                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                // More specific - require longer keys and exclude common false positives
345                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    /// Get patterns for a specific tool
358    pub fn get_tool_patterns(&self, tool: &str) -> Option<&Vec<ToolPattern>> {
359        self.patterns_by_tool.get(tool)
360    }
361
362    /// Get all generic patterns
363    pub fn get_generic_patterns(&self) -> &Vec<GenericPattern> {
364        &self.generic_patterns
365    }
366
367    /// Get all supported tools
368    pub fn get_supported_tools(&self) -> Vec<String> {
369        self.patterns_by_tool.keys().cloned().collect()
370    }
371
372    /// Get patterns for JavaScript/TypeScript frameworks
373    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    /// Check if this pattern should be treated as a high-confidence match given the context
393    pub fn assess_confidence(&self, file_content: &str, line_content: &str) -> f32 {
394        let mut confidence: f32 = 0.5; // Base confidence
395
396        // Increase confidence for context keywords
397        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        // Decrease confidence for false positive indicators
407        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    /// Get severity adjusted for public safety
420    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}