syncable_cli/analyzer/
security_analyzer.rs

1//! # Security Analyzer
2//! 
3//! Comprehensive security analysis module that performs multi-layered security assessment:
4//! - Configuration security analysis (secrets, insecure settings)
5//! - Code security patterns (language/framework-specific issues)
6//! - Infrastructure security (Docker, compose configurations)
7//! - Security policy recommendations and compliance guidance
8//! - Security scoring with actionable remediation steps
9
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::fs;
13use std::time::Instant;
14use std::process::Command;
15use regex::Regex;
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18use log::{info, debug};
19use rayon::prelude::*;
20use indicatif::{ProgressBar, ProgressStyle, MultiProgress};
21
22use crate::analyzer::{ProjectAnalysis, DetectedLanguage, DetectedTechnology, EnvVar};
23use crate::analyzer::dependency_parser::Language;
24use crate::analyzer::security::{
25    ModularSecurityAnalyzer, SecurityAnalysisConfig as NewSecurityAnalysisConfig
26};
27use crate::analyzer::security::core::SecurityReport as NewSecurityReport;
28
29#[derive(Debug, Error)]
30pub enum SecurityError {
31    #[error("Security analysis failed: {0}")]
32    AnalysisFailed(String),
33    
34    #[error("Configuration analysis error: {0}")]
35    ConfigAnalysisError(String),
36    
37    #[error("Code pattern analysis error: {0}")]
38    CodePatternError(String),
39    
40    #[error("Infrastructure analysis error: {0}")]
41    InfrastructureError(String),
42    
43    #[error("IO error: {0}")]
44    Io(#[from] std::io::Error),
45    
46    #[error("Regex error: {0}")]
47    Regex(#[from] regex::Error),
48}
49
50/// Security finding severity levels
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub enum SecuritySeverity {
53    Critical,
54    High,
55    Medium,
56    Low,
57    Info,
58}
59
60/// Categories of security findings
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
62pub enum SecurityCategory {
63    /// Exposed secrets, API keys, passwords
64    SecretsExposure,
65    /// Insecure configuration settings
66    InsecureConfiguration,
67    /// Language/framework-specific security patterns
68    CodeSecurityPattern,
69    /// Infrastructure and deployment security
70    InfrastructureSecurity,
71    /// Authentication and authorization issues
72    AuthenticationSecurity,
73    /// Data protection and privacy concerns
74    DataProtection,
75    /// Network and communication security
76    NetworkSecurity,
77    /// Compliance and regulatory requirements
78    Compliance,
79}
80
81/// A security finding with details and remediation
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SecurityFinding {
84    pub id: String,
85    pub title: String,
86    pub description: String,
87    pub severity: SecuritySeverity,
88    pub category: SecurityCategory,
89    pub file_path: Option<PathBuf>,
90    pub line_number: Option<usize>,
91    pub column_number: Option<usize>,
92    pub evidence: Option<String>,
93    pub remediation: Vec<String>,
94    pub references: Vec<String>,
95    pub cwe_id: Option<String>,
96    pub compliance_frameworks: Vec<String>,
97}
98
99/// Comprehensive security analysis report
100#[derive(Debug, Serialize, Deserialize)]
101pub struct SecurityReport {
102    pub analyzed_at: chrono::DateTime<chrono::Utc>,
103    pub overall_score: f32, // 0-100, higher is better
104    pub risk_level: SecuritySeverity,
105    pub total_findings: usize,
106    pub findings_by_severity: HashMap<SecuritySeverity, usize>,
107    pub findings_by_category: HashMap<SecurityCategory, usize>,
108    pub findings: Vec<SecurityFinding>,
109    pub recommendations: Vec<String>,
110    pub compliance_status: HashMap<String, ComplianceStatus>,
111}
112
113/// Compliance framework status
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ComplianceStatus {
116    pub framework: String,
117    pub coverage: f32, // 0-100%
118    pub missing_controls: Vec<String>,
119    pub recommendations: Vec<String>,
120}
121
122/// Configuration for security analysis
123#[derive(Debug, Clone)]
124pub struct SecurityAnalysisConfig {
125    pub include_low_severity: bool,
126    pub check_secrets: bool,
127    pub check_code_patterns: bool,
128    pub check_infrastructure: bool,
129    pub check_compliance: bool,
130    pub frameworks_to_check: Vec<String>,
131    pub ignore_patterns: Vec<String>,
132    /// Whether to skip scanning files that are gitignored
133    pub skip_gitignored_files: bool,
134    /// Whether to downgrade severity for gitignored files instead of skipping
135    pub downgrade_gitignored_severity: bool,
136}
137
138impl Default for SecurityAnalysisConfig {
139    fn default() -> Self {
140        Self {
141            include_low_severity: false,
142            check_secrets: true,
143            check_code_patterns: true,
144            check_infrastructure: true,
145            check_compliance: true,
146            frameworks_to_check: vec![
147                "SOC2".to_string(),
148                "GDPR".to_string(),
149                "OWASP".to_string(),
150            ],
151            ignore_patterns: vec![
152                "node_modules".to_string(),
153                ".git".to_string(),
154                "target".to_string(),
155                "build".to_string(),
156                ".next".to_string(),
157                "dist".to_string(),
158                "test".to_string(),
159                "tests".to_string(),
160                "*.json".to_string(), // Exclude JSON files that often contain hashes
161                "*.lock".to_string(), // Exclude lock files with checksums
162                "*_sample.*".to_string(), // Exclude sample files
163                "*audit*".to_string(), // Exclude audit reports
164            ],
165            skip_gitignored_files: true, // Default to skipping gitignored files
166            downgrade_gitignored_severity: false, // Skip entirely by default
167        }
168    }
169}
170
171pub struct SecurityAnalyzer {
172    config: SecurityAnalysisConfig,
173    secret_patterns: Vec<SecretPattern>,
174    security_rules: HashMap<Language, Vec<SecurityRule>>,
175    git_ignore_cache: std::sync::Mutex<HashMap<PathBuf, bool>>,
176    project_root: Option<PathBuf>,
177}
178
179/// Pattern for detecting secrets and sensitive data
180struct SecretPattern {
181    name: String,
182    pattern: Regex,
183    severity: SecuritySeverity,
184    description: String,
185}
186
187/// Security rule for code pattern analysis
188struct SecurityRule {
189    id: String,
190    name: String,
191    pattern: Regex,
192    severity: SecuritySeverity,
193    category: SecurityCategory,
194    description: String,
195    remediation: Vec<String>,
196    cwe_id: Option<String>,
197}
198
199impl SecurityAnalyzer {
200    pub fn new() -> Result<Self, SecurityError> {
201        Self::with_config(SecurityAnalysisConfig::default())
202    }
203    
204    pub fn with_config(config: SecurityAnalysisConfig) -> Result<Self, SecurityError> {
205        let secret_patterns = Self::initialize_secret_patterns()?;
206        let security_rules = Self::initialize_security_rules()?;
207        
208        Ok(Self {
209            config,
210            secret_patterns,
211            security_rules,
212            git_ignore_cache: std::sync::Mutex::new(HashMap::new()),
213            project_root: None,
214        })
215    }
216    
217    /// Enhanced security analysis using the new modular approach
218    pub fn analyze_security_enhanced(&mut self, analysis: &ProjectAnalysis) -> Result<NewSecurityReport, SecurityError> {
219        let start_time = Instant::now();
220        info!("Starting enhanced modular security analysis");
221        
222        // Create modular analyzer with JavaScript-specific configuration if JS/TS is detected
223        let has_javascript = analysis.languages.iter()
224            .any(|lang| matches!(lang.name.as_str(), "JavaScript" | "TypeScript" | "JSX" | "TSX"));
225        
226        let config = if has_javascript {
227            NewSecurityAnalysisConfig::for_javascript()
228        } else {
229            NewSecurityAnalysisConfig::default()
230        };
231        
232        let mut modular_analyzer = ModularSecurityAnalyzer::with_config(config)
233            .map_err(|e| SecurityError::AnalysisFailed(e.to_string()))?;
234        
235        // Use the modular analyzer
236        let enhanced_report = modular_analyzer.analyze_project(&analysis.project_root, &analysis.languages)
237            .map_err(|e| SecurityError::AnalysisFailed(e.to_string()))?;
238        
239        // For now, just return the enhanced report as-is
240        // TODO: Combine with existing findings if needed
241        
242        // Build final report
243        let duration = start_time.elapsed().as_secs_f32();
244        info!("Enhanced security analysis completed in {:.1}s - Found {} issues", duration, enhanced_report.total_findings);
245        
246        Ok(enhanced_report)
247    }
248    
249    /// Perform comprehensive security analysis with appropriate progress for verbosity level
250    pub fn analyze_security(&mut self, analysis: &ProjectAnalysis) -> Result<SecurityReport, SecurityError> {
251        let start_time = Instant::now();
252        info!("Starting comprehensive security analysis");
253        
254        // Set project root for gitignore checking
255        self.project_root = Some(analysis.project_root.clone());
256        
257        // Check if we're in verbose mode by checking log level
258        let is_verbose = log::max_level() >= log::LevelFilter::Info;
259        
260        // Set up progress tracking appropriate for verbosity
261        let multi_progress = MultiProgress::new();
262        
263        // In verbose mode, we'll completely skip adding progress bars to avoid visual conflicts
264        
265        // Count enabled analysis phases
266        let mut total_phases = 0;
267        if self.config.check_secrets { total_phases += 1; }
268        if self.config.check_code_patterns { total_phases += 1; }
269        if self.config.check_infrastructure { total_phases += 1; }
270        total_phases += 2; // env vars and framework analysis always run
271        
272        // Create appropriate progress indicator based on verbosity
273        let main_pb = if is_verbose {
274            None // No main progress bar in verbose mode to avoid conflicts with logs
275        } else {
276            // Normal mode: Rich progress bar
277            let pb = multi_progress.add(ProgressBar::new(100));
278            pb.set_style(
279                ProgressStyle::default_bar()
280                    .template("🛡️  {msg} {bar:50.cyan/blue} {percent}% [{elapsed_precise}]")
281                    .unwrap()
282                    .progress_chars("██▉▊▋▌▍▎▏  "),
283            );
284            Some(pb)
285        };
286        
287        let mut findings = Vec::new();
288        let phase_weight = if is_verbose { 1u64 } else { 100 / total_phases as u64 };
289        let mut current_progress = 0u64;
290        
291        // 1. Configuration Security Analysis
292        if self.config.check_secrets {
293            if let Some(ref pb) = main_pb {
294                pb.set_message("Analyzing configuration & secrets...");
295                pb.set_position(current_progress);
296            }
297            
298            if is_verbose {
299                findings.extend(self.analyze_configuration_security(&analysis.project_root)?);
300            } else {
301                findings.extend(self.analyze_configuration_security_with_progress(&analysis.project_root, &multi_progress)?);
302            }
303            
304            if let Some(ref pb) = main_pb {
305                current_progress += phase_weight;
306                pb.set_position(current_progress);
307            }
308        }
309        
310        // 2. Code Security Patterns
311        if self.config.check_code_patterns {
312            if let Some(ref pb) = main_pb {
313                pb.set_message("Analyzing code security patterns...");
314            }
315            
316            if is_verbose {
317                findings.extend(self.analyze_code_security_patterns(&analysis.project_root, &analysis.languages)?);
318            } else {
319                findings.extend(self.analyze_code_security_patterns_with_progress(&analysis.project_root, &analysis.languages, &multi_progress)?);
320            }
321            
322            if let Some(ref pb) = main_pb {
323                current_progress += phase_weight;
324                pb.set_position(current_progress);
325            }
326        }
327        
328        // 3. Infrastructure Security (skipped - not implemented yet)
329        // TODO: Implement infrastructure security analysis
330        // Currently all infrastructure analysis methods return empty results
331        
332        // 4. Environment Variables Security
333        if let Some(ref pb) = main_pb {
334            pb.set_message("Analyzing environment variables...");
335        }
336        
337        findings.extend(self.analyze_environment_security(&analysis.environment_variables));
338        if let Some(ref pb) = main_pb {
339            current_progress += phase_weight;
340            pb.set_position(current_progress);
341        }
342        
343        // 5. Framework-specific Security (skipped - not implemented yet)
344        // TODO: Implement framework-specific security analysis
345        // Currently all framework analysis methods return empty results
346        
347        if let Some(ref pb) = main_pb {
348            current_progress = 100;
349            pb.set_position(current_progress);
350        }
351        
352        // Processing phase
353        if let Some(ref pb) = main_pb {
354            pb.set_message("Processing findings & generating report...");
355        }
356        
357        // DEDUPLICATION: Remove duplicate findings for the same secret/issue
358        let pre_dedup_count = findings.len();
359        findings = self.deduplicate_findings(findings);
360        let post_dedup_count = findings.len();
361        
362        if pre_dedup_count != post_dedup_count {
363            info!("Deduplicated {} redundant findings, {} unique findings remain", 
364                  pre_dedup_count - post_dedup_count, post_dedup_count);
365        }
366        
367        // Filter findings based on configuration
368        let pre_filter_count = findings.len();
369        if !self.config.include_low_severity {
370            findings.retain(|f| f.severity != SecuritySeverity::Low && f.severity != SecuritySeverity::Info);
371        }
372        
373        // Sort by severity (most critical first)
374        findings.sort_by(|a, b| a.severity.cmp(&b.severity));
375        
376        // Calculate metrics
377        let total_findings = findings.len();
378        let findings_by_severity = self.count_by_severity(&findings);
379        let findings_by_category = self.count_by_category(&findings);
380        let overall_score = self.calculate_security_score(&findings);
381        let risk_level = self.determine_risk_level(&findings);
382        
383        // Generate compliance status (disabled - not implemented yet)
384        // TODO: Implement compliance assessment
385        let compliance_status = HashMap::new();
386        
387        // Generate recommendations
388        let recommendations = self.generate_recommendations(&findings, &analysis.technologies);
389        
390        // Complete with summary
391        let duration = start_time.elapsed().as_secs_f32();
392        if let Some(pb) = main_pb {
393            pb.finish_with_message(format!("✅ Security analysis completed in {:.1}s - Found {} issues", duration, total_findings));
394        }
395        
396        // Print summary
397        if pre_filter_count != total_findings {
398            info!("Found {} total findings, showing {} after filtering", pre_filter_count, total_findings);
399        } else {
400            info!("Found {} security findings", total_findings);
401        }
402        
403        Ok(SecurityReport {
404            analyzed_at: chrono::Utc::now(),
405            overall_score,
406            risk_level,
407            total_findings,
408            findings_by_severity,
409            findings_by_category,
410            findings,
411            recommendations,
412            compliance_status,
413        })
414    }
415    
416    /// Check if a file is gitignored using git check-ignore command
417    fn is_file_gitignored(&self, file_path: &Path) -> bool {
418        // Return false if we don't have project root set
419        let project_root = match &self.project_root {
420            Some(root) => root,
421            None => return false,
422        };
423        
424        // Use cache to avoid repeated git calls
425        if let Ok(cache) = self.git_ignore_cache.lock() {
426            if let Some(&cached_result) = cache.get(file_path) {
427                return cached_result;
428            }
429        }
430        
431        // Check if this is a git repository
432        if !project_root.join(".git").exists() {
433            debug!("Not a git repository, treating all files as tracked");
434            return false;
435        }
436        
437        // First, try git check-ignore for the most accurate result
438        let git_result = Command::new("git")
439            .args(&["check-ignore", "--quiet"])
440            .arg(file_path)
441            .current_dir(project_root)
442            .output()
443            .map(|output| output.status.success())
444            .unwrap_or(false);
445        
446        // If git check-ignore says it's ignored, trust it
447        if git_result {
448            if let Ok(mut cache) = self.git_ignore_cache.lock() {
449                cache.insert(file_path.to_path_buf(), true);
450            }
451            return true;
452        }
453        
454        // Fallback: Parse .gitignore files manually for common patterns
455        // This helps when git check-ignore might not work perfectly in all scenarios
456        let manual_result = self.check_gitignore_patterns(file_path, project_root);
457        
458        // Cache the result (prefer git result, fallback to manual)
459        let final_result = git_result || manual_result;
460        if let Ok(mut cache) = self.git_ignore_cache.lock() {
461            cache.insert(file_path.to_path_buf(), final_result);
462        }
463        
464        final_result
465    }
466    
467    /// Manually check gitignore patterns as a fallback
468    fn check_gitignore_patterns(&self, file_path: &Path, project_root: &Path) -> bool {
469        // Get relative path from project root
470        let relative_path = match file_path.strip_prefix(project_root) {
471            Ok(rel) => rel,
472            Err(_) => return false,
473        };
474        
475        let path_str = relative_path.to_string_lossy();
476        let file_name = relative_path.file_name()
477            .and_then(|n| n.to_str())
478            .unwrap_or("");
479        
480        // Read .gitignore file
481        let gitignore_path = project_root.join(".gitignore");
482        if let Ok(gitignore_content) = fs::read_to_string(&gitignore_path) {
483            for line in gitignore_content.lines() {
484                let pattern = line.trim();
485                if pattern.is_empty() || pattern.starts_with('#') {
486                    continue;
487                }
488                
489                // Check if this pattern matches our file
490                if self.matches_gitignore_pattern(pattern, &path_str, file_name) {
491                    debug!("File {} matches gitignore pattern: {}", path_str, pattern);
492                    return true;
493                }
494            }
495        }
496        
497        // Also check global gitignore patterns for common .env patterns
498        self.matches_common_env_patterns(file_name)
499    }
500    
501    /// Check if a file matches a specific gitignore pattern
502    fn matches_gitignore_pattern(&self, pattern: &str, path_str: &str, file_name: &str) -> bool {
503        // Handle different types of patterns
504        if pattern.contains('*') {
505            // Wildcard patterns
506            if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
507                // Try matching both full path and just filename
508                if glob_pattern.matches(path_str) || glob_pattern.matches(file_name) {
509                    return true;
510                }
511            }
512        } else if pattern.starts_with('/') {
513            // Absolute path from repo root
514            let abs_pattern = &pattern[1..];
515            if path_str == abs_pattern {
516                return true;
517            }
518        } else {
519            // Simple pattern - could match anywhere in path
520            if path_str == pattern || 
521               file_name == pattern || 
522               path_str.ends_with(&format!("/{}", pattern)) {
523                return true;
524            }
525        }
526        
527        false
528    }
529    
530    /// Check against common .env file patterns that should typically be ignored
531    fn matches_common_env_patterns(&self, file_name: &str) -> bool {
532        let common_env_patterns = [
533            ".env",
534            ".env.local",
535            ".env.development", 
536            ".env.production",
537            ".env.staging",
538            ".env.test",
539            ".env.example", // Usually committed but should be treated carefully
540        ];
541        
542        // Exact matches
543        if common_env_patterns.contains(&file_name) {
544            return file_name != ".env.example"; // .env.example is usually committed
545        }
546        
547        // Pattern matches
548        if file_name.starts_with(".env.") || 
549           file_name.ends_with(".env") ||
550           (file_name.starts_with(".") && file_name.contains("env")) {
551            // Be conservative - only ignore if it's clearly a local/environment specific file
552            return !file_name.contains("example") && 
553                   !file_name.contains("sample") && 
554                   !file_name.contains("template");
555        }
556        
557        false
558    }
559    
560    /// Check if a file is actually tracked by git
561    fn is_file_tracked(&self, file_path: &Path) -> bool {
562        let project_root = match &self.project_root {
563            Some(root) => root,
564            None => return true, // Assume tracked if no project root
565        };
566        
567        // Check if this is a git repository
568        if !project_root.join(".git").exists() {
569            return true; // Not a git repo, treat as tracked
570        }
571        
572        // Use git ls-files to check if file is tracked
573        Command::new("git")
574            .args(&["ls-files", "--error-unmatch"])
575            .arg(file_path)
576            .current_dir(project_root)
577            .output()
578            .map(|output| output.status.success())
579            .unwrap_or(true) // Default to tracked if git command fails
580    }
581    
582    /// Determine the appropriate severity for a secret finding based on git status
583    fn determine_secret_severity(&self, file_path: &Path, original_severity: SecuritySeverity) -> (SecuritySeverity, Vec<String>) {
584        let mut additional_remediation = Vec::new();
585        
586        // Check if file is gitignored
587        if self.is_file_gitignored(file_path) {
588            if self.config.skip_gitignored_files {
589                // Return Info level to indicate this should be skipped
590                return (SecuritySeverity::Info, vec!["File is properly gitignored".to_string()]);
591            } else if self.config.downgrade_gitignored_severity {
592                // Downgrade severity for gitignored files
593                let downgraded = match original_severity {
594                    SecuritySeverity::Critical => SecuritySeverity::Medium,
595                    SecuritySeverity::High => SecuritySeverity::Low,
596                    SecuritySeverity::Medium => SecuritySeverity::Low,
597                    SecuritySeverity::Low => SecuritySeverity::Info,
598                    SecuritySeverity::Info => SecuritySeverity::Info,
599                };
600                additional_remediation.push("Note: File is gitignored, reducing severity".to_string());
601                return (downgraded, additional_remediation);
602            }
603        }
604        
605        // Check if file is tracked by git
606        if !self.is_file_tracked(file_path) {
607            additional_remediation.push("Ensure this file is added to .gitignore to prevent accidental commits".to_string());
608        } else {
609            // File is tracked - this is a serious issue
610            additional_remediation.push("⚠️  CRITICAL: This file is tracked by git! Secrets may be in version history.".to_string());
611            additional_remediation.push("Consider using git-filter-branch or BFG Repo-Cleaner to remove from history".to_string());
612            additional_remediation.push("Rotate any exposed secrets immediately".to_string());
613            
614            // Upgrade severity for tracked files
615            let upgraded = match original_severity {
616                SecuritySeverity::High => SecuritySeverity::Critical,
617                SecuritySeverity::Medium => SecuritySeverity::High,
618                SecuritySeverity::Low => SecuritySeverity::Medium,
619                other => other,
620            };
621            return (upgraded, additional_remediation);
622        }
623        
624        (original_severity, additional_remediation)
625    }
626    
627    /// Initialize secret detection patterns
628    fn initialize_secret_patterns() -> Result<Vec<SecretPattern>, SecurityError> {
629        let patterns = vec![
630            // API Keys and Tokens - Specific patterns first
631            ("AWS Access Key", r"AKIA[0-9A-Z]{16}", SecuritySeverity::Critical),
632            ("AWS Secret Key", r#"(?i)(aws[_-]?secret|secret[_-]?access[_-]?key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?"#, SecuritySeverity::Critical),
633            ("S3 Secret Key", r#"(?i)(s3[_-]?secret[_-]?key|linode[_-]?s3[_-]?secret)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}["']?"#, SecuritySeverity::High),
634            ("GitHub Token", r"gh[pousr]_[A-Za-z0-9_]{36,255}", SecuritySeverity::High),
635            ("OpenAI API Key", r"sk-[A-Za-z0-9]{48}", SecuritySeverity::High),
636            ("Stripe API Key", r"sk_live_[0-9a-zA-Z]{24}", SecuritySeverity::Critical),
637            ("Stripe Publishable Key", r"pk_live_[0-9a-zA-Z]{24}", SecuritySeverity::Medium),
638            
639            // Database URLs and Passwords - Enhanced to avoid env var false positives
640            ("Hardcoded Database URL", r#"(?i)(database_url|db_url)["']?\s*[:=]\s*["']?(postgresql|mysql|mongodb)://[^"'\s]+"#, SecuritySeverity::Critical),
641            ("Hardcoded Password", r#"(?i)(password|passwd|pwd)["']?\s*[:=]\s*["']?[^"']{6,}["']?"#, SecuritySeverity::High),
642            ("JWT Secret", r#"(?i)(jwt[_-]?secret)["']?\s*[:=]\s*["']?[A-Za-z0-9_\-+/=]{20,}"#, SecuritySeverity::High),
643            
644            // Private Keys
645            ("RSA Private Key", r"-----BEGIN RSA PRIVATE KEY-----", SecuritySeverity::Critical),
646            ("SSH Private Key", r"-----BEGIN OPENSSH PRIVATE KEY-----", SecuritySeverity::Critical),
647            ("PGP Private Key", r"-----BEGIN PGP PRIVATE KEY BLOCK-----", SecuritySeverity::Critical),
648            
649            // Cloud Provider Keys
650            ("Google Cloud Service Account", r#""type":\s*"service_account""#, SecuritySeverity::High),
651            ("Azure Storage Key", r"DefaultEndpointsProtocol=https;AccountName=", SecuritySeverity::High),
652            
653            // Client-side exposed environment variables (these are the real security issues)
654            ("Client-side Exposed Secret", r#"(?i)(REACT_APP_|NEXT_PUBLIC_|VUE_APP_|VITE_)[A-Z_]*(?:SECRET|KEY|TOKEN|PASSWORD|API)[A-Z_]*["']?\s*[:=]\s*["']?[A-Za-z0-9_\-+/=]{10,}"#, SecuritySeverity::High),
655            
656            // Hardcoded API keys (not environment variable access)
657            ("Hardcoded API Key", r#"(?i)(api[_-]?key|apikey)["']?\s*[:=]\s*["']?[A-Za-z0-9_\-]{20,}["']?"#, SecuritySeverity::High),
658            
659            // Generic secrets that are clearly hardcoded (not env var access)
660            ("Hardcoded Secret", r#"(?i)(secret|token)["']?\s*[:=]\s*["']?[A-Za-z0-9_\-+/=]{24,}["']?"#, SecuritySeverity::Medium),
661        ];
662        
663        patterns.into_iter()
664            .map(|(name, pattern, severity)| {
665                Ok(SecretPattern {
666                    name: name.to_string(),
667                    pattern: Regex::new(pattern)?,
668                    severity,
669                    description: format!("Potential {} found in code", name),
670                })
671            })
672            .collect()
673    }
674    
675    /// Initialize language-specific security rules
676    fn initialize_security_rules() -> Result<HashMap<Language, Vec<SecurityRule>>, SecurityError> {
677        let mut rules = HashMap::new();
678        
679        // JavaScript/TypeScript Rules
680        rules.insert(Language::JavaScript, vec![
681            SecurityRule {
682                id: "js-001".to_string(),
683                name: "Eval Usage".to_string(),
684                pattern: Regex::new(r"\beval\s*\(")?,
685                severity: SecuritySeverity::High,
686                category: SecurityCategory::CodeSecurityPattern,
687                description: "Use of eval() can lead to code injection vulnerabilities".to_string(),
688                remediation: vec![
689                    "Avoid using eval() with user input".to_string(),
690                    "Use JSON.parse() for parsing JSON data".to_string(),
691                    "Consider using safer alternatives like Function constructor with validation".to_string(),
692                ],
693                cwe_id: Some("CWE-95".to_string()),
694            },
695            SecurityRule {
696                id: "js-002".to_string(),
697                name: "innerHTML Usage".to_string(),
698                pattern: Regex::new(r"\.innerHTML\s*=")?,
699                severity: SecuritySeverity::Medium,
700                category: SecurityCategory::CodeSecurityPattern,
701                description: "innerHTML can lead to XSS vulnerabilities if used with unsanitized data".to_string(),
702                remediation: vec![
703                    "Use textContent instead of innerHTML for text".to_string(),
704                    "Sanitize HTML content before setting innerHTML".to_string(),
705                    "Consider using secure templating libraries".to_string(),
706                ],
707                cwe_id: Some("CWE-79".to_string()),
708            },
709        ]);
710        
711        // Python Rules
712        rules.insert(Language::Python, vec![
713            SecurityRule {
714                id: "py-001".to_string(),
715                name: "SQL Injection Risk".to_string(),
716                pattern: Regex::new(r#"\.execute\s*\(\s*[f]?["'][^"']*%[sd]"#)?,
717                severity: SecuritySeverity::High,
718                category: SecurityCategory::CodeSecurityPattern,
719                description: "String formatting in SQL queries can lead to SQL injection".to_string(),
720                remediation: vec![
721                    "Use parameterized queries instead of string formatting".to_string(),
722                    "Use ORM query builders where possible".to_string(),
723                    "Validate and sanitize all user inputs".to_string(),
724                ],
725                cwe_id: Some("CWE-89".to_string()),
726            },
727            SecurityRule {
728                id: "py-002".to_string(),
729                name: "Pickle Usage".to_string(),
730                pattern: Regex::new(r"\bpickle\.loads?\s*\(")?,
731                severity: SecuritySeverity::High,
732                category: SecurityCategory::CodeSecurityPattern,
733                description: "Pickle can execute arbitrary code during deserialization".to_string(),
734                remediation: vec![
735                    "Avoid pickle for untrusted data".to_string(),
736                    "Use JSON or other safe serialization formats".to_string(),
737                    "If pickle is necessary, validate data sources".to_string(),
738                ],
739                cwe_id: Some("CWE-502".to_string()),
740            },
741        ]);
742        
743        // Add more language rules as needed...
744        
745        Ok(rules)
746    }
747    
748    /// Analyze configuration files for security issues with appropriate progress tracking
749    fn analyze_configuration_security_with_progress(&self, project_root: &Path, multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
750        debug!("Analyzing configuration security");
751        let mut findings = Vec::new();
752        
753        // Collect relevant files
754        let config_files = self.collect_config_files(project_root)?;
755        
756        if config_files.is_empty() {
757            info!("No configuration files found");
758            return Ok(findings);
759        }
760        
761        let is_verbose = log::max_level() >= log::LevelFilter::Info;
762        
763        info!("📁 Found {} configuration files to analyze", config_files.len());
764        
765        // Create appropriate progress tracking - completely skip in verbose mode
766        let file_pb = if is_verbose {
767            None // No progress bars at all in verbose mode
768        } else {
769            // Normal mode: Show detailed progress
770            let pb = multi_progress.add(ProgressBar::new(config_files.len() as u64));
771            pb.set_style(
772                ProgressStyle::default_bar()
773                    .template("  🔍 {msg} {bar:40.cyan/blue} {pos}/{len} files ({percent}%)")
774                    .unwrap()
775                    .progress_chars("████▉▊▋▌▍▎▏  "),
776            );
777            pb.set_message("Scanning configuration files...");
778            Some(pb)
779        };
780        
781        // Use atomic counter for progress updates if needed
782        use std::sync::atomic::{AtomicUsize, Ordering};
783        use std::sync::Arc;
784        let processed_count = Arc::new(AtomicUsize::new(0));
785        
786        // Analyze each file with appropriate progress tracking
787        let file_findings: Vec<Vec<SecurityFinding>> = config_files
788            .par_iter()
789            .map(|file_path| {
790                let result = self.analyze_file_for_secrets(file_path);
791                
792                // Update progress only in non-verbose mode
793                if let Some(ref pb) = file_pb {
794                    let current = processed_count.fetch_add(1, Ordering::Relaxed) + 1;
795                    if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
796                        // Truncate long filenames for better display
797                        let display_name = if file_name.len() > 30 {
798                            format!("...{}", &file_name[file_name.len()-27..])
799                        } else {
800                            file_name.to_string()
801                        };
802                        pb.set_message(format!("Scanning {}", display_name));
803                    }
804                    pb.set_position(current as u64);
805                }
806                
807                result
808            })
809            .filter_map(|result| result.ok())
810            .collect();
811        
812        // Finish progress tracking
813        if let Some(pb) = file_pb {
814            pb.finish_with_message(format!("✅ Scanned {} configuration files", config_files.len()));
815        }
816        
817        for mut file_findings in file_findings {
818            findings.append(&mut file_findings);
819        }
820        
821        // Check for common insecure configurations
822        findings.extend(self.check_insecure_configurations(project_root)?);
823        
824        info!("🔍 Found {} configuration security findings", findings.len());
825        Ok(findings)
826    }
827    
828    /// Direct configuration security analysis without progress bars
829    fn analyze_configuration_security(&self, project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
830        debug!("Analyzing configuration security");
831        let mut findings = Vec::new();
832        
833        // Collect relevant files
834        let config_files = self.collect_config_files(project_root)?;
835        
836        if config_files.is_empty() {
837            info!("No configuration files found");
838            return Ok(findings);
839        }
840        
841        info!("📁 Found {} configuration files to analyze", config_files.len());
842        
843        // Analyze each file directly without progress tracking
844        let file_findings: Vec<Vec<SecurityFinding>> = config_files
845            .par_iter()
846            .map(|file_path| self.analyze_file_for_secrets(file_path))
847            .filter_map(|result| result.ok())
848            .collect();
849        
850        for mut file_findings in file_findings {
851            findings.append(&mut file_findings);
852        }
853        
854        // Check for common insecure configurations
855        findings.extend(self.check_insecure_configurations(project_root)?);
856        
857        info!("🔍 Found {} configuration security findings", findings.len());
858        Ok(findings)
859    }
860    
861    /// Analyze code for security patterns with appropriate progress tracking
862    fn analyze_code_security_patterns_with_progress(&self, project_root: &Path, languages: &[DetectedLanguage], multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
863        debug!("Analyzing code security patterns");
864        let mut findings = Vec::new();
865        
866        // Count total source files across all languages
867        let mut total_files = 0;
868        let mut language_files = Vec::new();
869        
870        for language in languages {
871            if let Some(lang) = Language::from_string(&language.name) {
872                if let Some(_rules) = self.security_rules.get(&lang) {
873                    let source_files = self.collect_source_files(project_root, &language.name)?;
874                    total_files += source_files.len();
875                    language_files.push((language, source_files));
876                }
877            }
878        }
879        
880        if total_files == 0 {
881            info!("No source files found for code pattern analysis");
882            return Ok(findings);
883        }
884        
885        let is_verbose = log::max_level() >= log::LevelFilter::Info;
886        
887        info!("📄 Found {} source files across {} languages", total_files, language_files.len());
888        
889        // Create appropriate progress tracking
890        let code_pb = if is_verbose {
891            // Verbose mode: No sub-progress to avoid visual clutter
892            None
893        } else {
894            // Normal mode: Show detailed progress
895            let pb = multi_progress.add(ProgressBar::new(total_files as u64));
896            pb.set_style(
897                ProgressStyle::default_bar()
898                    .template("  📄 {msg} {bar:40.yellow/white} {pos}/{len} files ({percent}%)")
899                    .unwrap()
900                    .progress_chars("████▉▊▋▌▍▎▏  "),
901            );
902            pb.set_message("Scanning source code...");
903            Some(pb)
904        };
905    
906        
907        // Use atomic counter for progress if needed
908        use std::sync::atomic::{AtomicUsize, Ordering};
909        use std::sync::Arc;
910        let processed_count = Arc::new(AtomicUsize::new(0));
911        
912        // Process all languages
913        for (language, source_files) in language_files {
914            if let Some(lang) = Language::from_string(&language.name) {
915                if let Some(rules) = self.security_rules.get(&lang) {
916                let file_findings: Vec<Vec<SecurityFinding>> = source_files
917                    .par_iter()
918                    .map(|file_path| {
919                        let result = self.analyze_file_with_rules(file_path, rules);
920                        
921                        // Update progress only in non-verbose mode
922                        if let Some(ref pb) = code_pb {
923                            let current = processed_count.fetch_add(1, Ordering::Relaxed) + 1;
924                            if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
925                                let display_name = if file_name.len() > 25 {
926                                    format!("...{}", &file_name[file_name.len()-22..])
927                                } else {
928                                    file_name.to_string()
929                                };
930                                pb.set_message(format!("Scanning {} ({})", display_name, language.name));
931                            }
932                            pb.set_position(current as u64);
933                        }
934                        
935                        result
936                    })
937                    .filter_map(|result| result.ok())
938                    .collect();
939                
940                for mut file_findings in file_findings {
941                    findings.append(&mut file_findings);
942                }
943                }
944            }
945        }
946        
947        // Finish progress tracking
948        if let Some(pb) = code_pb {
949            pb.finish_with_message(format!("✅ Scanned {} source files", total_files));
950        }
951        
952        info!("🔍 Found {} code security findings", findings.len());
953        Ok(findings)
954    }
955    
956    /// Direct code security analysis without progress bars
957    fn analyze_code_security_patterns(&self, project_root: &Path, languages: &[DetectedLanguage]) -> Result<Vec<SecurityFinding>, SecurityError> {
958        debug!("Analyzing code security patterns");
959        let mut findings = Vec::new();
960        
961        // Count total source files across all languages
962        let mut total_files = 0;
963        let mut language_files = Vec::new();
964        
965        for language in languages {
966            if let Some(lang) = Language::from_string(&language.name) {
967                if let Some(_rules) = self.security_rules.get(&lang) {
968                    let source_files = self.collect_source_files(project_root, &language.name)?;
969                    total_files += source_files.len();
970                    language_files.push((language, source_files));
971                }
972            }
973        }
974        
975        if total_files == 0 {
976            info!("No source files found for code pattern analysis");
977            return Ok(findings);
978        }
979        
980        info!("📄 Found {} source files across {} languages", total_files, language_files.len());
981        
982        // Process all languages without progress tracking
983        for (language, source_files) in language_files {
984            if let Some(lang) = Language::from_string(&language.name) {
985                if let Some(rules) = self.security_rules.get(&lang) {
986                let file_findings: Vec<Vec<SecurityFinding>> = source_files
987                    .par_iter()
988                    .map(|file_path| self.analyze_file_with_rules(file_path, rules))
989                    .filter_map(|result| result.ok())
990                    .collect();
991                
992                for mut file_findings in file_findings {
993                    findings.append(&mut file_findings);
994                }
995                }
996            }
997        }
998
999        info!("🔍 Found {} code security findings", findings.len());
1000        Ok(findings)
1001    }
1002    
1003    /// Analyze infrastructure configurations with appropriate progress tracking
1004    fn analyze_infrastructure_security_with_progress(&self, project_root: &Path, _technologies: &[DetectedTechnology], multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
1005        debug!("Analyzing infrastructure security");
1006        let mut findings = Vec::new();
1007        
1008        let is_verbose = log::max_level() >= log::LevelFilter::Info;
1009        
1010        // Create appropriate progress indicator
1011        let infra_pb = if is_verbose {
1012            // Verbose mode: No spinner to avoid conflicts with logs
1013            None
1014        } else {
1015            // Normal mode: Show spinner
1016            let pb = multi_progress.add(ProgressBar::new_spinner());
1017            pb.set_style(
1018                ProgressStyle::default_spinner()
1019                    .template("  🏗️  {msg} {spinner:.magenta}")
1020                    .unwrap()
1021                    .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
1022            );
1023            pb.enable_steady_tick(std::time::Duration::from_millis(100));
1024            Some(pb)
1025        };
1026        
1027        // Check Dockerfile security
1028        if let Some(ref pb) = infra_pb {
1029            pb.set_message("Checking Dockerfiles & Compose files...");
1030        }
1031        findings.extend(self.analyze_dockerfile_security(project_root)?);
1032        findings.extend(self.analyze_compose_security(project_root)?);
1033        
1034        // Check CI/CD configurations
1035        if let Some(ref pb) = infra_pb {
1036            pb.set_message("Checking CI/CD configurations...");
1037        }
1038        findings.extend(self.analyze_cicd_security(project_root)?);
1039        
1040        // Finish progress tracking
1041        if let Some(pb) = infra_pb {
1042            pb.finish_with_message("✅ Infrastructure analysis complete");
1043        }
1044        info!("🔍 Found {} infrastructure security findings", findings.len());
1045        
1046        Ok(findings)
1047    }
1048    
1049    /// Direct infrastructure security analysis without progress bars
1050    fn analyze_infrastructure_security(&self, project_root: &Path, _technologies: &[DetectedTechnology]) -> Result<Vec<SecurityFinding>, SecurityError> {
1051        debug!("Analyzing infrastructure security");
1052        let mut findings = Vec::new();
1053        
1054        // Check Dockerfile security
1055        findings.extend(self.analyze_dockerfile_security(project_root)?);
1056        findings.extend(self.analyze_compose_security(project_root)?);
1057        
1058        // Check CI/CD configurations
1059        findings.extend(self.analyze_cicd_security(project_root)?);
1060        
1061        info!("🔍 Found {} infrastructure security findings", findings.len());
1062        Ok(findings)
1063    }
1064    
1065    /// Analyze environment variables for security issues
1066    fn analyze_environment_security(&self, env_vars: &[EnvVar]) -> Vec<SecurityFinding> {
1067        let mut findings = Vec::new();
1068        
1069        for env_var in env_vars {
1070            // Check for sensitive variable names without proper protection
1071            if self.is_sensitive_env_var(&env_var.name) && env_var.default_value.is_some() {
1072                findings.push(SecurityFinding {
1073                    id: format!("env-{}", env_var.name.to_lowercase()),
1074                    title: "Sensitive Environment Variable with Default Value".to_string(),
1075                    description: format!("Environment variable '{}' appears to contain sensitive data but has a default value", env_var.name),
1076                    severity: SecuritySeverity::Medium,
1077                    category: SecurityCategory::SecretsExposure,
1078                    file_path: None,
1079                    line_number: None,
1080                    column_number: None,
1081                    evidence: Some(format!("Variable: {} = {:?}", env_var.name, env_var.default_value)),
1082                    remediation: vec![
1083                        "Remove default value for sensitive environment variables".to_string(),
1084                        "Use a secure secret management system".to_string(),
1085                        "Document required environment variables separately".to_string(),
1086                    ],
1087                    references: vec![
1088                        "https://owasp.org/www-project-top-ten/2021/A05_2021-Security_Misconfiguration/".to_string(),
1089                    ],
1090                    cwe_id: Some("CWE-200".to_string()),
1091                    compliance_frameworks: vec!["SOC2".to_string(), "GDPR".to_string()],
1092                });
1093            }
1094        }
1095        
1096        findings
1097    }
1098    
1099    /// Analyze framework-specific security configurations with appropriate progress
1100    fn analyze_framework_security_with_progress(&self, project_root: &Path, technologies: &[DetectedTechnology], multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
1101        debug!("Analyzing framework-specific security");
1102        let mut findings = Vec::new();
1103        
1104        let framework_count = technologies.len();
1105        if framework_count == 0 {
1106            info!("No frameworks detected for security analysis");
1107            return Ok(findings);
1108        }
1109        
1110        let is_verbose = log::max_level() >= log::LevelFilter::Info;
1111        
1112        info!("🔧 Found {} frameworks to analyze", framework_count);
1113        
1114        // Create appropriate progress indicator
1115        let fw_pb = if is_verbose {
1116            // Verbose mode: No spinner to avoid conflicts with logs
1117            None
1118        } else {
1119            // Normal mode: Show spinner
1120            let pb = multi_progress.add(ProgressBar::new_spinner());
1121            pb.set_style(
1122                ProgressStyle::default_spinner()
1123                    .template("  🔧 {msg} {spinner:.cyan}")
1124                    .unwrap()
1125                    .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
1126            );
1127            pb.enable_steady_tick(std::time::Duration::from_millis(120));
1128            Some(pb)
1129        };
1130        
1131        for tech in technologies {
1132            if let Some(ref pb) = fw_pb {
1133                pb.set_message(format!("Checking {} configuration...", tech.name));
1134            }
1135            
1136            match tech.name.as_str() {
1137                "Express.js" | "Express" => {
1138                    findings.extend(self.analyze_express_security(project_root)?);
1139                },
1140                "Django" => {
1141                    findings.extend(self.analyze_django_security(project_root)?);
1142                },
1143                "Spring Boot" => {
1144                    findings.extend(self.analyze_spring_security(project_root)?);
1145                },
1146                "Next.js" => {
1147                    findings.extend(self.analyze_nextjs_security(project_root)?);
1148                },
1149                // Add more frameworks as needed
1150                _ => {}
1151            }
1152        }
1153        
1154        // Finish progress tracking
1155        if let Some(pb) = fw_pb {
1156            pb.finish_with_message("✅ Framework analysis complete");
1157        }
1158        info!("🔍 Found {} framework security findings", findings.len());
1159        
1160        Ok(findings)
1161    }
1162    
1163    /// Direct framework security analysis without progress bars
1164    fn analyze_framework_security(&self, project_root: &Path, technologies: &[DetectedTechnology]) -> Result<Vec<SecurityFinding>, SecurityError> {
1165        debug!("Analyzing framework-specific security");
1166        let mut findings = Vec::new();
1167        
1168        let framework_count = technologies.len();
1169        if framework_count == 0 {
1170            info!("No frameworks detected for security analysis");
1171            return Ok(findings);
1172        }
1173        
1174        info!("🔧 Found {} frameworks to analyze", framework_count);
1175        
1176        for tech in technologies {
1177            match tech.name.as_str() {
1178                "Express.js" | "Express" => {
1179                    findings.extend(self.analyze_express_security(project_root)?);
1180                },
1181                "Django" => {
1182                    findings.extend(self.analyze_django_security(project_root)?);
1183                },
1184                "Spring Boot" => {
1185                    findings.extend(self.analyze_spring_security(project_root)?);
1186                },
1187                "Next.js" => {
1188                    findings.extend(self.analyze_nextjs_security(project_root)?);
1189                },
1190                // Add more frameworks as needed
1191                _ => {}
1192            }
1193        }
1194        
1195        info!("🔍 Found {} framework security findings", findings.len());
1196        Ok(findings)
1197    }
1198    
1199    // Helper methods for specific analyses...
1200    
1201    fn collect_config_files(&self, project_root: &Path) -> Result<Vec<PathBuf>, SecurityError> {
1202        let patterns = vec![
1203            "*.env*", "*.conf", "*.config", "*.ini", "*.yaml", "*.yml", 
1204            "*.toml", "docker-compose*.yml", "Dockerfile*",
1205            ".github/**/*.yml", ".gitlab-ci.yml", "package.json",
1206            "requirements.txt", "Cargo.toml", "go.mod", "pom.xml",
1207        ];
1208        
1209        let mut files = crate::common::file_utils::find_files_by_patterns(project_root, &patterns)
1210            .map_err(|e| SecurityError::Io(e))?;
1211        
1212        // Filter out files matching ignore patterns
1213        files.retain(|file| {
1214            let file_name = file.file_name()
1215                .and_then(|n| n.to_str())
1216                .unwrap_or("");
1217            let file_path = file.to_string_lossy();
1218            
1219            !self.config.ignore_patterns.iter().any(|pattern| {
1220                if pattern.contains('*') {
1221                    // Use glob matching for wildcard patterns
1222                    glob::Pattern::new(pattern)
1223                        .map(|p| p.matches(&file_path) || p.matches(file_name))
1224                        .unwrap_or(false)
1225                } else {
1226                    // Exact string matching
1227                    file_path.contains(pattern) || file_name.contains(pattern)
1228                }
1229            })
1230        });
1231        
1232        Ok(files)
1233    }
1234    
1235    fn analyze_file_for_secrets(&self, file_path: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1236        let content = fs::read_to_string(file_path)?;
1237        let mut findings = Vec::new();
1238        
1239        for (line_num, line) in content.lines().enumerate() {
1240            for pattern in &self.secret_patterns {
1241                if let Some(match_) = pattern.pattern.find(line) {
1242                    // Skip if it looks like a placeholder or example
1243                    if self.is_likely_placeholder(line) {
1244                        continue;
1245                    }
1246                    
1247                    // NEW: Skip if this is legitimate environment variable usage
1248                    if self.is_legitimate_env_var_usage(line, file_path) {
1249                        debug!("Skipping legitimate env var usage: {}", line.trim());
1250                        continue;
1251                    }
1252                    
1253                    // Determine severity based on git status
1254                    let (severity, additional_remediation) = self.determine_secret_severity(file_path, pattern.severity.clone());
1255                    
1256                    // Skip if severity is Info (indicates gitignored and should be skipped)
1257                    if self.config.skip_gitignored_files && severity == SecuritySeverity::Info {
1258                        debug!("Skipping secret in gitignored file: {}", file_path.display());
1259                        continue;
1260                    }
1261                    
1262                    // Build base remediation steps
1263                    let mut remediation = vec![
1264                        "Remove sensitive data from source code".to_string(),
1265                        "Use environment variables for secrets".to_string(),
1266                        "Consider using a secure secret management service".to_string(),
1267                    ];
1268                    
1269                    // Add git-specific remediation based on file status
1270                    remediation.extend(additional_remediation);
1271                    
1272                    // Add generic gitignore advice if not already covered
1273                    if !self.is_file_gitignored(file_path) && !self.is_file_tracked(file_path) {
1274                        remediation.push("Add this file to .gitignore to prevent accidental commits".to_string());
1275                    }
1276                    
1277                    // Create enhanced finding with git-aware severity and remediation
1278                    let mut description = pattern.description.clone();
1279                    if self.is_file_tracked(file_path) {
1280                        description.push_str(" (⚠️  WARNING: File is tracked by git - secrets may be in version history!)");
1281                    } else if self.is_file_gitignored(file_path) {
1282                        description.push_str(" (ℹ️  Note: File is gitignored)");
1283                    }
1284                    
1285                    findings.push(SecurityFinding {
1286                        id: format!("secret-{}-{}", pattern.name.to_lowercase().replace(' ', "-"), line_num),
1287                        title: format!("Potential {} Exposure", pattern.name),
1288                        description,
1289                        severity,
1290                        category: SecurityCategory::SecretsExposure,
1291                        file_path: Some(file_path.to_path_buf()),
1292                        line_number: Some(line_num + 1),
1293                        column_number: Some(match_.start() + 1), // 1-indexed column position
1294                        evidence: Some(format!("Line: {}", line.trim())),
1295                        remediation,
1296                        references: vec![
1297                            "https://owasp.org/www-project-top-ten/2021/A05_2021-Security_Misconfiguration/".to_string(),
1298                        ],
1299                        cwe_id: Some("CWE-200".to_string()),
1300                        compliance_frameworks: vec!["SOC2".to_string(), "GDPR".to_string()],
1301                    });
1302                }
1303            }
1304        }
1305        
1306        Ok(findings)
1307    }
1308    
1309    /// Check if a line represents legitimate environment variable usage (not a security issue)
1310    fn is_legitimate_env_var_usage(&self, line: &str, file_path: &Path) -> bool {
1311        let line_trimmed = line.trim();
1312        
1313        // Check for common legitimate environment variable access patterns
1314        let legitimate_env_patterns = [
1315            // Node.js/JavaScript patterns
1316            r"process\.env\.[A-Z_]+",
1317            r#"process\.env\[['""][A-Z_]+['"]\]"#,
1318            
1319            // Vite/Modern JS patterns  
1320            r"import\.meta\.env\.[A-Z_]+",
1321            r#"import\.meta\.env\[['""][A-Z_]+['"]\]"#,
1322            
1323            // Python patterns
1324            r#"os\.environ\.get\(["'][A-Z_]+["']\)"#,
1325            r#"os\.environ\[["'][A-Z_]+["']\]"#,
1326            r#"getenv\(["'][A-Z_]+["']\)"#,
1327            
1328            // Rust patterns
1329            r#"env::var\("([A-Z_]+)"\)"#,
1330            r#"std::env::var\("([A-Z_]+)"\)"#,
1331            
1332            // Go patterns
1333            r#"os\.Getenv\(["'][A-Z_]+["']\)"#,
1334            
1335            // Java patterns
1336            r#"System\.getenv\(["'][A-Z_]+["']\)"#,
1337            
1338            // Shell/Docker patterns
1339            r"\$\{?[A-Z_]+\}?",
1340            r"ENV [A-Z_]+",
1341            
1342            // Config file access patterns
1343            r"config\.[a-z_]+\.[A-Z_]+",
1344            r"settings\.[A-Z_]+",
1345            r"env\.[A-Z_]+",
1346        ];
1347        
1348        // Check if the line matches any legitimate environment variable access pattern
1349        for pattern_str in &legitimate_env_patterns {
1350            if let Ok(pattern) = Regex::new(pattern_str) {
1351                if pattern.is_match(line_trimmed) {
1352                    // Additional context checks to make sure this is really legitimate
1353                    
1354                    // Check if this is in a server-side context (not client-side)
1355                    if self.is_server_side_file(file_path) {
1356                        return true;
1357                    }
1358                    
1359                    // Check if this is NOT a client-side exposed variable
1360                    if !self.is_client_side_exposed_env_var(line_trimmed) {
1361                        return true;
1362                    }
1363                }
1364            }
1365        }
1366        
1367        // Check for assignment vs access - assignments might be setting up environment variables
1368        // which could be legitimate in certain contexts
1369        if self.is_env_var_assignment_context(line_trimmed, file_path) {
1370            return true;
1371        }
1372        
1373        false
1374    }
1375    
1376    /// Check if a file is likely server-side code (vs client-side)
1377    fn is_server_side_file(&self, file_path: &Path) -> bool {
1378        let path_str = file_path.to_string_lossy().to_lowercase();
1379        let file_name = file_path.file_name()
1380            .and_then(|n| n.to_str())
1381            .unwrap_or("")
1382            .to_lowercase();
1383        
1384        // Server-side indicators
1385        let server_indicators = [
1386            "/server/", "/api/", "/backend/", "/src/app/api/", "/pages/api/",
1387            "/routes/", "/controllers/", "/middleware/", "/models/",
1388            "/lib/", "/utils/", "/services/", "/config/",
1389            "server.js", "index.js", "app.js", "main.js",
1390            ".env", "dockerfile", "docker-compose",
1391        ];
1392        
1393        // Client-side indicators (these should return false)
1394        let client_indicators = [
1395            "/public/", "/static/", "/assets/", "/components/", "/pages/",
1396            "/src/components/", "/src/pages/", "/client/", "/frontend/",
1397            "index.html", ".html", "/dist/", "/build/",
1398            "dist/", "build/", "public/", "static/", "assets/",
1399        ];
1400        
1401        // If it's clearly client-side, return false
1402        if client_indicators.iter().any(|indicator| path_str.contains(indicator)) {
1403            return false;
1404        }
1405        
1406        // If it has server-side indicators, return true
1407        if server_indicators.iter().any(|indicator| 
1408            path_str.contains(indicator) || file_name.contains(indicator)
1409        ) {
1410            return true;
1411        }
1412        
1413        // Default to true for ambiguous cases (be conservative about flagging env var usage)
1414        true
1415    }
1416    
1417    /// Check if an environment variable is exposed to client-side (security issue)
1418    fn is_client_side_exposed_env_var(&self, line: &str) -> bool {
1419        let client_prefixes = [
1420            "REACT_APP_", "NEXT_PUBLIC_", "VUE_APP_", "VITE_", 
1421            "GATSBY_", "PUBLIC_", "NUXT_PUBLIC_",
1422        ];
1423        
1424        client_prefixes.iter().any(|prefix| line.contains(prefix))
1425    }
1426    
1427    /// Check if this is a legitimate environment variable assignment context
1428    fn is_env_var_assignment_context(&self, line: &str, file_path: &Path) -> bool {
1429        let path_str = file_path.to_string_lossy().to_lowercase();
1430        let file_name = file_path.file_name()
1431            .and_then(|n| n.to_str())
1432            .unwrap_or("")
1433            .to_lowercase();
1434        
1435        // Only very specific configuration files where env var assignments are expected
1436        // Be more restrictive to avoid false positives
1437        let env_config_files = [
1438            ".env", 
1439            "docker-compose.yml", "docker-compose.yaml",
1440            ".env.example", ".env.sample", ".env.template",
1441            ".env.local", ".env.development", ".env.production", ".env.staging",
1442        ];
1443        
1444        // Check for exact filename matches for .env files (most common legitimate case)
1445        if env_config_files.iter().any(|pattern| file_name == *pattern) {
1446            return true;
1447        }
1448        
1449        // Docker files are also legitimate for environment variable assignment
1450        if file_name.starts_with("dockerfile") || file_name == "dockerfile" {
1451            return true;
1452        }
1453        
1454        // Shell scripts or CI/CD files
1455        if file_name.ends_with(".sh") || 
1456           file_name.ends_with(".bash") ||
1457           path_str.contains(".github/workflows/") ||
1458           path_str.contains(".gitlab-ci") {
1459            return true;
1460        }
1461        
1462        // Lines that are clearly setting up environment variables for child processes
1463        // Only match very specific patterns that indicate legitimate environment setup
1464        let setup_patterns = [
1465            r"export [A-Z_]+=",           // Shell export
1466            r"ENV [A-Z_]+=",              // Dockerfile ENV
1467            r"^\s*environment:\s*$",      // Docker Compose environment section header
1468            r"^\s*env:\s*$",              // Kubernetes env section header
1469            r"process\.env\.[A-Z_]+ =",   // Explicitly setting process.env (rare but legitimate)
1470        ];
1471        
1472        for pattern_str in &setup_patterns {
1473            if let Ok(pattern) = Regex::new(pattern_str) {
1474                if pattern.is_match(line) {
1475                    return true;
1476                }
1477            }
1478        }
1479        
1480        false
1481    }
1482    
1483    fn is_likely_placeholder(&self, line: &str) -> bool {
1484        let placeholder_indicators = [
1485            "example", "placeholder", "your_", "insert_", "replace_",
1486            "xxx", "yyy", "zzz", "fake", "dummy", "test_key",
1487            "sk-xxxxxxxx", "AKIA00000000",
1488        ];
1489        
1490        let hash_indicators = [
1491            "checksum", "hash", "sha1", "sha256", "md5", "commit",
1492            "fingerprint", "digest", "advisory", "ghsa-", "cve-",
1493            "rustc_fingerprint", "last-commit", "references",
1494        ];
1495        
1496        let line_lower = line.to_lowercase();
1497        
1498        // Check for placeholder indicators
1499        if placeholder_indicators.iter().any(|indicator| line_lower.contains(indicator)) {
1500            return true;
1501        }
1502        
1503        // Check for hash/checksum context
1504        if hash_indicators.iter().any(|indicator| line_lower.contains(indicator)) {
1505            return true;
1506        }
1507        
1508        // Check if it's a URL or path (often contains hash-like strings)
1509        if line_lower.contains("http") || line_lower.contains("github.com") {
1510            return true;
1511        }
1512        
1513        // Check if it's likely a hex-only string (git commits, checksums)
1514        if let Some(potential_hash) = self.extract_potential_hash(line) {
1515            if potential_hash.len() >= 32 && self.is_hex_only(&potential_hash) {
1516                return true; // Likely a SHA hash
1517            }
1518        }
1519        
1520        false
1521    }
1522    
1523    fn extract_potential_hash(&self, line: &str) -> Option<String> {
1524        // Look for quoted strings that might be hashes
1525        if let Some(start) = line.find('"') {
1526            if let Some(end) = line[start + 1..].find('"') {
1527                let potential = &line[start + 1..start + 1 + end];
1528                if potential.len() >= 32 {
1529                    return Some(potential.to_string());
1530                }
1531            }
1532        }
1533        None
1534    }
1535    
1536    fn is_hex_only(&self, s: &str) -> bool {
1537        s.chars().all(|c| c.is_ascii_hexdigit())
1538    }
1539    
1540    fn is_sensitive_env_var(&self, name: &str) -> bool {
1541        let sensitive_patterns = [
1542            "password", "secret", "key", "token", "auth", "api",
1543            "private", "credential", "cert", "ssl", "tls",
1544        ];
1545        
1546        let name_lower = name.to_lowercase();
1547        sensitive_patterns.iter().any(|pattern| name_lower.contains(pattern))
1548    }
1549    
1550    // Placeholder implementations for specific framework analysis
1551    fn analyze_express_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1552        // TODO: Implement Express.js specific security checks
1553        Ok(vec![])
1554    }
1555    
1556    fn analyze_django_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1557        // TODO: Implement Django specific security checks
1558        Ok(vec![])
1559    }
1560    
1561    fn analyze_spring_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1562        // TODO: Implement Spring Boot specific security checks
1563        Ok(vec![])
1564    }
1565    
1566    fn analyze_nextjs_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1567        // TODO: Implement Next.js specific security checks
1568        Ok(vec![])
1569    }
1570    
1571    fn analyze_dockerfile_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1572        // TODO: Implement Dockerfile security analysis
1573        Ok(vec![])
1574    }
1575    
1576    fn analyze_compose_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1577        // TODO: Implement Docker Compose security analysis
1578        Ok(vec![])
1579    }
1580    
1581    fn analyze_cicd_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1582        // TODO: Implement CI/CD security analysis
1583        Ok(vec![])
1584    }
1585    
1586    // Additional helper methods...
1587    fn collect_source_files(&self, project_root: &Path, language: &str) -> Result<Vec<PathBuf>, SecurityError> {
1588        // TODO: Implement source file collection based on language
1589        Ok(vec![])
1590    }
1591    
1592    fn analyze_file_with_rules(&self, _file_path: &Path, _rules: &[SecurityRule]) -> Result<Vec<SecurityFinding>, SecurityError> {
1593        // TODO: Implement rule-based file analysis
1594        Ok(vec![])
1595    }
1596    
1597    fn check_insecure_configurations(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1598        // TODO: Implement insecure configuration checks
1599        Ok(vec![])
1600    }
1601    
1602    /// Deduplicate findings to avoid multiple reports for the same secret/issue
1603    fn deduplicate_findings(&self, mut findings: Vec<SecurityFinding>) -> Vec<SecurityFinding> {
1604        use std::collections::HashSet;
1605        
1606        let mut seen_secrets: HashSet<String> = HashSet::new();
1607        let mut deduplicated = Vec::new();
1608        
1609        // Sort by priority: more specific patterns first, then by severity
1610        findings.sort_by(|a, b| {
1611            // First, prioritize specific patterns over generic ones
1612            let a_priority = self.get_pattern_priority(&a.title);
1613            let b_priority = self.get_pattern_priority(&b.title);
1614            
1615            match a_priority.cmp(&b_priority) {
1616                std::cmp::Ordering::Equal => {
1617                    // If same priority, sort by severity (most critical first)
1618                    a.severity.cmp(&b.severity)
1619                }
1620                other => other
1621            }
1622        });
1623        
1624        for finding in findings {
1625            let key = self.generate_finding_key(&finding);
1626            
1627            if !seen_secrets.contains(&key) {
1628                seen_secrets.insert(key);
1629                deduplicated.push(finding);
1630            }
1631        }
1632        
1633        deduplicated
1634    }
1635    
1636    /// Generate a unique key for deduplication based on the type of finding
1637    fn generate_finding_key(&self, finding: &SecurityFinding) -> String {
1638        match finding.category {
1639            SecurityCategory::SecretsExposure => {
1640                // For secrets, deduplicate based on file path and the actual secret content
1641                if let Some(evidence) = &finding.evidence {
1642                    if let Some(file_path) = &finding.file_path {
1643                        // Extract the secret value from the evidence line
1644                        if let Some(secret_value) = self.extract_secret_value(evidence) {
1645                            return format!("secret:{}:{}", file_path.display(), secret_value);
1646                        }
1647                        // Fallback to file + line if we can't extract the value
1648                        if let Some(line_num) = finding.line_number {
1649                            return format!("secret:{}:{}", file_path.display(), line_num);
1650                        }
1651                    }
1652                }
1653                // Fallback for environment variables or other secrets without file paths
1654                format!("secret:{}", finding.title)
1655            }
1656            _ => {
1657                // For non-secret findings, use file path + line number + title
1658                if let Some(file_path) = &finding.file_path {
1659                    if let Some(line_num) = finding.line_number {
1660                        format!("other:{}:{}:{}", file_path.display(), line_num, finding.title)
1661                    } else {
1662                        format!("other:{}:{}", file_path.display(), finding.title)
1663                    }
1664                } else {
1665                    format!("other:{}", finding.title)
1666                }
1667            }
1668        }
1669    }
1670    
1671    /// Extract secret value from evidence line for deduplication
1672    fn extract_secret_value(&self, evidence: &str) -> Option<String> {
1673        // Look for patterns like "KEY=value" or "KEY: value"
1674        if let Some(pos) = evidence.find('=') {
1675            let value = evidence[pos + 1..].trim();
1676            // Remove quotes if present
1677            let value = value.trim_matches('"').trim_matches('\'');
1678            if value.len() > 10 { // Only consider substantial values
1679                return Some(value.to_string());
1680            }
1681        }
1682        
1683        // Look for patterns like "key: value" in YAML/JSON
1684        if let Some(pos) = evidence.find(':') {
1685            let value = evidence[pos + 1..].trim();
1686            let value = value.trim_matches('"').trim_matches('\'');
1687            if value.len() > 10 {
1688                return Some(value.to_string());
1689            }
1690        }
1691        
1692        None
1693    }
1694    
1695    /// Get pattern priority for deduplication (lower number = higher priority)
1696    fn get_pattern_priority(&self, title: &str) -> u8 {
1697        // Most specific patterns get highest priority (lowest number)
1698        if title.contains("AWS Access Key") { return 1; }
1699        if title.contains("AWS Secret Key") { return 1; }
1700        if title.contains("S3 Secret Key") { return 1; }
1701        if title.contains("GitHub Token") { return 1; }
1702        if title.contains("OpenAI API Key") { return 1; }
1703        if title.contains("Stripe") { return 1; }
1704        if title.contains("RSA Private Key") { return 1; }
1705        if title.contains("SSH Private Key") { return 1; }
1706        
1707        // JWT and specific API keys are more specific than generic
1708        if title.contains("JWT Secret") { return 2; }
1709        if title.contains("Database URL") { return 2; }
1710        
1711        // Generic API key patterns are less specific
1712        if title.contains("API Key") { return 3; }
1713        
1714        // Environment variable findings are less specific
1715        if title.contains("Environment Variable") { return 4; }
1716        
1717        // Generic patterns get lowest priority (highest number)
1718        if title.contains("Generic Secret") { return 5; }
1719        
1720        // Default priority for other patterns
1721        3
1722    }
1723    
1724    fn count_by_severity(&self, findings: &[SecurityFinding]) -> HashMap<SecuritySeverity, usize> {
1725        let mut counts = HashMap::new();
1726        for finding in findings {
1727            *counts.entry(finding.severity.clone()).or_insert(0) += 1;
1728        }
1729        counts
1730    }
1731    
1732    fn count_by_category(&self, findings: &[SecurityFinding]) -> HashMap<SecurityCategory, usize> {
1733        let mut counts = HashMap::new();
1734        for finding in findings {
1735            *counts.entry(finding.category.clone()).or_insert(0) += 1;
1736        }
1737        counts
1738    }
1739    
1740    fn calculate_security_score(&self, findings: &[SecurityFinding]) -> f32 {
1741        if findings.is_empty() {
1742            return 100.0;
1743        }
1744        
1745        let total_penalty = findings.iter().map(|f| match f.severity {
1746            SecuritySeverity::Critical => 25.0,
1747            SecuritySeverity::High => 15.0,
1748            SecuritySeverity::Medium => 8.0,
1749            SecuritySeverity::Low => 3.0,
1750            SecuritySeverity::Info => 1.0,
1751        }).sum::<f32>();
1752        
1753        (100.0 - total_penalty).max(0.0)
1754    }
1755    
1756    fn determine_risk_level(&self, findings: &[SecurityFinding]) -> SecuritySeverity {
1757        if findings.iter().any(|f| f.severity == SecuritySeverity::Critical) {
1758            SecuritySeverity::Critical
1759        } else if findings.iter().any(|f| f.severity == SecuritySeverity::High) {
1760            SecuritySeverity::High
1761        } else if findings.iter().any(|f| f.severity == SecuritySeverity::Medium) {
1762            SecuritySeverity::Medium
1763        } else if !findings.is_empty() {
1764            SecuritySeverity::Low
1765        } else {
1766            SecuritySeverity::Info
1767        }
1768    }
1769    
1770    fn assess_compliance(&self, _findings: &[SecurityFinding], _technologies: &[DetectedTechnology]) -> HashMap<String, ComplianceStatus> {
1771        // TODO: Implement compliance assessment
1772        HashMap::new()
1773    }
1774    
1775    fn generate_recommendations(&self, findings: &[SecurityFinding], _technologies: &[DetectedTechnology]) -> Vec<String> {
1776        let mut recommendations = Vec::new();
1777        
1778        if findings.iter().any(|f| f.category == SecurityCategory::SecretsExposure) {
1779            recommendations.push("Implement a secure secret management strategy".to_string());
1780        }
1781        
1782        if findings.iter().any(|f| f.severity == SecuritySeverity::Critical) {
1783            recommendations.push("Address critical security findings immediately".to_string());
1784        }
1785        
1786        recommendations
1787    }
1788}
1789
1790
1791
1792#[cfg(test)]
1793mod tests {
1794    use super::*;
1795    
1796    #[test]
1797    fn test_security_score_calculation() {
1798        let analyzer = SecurityAnalyzer::new().unwrap();
1799        
1800        let findings = vec![
1801            SecurityFinding {
1802                id: "test-1".to_string(),
1803                title: "Test Critical".to_string(),
1804                description: "Test".to_string(),
1805                severity: SecuritySeverity::Critical,
1806                category: SecurityCategory::SecretsExposure,
1807                file_path: None,
1808                line_number: None,
1809                column_number: None,
1810                evidence: None,
1811                remediation: vec![],
1812                references: vec![],
1813                cwe_id: None,
1814                compliance_frameworks: vec![],
1815            }
1816        ];
1817        
1818        let score = analyzer.calculate_security_score(&findings);
1819        assert_eq!(score, 75.0); // 100 - 25 (critical penalty)
1820    }
1821    
1822    #[test]
1823    fn test_secret_pattern_matching() {
1824        let analyzer = SecurityAnalyzer::new().unwrap();
1825        
1826        // Test if placeholder detection works
1827        assert!(analyzer.is_likely_placeholder("API_KEY=sk-xxxxxxxxxxxxxxxx"));
1828        assert!(!analyzer.is_likely_placeholder("API_KEY=sk-1234567890abcdef"));
1829    }
1830    
1831    #[test]
1832    fn test_sensitive_env_var_detection() {
1833        let analyzer = SecurityAnalyzer::new().unwrap();
1834        
1835        assert!(analyzer.is_sensitive_env_var("DATABASE_PASSWORD"));
1836        assert!(analyzer.is_sensitive_env_var("JWT_SECRET"));
1837        assert!(!analyzer.is_sensitive_env_var("PORT"));
1838        assert!(!analyzer.is_sensitive_env_var("NODE_ENV"));
1839    }
1840    
1841    #[test]
1842    fn test_gitignore_aware_severity() {
1843        use tempfile::TempDir;
1844        use std::fs;
1845        use std::process::Command;
1846        
1847        let temp_dir = TempDir::new().unwrap();
1848        let project_root = temp_dir.path();
1849        
1850        // Initialize a real git repo
1851        let git_init = Command::new("git")
1852            .args(&["init"])
1853            .current_dir(project_root)
1854            .output();
1855        
1856        // Skip test if git is not available
1857        if git_init.is_err() {
1858            println!("Skipping gitignore test - git not available");
1859            return;
1860        }
1861        
1862        // Create .gitignore file
1863        fs::write(project_root.join(".gitignore"), ".env\n.env.local\n").unwrap();
1864        
1865        // Stage and commit .gitignore to make it effective
1866        let _ = Command::new("git")
1867            .args(&["add", ".gitignore"])
1868            .current_dir(project_root)
1869            .output();
1870        let _ = Command::new("git")
1871            .args(&["config", "user.email", "test@example.com"])
1872            .current_dir(project_root)
1873            .output();
1874        let _ = Command::new("git")
1875            .args(&["config", "user.name", "Test User"])
1876            .current_dir(project_root)
1877            .output();
1878        let _ = Command::new("git")
1879            .args(&["commit", "-m", "Add gitignore"])
1880            .current_dir(project_root)
1881            .output();
1882        
1883        let mut analyzer = SecurityAnalyzer::new().unwrap();
1884        analyzer.project_root = Some(project_root.to_path_buf());
1885        
1886        // Test file that would be gitignored
1887        let env_file = project_root.join(".env");
1888        fs::write(&env_file, "API_KEY=sk-1234567890abcdef").unwrap();
1889        
1890        // Test severity determination for gitignored file
1891        let (severity, remediation) = analyzer.determine_secret_severity(&env_file, SecuritySeverity::High);
1892        
1893        // With default config, gitignored files should be marked as Info (skipped)
1894        assert_eq!(severity, SecuritySeverity::Info);
1895        assert!(remediation.iter().any(|r| r.contains("gitignored")));
1896    }
1897    
1898    #[test]
1899    fn test_gitignore_config_options() {
1900        let mut config = SecurityAnalysisConfig::default();
1901        
1902        // Test default configuration
1903        assert!(config.skip_gitignored_files);
1904        assert!(!config.downgrade_gitignored_severity);
1905        
1906        // Test downgrade mode
1907        config.skip_gitignored_files = false;
1908        config.downgrade_gitignored_severity = true;
1909        
1910        let analyzer = SecurityAnalyzer::with_config(config).unwrap();
1911        // Additional test logic could be added here for downgrade behavior
1912    }
1913    
1914    #[test]
1915    fn test_gitignore_pattern_matching() {
1916        let analyzer = SecurityAnalyzer::new().unwrap();
1917        
1918        // Test wildcard patterns - *.env matches files ending with .env
1919        assert!(!analyzer.matches_gitignore_pattern("*.env", ".env.local", ".env.local")); // Doesn't end with .env
1920        assert!(analyzer.matches_gitignore_pattern("*.env", "production.env", "production.env")); // Ends with .env
1921        assert!(analyzer.matches_gitignore_pattern(".env*", ".env.production", ".env.production")); // Starts with .env
1922        assert!(analyzer.matches_gitignore_pattern("*.log", "app.log", "app.log"));
1923        
1924        // Test exact patterns
1925        assert!(analyzer.matches_gitignore_pattern(".env", ".env", ".env"));
1926        assert!(!analyzer.matches_gitignore_pattern(".env", ".env.local", ".env.local"));
1927        
1928        // Test directory patterns
1929        assert!(analyzer.matches_gitignore_pattern("/config.json", "config.json", "config.json"));
1930        assert!(!analyzer.matches_gitignore_pattern("/config.json", "src/config.json", "config.json"));
1931        
1932        // Test common .env patterns that should work
1933        assert!(analyzer.matches_gitignore_pattern(".env*", ".env", ".env"));
1934        assert!(analyzer.matches_gitignore_pattern(".env*", ".env.local", ".env.local"));
1935        assert!(analyzer.matches_gitignore_pattern(".env.*", ".env.production", ".env.production"));
1936    }
1937    
1938    #[test]
1939    fn test_common_env_patterns() {
1940        let analyzer = SecurityAnalyzer::new().unwrap();
1941        
1942        // Should match common .env files
1943        assert!(analyzer.matches_common_env_patterns(".env"));
1944        assert!(analyzer.matches_common_env_patterns(".env.local"));
1945        assert!(analyzer.matches_common_env_patterns(".env.production"));
1946        assert!(analyzer.matches_common_env_patterns(".env.development"));
1947        assert!(analyzer.matches_common_env_patterns(".env.test"));
1948        
1949        // Should NOT match example/template files (usually committed)
1950        assert!(!analyzer.matches_common_env_patterns(".env.example"));
1951        assert!(!analyzer.matches_common_env_patterns(".env.sample"));
1952        assert!(!analyzer.matches_common_env_patterns(".env.template"));
1953        
1954        // Should not match non-env files
1955        assert!(!analyzer.matches_common_env_patterns("config.json"));
1956        assert!(!analyzer.matches_common_env_patterns("package.json"));
1957    }
1958    
1959    #[test]
1960    fn test_legitimate_env_var_usage() {
1961        let analyzer = SecurityAnalyzer::new().unwrap();
1962        
1963        // Create mock file paths
1964        let server_file = Path::new("src/server/config.js");
1965        let client_file = Path::new("src/components/MyComponent.js");
1966        
1967        // Test legitimate server-side environment variable usage (should NOT be flagged)
1968        assert!(analyzer.is_legitimate_env_var_usage("const apiKey = process.env.RESEND_API_KEY;", server_file));
1969        assert!(analyzer.is_legitimate_env_var_usage("const dbUrl = process.env.DATABASE_URL;", server_file));
1970        assert!(analyzer.is_legitimate_env_var_usage("api_key = os.environ.get('API_KEY')", server_file));
1971        assert!(analyzer.is_legitimate_env_var_usage("let secret = env::var(\"JWT_SECRET\")?;", server_file));
1972        
1973        // Test client-side environment variable usage (legitimate if not exposed)
1974        assert!(analyzer.is_legitimate_env_var_usage("const apiUrl = process.env.API_URL;", client_file));
1975        
1976        // Test client-side exposed variables (these ARE client-side exposed - security issues)
1977        assert!(analyzer.is_client_side_exposed_env_var("process.env.REACT_APP_SECRET_KEY"));
1978        assert!(analyzer.is_client_side_exposed_env_var("process.env.NEXT_PUBLIC_API_SECRET"));
1979        
1980        // Test hardcoded secrets (should NOT be legitimate)
1981        assert!(!analyzer.is_legitimate_env_var_usage("const apiKey = 'sk-1234567890abcdef';", server_file));
1982        assert!(!analyzer.is_legitimate_env_var_usage("password = 'hardcoded123'", server_file));
1983    }
1984    
1985    #[test]
1986    fn test_server_vs_client_side_detection() {
1987        let analyzer = SecurityAnalyzer::new().unwrap();
1988        
1989        // Server-side files
1990        assert!(analyzer.is_server_side_file(Path::new("src/server/app.js")));
1991        assert!(analyzer.is_server_side_file(Path::new("src/api/users.js")));
1992        assert!(analyzer.is_server_side_file(Path::new("pages/api/auth.js")));
1993        assert!(analyzer.is_server_side_file(Path::new("src/lib/database.js")));
1994        assert!(analyzer.is_server_side_file(Path::new(".env")));
1995        assert!(analyzer.is_server_side_file(Path::new("server.js")));
1996        
1997        // Client-side files
1998        assert!(!analyzer.is_server_side_file(Path::new("src/components/Button.jsx")));
1999        assert!(!analyzer.is_server_side_file(Path::new("public/index.html")));
2000        assert!(!analyzer.is_server_side_file(Path::new("src/pages/home.js")));
2001        assert!(!analyzer.is_server_side_file(Path::new("dist/bundle.js")));
2002        
2003        // Ambiguous files (default to server-side for conservative detection)
2004        assert!(analyzer.is_server_side_file(Path::new("src/utils/helper.js")));
2005        assert!(analyzer.is_server_side_file(Path::new("config/settings.js")));
2006    }
2007    
2008    #[test]
2009    fn test_client_side_exposed_env_vars() {
2010        let analyzer = SecurityAnalyzer::new().unwrap();
2011        
2012        // These should be flagged as client-side exposed (security issues)
2013        assert!(analyzer.is_client_side_exposed_env_var("process.env.REACT_APP_SECRET"));
2014        assert!(analyzer.is_client_side_exposed_env_var("import.meta.env.VITE_API_KEY"));
2015        assert!(analyzer.is_client_side_exposed_env_var("process.env.NEXT_PUBLIC_SECRET"));
2016        assert!(analyzer.is_client_side_exposed_env_var("process.env.VUE_APP_TOKEN"));
2017        
2018        // These should NOT be flagged as client-side exposed
2019        assert!(!analyzer.is_client_side_exposed_env_var("process.env.DATABASE_URL"));
2020        assert!(!analyzer.is_client_side_exposed_env_var("process.env.JWT_SECRET"));
2021        assert!(!analyzer.is_client_side_exposed_env_var("process.env.API_KEY"));
2022    }
2023    
2024    #[test]
2025    fn test_env_var_assignment_context() {
2026        let analyzer = SecurityAnalyzer::new().unwrap();
2027        
2028        // Configuration files where assignments are legitimate
2029        assert!(analyzer.is_env_var_assignment_context("API_KEY=sk-test123", Path::new(".env")));
2030        assert!(analyzer.is_env_var_assignment_context("DATABASE_URL=postgres://", Path::new("docker-compose.yml")));
2031        assert!(analyzer.is_env_var_assignment_context("export SECRET=test", Path::new("setup.sh")));
2032        
2033        // Regular source files where assignments might be suspicious
2034        assert!(!analyzer.is_env_var_assignment_context("const secret = 'hardcoded'", Path::new("src/app.js")));
2035    }
2036    
2037    #[test]
2038    fn test_enhanced_secret_patterns() {
2039        let analyzer = SecurityAnalyzer::new().unwrap();
2040        
2041        // Test that hardcoded secrets are still detected
2042        let hardcoded_patterns = [
2043            "apikey = 'sk-1234567890abcdef1234567890abcdef12345678'",
2044            "const secret = 'my-super-secret-token-12345678901234567890'",
2045            "password = 'hardcoded123456'",
2046        ];
2047        
2048        for pattern in &hardcoded_patterns {
2049            let has_secret = analyzer.secret_patterns.iter().any(|sp| sp.pattern.is_match(pattern));
2050            assert!(has_secret, "Should detect hardcoded secret in: {}", pattern);
2051        }
2052        
2053        // Test that legitimate env var usage is NOT detected as secret
2054        let legitimate_patterns = [
2055            "const apiKey = process.env.API_KEY;",
2056            "const dbUrl = process.env.DATABASE_URL || 'fallback';",
2057            "api_key = os.environ.get('API_KEY')",
2058            "let secret = env::var(\"JWT_SECRET\")?;",
2059        ];
2060        
2061        for pattern in &legitimate_patterns {
2062            // These should either not match any secret pattern, or be filtered out by context detection
2063            let matches_old_generic_pattern = pattern.to_lowercase().contains("secret") || 
2064                                            pattern.to_lowercase().contains("key");
2065            
2066            // Our new patterns should be more specific and not match env var access
2067            let matches_new_patterns = analyzer.secret_patterns.iter()
2068                .filter(|sp| sp.name.contains("Hardcoded"))
2069                .any(|sp| sp.pattern.is_match(pattern));
2070            
2071            assert!(!matches_new_patterns, "Should NOT detect legitimate env var usage as hardcoded secret: {}", pattern);
2072        }
2073    }
2074    
2075    #[test]
2076    fn test_context_aware_false_positive_reduction() {
2077        use tempfile::TempDir;
2078        
2079        let temp_dir = TempDir::new().unwrap();
2080        let server_file = temp_dir.path().join("src/server/config.js");
2081        
2082        // Create directory structure
2083        std::fs::create_dir_all(server_file.parent().unwrap()).unwrap();
2084        
2085        // Write a file with legitimate environment variable usage
2086        let content = r#"
2087const config = {
2088    apiKey: process.env.RESEND_API_KEY,
2089    databaseUrl: process.env.DATABASE_URL,
2090    jwtSecret: process.env.JWT_SECRET,
2091    port: process.env.PORT || 3000
2092};
2093"#;
2094        
2095        std::fs::write(&server_file, content).unwrap();
2096        
2097        let analyzer = SecurityAnalyzer::new().unwrap();
2098        let findings = analyzer.analyze_file_for_secrets(&server_file).unwrap();
2099        
2100        // Should have zero findings because all are legitimate env var usage
2101        assert_eq!(findings.len(), 0, "Should not flag legitimate environment variable usage as security issues");
2102    }
2103}