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