1use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::fs;
13use std::time::Instant;
14use regex::Regex;
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17use log::{info, debug};
18use rayon::prelude::*;
19use indicatif::{ProgressBar, ProgressStyle, MultiProgress};
20
21use crate::analyzer::{ProjectAnalysis, DetectedLanguage, DetectedTechnology, EnvVar};
22use crate::analyzer::dependency_parser::Language;
23
24#[derive(Debug, Error)]
25pub enum SecurityError {
26 #[error("Security analysis failed: {0}")]
27 AnalysisFailed(String),
28
29 #[error("Configuration analysis error: {0}")]
30 ConfigAnalysisError(String),
31
32 #[error("Code pattern analysis error: {0}")]
33 CodePatternError(String),
34
35 #[error("Infrastructure analysis error: {0}")]
36 InfrastructureError(String),
37
38 #[error("IO error: {0}")]
39 Io(#[from] std::io::Error),
40
41 #[error("Regex error: {0}")]
42 Regex(#[from] regex::Error),
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub enum SecuritySeverity {
48 Critical,
49 High,
50 Medium,
51 Low,
52 Info,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
57pub enum SecurityCategory {
58 SecretsExposure,
60 InsecureConfiguration,
62 CodeSecurityPattern,
64 InfrastructureSecurity,
66 AuthenticationSecurity,
68 DataProtection,
70 NetworkSecurity,
72 Compliance,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct SecurityFinding {
79 pub id: String,
80 pub title: String,
81 pub description: String,
82 pub severity: SecuritySeverity,
83 pub category: SecurityCategory,
84 pub file_path: Option<PathBuf>,
85 pub line_number: Option<usize>,
86 pub evidence: Option<String>,
87 pub remediation: Vec<String>,
88 pub references: Vec<String>,
89 pub cwe_id: Option<String>,
90 pub compliance_frameworks: Vec<String>,
91}
92
93#[derive(Debug, Serialize, Deserialize)]
95pub struct SecurityReport {
96 pub analyzed_at: chrono::DateTime<chrono::Utc>,
97 pub overall_score: f32, pub risk_level: SecuritySeverity,
99 pub total_findings: usize,
100 pub findings_by_severity: HashMap<SecuritySeverity, usize>,
101 pub findings_by_category: HashMap<SecurityCategory, usize>,
102 pub findings: Vec<SecurityFinding>,
103 pub recommendations: Vec<String>,
104 pub compliance_status: HashMap<String, ComplianceStatus>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ComplianceStatus {
110 pub framework: String,
111 pub coverage: f32, pub missing_controls: Vec<String>,
113 pub recommendations: Vec<String>,
114}
115
116#[derive(Debug, Clone)]
118pub struct SecurityAnalysisConfig {
119 pub include_low_severity: bool,
120 pub check_secrets: bool,
121 pub check_code_patterns: bool,
122 pub check_infrastructure: bool,
123 pub check_compliance: bool,
124 pub frameworks_to_check: Vec<String>,
125 pub ignore_patterns: Vec<String>,
126}
127
128impl Default for SecurityAnalysisConfig {
129 fn default() -> Self {
130 Self {
131 include_low_severity: false,
132 check_secrets: true,
133 check_code_patterns: true,
134 check_infrastructure: true,
135 check_compliance: true,
136 frameworks_to_check: vec![
137 "SOC2".to_string(),
138 "GDPR".to_string(),
139 "OWASP".to_string(),
140 ],
141 ignore_patterns: vec![
142 "node_modules".to_string(),
143 ".git".to_string(),
144 "target".to_string(),
145 "build".to_string(),
146 ".next".to_string(),
147 "dist".to_string(),
148 "test".to_string(),
149 "tests".to_string(),
150 "*.json".to_string(), "*.lock".to_string(), "*_sample.*".to_string(), "*audit*".to_string(), ],
155 }
156 }
157}
158
159pub struct SecurityAnalyzer {
160 config: SecurityAnalysisConfig,
161 secret_patterns: Vec<SecretPattern>,
162 security_rules: HashMap<Language, Vec<SecurityRule>>,
163}
164
165struct SecretPattern {
167 name: String,
168 pattern: Regex,
169 severity: SecuritySeverity,
170 description: String,
171}
172
173struct SecurityRule {
175 id: String,
176 name: String,
177 pattern: Regex,
178 severity: SecuritySeverity,
179 category: SecurityCategory,
180 description: String,
181 remediation: Vec<String>,
182 cwe_id: Option<String>,
183}
184
185impl SecurityAnalyzer {
186 pub fn new() -> Result<Self, SecurityError> {
187 Self::with_config(SecurityAnalysisConfig::default())
188 }
189
190 pub fn with_config(config: SecurityAnalysisConfig) -> Result<Self, SecurityError> {
191 let secret_patterns = Self::initialize_secret_patterns()?;
192 let security_rules = Self::initialize_security_rules()?;
193
194 Ok(Self {
195 config,
196 secret_patterns,
197 security_rules,
198 })
199 }
200
201 pub fn analyze_security(&self, analysis: &ProjectAnalysis) -> Result<SecurityReport, SecurityError> {
203 let start_time = Instant::now();
204 info!("Starting comprehensive security analysis");
205
206 let is_verbose = log::max_level() >= log::LevelFilter::Info;
208
209 let multi_progress = MultiProgress::new();
211
212 let mut total_phases = 0;
216 if self.config.check_secrets { total_phases += 1; }
217 if self.config.check_code_patterns { total_phases += 1; }
218 if self.config.check_infrastructure { total_phases += 1; }
219 total_phases += 2; let main_pb = if is_verbose {
223 None } else {
225 let pb = multi_progress.add(ProgressBar::new(100));
227 pb.set_style(
228 ProgressStyle::default_bar()
229 .template("🛡️ {msg} {bar:50.cyan/blue} {percent}% [{elapsed_precise}]")
230 .unwrap()
231 .progress_chars("██▉▊▋▌▍▎▏ "),
232 );
233 Some(pb)
234 };
235
236 let mut findings = Vec::new();
237 let phase_weight = if is_verbose { 1u64 } else { 100 / total_phases as u64 };
238 let mut current_progress = 0u64;
239
240 if self.config.check_secrets {
242 if let Some(ref pb) = main_pb {
243 pb.set_message("Analyzing configuration & secrets...");
244 pb.set_position(current_progress);
245 }
246
247 if is_verbose {
248 findings.extend(self.analyze_configuration_security(&analysis.project_root)?);
249 } else {
250 findings.extend(self.analyze_configuration_security_with_progress(&analysis.project_root, &multi_progress)?);
251 }
252
253 if let Some(ref pb) = main_pb {
254 current_progress += phase_weight;
255 pb.set_position(current_progress);
256 }
257 }
258
259 if self.config.check_code_patterns {
261 if let Some(ref pb) = main_pb {
262 pb.set_message("Analyzing code security patterns...");
263 }
264
265 if is_verbose {
266 findings.extend(self.analyze_code_security_patterns(&analysis.project_root, &analysis.languages)?);
267 } else {
268 findings.extend(self.analyze_code_security_patterns_with_progress(&analysis.project_root, &analysis.languages, &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 if let Some(ref pb) = main_pb {
283 pb.set_message("Analyzing environment variables...");
284 }
285
286 findings.extend(self.analyze_environment_security(&analysis.environment_variables));
287 if let Some(ref pb) = main_pb {
288 current_progress += phase_weight;
289 pb.set_position(current_progress);
290 }
291
292 if let Some(ref pb) = main_pb {
297 current_progress = 100;
298 pb.set_position(current_progress);
299 }
300
301 if let Some(ref pb) = main_pb {
303 pb.set_message("Processing findings & generating report...");
304 }
305
306 let pre_dedup_count = findings.len();
308 findings = self.deduplicate_findings(findings);
309 let post_dedup_count = findings.len();
310
311 if pre_dedup_count != post_dedup_count {
312 info!("Deduplicated {} redundant findings, {} unique findings remain",
313 pre_dedup_count - post_dedup_count, post_dedup_count);
314 }
315
316 let pre_filter_count = findings.len();
318 if !self.config.include_low_severity {
319 findings.retain(|f| f.severity != SecuritySeverity::Low && f.severity != SecuritySeverity::Info);
320 }
321
322 findings.sort_by(|a, b| a.severity.cmp(&b.severity));
324
325 let total_findings = findings.len();
327 let findings_by_severity = self.count_by_severity(&findings);
328 let findings_by_category = self.count_by_category(&findings);
329 let overall_score = self.calculate_security_score(&findings);
330 let risk_level = self.determine_risk_level(&findings);
331
332 let compliance_status = HashMap::new();
335
336 let recommendations = self.generate_recommendations(&findings, &analysis.technologies);
338
339 let duration = start_time.elapsed().as_secs_f32();
341 if let Some(pb) = main_pb {
342 pb.finish_with_message(format!("✅ Security analysis completed in {:.1}s - Found {} issues", duration, total_findings));
343 }
344
345 if pre_filter_count != total_findings {
347 info!("Found {} total findings, showing {} after filtering", pre_filter_count, total_findings);
348 } else {
349 info!("Found {} security findings", total_findings);
350 }
351
352 Ok(SecurityReport {
353 analyzed_at: chrono::Utc::now(),
354 overall_score,
355 risk_level,
356 total_findings,
357 findings_by_severity,
358 findings_by_category,
359 findings,
360 recommendations,
361 compliance_status,
362 })
363 }
364
365 fn initialize_secret_patterns() -> Result<Vec<SecretPattern>, SecurityError> {
367 let patterns = vec![
368 ("AWS Access Key", r"AKIA[0-9A-Z]{16}", SecuritySeverity::Critical),
370 ("AWS Secret Key", r#"(?i)(aws[_-]?secret|secret[_-]?access[_-]?key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?"#, SecuritySeverity::Critical),
371 ("S3 Secret Key", r#"(?i)(s3[_-]?secret[_-]?key|linode[_-]?s3[_-]?secret)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}["']?"#, SecuritySeverity::High),
372 ("GitHub Token", r"gh[pousr]_[A-Za-z0-9_]{36,255}", SecuritySeverity::High),
373 ("OpenAI API Key", r"sk-[A-Za-z0-9]{48}", SecuritySeverity::High),
374 ("Stripe API Key", r"sk_live_[0-9a-zA-Z]{24}", SecuritySeverity::Critical),
375 ("Stripe Publishable Key", r"pk_live_[0-9a-zA-Z]{24}", SecuritySeverity::Medium),
376
377 ("Database URL", r#"(?i)(database_url|db_url)["']?\s*[:=]\s*["']?[^"'\s]+"#, SecuritySeverity::High),
379 ("Password", r#"(?i)(password|passwd|pwd)["']?\s*[:=]\s*["']?[^"']{6,}"#, SecuritySeverity::Medium),
380 ("JWT Secret", r#"(?i)(jwt[_-]?secret)["']?\s*[:=]\s*["']?[A-Za-z0-9_\-+/=]{20,}"#, SecuritySeverity::High),
381
382 ("RSA Private Key", r"-----BEGIN RSA PRIVATE KEY-----", SecuritySeverity::Critical),
384 ("SSH Private Key", r"-----BEGIN OPENSSH PRIVATE KEY-----", SecuritySeverity::Critical),
385 ("PGP Private Key", r"-----BEGIN PGP PRIVATE KEY BLOCK-----", SecuritySeverity::Critical),
386
387 ("Google Cloud Service Account", r#""type":\s*"service_account""#, SecuritySeverity::High),
389 ("Azure Storage Key", r"DefaultEndpointsProtocol=https;AccountName=", SecuritySeverity::High),
390
391 ("Generic API Key", r#"(?i)(api[_-]?key|apikey)["']?\s*[:=]\s*["']?[A-Za-z0-9_\-]{20,}"#, SecuritySeverity::High),
393 ("Generic Secret", r#"(?i)(secret|token|key)["']?\s*[:=]\s*["']?[A-Za-z0-9_\-+/=]{24,}"#, SecuritySeverity::Medium),
394 ];
395
396 patterns.into_iter()
397 .map(|(name, pattern, severity)| {
398 Ok(SecretPattern {
399 name: name.to_string(),
400 pattern: Regex::new(pattern)?,
401 severity,
402 description: format!("Potential {} found in code", name),
403 })
404 })
405 .collect()
406 }
407
408 fn initialize_security_rules() -> Result<HashMap<Language, Vec<SecurityRule>>, SecurityError> {
410 let mut rules = HashMap::new();
411
412 rules.insert(Language::JavaScript, vec![
414 SecurityRule {
415 id: "js-001".to_string(),
416 name: "Eval Usage".to_string(),
417 pattern: Regex::new(r"\beval\s*\(")?,
418 severity: SecuritySeverity::High,
419 category: SecurityCategory::CodeSecurityPattern,
420 description: "Use of eval() can lead to code injection vulnerabilities".to_string(),
421 remediation: vec![
422 "Avoid using eval() with user input".to_string(),
423 "Use JSON.parse() for parsing JSON data".to_string(),
424 "Consider using safer alternatives like Function constructor with validation".to_string(),
425 ],
426 cwe_id: Some("CWE-95".to_string()),
427 },
428 SecurityRule {
429 id: "js-002".to_string(),
430 name: "innerHTML Usage".to_string(),
431 pattern: Regex::new(r"\.innerHTML\s*=")?,
432 severity: SecuritySeverity::Medium,
433 category: SecurityCategory::CodeSecurityPattern,
434 description: "innerHTML can lead to XSS vulnerabilities if used with unsanitized data".to_string(),
435 remediation: vec![
436 "Use textContent instead of innerHTML for text".to_string(),
437 "Sanitize HTML content before setting innerHTML".to_string(),
438 "Consider using secure templating libraries".to_string(),
439 ],
440 cwe_id: Some("CWE-79".to_string()),
441 },
442 ]);
443
444 rules.insert(Language::Python, vec![
446 SecurityRule {
447 id: "py-001".to_string(),
448 name: "SQL Injection Risk".to_string(),
449 pattern: Regex::new(r#"\.execute\s*\(\s*[f]?["'][^"']*%[sd]"#)?,
450 severity: SecuritySeverity::High,
451 category: SecurityCategory::CodeSecurityPattern,
452 description: "String formatting in SQL queries can lead to SQL injection".to_string(),
453 remediation: vec![
454 "Use parameterized queries instead of string formatting".to_string(),
455 "Use ORM query builders where possible".to_string(),
456 "Validate and sanitize all user inputs".to_string(),
457 ],
458 cwe_id: Some("CWE-89".to_string()),
459 },
460 SecurityRule {
461 id: "py-002".to_string(),
462 name: "Pickle Usage".to_string(),
463 pattern: Regex::new(r"\bpickle\.loads?\s*\(")?,
464 severity: SecuritySeverity::High,
465 category: SecurityCategory::CodeSecurityPattern,
466 description: "Pickle can execute arbitrary code during deserialization".to_string(),
467 remediation: vec![
468 "Avoid pickle for untrusted data".to_string(),
469 "Use JSON or other safe serialization formats".to_string(),
470 "If pickle is necessary, validate data sources".to_string(),
471 ],
472 cwe_id: Some("CWE-502".to_string()),
473 },
474 ]);
475
476 Ok(rules)
479 }
480
481 fn analyze_configuration_security_with_progress(&self, project_root: &Path, multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
483 debug!("Analyzing configuration security");
484 let mut findings = Vec::new();
485
486 let config_files = self.collect_config_files(project_root)?;
488
489 if config_files.is_empty() {
490 info!("No configuration files found");
491 return Ok(findings);
492 }
493
494 let is_verbose = log::max_level() >= log::LevelFilter::Info;
495
496 info!("📁 Found {} configuration files to analyze", config_files.len());
497
498 let file_pb = if is_verbose {
500 None } else {
502 let pb = multi_progress.add(ProgressBar::new(config_files.len() as u64));
504 pb.set_style(
505 ProgressStyle::default_bar()
506 .template(" 🔍 {msg} {bar:40.cyan/blue} {pos}/{len} files ({percent}%)")
507 .unwrap()
508 .progress_chars("████▉▊▋▌▍▎▏ "),
509 );
510 pb.set_message("Scanning configuration files...");
511 Some(pb)
512 };
513
514 use std::sync::atomic::{AtomicUsize, Ordering};
516 use std::sync::Arc;
517 let processed_count = Arc::new(AtomicUsize::new(0));
518
519 let file_findings: Vec<Vec<SecurityFinding>> = config_files
521 .par_iter()
522 .map(|file_path| {
523 let result = self.analyze_file_for_secrets(file_path);
524
525 if let Some(ref pb) = file_pb {
527 let current = processed_count.fetch_add(1, Ordering::Relaxed) + 1;
528 if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
529 let display_name = if file_name.len() > 30 {
531 format!("...{}", &file_name[file_name.len()-27..])
532 } else {
533 file_name.to_string()
534 };
535 pb.set_message(format!("Scanning {}", display_name));
536 }
537 pb.set_position(current as u64);
538 }
539
540 result
541 })
542 .filter_map(|result| result.ok())
543 .collect();
544
545 if let Some(pb) = file_pb {
547 pb.finish_with_message(format!("✅ Scanned {} configuration files", config_files.len()));
548 }
549
550 for mut file_findings in file_findings {
551 findings.append(&mut file_findings);
552 }
553
554 findings.extend(self.check_insecure_configurations(project_root)?);
556
557 info!("🔍 Found {} configuration security findings", findings.len());
558 Ok(findings)
559 }
560
561 fn analyze_configuration_security(&self, project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
563 debug!("Analyzing configuration security");
564 let mut findings = Vec::new();
565
566 let config_files = self.collect_config_files(project_root)?;
568
569 if config_files.is_empty() {
570 info!("No configuration files found");
571 return Ok(findings);
572 }
573
574 info!("📁 Found {} configuration files to analyze", config_files.len());
575
576 let file_findings: Vec<Vec<SecurityFinding>> = config_files
578 .par_iter()
579 .map(|file_path| self.analyze_file_for_secrets(file_path))
580 .filter_map(|result| result.ok())
581 .collect();
582
583 for mut file_findings in file_findings {
584 findings.append(&mut file_findings);
585 }
586
587 findings.extend(self.check_insecure_configurations(project_root)?);
589
590 info!("🔍 Found {} configuration security findings", findings.len());
591 Ok(findings)
592 }
593
594 fn analyze_code_security_patterns_with_progress(&self, project_root: &Path, languages: &[DetectedLanguage], multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
596 debug!("Analyzing code security patterns");
597 let mut findings = Vec::new();
598
599 let mut total_files = 0;
601 let mut language_files = Vec::new();
602
603 for language in languages {
604 if let Some(_rules) = self.security_rules.get(&Language::from_string(&language.name)) {
605 let source_files = self.collect_source_files(project_root, &language.name)?;
606 total_files += source_files.len();
607 language_files.push((language, source_files));
608 }
609 }
610
611 if total_files == 0 {
612 info!("No source files found for code pattern analysis");
613 return Ok(findings);
614 }
615
616 let is_verbose = log::max_level() >= log::LevelFilter::Info;
617
618 info!("📄 Found {} source files across {} languages", total_files, language_files.len());
619
620 let code_pb = if is_verbose {
622 None
624 } else {
625 let pb = multi_progress.add(ProgressBar::new(total_files as u64));
627 pb.set_style(
628 ProgressStyle::default_bar()
629 .template(" 📄 {msg} {bar:40.yellow/white} {pos}/{len} files ({percent}%)")
630 .unwrap()
631 .progress_chars("████▉▊▋▌▍▎▏ "),
632 );
633 pb.set_message("Scanning source code...");
634 Some(pb)
635 };
636
637 use std::sync::atomic::{AtomicUsize, Ordering};
639 use std::sync::Arc;
640 let processed_count = Arc::new(AtomicUsize::new(0));
641
642 for (language, source_files) in language_files {
644 if let Some(rules) = self.security_rules.get(&Language::from_string(&language.name)) {
645 let file_findings: Vec<Vec<SecurityFinding>> = source_files
646 .par_iter()
647 .map(|file_path| {
648 let result = self.analyze_file_with_rules(file_path, rules);
649
650 if let Some(ref pb) = code_pb {
652 let current = processed_count.fetch_add(1, Ordering::Relaxed) + 1;
653 if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
654 let display_name = if file_name.len() > 25 {
655 format!("...{}", &file_name[file_name.len()-22..])
656 } else {
657 file_name.to_string()
658 };
659 pb.set_message(format!("Scanning {} ({})", display_name, language.name));
660 }
661 pb.set_position(current as u64);
662 }
663
664 result
665 })
666 .filter_map(|result| result.ok())
667 .collect();
668
669 for mut file_findings in file_findings {
670 findings.append(&mut file_findings);
671 }
672 }
673 }
674
675 if let Some(pb) = code_pb {
677 pb.finish_with_message(format!("✅ Scanned {} source files", total_files));
678 }
679
680 info!("🔍 Found {} code security findings", findings.len());
681 Ok(findings)
682 }
683
684 fn analyze_code_security_patterns(&self, project_root: &Path, languages: &[DetectedLanguage]) -> Result<Vec<SecurityFinding>, SecurityError> {
686 debug!("Analyzing code security patterns");
687 let mut findings = Vec::new();
688
689 let mut total_files = 0;
691 let mut language_files = Vec::new();
692
693 for language in languages {
694 if let Some(_rules) = self.security_rules.get(&Language::from_string(&language.name)) {
695 let source_files = self.collect_source_files(project_root, &language.name)?;
696 total_files += source_files.len();
697 language_files.push((language, source_files));
698 }
699 }
700
701 if total_files == 0 {
702 info!("No source files found for code pattern analysis");
703 return Ok(findings);
704 }
705
706 info!("📄 Found {} source files across {} languages", total_files, language_files.len());
707
708 for (language, source_files) in language_files {
710 if let Some(rules) = self.security_rules.get(&Language::from_string(&language.name)) {
711 let file_findings: Vec<Vec<SecurityFinding>> = source_files
712 .par_iter()
713 .map(|file_path| self.analyze_file_with_rules(file_path, rules))
714 .filter_map(|result| result.ok())
715 .collect();
716
717 for mut file_findings in file_findings {
718 findings.append(&mut file_findings);
719 }
720 }
721 }
722
723 info!("🔍 Found {} code security findings", findings.len());
724 Ok(findings)
725 }
726
727 fn analyze_infrastructure_security_with_progress(&self, project_root: &Path, _technologies: &[DetectedTechnology], multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
729 debug!("Analyzing infrastructure security");
730 let mut findings = Vec::new();
731
732 let is_verbose = log::max_level() >= log::LevelFilter::Info;
733
734 let infra_pb = if is_verbose {
736 None
738 } else {
739 let pb = multi_progress.add(ProgressBar::new_spinner());
741 pb.set_style(
742 ProgressStyle::default_spinner()
743 .template(" 🏗️ {msg} {spinner:.magenta}")
744 .unwrap()
745 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
746 );
747 pb.enable_steady_tick(std::time::Duration::from_millis(100));
748 Some(pb)
749 };
750
751 if let Some(ref pb) = infra_pb {
753 pb.set_message("Checking Dockerfiles & Compose files...");
754 }
755 findings.extend(self.analyze_dockerfile_security(project_root)?);
756 findings.extend(self.analyze_compose_security(project_root)?);
757
758 if let Some(ref pb) = infra_pb {
760 pb.set_message("Checking CI/CD configurations...");
761 }
762 findings.extend(self.analyze_cicd_security(project_root)?);
763
764 if let Some(pb) = infra_pb {
766 pb.finish_with_message("✅ Infrastructure analysis complete");
767 }
768 info!("🔍 Found {} infrastructure security findings", findings.len());
769
770 Ok(findings)
771 }
772
773 fn analyze_infrastructure_security(&self, project_root: &Path, _technologies: &[DetectedTechnology]) -> Result<Vec<SecurityFinding>, SecurityError> {
775 debug!("Analyzing infrastructure security");
776 let mut findings = Vec::new();
777
778 findings.extend(self.analyze_dockerfile_security(project_root)?);
780 findings.extend(self.analyze_compose_security(project_root)?);
781
782 findings.extend(self.analyze_cicd_security(project_root)?);
784
785 info!("🔍 Found {} infrastructure security findings", findings.len());
786 Ok(findings)
787 }
788
789 fn analyze_environment_security(&self, env_vars: &[EnvVar]) -> Vec<SecurityFinding> {
791 let mut findings = Vec::new();
792
793 for env_var in env_vars {
794 if self.is_sensitive_env_var(&env_var.name) && env_var.default_value.is_some() {
796 findings.push(SecurityFinding {
797 id: format!("env-{}", env_var.name.to_lowercase()),
798 title: "Sensitive Environment Variable with Default Value".to_string(),
799 description: format!("Environment variable '{}' appears to contain sensitive data but has a default value", env_var.name),
800 severity: SecuritySeverity::Medium,
801 category: SecurityCategory::SecretsExposure,
802 file_path: None,
803 line_number: None,
804 evidence: Some(format!("Variable: {} = {:?}", env_var.name, env_var.default_value)),
805 remediation: vec![
806 "Remove default value for sensitive environment variables".to_string(),
807 "Use a secure secret management system".to_string(),
808 "Document required environment variables separately".to_string(),
809 ],
810 references: vec![
811 "https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure".to_string(),
812 ],
813 cwe_id: Some("CWE-200".to_string()),
814 compliance_frameworks: vec!["SOC2".to_string(), "GDPR".to_string()],
815 });
816 }
817 }
818
819 findings
820 }
821
822 fn analyze_framework_security_with_progress(&self, project_root: &Path, technologies: &[DetectedTechnology], multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
824 debug!("Analyzing framework-specific security");
825 let mut findings = Vec::new();
826
827 let framework_count = technologies.len();
828 if framework_count == 0 {
829 info!("No frameworks detected for security analysis");
830 return Ok(findings);
831 }
832
833 let is_verbose = log::max_level() >= log::LevelFilter::Info;
834
835 info!("🔧 Found {} frameworks to analyze", framework_count);
836
837 let fw_pb = if is_verbose {
839 None
841 } else {
842 let pb = multi_progress.add(ProgressBar::new_spinner());
844 pb.set_style(
845 ProgressStyle::default_spinner()
846 .template(" 🔧 {msg} {spinner:.cyan}")
847 .unwrap()
848 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
849 );
850 pb.enable_steady_tick(std::time::Duration::from_millis(120));
851 Some(pb)
852 };
853
854 for tech in technologies {
855 if let Some(ref pb) = fw_pb {
856 pb.set_message(format!("Checking {} configuration...", tech.name));
857 }
858
859 match tech.name.as_str() {
860 "Express.js" | "Express" => {
861 findings.extend(self.analyze_express_security(project_root)?);
862 },
863 "Django" => {
864 findings.extend(self.analyze_django_security(project_root)?);
865 },
866 "Spring Boot" => {
867 findings.extend(self.analyze_spring_security(project_root)?);
868 },
869 "Next.js" => {
870 findings.extend(self.analyze_nextjs_security(project_root)?);
871 },
872 _ => {}
874 }
875 }
876
877 if let Some(pb) = fw_pb {
879 pb.finish_with_message("✅ Framework analysis complete");
880 }
881 info!("🔍 Found {} framework security findings", findings.len());
882
883 Ok(findings)
884 }
885
886 fn analyze_framework_security(&self, project_root: &Path, technologies: &[DetectedTechnology]) -> Result<Vec<SecurityFinding>, SecurityError> {
888 debug!("Analyzing framework-specific security");
889 let mut findings = Vec::new();
890
891 let framework_count = technologies.len();
892 if framework_count == 0 {
893 info!("No frameworks detected for security analysis");
894 return Ok(findings);
895 }
896
897 info!("🔧 Found {} frameworks to analyze", framework_count);
898
899 for tech in technologies {
900 match tech.name.as_str() {
901 "Express.js" | "Express" => {
902 findings.extend(self.analyze_express_security(project_root)?);
903 },
904 "Django" => {
905 findings.extend(self.analyze_django_security(project_root)?);
906 },
907 "Spring Boot" => {
908 findings.extend(self.analyze_spring_security(project_root)?);
909 },
910 "Next.js" => {
911 findings.extend(self.analyze_nextjs_security(project_root)?);
912 },
913 _ => {}
915 }
916 }
917
918 info!("🔍 Found {} framework security findings", findings.len());
919 Ok(findings)
920 }
921
922 fn collect_config_files(&self, project_root: &Path) -> Result<Vec<PathBuf>, SecurityError> {
925 let patterns = vec![
926 "*.env*", "*.conf", "*.config", "*.ini", "*.yaml", "*.yml",
927 "*.toml", "docker-compose*.yml", "Dockerfile*",
928 ".github/**/*.yml", ".gitlab-ci.yml", "package.json",
929 "requirements.txt", "Cargo.toml", "go.mod", "pom.xml",
930 ];
931
932 let mut files = crate::common::file_utils::find_files_by_patterns(project_root, &patterns)
933 .map_err(|e| SecurityError::Io(e))?;
934
935 files.retain(|file| {
937 let file_name = file.file_name()
938 .and_then(|n| n.to_str())
939 .unwrap_or("");
940 let file_path = file.to_string_lossy();
941
942 !self.config.ignore_patterns.iter().any(|pattern| {
943 if pattern.contains('*') {
944 glob::Pattern::new(pattern)
946 .map(|p| p.matches(&file_path) || p.matches(file_name))
947 .unwrap_or(false)
948 } else {
949 file_path.contains(pattern) || file_name.contains(pattern)
951 }
952 })
953 });
954
955 Ok(files)
956 }
957
958 fn analyze_file_for_secrets(&self, file_path: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
959 let content = fs::read_to_string(file_path)?;
960 let mut findings = Vec::new();
961
962 for (line_num, line) in content.lines().enumerate() {
963 for pattern in &self.secret_patterns {
964 if let Some(captures) = pattern.pattern.find(line) {
965 if self.is_likely_placeholder(line) {
967 continue;
968 }
969
970 findings.push(SecurityFinding {
971 id: format!("secret-{}-{}", pattern.name.to_lowercase().replace(' ', "-"), line_num),
972 title: format!("Potential {} Exposure", pattern.name),
973 description: pattern.description.clone(),
974 severity: pattern.severity.clone(),
975 category: SecurityCategory::SecretsExposure,
976 file_path: Some(file_path.to_path_buf()),
977 line_number: Some(line_num + 1),
978 evidence: Some(format!("Line: {}", line.trim())),
979 remediation: vec![
980 "Remove sensitive data from source code".to_string(),
981 "Use environment variables for secrets".to_string(),
982 "Consider using a secure secret management service".to_string(),
983 "Add this file to .gitignore if it contains secrets".to_string(),
984 ],
985 references: vec![
986 "https://owasp.org/www-project-top-ten/2021/A05_2021-Security_Misconfiguration/".to_string(),
987 ],
988 cwe_id: Some("CWE-200".to_string()),
989 compliance_frameworks: vec!["SOC2".to_string(), "GDPR".to_string()],
990 });
991 }
992 }
993 }
994
995 Ok(findings)
996 }
997
998 fn is_likely_placeholder(&self, line: &str) -> bool {
999 let placeholder_indicators = [
1000 "example", "placeholder", "your_", "insert_", "replace_",
1001 "xxx", "yyy", "zzz", "fake", "dummy", "test_key",
1002 "sk-xxxxxxxx", "AKIA00000000",
1003 ];
1004
1005 let hash_indicators = [
1006 "checksum", "hash", "sha1", "sha256", "md5", "commit",
1007 "fingerprint", "digest", "advisory", "ghsa-", "cve-",
1008 "rustc_fingerprint", "last-commit", "references",
1009 ];
1010
1011 let line_lower = line.to_lowercase();
1012
1013 if placeholder_indicators.iter().any(|indicator| line_lower.contains(indicator)) {
1015 return true;
1016 }
1017
1018 if hash_indicators.iter().any(|indicator| line_lower.contains(indicator)) {
1020 return true;
1021 }
1022
1023 if line_lower.contains("http") || line_lower.contains("github.com") {
1025 return true;
1026 }
1027
1028 if let Some(potential_hash) = self.extract_potential_hash(line) {
1030 if potential_hash.len() >= 32 && self.is_hex_only(&potential_hash) {
1031 return true; }
1033 }
1034
1035 false
1036 }
1037
1038 fn extract_potential_hash(&self, line: &str) -> Option<String> {
1039 if let Some(start) = line.find('"') {
1041 if let Some(end) = line[start + 1..].find('"') {
1042 let potential = &line[start + 1..start + 1 + end];
1043 if potential.len() >= 32 {
1044 return Some(potential.to_string());
1045 }
1046 }
1047 }
1048 None
1049 }
1050
1051 fn is_hex_only(&self, s: &str) -> bool {
1052 s.chars().all(|c| c.is_ascii_hexdigit())
1053 }
1054
1055 fn is_sensitive_env_var(&self, name: &str) -> bool {
1056 let sensitive_patterns = [
1057 "password", "secret", "key", "token", "auth", "api",
1058 "private", "credential", "cert", "ssl", "tls",
1059 ];
1060
1061 let name_lower = name.to_lowercase();
1062 sensitive_patterns.iter().any(|pattern| name_lower.contains(pattern))
1063 }
1064
1065 fn analyze_express_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1067 Ok(vec![])
1069 }
1070
1071 fn analyze_django_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1072 Ok(vec![])
1074 }
1075
1076 fn analyze_spring_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1077 Ok(vec![])
1079 }
1080
1081 fn analyze_nextjs_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1082 Ok(vec![])
1084 }
1085
1086 fn analyze_dockerfile_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1087 Ok(vec![])
1089 }
1090
1091 fn analyze_compose_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1092 Ok(vec![])
1094 }
1095
1096 fn analyze_cicd_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1097 Ok(vec![])
1099 }
1100
1101 fn collect_source_files(&self, project_root: &Path, language: &str) -> Result<Vec<PathBuf>, SecurityError> {
1103 Ok(vec![])
1105 }
1106
1107 fn analyze_file_with_rules(&self, _file_path: &Path, _rules: &[SecurityRule]) -> Result<Vec<SecurityFinding>, SecurityError> {
1108 Ok(vec![])
1110 }
1111
1112 fn check_insecure_configurations(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1113 Ok(vec![])
1115 }
1116
1117 fn deduplicate_findings(&self, mut findings: Vec<SecurityFinding>) -> Vec<SecurityFinding> {
1119 use std::collections::HashSet;
1120
1121 let mut seen_secrets: HashSet<String> = HashSet::new();
1122 let mut deduplicated = Vec::new();
1123
1124 findings.sort_by(|a, b| {
1126 let a_priority = self.get_pattern_priority(&a.title);
1128 let b_priority = self.get_pattern_priority(&b.title);
1129
1130 match a_priority.cmp(&b_priority) {
1131 std::cmp::Ordering::Equal => {
1132 a.severity.cmp(&b.severity)
1134 }
1135 other => other
1136 }
1137 });
1138
1139 for finding in findings {
1140 let key = self.generate_finding_key(&finding);
1141
1142 if !seen_secrets.contains(&key) {
1143 seen_secrets.insert(key);
1144 deduplicated.push(finding);
1145 }
1146 }
1147
1148 deduplicated
1149 }
1150
1151 fn generate_finding_key(&self, finding: &SecurityFinding) -> String {
1153 match finding.category {
1154 SecurityCategory::SecretsExposure => {
1155 if let Some(evidence) = &finding.evidence {
1157 if let Some(file_path) = &finding.file_path {
1158 if let Some(secret_value) = self.extract_secret_value(evidence) {
1160 return format!("secret:{}:{}", file_path.display(), secret_value);
1161 }
1162 if let Some(line_num) = finding.line_number {
1164 return format!("secret:{}:{}", file_path.display(), line_num);
1165 }
1166 }
1167 }
1168 format!("secret:{}", finding.title)
1170 }
1171 _ => {
1172 if let Some(file_path) = &finding.file_path {
1174 if let Some(line_num) = finding.line_number {
1175 format!("other:{}:{}:{}", file_path.display(), line_num, finding.title)
1176 } else {
1177 format!("other:{}:{}", file_path.display(), finding.title)
1178 }
1179 } else {
1180 format!("other:{}", finding.title)
1181 }
1182 }
1183 }
1184 }
1185
1186 fn extract_secret_value(&self, evidence: &str) -> Option<String> {
1188 if let Some(pos) = evidence.find('=') {
1190 let value = evidence[pos + 1..].trim();
1191 let value = value.trim_matches('"').trim_matches('\'');
1193 if value.len() > 10 { return Some(value.to_string());
1195 }
1196 }
1197
1198 if let Some(pos) = evidence.find(':') {
1200 let value = evidence[pos + 1..].trim();
1201 let value = value.trim_matches('"').trim_matches('\'');
1202 if value.len() > 10 {
1203 return Some(value.to_string());
1204 }
1205 }
1206
1207 None
1208 }
1209
1210 fn get_pattern_priority(&self, title: &str) -> u8 {
1212 if title.contains("AWS Access Key") { return 1; }
1214 if title.contains("AWS Secret Key") { return 1; }
1215 if title.contains("S3 Secret Key") { return 1; }
1216 if title.contains("GitHub Token") { return 1; }
1217 if title.contains("OpenAI API Key") { return 1; }
1218 if title.contains("Stripe") { return 1; }
1219 if title.contains("RSA Private Key") { return 1; }
1220 if title.contains("SSH Private Key") { return 1; }
1221
1222 if title.contains("JWT Secret") { return 2; }
1224 if title.contains("Database URL") { return 2; }
1225
1226 if title.contains("API Key") { return 3; }
1228
1229 if title.contains("Environment Variable") { return 4; }
1231
1232 if title.contains("Generic Secret") { return 5; }
1234
1235 3
1237 }
1238
1239 fn count_by_severity(&self, findings: &[SecurityFinding]) -> HashMap<SecuritySeverity, usize> {
1240 let mut counts = HashMap::new();
1241 for finding in findings {
1242 *counts.entry(finding.severity.clone()).or_insert(0) += 1;
1243 }
1244 counts
1245 }
1246
1247 fn count_by_category(&self, findings: &[SecurityFinding]) -> HashMap<SecurityCategory, usize> {
1248 let mut counts = HashMap::new();
1249 for finding in findings {
1250 *counts.entry(finding.category.clone()).or_insert(0) += 1;
1251 }
1252 counts
1253 }
1254
1255 fn calculate_security_score(&self, findings: &[SecurityFinding]) -> f32 {
1256 if findings.is_empty() {
1257 return 100.0;
1258 }
1259
1260 let total_penalty = findings.iter().map(|f| match f.severity {
1261 SecuritySeverity::Critical => 25.0,
1262 SecuritySeverity::High => 15.0,
1263 SecuritySeverity::Medium => 8.0,
1264 SecuritySeverity::Low => 3.0,
1265 SecuritySeverity::Info => 1.0,
1266 }).sum::<f32>();
1267
1268 (100.0 - total_penalty).max(0.0)
1269 }
1270
1271 fn determine_risk_level(&self, findings: &[SecurityFinding]) -> SecuritySeverity {
1272 if findings.iter().any(|f| f.severity == SecuritySeverity::Critical) {
1273 SecuritySeverity::Critical
1274 } else if findings.iter().any(|f| f.severity == SecuritySeverity::High) {
1275 SecuritySeverity::High
1276 } else if findings.iter().any(|f| f.severity == SecuritySeverity::Medium) {
1277 SecuritySeverity::Medium
1278 } else if !findings.is_empty() {
1279 SecuritySeverity::Low
1280 } else {
1281 SecuritySeverity::Info
1282 }
1283 }
1284
1285 fn assess_compliance(&self, _findings: &[SecurityFinding], _technologies: &[DetectedTechnology]) -> HashMap<String, ComplianceStatus> {
1286 HashMap::new()
1288 }
1289
1290 fn generate_recommendations(&self, findings: &[SecurityFinding], _technologies: &[DetectedTechnology]) -> Vec<String> {
1291 let mut recommendations = Vec::new();
1292
1293 if findings.iter().any(|f| f.category == SecurityCategory::SecretsExposure) {
1294 recommendations.push("Implement a secure secret management strategy".to_string());
1295 }
1296
1297 if findings.iter().any(|f| f.severity == SecuritySeverity::Critical) {
1298 recommendations.push("Address critical security findings immediately".to_string());
1299 }
1300
1301 recommendations
1304 }
1305}
1306
1307impl Language {
1308 fn from_string(name: &str) -> Self {
1309 match name.to_lowercase().as_str() {
1310 "rust" => Language::Rust,
1311 "javascript" | "js" => Language::JavaScript,
1312 "typescript" | "ts" => Language::TypeScript,
1313 "python" | "py" => Language::Python,
1314 "go" | "golang" => Language::Go,
1315 "java" => Language::Java,
1316 "kotlin" => Language::Kotlin,
1317 _ => Language::Unknown,
1318 }
1319 }
1320}
1321
1322#[cfg(test)]
1323mod tests {
1324 use super::*;
1325
1326 #[test]
1327 fn test_security_score_calculation() {
1328 let analyzer = SecurityAnalyzer::new().unwrap();
1329
1330 let findings = vec![
1331 SecurityFinding {
1332 id: "test-1".to_string(),
1333 title: "Test Critical".to_string(),
1334 description: "Test".to_string(),
1335 severity: SecuritySeverity::Critical,
1336 category: SecurityCategory::SecretsExposure,
1337 file_path: None,
1338 line_number: None,
1339 evidence: None,
1340 remediation: vec![],
1341 references: vec![],
1342 cwe_id: None,
1343 compliance_frameworks: vec![],
1344 }
1345 ];
1346
1347 let score = analyzer.calculate_security_score(&findings);
1348 assert_eq!(score, 75.0); }
1350
1351 #[test]
1352 fn test_secret_pattern_matching() {
1353 let analyzer = SecurityAnalyzer::new().unwrap();
1354
1355 assert!(analyzer.is_likely_placeholder("API_KEY=sk-xxxxxxxxxxxxxxxx"));
1357 assert!(!analyzer.is_likely_placeholder("API_KEY=sk-1234567890abcdef"));
1358 }
1359
1360 #[test]
1361 fn test_sensitive_env_var_detection() {
1362 let analyzer = SecurityAnalyzer::new().unwrap();
1363
1364 assert!(analyzer.is_sensitive_env_var("DATABASE_PASSWORD"));
1365 assert!(analyzer.is_sensitive_env_var("JWT_SECRET"));
1366 assert!(!analyzer.is_sensitive_env_var("PORT"));
1367 assert!(!analyzer.is_sensitive_env_var("NODE_ENV"));
1368 }
1369}