syncable_cli/analyzer/security/
javascript.rs

1//! # JavaScript/TypeScript Security Analyzer
2//! 
3//! Specialized security analyzer for JavaScript and TypeScript applications.
4//! 
5//! This analyzer focuses on:
6//! - Framework-specific secret patterns (React, Vue, Angular, etc.)
7//! - Environment variable misuse
8//! - Hardcoded API keys in configuration objects
9//! - Client-side secret exposure patterns
10//! - Common JS/TS anti-patterns
11
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::fs;
15use regex::Regex;
16use log::{debug, info};
17
18use super::{SecurityError, SecurityFinding, SecuritySeverity, SecurityCategory, SecurityReport, SecurityAnalysisConfig, GitIgnoreAnalyzer, GitIgnoreRisk};
19
20/// JavaScript/TypeScript specific security analyzer
21pub struct JavaScriptSecurityAnalyzer {
22    config: SecurityAnalysisConfig,
23    js_patterns: Vec<JavaScriptSecretPattern>,
24    framework_patterns: HashMap<String, Vec<FrameworkPattern>>,
25    env_var_patterns: Vec<EnvVarPattern>,
26    gitignore_analyzer: Option<GitIgnoreAnalyzer>,
27}
28
29/// JavaScript-specific secret pattern
30#[derive(Debug, Clone)]
31pub struct JavaScriptSecretPattern {
32    pub id: String,
33    pub name: String,
34    pub pattern: Regex,
35    pub severity: SecuritySeverity,
36    pub description: String,
37    pub context_indicators: Vec<String>, // Code context that increases confidence
38    pub false_positive_indicators: Vec<String>, // Context that suggests false positive
39}
40
41/// Framework-specific patterns
42#[derive(Debug, Clone)]
43pub struct FrameworkPattern {
44    pub pattern: Regex,
45    pub severity: SecuritySeverity,
46    pub description: String,
47    pub file_extensions: Vec<String>,
48}
49
50/// Environment variable patterns
51#[derive(Debug, Clone)]
52pub struct EnvVarPattern {
53    pub pattern: Regex,
54    pub severity: SecuritySeverity,
55    pub description: String,
56    pub public_prefixes: Vec<String>, // Prefixes that indicate public env vars
57}
58
59impl JavaScriptSecurityAnalyzer {
60    pub fn new() -> Result<Self, SecurityError> {
61        Self::with_config(SecurityAnalysisConfig::default())
62    }
63    
64    pub fn with_config(config: SecurityAnalysisConfig) -> Result<Self, SecurityError> {
65        let js_patterns = Self::initialize_js_patterns()?;
66        let framework_patterns = Self::initialize_framework_patterns()?;
67        let env_var_patterns = Self::initialize_env_var_patterns()?;
68        
69        Ok(Self {
70            config,
71            js_patterns,
72            framework_patterns,
73            env_var_patterns,
74            gitignore_analyzer: None, // Will be initialized in analyze_project
75        })
76    }
77    
78    /// Analyze a JavaScript/TypeScript project
79    pub fn analyze_project(&mut self, project_root: &Path) -> Result<SecurityReport, SecurityError> {
80        let mut findings = Vec::new();
81        
82        // Initialize gitignore analyzer for comprehensive file protection assessment
83        let mut gitignore_analyzer = GitIgnoreAnalyzer::new(project_root)
84            .map_err(|e| SecurityError::AnalysisFailed(format!("Failed to initialize gitignore analyzer: {}", e)))?;
85        
86        info!("🔍 Using gitignore-aware security analysis for {}", project_root.display());
87        
88        // Get JS/TS files using gitignore-aware collection
89        let js_extensions = ["js", "jsx", "ts", "tsx", "vue", "svelte"];
90        let js_files = gitignore_analyzer.get_files_to_analyze(&js_extensions)
91            .map_err(|e| SecurityError::Io(e))?
92            .into_iter()
93            .filter(|file| {
94                if let Some(ext) = file.extension().and_then(|e| e.to_str()) {
95                    js_extensions.contains(&ext)
96                } else {
97                    false
98                }
99            })
100            .collect::<Vec<_>>();
101        
102        info!("Found {} JavaScript/TypeScript files to analyze (gitignore-filtered)", js_files.len());
103        
104        // Analyze each file with gitignore context
105        for file_path in &js_files {
106            let gitignore_status = gitignore_analyzer.analyze_file(file_path);
107            let mut file_findings = self.analyze_js_file(file_path)?;
108            
109            // Enhance findings with gitignore risk assessment
110            for finding in &mut file_findings {
111                self.enhance_finding_with_gitignore_status(finding, &gitignore_status);
112            }
113            
114            findings.extend(file_findings);
115        }
116        
117        // Analyze package.json and other config files with gitignore awareness
118        findings.extend(self.analyze_config_files_with_gitignore(project_root, &mut gitignore_analyzer)?);
119        
120        // Comprehensive environment file analysis with gitignore risk assessment
121        findings.extend(self.analyze_env_files_with_gitignore(project_root, &mut gitignore_analyzer)?);
122        
123        // Generate gitignore recommendations for any secret files found
124        let secret_files: Vec<PathBuf> = findings.iter()
125            .filter_map(|f| f.file_path.as_ref())
126            .cloned()
127            .collect();
128        
129        let gitignore_recommendations = gitignore_analyzer.generate_gitignore_recommendations(&secret_files);
130        
131        // Create report with enhanced recommendations
132        let mut report = SecurityReport::from_findings(findings);
133        report.recommendations.extend(gitignore_recommendations);
134        
135        Ok(report)
136    }
137    
138    /// Initialize JavaScript-specific secret patterns
139    fn initialize_js_patterns() -> Result<Vec<JavaScriptSecretPattern>, SecurityError> {
140        let patterns = vec![
141            // Firebase config object
142            JavaScriptSecretPattern {
143                id: "js-firebase-config".to_string(),
144                name: "Firebase Configuration Object".to_string(),
145                pattern: Regex::new(r#"(?i)(?:const\s+|let\s+|var\s+)?firebaseConfig\s*[=:]\s*\{[^}]*apiKey\s*:\s*["']([^"']+)["'][^}]*\}"#)?,
146                severity: SecuritySeverity::Medium,
147                description: "Firebase configuration object with API key detected".to_string(),
148                context_indicators: vec!["initializeApp".to_string(), "firebase".to_string()],
149                false_positive_indicators: vec!["example".to_string(), "placeholder".to_string(), "your-api-key".to_string()],
150            },
151            
152            // Stripe publishable key (less sensitive but should be noted)
153            JavaScriptSecretPattern {
154                id: "js-stripe-public-key".to_string(),
155                name: "Stripe Publishable Key".to_string(),
156                pattern: Regex::new(r#"(?i)pk_(?:test_|live_)[a-zA-Z0-9]{24,}"#)?,
157                severity: SecuritySeverity::Low,
158                description: "Stripe publishable key detected (public but should be environment variable)".to_string(),
159                context_indicators: vec!["stripe".to_string(), "payment".to_string()],
160                false_positive_indicators: vec![],
161            },
162            
163            // Supabase anon key
164            JavaScriptSecretPattern {
165                id: "js-supabase-anon-key".to_string(),
166                name: "Supabase Anonymous Key".to_string(),
167                pattern: Regex::new(r#"(?i)(?:supabase|anon).*?["\']eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+["\']"#)?,
168                severity: SecuritySeverity::Medium,
169                description: "Supabase anonymous key detected".to_string(),
170                context_indicators: vec!["supabase".to_string(), "createClient".to_string()],
171                false_positive_indicators: vec!["example".to_string(), "placeholder".to_string()],
172            },
173            
174            // Auth0 configuration
175            JavaScriptSecretPattern {
176                id: "js-auth0-config".to_string(),
177                name: "Auth0 Configuration".to_string(),
178                pattern: Regex::new(r#"(?i)(?:domain|clientId)\s*:\s*["']([a-zA-Z0-9.-]+\.auth0\.com|[a-zA-Z0-9]{32})["']"#)?,
179                severity: SecuritySeverity::Medium,
180                description: "Auth0 configuration detected".to_string(),
181                context_indicators: vec!["auth0".to_string(), "webAuth".to_string()],
182                false_positive_indicators: vec!["example".to_string(), "your-domain".to_string()],
183            },
184            
185            // Process.env hardcoded values
186            JavaScriptSecretPattern {
187                id: "js-hardcoded-env".to_string(),
188                name: "Hardcoded process.env Assignment".to_string(),
189                pattern: Regex::new(r#"process\.env\.[A-Z_]+\s*=\s*["']([^"']+)["']"#)?,
190                severity: SecuritySeverity::High,
191                description: "Hardcoded assignment to process.env detected".to_string(),
192                context_indicators: vec![],
193                false_positive_indicators: vec!["development".to_string(), "test".to_string()],
194            },
195            
196            // Clerk keys
197            JavaScriptSecretPattern {
198                id: "js-clerk-key".to_string(),
199                name: "Clerk API Key".to_string(),
200                pattern: Regex::new(r#"(?i)(?:clerk|pk_test_|pk_live_)[a-zA-Z0-9_-]{20,}"#)?,
201                severity: SecuritySeverity::Medium,
202                description: "Clerk API key detected".to_string(),
203                context_indicators: vec!["clerk".to_string(), "ClerkProvider".to_string()],
204                false_positive_indicators: vec![],
205            },
206            
207            // Generic API key in object assignment
208            JavaScriptSecretPattern {
209                id: "js-api-key-object".to_string(),
210                name: "API Key in Object Assignment".to_string(),
211                pattern: Regex::new(r#"(?i)(?:apiKey|api_key|clientSecret|client_secret|accessToken|access_token|secretKey|secret_key)\s*:\s*["']([A-Za-z0-9_-]{20,})["']"#)?,
212                severity: SecuritySeverity::High,
213                description: "API key or secret assigned in object literal".to_string(),
214                context_indicators: vec!["fetch".to_string(), "axios".to_string(), "headers".to_string()],
215                false_positive_indicators: vec!["process.env".to_string(), "import.meta.env".to_string(), "placeholder".to_string()],
216            },
217            
218            // Bearer tokens in fetch headers
219            JavaScriptSecretPattern {
220                id: "js-bearer-token".to_string(),
221                name: "Bearer Token in Code".to_string(),
222                pattern: Regex::new(r#"(?i)(?:authorization|bearer)\s*:\s*["'](?:bearer\s+)?([A-Za-z0-9_-]{20,})["']"#)?,
223                severity: SecuritySeverity::Critical,
224                description: "Bearer token hardcoded in authorization header".to_string(),
225                context_indicators: vec!["fetch".to_string(), "axios".to_string(), "headers".to_string()],
226                false_positive_indicators: vec!["${".to_string(), "process.env".to_string(), "import.meta.env".to_string()],
227            },
228            
229            // Database connection strings
230            JavaScriptSecretPattern {
231                id: "js-database-url".to_string(),
232                name: "Database Connection URL".to_string(),
233                pattern: Regex::new(r#"(?i)(?:mongodb|postgres|mysql)://[^"'\s]+:[^"'\s]+@[^"'\s]+"#)?,
234                severity: SecuritySeverity::Critical,
235                description: "Database connection string with credentials detected".to_string(),
236                context_indicators: vec!["connect".to_string(), "mongoose".to_string(), "client".to_string()],
237                false_positive_indicators: vec!["localhost".to_string(), "example.com".to_string()],
238            },
239        ];
240        
241        Ok(patterns)
242    }
243    
244    /// Initialize framework-specific patterns
245    fn initialize_framework_patterns() -> Result<HashMap<String, Vec<FrameworkPattern>>, SecurityError> {
246        let mut frameworks = HashMap::new();
247        
248        // React patterns
249        frameworks.insert("react".to_string(), vec![
250            FrameworkPattern {
251                pattern: Regex::new(r#"(?i)react_app_[a-z_]+\s*=\s*["']([^"']+)["']"#)?,
252                severity: SecuritySeverity::Medium,
253                description: "React environment variable potentially exposed in build".to_string(),
254                file_extensions: vec!["js".to_string(), "jsx".to_string(), "ts".to_string(), "tsx".to_string()],
255            },
256        ]);
257        
258        // Next.js patterns
259        frameworks.insert("nextjs".to_string(), vec![
260            FrameworkPattern {
261                pattern: Regex::new(r#"(?i)next_public_[a-z_]+\s*=\s*["']([^"']+)["']"#)?,
262                severity: SecuritySeverity::Low,
263                description: "Next.js public environment variable (ensure it should be public)".to_string(),
264                file_extensions: vec!["js".to_string(), "jsx".to_string(), "ts".to_string(), "tsx".to_string()],
265            },
266        ]);
267        
268        // Vite patterns
269        frameworks.insert("vite".to_string(), vec![
270            FrameworkPattern {
271                pattern: Regex::new(r#"(?i)vite_[a-z_]+\s*=\s*["']([^"']+)["']"#)?,
272                severity: SecuritySeverity::Medium,
273                description: "Vite environment variable potentially exposed in build".to_string(),
274                file_extensions: vec!["js".to_string(), "jsx".to_string(), "ts".to_string(), "tsx".to_string(), "vue".to_string()],
275            },
276        ]);
277        
278        Ok(frameworks)
279    }
280    
281    /// Initialize environment variable patterns
282    fn initialize_env_var_patterns() -> Result<Vec<EnvVarPattern>, SecurityError> {
283        let patterns = vec![
284            EnvVarPattern {
285                pattern: Regex::new(r#"process\.env\.([A-Z_]+)"#)?,
286                severity: SecuritySeverity::Info,
287                description: "Environment variable usage detected".to_string(),
288                public_prefixes: vec![
289                    "REACT_APP_".to_string(),
290                    "NEXT_PUBLIC_".to_string(),
291                    "VITE_".to_string(),
292                    "VUE_APP_".to_string(),
293                    "EXPO_PUBLIC_".to_string(),
294                    "NUXT_PUBLIC_".to_string(),
295                ],
296            },
297            EnvVarPattern {
298                pattern: Regex::new(r#"import\.meta\.env\.([A-Z_]+)"#)?,
299                severity: SecuritySeverity::Info,
300                description: "Vite environment variable usage detected".to_string(),
301                public_prefixes: vec!["VITE_".to_string()],
302            },
303        ];
304        
305        Ok(patterns)
306    }
307    
308    /// Collect all JavaScript/TypeScript files
309    fn collect_js_files(&self, project_root: &Path) -> Result<Vec<PathBuf>, SecurityError> {
310        let extensions = ["js", "jsx", "ts", "tsx", "vue", "svelte"];
311        let mut files = Vec::new();
312        
313        fn collect_recursive(dir: &Path, extensions: &[&str], files: &mut Vec<PathBuf>) -> Result<(), std::io::Error> {
314            for entry in fs::read_dir(dir)? {
315                let entry = entry?;
316                let path = entry.path();
317                
318                if path.is_dir() {
319                    // Skip common build/dependency directories
320                    if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
321                        if matches!(dir_name, "node_modules" | ".git" | "build" | "dist" | ".next" | "coverage") {
322                            continue;
323                        }
324                    }
325                    collect_recursive(&path, extensions, files)?;
326                } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
327                    if extensions.contains(&ext) {
328                        files.push(path);
329                    }
330                }
331            }
332            Ok(())
333        }
334        
335        collect_recursive(project_root, &extensions, &mut files)?;
336        Ok(files)
337    }
338    
339    /// Analyze a single JavaScript/TypeScript file
340    fn analyze_js_file(&self, file_path: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
341        let content = fs::read_to_string(file_path)?;
342        let mut findings = Vec::new();
343        
344        // Check against JavaScript-specific patterns
345        for pattern in &self.js_patterns {
346            findings.extend(self.check_pattern_in_content(&content, pattern, file_path)?);
347        }
348        
349        // Check environment variable usage
350        findings.extend(self.check_env_var_usage(&content, file_path)?);
351        
352        Ok(findings)
353    }
354    
355    /// Check a specific pattern in file content
356    fn check_pattern_in_content(
357        &self,
358        content: &str,
359        pattern: &JavaScriptSecretPattern,
360        file_path: &Path,
361    ) -> Result<Vec<SecurityFinding>, SecurityError> {
362        let mut findings = Vec::new();
363        
364        for (line_num, line) in content.lines().enumerate() {
365            if let Some(captures) = pattern.pattern.captures(line) {
366                // Check for false positive indicators
367                if pattern.false_positive_indicators.iter().any(|indicator| {
368                    line.to_lowercase().contains(&indicator.to_lowercase())
369                }) {
370                    debug!("Skipping potential false positive in {}: {}", file_path.display(), line.trim());
371                    continue;
372                }
373                
374                // Extract the secret value and position if captured
375                let (evidence, column_number) = if captures.len() > 1 {
376                    if let Some(match_) = captures.get(1) {
377                        (Some(match_.as_str().to_string()), Some(match_.start() + 1))
378                    } else {
379                        (Some(line.trim().to_string()), None)
380                    }
381                } else {
382                    // For patterns without capture groups, use the full match
383                    if let Some(match_) = captures.get(0) {
384                        (Some(line.trim().to_string()), Some(match_.start() + 1))
385                    } else {
386                        (Some(line.trim().to_string()), None)
387                    }
388                };
389                
390                // Check context for confidence scoring
391                let context_score = self.calculate_context_confidence(content, &pattern.context_indicators);
392                let adjusted_severity = self.adjust_severity_by_context(pattern.severity.clone(), context_score);
393                
394                findings.push(SecurityFinding {
395                    id: format!("{}-{}", pattern.id, line_num),
396                    title: format!("{} Detected", pattern.name),
397                    description: format!("{} (Context confidence: {:.1})", pattern.description, context_score),
398                    severity: adjusted_severity,
399                    category: SecurityCategory::SecretsExposure,
400                    file_path: Some(file_path.to_path_buf()),
401                    line_number: Some(line_num + 1),
402                    column_number,
403                    evidence,
404                    remediation: self.generate_js_remediation(&pattern.id),
405                    references: vec![
406                        "https://owasp.org/www-project-top-ten/2021/A05_2021-Security_Misconfiguration/".to_string(),
407                        "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html".to_string(),
408                    ],
409                    cwe_id: Some("CWE-200".to_string()),
410                    compliance_frameworks: vec!["SOC2".to_string(), "GDPR".to_string()],
411                });
412            }
413        }
414        
415        Ok(findings)
416    }
417    
418    /// Check environment variable usage patterns with context-aware detection
419    fn check_env_var_usage(&self, content: &str, file_path: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
420        let mut findings = Vec::new();
421        
422        // Determine if this is likely server-side or client-side code
423        let is_server_side = self.is_server_side_file(file_path, content);
424        
425        for pattern in &self.env_var_patterns {
426            for (line_num, line) in content.lines().enumerate() {
427                if let Some(captures) = pattern.pattern.captures(line) {
428                    if let Some(var_name) = captures.get(1) {
429                        let var_name = var_name.as_str();
430                        
431                        // Check if this is a public environment variable
432                        let is_public = pattern.public_prefixes.iter().any(|prefix| var_name.starts_with(prefix));
433                        
434                        // Context-aware detection: Only flag as problematic if:
435                        // 1. It's a sensitive variable AND
436                        // 2. It's in client-side code AND 
437                        // 3. It doesn't have a public prefix
438                        if !is_public && self.is_sensitive_var_name(var_name) && !is_server_side {
439                            // Extract column position from the pattern match
440                            let column_number = captures.get(0)
441                                .map(|m| m.start() + 1);
442                            
443                            findings.push(SecurityFinding {
444                                id: format!("js-env-sensitive-{}", line_num),
445                                title: "Sensitive Environment Variable in Client Code".to_string(),
446                                description: format!("Environment variable '{}' appears sensitive and may be exposed to client in browser code", var_name),
447                                severity: SecuritySeverity::High,
448                                category: SecurityCategory::SecretsExposure,
449                                file_path: Some(file_path.to_path_buf()),
450                                line_number: Some(line_num + 1),
451                                column_number,
452                                evidence: Some(line.trim().to_string()),
453                                remediation: vec![
454                                    "Move sensitive environment variables to server-side code".to_string(),
455                                    "Use public environment variable prefixes only for non-sensitive data".to_string(),
456                                    "Consider using a backend API endpoint to handle sensitive operations".to_string(),
457                                ],
458                                references: vec![
459                                    "https://nextjs.org/docs/basic-features/environment-variables".to_string(),
460                                    "https://vitejs.dev/guide/env-and-mode.html".to_string(),
461                                ],
462                                cwe_id: Some("CWE-200".to_string()),
463                                compliance_frameworks: vec!["SOC2".to_string()],
464                            });
465                        }
466                        // For server-side code using environment variables, this is GOOD practice - don't flag it
467                    }
468                }
469            }
470        }
471        
472        Ok(findings)
473    }
474    
475    /// Analyze configuration files (package.json, etc.)
476    fn analyze_config_files(&self, project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
477        let mut findings = Vec::new();
478        
479        // Check package.json for exposed scripts or configs
480        let package_json = project_root.join("package.json");
481        if package_json.exists() {
482            findings.extend(self.analyze_package_json(&package_json)?);
483        }
484        
485        Ok(findings)
486    }
487    
488    /// Analyze package.json for security issues
489    fn analyze_package_json(&self, package_json: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
490        let mut findings = Vec::new();
491        let content = fs::read_to_string(package_json)?;
492        
493        // Look for hardcoded secrets in scripts or config
494        if content.contains("REACT_APP_") || content.contains("NEXT_PUBLIC_") || content.contains("VITE_") {
495            for (line_num, line) in content.lines().enumerate() {
496                if line.contains("sk_") || line.contains("pk_live_") || line.contains("eyJ") {
497                    findings.push(SecurityFinding {
498                        id: format!("package-json-secret-{}", line_num),
499                        title: "Potential Secret in package.json".to_string(),
500                        description: "Potential API key or token found in package.json".to_string(),
501                        severity: SecuritySeverity::High,
502                        category: SecurityCategory::SecretsExposure,
503                        file_path: Some(package_json.to_path_buf()),
504                        line_number: Some(line_num + 1),
505                        column_number: None,
506                        evidence: Some(line.trim().to_string()),
507                        remediation: vec![
508                            "Remove secrets from package.json".to_string(),
509                            "Use environment variables instead".to_string(),
510                            "Add package.json to .gitignore if it contains secrets (not recommended)".to_string(),
511                        ],
512                        references: vec![
513                            "https://docs.npmjs.com/cli/v8/configuring-npm/package-json".to_string(),
514                        ],
515                        cwe_id: Some("CWE-200".to_string()),
516                        compliance_frameworks: vec!["SOC2".to_string()],
517                    });
518                }
519            }
520        }
521        
522        Ok(findings)
523    }
524    
525    /// Analyze environment files
526    fn analyze_env_files(&self, project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
527        let mut findings = Vec::new();
528        
529        // Check for .env files that might be accidentally committed
530        let env_files = [".env", ".env.local", ".env.production", ".env.development"];
531        
532        for env_file in &env_files {
533            // Skip template/example files
534            if self.is_template_file(env_file) {
535                debug!("Skipping template env file: {}", env_file);
536                continue;
537            }
538            
539            let env_path = project_root.join(env_file);
540            if env_path.exists() {
541                // Check if this file should be tracked by git
542                findings.push(SecurityFinding {
543                    id: format!("env-file-{}", env_file.replace('.', "-")),
544                    title: "Environment File Detected".to_string(),
545                    description: format!("Environment file '{}' found - ensure it's properly protected", env_file),
546                    severity: SecuritySeverity::Medium,
547                    category: SecurityCategory::SecretsExposure,
548                    file_path: Some(env_path),
549                    line_number: None,
550                    column_number: None,
551                    evidence: None,
552                    remediation: vec![
553                        "Ensure environment files are in .gitignore".to_string(),
554                        "Use .env.example files for documentation".to_string(),
555                        "Never commit actual environment files to version control".to_string(),
556                    ],
557                    references: vec![
558                        "https://github.com/motdotla/dotenv#should-i-commit-my-env-file".to_string(),
559                    ],
560                    cwe_id: Some("CWE-200".to_string()),
561                    compliance_frameworks: vec!["SOC2".to_string()],
562                });
563            }
564        }
565        
566        Ok(findings)
567    }
568    
569    /// Calculate confidence score based on context indicators
570    fn calculate_context_confidence(&self, content: &str, indicators: &[String]) -> f32 {
571        let total_indicators = indicators.len() as f32;
572        if total_indicators == 0.0 {
573            return 0.5; // Neutral confidence
574        }
575        
576        let found_indicators = indicators.iter()
577            .filter(|indicator| content.to_lowercase().contains(&indicator.to_lowercase()))
578            .count() as f32;
579        
580        found_indicators / total_indicators
581    }
582    
583    /// Adjust severity based on context confidence
584    fn adjust_severity_by_context(&self, base_severity: SecuritySeverity, confidence: f32) -> SecuritySeverity {
585        match base_severity {
586            SecuritySeverity::Critical => base_severity, // Keep critical as-is
587            SecuritySeverity::High => {
588                if confidence < 0.3 {
589                    SecuritySeverity::Medium
590                } else {
591                    base_severity
592                }
593            }
594            SecuritySeverity::Medium => {
595                if confidence > 0.7 {
596                    SecuritySeverity::High
597                } else if confidence < 0.3 {
598                    SecuritySeverity::Low
599                } else {
600                    base_severity
601                }
602            }
603            _ => base_severity,
604        }
605    }
606    
607    /// Check if a variable name appears sensitive
608    fn is_sensitive_var_name(&self, var_name: &str) -> bool {
609        let sensitive_keywords = [
610            "SECRET", "KEY", "TOKEN", "PASSWORD", "PASS", "AUTH", "API",
611            "PRIVATE", "CREDENTIAL", "CERT", "SSL", "TLS", "OAUTH",
612            "CLIENT_SECRET", "ACCESS_TOKEN", "REFRESH_TOKEN",
613        ];
614        
615        let var_upper = var_name.to_uppercase();
616        sensitive_keywords.iter().any(|keyword| var_upper.contains(keyword))
617    }
618    
619    /// Determine if a JavaScript file is likely server-side or client-side
620    fn is_server_side_file(&self, file_path: &Path, content: &str) -> bool {
621        // Check file path indicators
622        let path_str = file_path.to_string_lossy().to_lowercase();
623        let server_path_indicators = [
624            "/server/", "/backend/", "/api/", "/routes/", "/controllers/",
625            "/middleware/", "/models/", "/services/", "/utils/", "/lib/",
626            "server.js", "server.ts", "index.js", "index.ts", "app.js", "app.ts",
627            "/pages/api/", "/app/api/", // Next.js API routes
628            "server-side", "backend", "node_modules", // Clear server indicators
629        ];
630        
631        let client_path_indicators = [
632            "/client/", "/frontend/", "/public/", "/static/", "/assets/",
633            "/components/", "/views/", "/pages/", "/src/components/",
634            "client.js", "client.ts", "main.js", "main.ts", "app.tsx", "index.html",
635        ];
636        
637        // Strong server-side path indicators
638        if server_path_indicators.iter().any(|indicator| path_str.contains(indicator)) {
639            return true;
640        }
641        
642        // Strong client-side path indicators
643        if client_path_indicators.iter().any(|indicator| path_str.contains(indicator)) {
644            return false;
645        }
646        
647        // Check content for server-side indicators
648        let server_content_indicators = [
649            "require(", "module.exports", "exports.", "__dirname", "__filename",
650            "process.env", "process.exit", "process.argv", "fs.readFile", "fs.writeFile",
651            "http.createServer", "express(", "app.listen", "app.use", "app.get", "app.post",
652            "import express", "import fs", "import path", "import http", "import https",
653            "cors(", "bodyParser", "middleware", "mongoose.connect", "sequelize",
654            "jwt.sign", "bcrypt", "crypto.createHash", "nodemailer", "socket.io",
655            "console.log", // While not exclusive, very common in server code
656        ];
657        
658        let client_content_indicators = [
659            "document.", "window.", "navigator.", "localStorage", "sessionStorage",
660            "addEventListener", "querySelector", "getElementById", "fetch(",
661            "XMLHttpRequest", "React.", "ReactDOM", "useState", "useEffect",
662            "Vue.", "Angular", "svelte", "alert(", "confirm(", "prompt(",
663            "location.href", "history.push", "router.push", "browser",
664        ];
665        
666        let server_matches = server_content_indicators.iter()
667            .filter(|&indicator| content.contains(indicator))
668            .count();
669            
670        let client_matches = client_content_indicators.iter()
671            .filter(|&indicator| content.contains(indicator))
672            .count();
673        
674        // If we have server indicators and no clear client indicators, assume server-side
675        if server_matches > 0 && client_matches == 0 {
676            return true;
677        }
678        
679        // If we have client indicators and no server indicators, assume client-side
680        if client_matches > 0 && server_matches == 0 {
681            return false;
682        }
683        
684        // If mixed or unclear, use a heuristic
685        if server_matches > client_matches {
686            return true;
687        }
688        
689        // Default to client-side for mixed/unclear files (safer for security)
690        false
691    }
692    
693    /// Generate JavaScript-specific remediation advice
694    fn generate_js_remediation(&self, pattern_id: &str) -> Vec<String> {
695        match pattern_id {
696            id if id.contains("firebase") => vec![
697                "Move Firebase configuration to environment variables".to_string(),
698                "Use Firebase App Check for additional security".to_string(),
699                "Implement proper Firebase security rules".to_string(),
700            ],
701            id if id.contains("stripe") => vec![
702                "Use environment variables for Stripe keys".to_string(),
703                "Ensure you're using publishable keys in client-side code".to_string(),
704                "Keep secret keys on the server side only".to_string(),
705            ],
706            id if id.contains("bearer") => vec![
707                "Never hardcode bearer tokens in client-side code".to_string(),
708                "Use secure token storage mechanisms".to_string(),
709                "Implement token refresh flows".to_string(),
710            ],
711            _ => vec![
712                "Move secrets to environment variables".to_string(),
713                "Use server-side API routes for sensitive operations".to_string(),
714                "Implement proper secret management practices".to_string(),
715            ],
716        }
717    }
718    
719    /// Enhance a security finding with gitignore risk assessment
720    fn enhance_finding_with_gitignore_status(
721        &self,
722        finding: &mut SecurityFinding,
723        gitignore_status: &super::gitignore::GitIgnoreStatus,
724    ) {
725        // Adjust severity based on gitignore risk
726        finding.severity = match gitignore_status.risk_level {
727            GitIgnoreRisk::Tracked => SecuritySeverity::Critical, // Always critical if tracked
728            GitIgnoreRisk::Exposed => {
729                // Upgrade severity if exposed
730                match &finding.severity {
731                    SecuritySeverity::Medium => SecuritySeverity::High,
732                    SecuritySeverity::Low => SecuritySeverity::Medium,
733                    other => other.clone(),
734                }
735            }
736            GitIgnoreRisk::Protected => {
737                // Downgrade slightly if protected
738                match &finding.severity {
739                    SecuritySeverity::Critical => SecuritySeverity::High,
740                    SecuritySeverity::High => SecuritySeverity::Medium,
741                    other => other.clone(),
742                }
743            }
744            GitIgnoreRisk::Safe => finding.severity.clone(),
745        };
746        
747        // Add gitignore context to description
748        finding.description.push_str(&format!(" (GitIgnore: {})", gitignore_status.description()));
749        
750        // Add gitignore-specific remediation
751        let gitignore_action = gitignore_status.recommended_action();
752        if gitignore_action != "No action needed" {
753            finding.remediation.insert(0, format!("🔒 GitIgnore: {}", gitignore_action));
754        }
755        
756        // Add git history warning for tracked files
757        if gitignore_status.risk_level == GitIgnoreRisk::Tracked {
758            finding.remediation.insert(1, "⚠️ CRITICAL: Remove this file from git history using git-filter-branch or BFG Repo-Cleaner".to_string());
759            finding.remediation.insert(2, "🔑 Rotate any exposed secrets immediately".to_string());
760        }
761    }
762    
763    /// Analyze configuration files with gitignore awareness
764    fn analyze_config_files_with_gitignore(
765        &self,
766        project_root: &Path,
767        gitignore_analyzer: &mut GitIgnoreAnalyzer,
768    ) -> Result<Vec<SecurityFinding>, SecurityError> {
769        let mut findings = Vec::new();
770        
771        // Check package.json with gitignore assessment
772        let package_json = project_root.join("package.json");
773        if package_json.exists() {
774            let gitignore_status = gitignore_analyzer.analyze_file(&package_json);
775            let mut package_findings = self.analyze_package_json(&package_json)?;
776            
777            // Enhance findings with gitignore context
778            for finding in &mut package_findings {
779                self.enhance_finding_with_gitignore_status(finding, &gitignore_status);
780            }
781            
782            findings.extend(package_findings);
783        }
784        
785        // Check other common config files
786        let config_files = [
787            "tsconfig.json",
788            "vite.config.js",
789            "vite.config.ts", 
790            "next.config.js",
791            "next.config.ts",
792            "nuxt.config.js",
793            "nuxt.config.ts",
794            // Note: .env.example is now excluded as it's a template file
795        ];
796        
797        for config_file in &config_files {
798            // Skip template/example files
799            if self.is_template_file(config_file) {
800                debug!("Skipping template config file: {}", config_file);
801                continue;
802            }
803            
804            let config_path = project_root.join(config_file);
805            if config_path.exists() {
806                let gitignore_status = gitignore_analyzer.analyze_file(&config_path);
807                
808                // Only analyze if file contains potential secrets or is not properly protected
809                if gitignore_status.should_be_ignored || !gitignore_status.is_ignored {
810                    if let Ok(content) = fs::read_to_string(&config_path) {
811                        // Basic secret pattern check for config files
812                        if self.contains_potential_secrets(&content) {
813                            let mut finding = SecurityFinding {
814                                id: format!("config-file-{}", config_file.replace('.', "-")),
815                                title: "Potential Secrets in Configuration File".to_string(),
816                                description: format!("Configuration file '{}' may contain secrets", config_file),
817                                severity: SecuritySeverity::Medium,
818                                category: SecurityCategory::SecretsExposure,
819                                file_path: Some(config_path.clone()),
820                                line_number: None,
821                                column_number: None,
822                                evidence: None,
823                                remediation: vec![
824                                    "Review configuration file for hardcoded secrets".to_string(),
825                                    "Use environment variables for sensitive configuration".to_string(),
826                                ],
827                                references: vec![],
828                                cwe_id: Some("CWE-200".to_string()),
829                                compliance_frameworks: vec!["SOC2".to_string()],
830                            };
831                            
832                            self.enhance_finding_with_gitignore_status(&mut finding, &gitignore_status);
833                            findings.push(finding);
834                        }
835                    }
836                }
837            }
838        }
839        
840        Ok(findings)
841    }
842    
843    /// Check if a file is a template/example file that should be excluded from security alerts
844    fn is_template_file(&self, file_name: &str) -> bool {
845        let template_indicators = [
846            "sample", "example", "template", "template.env", "env.template",
847            "sample.env", "env.sample", "example.env", "env.example",
848            "examples", "samples", "templates", "demo", "test", 
849            ".env.sample", ".env.example", ".env.template", ".env.demo", ".env.test"
850        ];
851        
852        let file_name_lower = file_name.to_lowercase();
853        
854        // Check for exact matches or contains patterns
855        template_indicators.iter().any(|indicator| {
856            file_name_lower == *indicator || 
857            file_name_lower.contains(indicator) ||
858            file_name_lower.ends_with(indicator)
859        })
860    }
861
862    /// Analyze environment files with comprehensive gitignore risk assessment
863    fn analyze_env_files_with_gitignore(
864        &self,
865        project_root: &Path,
866        gitignore_analyzer: &mut GitIgnoreAnalyzer,
867    ) -> Result<Vec<SecurityFinding>, SecurityError> {
868        let mut findings = Vec::new();
869        
870        // Get all potential environment files using gitignore analyzer
871        let env_files = gitignore_analyzer.get_files_to_analyze(&[])
872            .map_err(|e| SecurityError::Io(e))?
873            .into_iter()
874            .filter(|file| {
875                if let Some(file_name) = file.file_name().and_then(|n| n.to_str()) {
876                    // Exclude template/example files from security alerts
877                    if self.is_template_file(file_name) {
878                        debug!("Skipping template file: {}", file_name);
879                        return false;
880                    }
881                    
882                    file_name.starts_with(".env") || 
883                    file_name.contains("credentials") || 
884                    file_name.contains("secrets") ||
885                    file_name.contains("config") ||
886                    file_name.ends_with(".key") ||
887                    file_name.ends_with(".pem")
888                } else {
889                    false
890                }
891            })
892            .collect::<Vec<_>>();
893        
894        for env_file in env_files {
895            let gitignore_status = gitignore_analyzer.analyze_file(&env_file);
896            let relative_path = env_file.strip_prefix(project_root)
897                .unwrap_or(&env_file);
898            
899            // Create finding based on gitignore risk assessment
900            let (severity, title, description) = match gitignore_status.risk_level {
901                GitIgnoreRisk::Tracked => (
902                    SecuritySeverity::Critical,
903                    "Secret File Tracked by Git".to_string(),
904                    format!("Secret file '{}' is tracked by git and may expose credentials in version history", relative_path.display()),
905                ),
906                GitIgnoreRisk::Exposed => (
907                    SecuritySeverity::High,
908                    "Secret File Not in GitIgnore".to_string(),
909                    format!("Secret file '{}' exists but is not protected by .gitignore", relative_path.display()),
910                ),
911                GitIgnoreRisk::Protected => (
912                    SecuritySeverity::Info,
913                    "Secret File Properly Protected".to_string(),
914                    format!("Secret file '{}' is properly ignored but detected for verification", relative_path.display()),
915                ),
916                GitIgnoreRisk::Safe => continue, // Skip files that appear safe
917            };
918            
919            let mut finding = SecurityFinding {
920                id: format!("env-file-{}", relative_path.to_string_lossy().replace('/', "-").replace('.', "-")),
921                title,
922                description,
923                severity,
924                category: SecurityCategory::SecretsExposure,
925                file_path: Some(env_file.clone()),
926                line_number: None,
927                column_number: None,
928                evidence: None,
929                remediation: vec![
930                    "Ensure sensitive files are in .gitignore".to_string(),
931                    "Use .env.example files for documentation".to_string(),
932                    "Never commit actual environment files to version control".to_string(),
933                ],
934                references: vec![
935                    "https://github.com/motdotla/dotenv#should-i-commit-my-env-file".to_string(),
936                ],
937                cwe_id: Some("CWE-200".to_string()),
938                compliance_frameworks: vec!["SOC2".to_string()],
939            };
940            
941            self.enhance_finding_with_gitignore_status(&mut finding, &gitignore_status);
942            findings.push(finding);
943        }
944        
945        Ok(findings)
946    }
947    
948    /// Check if content contains potential secrets (basic patterns)
949    fn contains_potential_secrets(&self, content: &str) -> bool {
950        let secret_indicators = [
951            "sk_", "pk_live_", "eyJ", "AKIA", "-----BEGIN",
952            "client_secret", "api_key", "access_token",
953            "private_key", "secret_key", "bearer",
954        ];
955        
956        let content_lower = content.to_lowercase();
957        secret_indicators.iter().any(|indicator| content_lower.contains(&indicator.to_lowercase()))
958    }
959}
960
961impl SecurityReport {
962    /// Create a security report from a list of findings
963    pub fn from_findings(findings: Vec<SecurityFinding>) -> Self {
964        let total_findings = findings.len();
965        let mut findings_by_severity = HashMap::new();
966        let mut findings_by_category = HashMap::new();
967        
968        for finding in &findings {
969            *findings_by_severity.entry(finding.severity.clone()).or_insert(0) += 1;
970            *findings_by_category.entry(finding.category.clone()).or_insert(0) += 1;
971        }
972        
973        // Calculate overall score (simple implementation)
974        let score_penalty = findings.iter().map(|f| match f.severity {
975            SecuritySeverity::Critical => 25.0,
976            SecuritySeverity::High => 15.0,
977            SecuritySeverity::Medium => 8.0,
978            SecuritySeverity::Low => 3.0,
979            SecuritySeverity::Info => 1.0,
980        }).sum::<f32>();
981        
982        let overall_score = (100.0 - score_penalty).max(0.0);
983        
984        // Determine risk level
985        let risk_level = if findings.iter().any(|f| f.severity == SecuritySeverity::Critical) {
986            SecuritySeverity::Critical
987        } else if findings.iter().any(|f| f.severity == SecuritySeverity::High) {
988            SecuritySeverity::High
989        } else if findings.iter().any(|f| f.severity == SecuritySeverity::Medium) {
990            SecuritySeverity::Medium
991        } else if !findings.is_empty() {
992            SecuritySeverity::Low
993        } else {
994            SecuritySeverity::Info
995        };
996        
997        Self {
998            analyzed_at: chrono::Utc::now(),
999            overall_score,
1000            risk_level,
1001            total_findings,
1002            findings_by_severity,
1003            findings_by_category,
1004            findings,
1005            recommendations: vec![
1006                "Review all detected secrets and move them to environment variables".to_string(),
1007                "Implement proper secret management practices".to_string(),
1008                "Use framework-specific environment variable patterns correctly".to_string(),
1009            ],
1010            compliance_status: HashMap::new(),
1011        }
1012    }
1013}