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