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(lang) = Language::from_string(&language.name) {
605 if let Some(_rules) = self.security_rules.get(&lang) {
606 let source_files = self.collect_source_files(project_root, &language.name)?;
607 total_files += source_files.len();
608 language_files.push((language, source_files));
609 }
610 }
611 }
612
613 if total_files == 0 {
614 info!("No source files found for code pattern analysis");
615 return Ok(findings);
616 }
617
618 let is_verbose = log::max_level() >= log::LevelFilter::Info;
619
620 info!("📄 Found {} source files across {} languages", total_files, language_files.len());
621
622 let code_pb = if is_verbose {
624 None
626 } else {
627 let pb = multi_progress.add(ProgressBar::new(total_files as u64));
629 pb.set_style(
630 ProgressStyle::default_bar()
631 .template(" 📄 {msg} {bar:40.yellow/white} {pos}/{len} files ({percent}%)")
632 .unwrap()
633 .progress_chars("████▉▊▋▌▍▎▏ "),
634 );
635 pb.set_message("Scanning source code...");
636 Some(pb)
637 };
638
639
640 use std::sync::atomic::{AtomicUsize, Ordering};
642 use std::sync::Arc;
643 let processed_count = Arc::new(AtomicUsize::new(0));
644
645 for (language, source_files) in language_files {
647 if let Some(lang) = Language::from_string(&language.name) {
648 if let Some(rules) = self.security_rules.get(&lang) {
649 let file_findings: Vec<Vec<SecurityFinding>> = source_files
650 .par_iter()
651 .map(|file_path| {
652 let result = self.analyze_file_with_rules(file_path, rules);
653
654 if let Some(ref pb) = code_pb {
656 let current = processed_count.fetch_add(1, Ordering::Relaxed) + 1;
657 if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
658 let display_name = if file_name.len() > 25 {
659 format!("...{}", &file_name[file_name.len()-22..])
660 } else {
661 file_name.to_string()
662 };
663 pb.set_message(format!("Scanning {} ({})", display_name, language.name));
664 }
665 pb.set_position(current as u64);
666 }
667
668 result
669 })
670 .filter_map(|result| result.ok())
671 .collect();
672
673 for mut file_findings in file_findings {
674 findings.append(&mut file_findings);
675 }
676 }
677 }
678 }
679
680 if let Some(pb) = code_pb {
682 pb.finish_with_message(format!("✅ Scanned {} source files", total_files));
683 }
684
685 info!("🔍 Found {} code security findings", findings.len());
686 Ok(findings)
687 }
688
689 fn analyze_code_security_patterns(&self, project_root: &Path, languages: &[DetectedLanguage]) -> Result<Vec<SecurityFinding>, SecurityError> {
691 debug!("Analyzing code security patterns");
692 let mut findings = Vec::new();
693
694 let mut total_files = 0;
696 let mut language_files = Vec::new();
697
698 for language in languages {
699 if let Some(lang) = Language::from_string(&language.name) {
700 if let Some(_rules) = self.security_rules.get(&lang) {
701 let source_files = self.collect_source_files(project_root, &language.name)?;
702 total_files += source_files.len();
703 language_files.push((language, source_files));
704 }
705 }
706 }
707
708 if total_files == 0 {
709 info!("No source files found for code pattern analysis");
710 return Ok(findings);
711 }
712
713 info!("📄 Found {} source files across {} languages", total_files, language_files.len());
714
715 for (language, source_files) in language_files {
717 if let Some(lang) = Language::from_string(&language.name) {
718 if let Some(rules) = self.security_rules.get(&lang) {
719 let file_findings: Vec<Vec<SecurityFinding>> = source_files
720 .par_iter()
721 .map(|file_path| self.analyze_file_with_rules(file_path, rules))
722 .filter_map(|result| result.ok())
723 .collect();
724
725 for mut file_findings in file_findings {
726 findings.append(&mut file_findings);
727 }
728 }
729 }
730 }
731
732 info!("🔍 Found {} code security findings", findings.len());
733 Ok(findings)
734 }
735
736 fn analyze_infrastructure_security_with_progress(&self, project_root: &Path, _technologies: &[DetectedTechnology], multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
738 debug!("Analyzing infrastructure security");
739 let mut findings = Vec::new();
740
741 let is_verbose = log::max_level() >= log::LevelFilter::Info;
742
743 let infra_pb = if is_verbose {
745 None
747 } else {
748 let pb = multi_progress.add(ProgressBar::new_spinner());
750 pb.set_style(
751 ProgressStyle::default_spinner()
752 .template(" 🏗️ {msg} {spinner:.magenta}")
753 .unwrap()
754 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
755 );
756 pb.enable_steady_tick(std::time::Duration::from_millis(100));
757 Some(pb)
758 };
759
760 if let Some(ref pb) = infra_pb {
762 pb.set_message("Checking Dockerfiles & Compose files...");
763 }
764 findings.extend(self.analyze_dockerfile_security(project_root)?);
765 findings.extend(self.analyze_compose_security(project_root)?);
766
767 if let Some(ref pb) = infra_pb {
769 pb.set_message("Checking CI/CD configurations...");
770 }
771 findings.extend(self.analyze_cicd_security(project_root)?);
772
773 if let Some(pb) = infra_pb {
775 pb.finish_with_message("✅ Infrastructure analysis complete");
776 }
777 info!("🔍 Found {} infrastructure security findings", findings.len());
778
779 Ok(findings)
780 }
781
782 fn analyze_infrastructure_security(&self, project_root: &Path, _technologies: &[DetectedTechnology]) -> Result<Vec<SecurityFinding>, SecurityError> {
784 debug!("Analyzing infrastructure security");
785 let mut findings = Vec::new();
786
787 findings.extend(self.analyze_dockerfile_security(project_root)?);
789 findings.extend(self.analyze_compose_security(project_root)?);
790
791 findings.extend(self.analyze_cicd_security(project_root)?);
793
794 info!("🔍 Found {} infrastructure security findings", findings.len());
795 Ok(findings)
796 }
797
798 fn analyze_environment_security(&self, env_vars: &[EnvVar]) -> Vec<SecurityFinding> {
800 let mut findings = Vec::new();
801
802 for env_var in env_vars {
803 if self.is_sensitive_env_var(&env_var.name) && env_var.default_value.is_some() {
805 findings.push(SecurityFinding {
806 id: format!("env-{}", env_var.name.to_lowercase()),
807 title: "Sensitive Environment Variable with Default Value".to_string(),
808 description: format!("Environment variable '{}' appears to contain sensitive data but has a default value", env_var.name),
809 severity: SecuritySeverity::Medium,
810 category: SecurityCategory::SecretsExposure,
811 file_path: None,
812 line_number: None,
813 evidence: Some(format!("Variable: {} = {:?}", env_var.name, env_var.default_value)),
814 remediation: vec![
815 "Remove default value for sensitive environment variables".to_string(),
816 "Use a secure secret management system".to_string(),
817 "Document required environment variables separately".to_string(),
818 ],
819 references: vec![
820 "https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure".to_string(),
821 ],
822 cwe_id: Some("CWE-200".to_string()),
823 compliance_frameworks: vec!["SOC2".to_string(), "GDPR".to_string()],
824 });
825 }
826 }
827
828 findings
829 }
830
831 fn analyze_framework_security_with_progress(&self, project_root: &Path, technologies: &[DetectedTechnology], multi_progress: &MultiProgress) -> Result<Vec<SecurityFinding>, SecurityError> {
833 debug!("Analyzing framework-specific security");
834 let mut findings = Vec::new();
835
836 let framework_count = technologies.len();
837 if framework_count == 0 {
838 info!("No frameworks detected for security analysis");
839 return Ok(findings);
840 }
841
842 let is_verbose = log::max_level() >= log::LevelFilter::Info;
843
844 info!("🔧 Found {} frameworks to analyze", framework_count);
845
846 let fw_pb = if is_verbose {
848 None
850 } else {
851 let pb = multi_progress.add(ProgressBar::new_spinner());
853 pb.set_style(
854 ProgressStyle::default_spinner()
855 .template(" 🔧 {msg} {spinner:.cyan}")
856 .unwrap()
857 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
858 );
859 pb.enable_steady_tick(std::time::Duration::from_millis(120));
860 Some(pb)
861 };
862
863 for tech in technologies {
864 if let Some(ref pb) = fw_pb {
865 pb.set_message(format!("Checking {} configuration...", tech.name));
866 }
867
868 match tech.name.as_str() {
869 "Express.js" | "Express" => {
870 findings.extend(self.analyze_express_security(project_root)?);
871 },
872 "Django" => {
873 findings.extend(self.analyze_django_security(project_root)?);
874 },
875 "Spring Boot" => {
876 findings.extend(self.analyze_spring_security(project_root)?);
877 },
878 "Next.js" => {
879 findings.extend(self.analyze_nextjs_security(project_root)?);
880 },
881 _ => {}
883 }
884 }
885
886 if let Some(pb) = fw_pb {
888 pb.finish_with_message("✅ Framework analysis complete");
889 }
890 info!("🔍 Found {} framework security findings", findings.len());
891
892 Ok(findings)
893 }
894
895 fn analyze_framework_security(&self, project_root: &Path, technologies: &[DetectedTechnology]) -> Result<Vec<SecurityFinding>, SecurityError> {
897 debug!("Analyzing framework-specific security");
898 let mut findings = Vec::new();
899
900 let framework_count = technologies.len();
901 if framework_count == 0 {
902 info!("No frameworks detected for security analysis");
903 return Ok(findings);
904 }
905
906 info!("🔧 Found {} frameworks to analyze", framework_count);
907
908 for tech in technologies {
909 match tech.name.as_str() {
910 "Express.js" | "Express" => {
911 findings.extend(self.analyze_express_security(project_root)?);
912 },
913 "Django" => {
914 findings.extend(self.analyze_django_security(project_root)?);
915 },
916 "Spring Boot" => {
917 findings.extend(self.analyze_spring_security(project_root)?);
918 },
919 "Next.js" => {
920 findings.extend(self.analyze_nextjs_security(project_root)?);
921 },
922 _ => {}
924 }
925 }
926
927 info!("🔍 Found {} framework security findings", findings.len());
928 Ok(findings)
929 }
930
931 fn collect_config_files(&self, project_root: &Path) -> Result<Vec<PathBuf>, SecurityError> {
934 let patterns = vec![
935 "*.env*", "*.conf", "*.config", "*.ini", "*.yaml", "*.yml",
936 "*.toml", "docker-compose*.yml", "Dockerfile*",
937 ".github/**/*.yml", ".gitlab-ci.yml", "package.json",
938 "requirements.txt", "Cargo.toml", "go.mod", "pom.xml",
939 ];
940
941 let mut files = crate::common::file_utils::find_files_by_patterns(project_root, &patterns)
942 .map_err(|e| SecurityError::Io(e))?;
943
944 files.retain(|file| {
946 let file_name = file.file_name()
947 .and_then(|n| n.to_str())
948 .unwrap_or("");
949 let file_path = file.to_string_lossy();
950
951 !self.config.ignore_patterns.iter().any(|pattern| {
952 if pattern.contains('*') {
953 glob::Pattern::new(pattern)
955 .map(|p| p.matches(&file_path) || p.matches(file_name))
956 .unwrap_or(false)
957 } else {
958 file_path.contains(pattern) || file_name.contains(pattern)
960 }
961 })
962 });
963
964 Ok(files)
965 }
966
967 fn analyze_file_for_secrets(&self, file_path: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
968 let content = fs::read_to_string(file_path)?;
969 let mut findings = Vec::new();
970
971 for (line_num, line) in content.lines().enumerate() {
972 for pattern in &self.secret_patterns {
973 if let Some(captures) = pattern.pattern.find(line) {
974 if self.is_likely_placeholder(line) {
976 continue;
977 }
978
979 findings.push(SecurityFinding {
980 id: format!("secret-{}-{}", pattern.name.to_lowercase().replace(' ', "-"), line_num),
981 title: format!("Potential {} Exposure", pattern.name),
982 description: pattern.description.clone(),
983 severity: pattern.severity.clone(),
984 category: SecurityCategory::SecretsExposure,
985 file_path: Some(file_path.to_path_buf()),
986 line_number: Some(line_num + 1),
987 evidence: Some(format!("Line: {}", line.trim())),
988 remediation: vec![
989 "Remove sensitive data from source code".to_string(),
990 "Use environment variables for secrets".to_string(),
991 "Consider using a secure secret management service".to_string(),
992 "Add this file to .gitignore if it contains secrets".to_string(),
993 ],
994 references: vec![
995 "https://owasp.org/www-project-top-ten/2021/A05_2021-Security_Misconfiguration/".to_string(),
996 ],
997 cwe_id: Some("CWE-200".to_string()),
998 compliance_frameworks: vec!["SOC2".to_string(), "GDPR".to_string()],
999 });
1000 }
1001 }
1002 }
1003
1004 Ok(findings)
1005 }
1006
1007 fn is_likely_placeholder(&self, line: &str) -> bool {
1008 let placeholder_indicators = [
1009 "example", "placeholder", "your_", "insert_", "replace_",
1010 "xxx", "yyy", "zzz", "fake", "dummy", "test_key",
1011 "sk-xxxxxxxx", "AKIA00000000",
1012 ];
1013
1014 let hash_indicators = [
1015 "checksum", "hash", "sha1", "sha256", "md5", "commit",
1016 "fingerprint", "digest", "advisory", "ghsa-", "cve-",
1017 "rustc_fingerprint", "last-commit", "references",
1018 ];
1019
1020 let line_lower = line.to_lowercase();
1021
1022 if placeholder_indicators.iter().any(|indicator| line_lower.contains(indicator)) {
1024 return true;
1025 }
1026
1027 if hash_indicators.iter().any(|indicator| line_lower.contains(indicator)) {
1029 return true;
1030 }
1031
1032 if line_lower.contains("http") || line_lower.contains("github.com") {
1034 return true;
1035 }
1036
1037 if let Some(potential_hash) = self.extract_potential_hash(line) {
1039 if potential_hash.len() >= 32 && self.is_hex_only(&potential_hash) {
1040 return true; }
1042 }
1043
1044 false
1045 }
1046
1047 fn extract_potential_hash(&self, line: &str) -> Option<String> {
1048 if let Some(start) = line.find('"') {
1050 if let Some(end) = line[start + 1..].find('"') {
1051 let potential = &line[start + 1..start + 1 + end];
1052 if potential.len() >= 32 {
1053 return Some(potential.to_string());
1054 }
1055 }
1056 }
1057 None
1058 }
1059
1060 fn is_hex_only(&self, s: &str) -> bool {
1061 s.chars().all(|c| c.is_ascii_hexdigit())
1062 }
1063
1064 fn is_sensitive_env_var(&self, name: &str) -> bool {
1065 let sensitive_patterns = [
1066 "password", "secret", "key", "token", "auth", "api",
1067 "private", "credential", "cert", "ssl", "tls",
1068 ];
1069
1070 let name_lower = name.to_lowercase();
1071 sensitive_patterns.iter().any(|pattern| name_lower.contains(pattern))
1072 }
1073
1074 fn analyze_express_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1076 Ok(vec![])
1078 }
1079
1080 fn analyze_django_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1081 Ok(vec![])
1083 }
1084
1085 fn analyze_spring_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1086 Ok(vec![])
1088 }
1089
1090 fn analyze_nextjs_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1091 Ok(vec![])
1093 }
1094
1095 fn analyze_dockerfile_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1096 Ok(vec![])
1098 }
1099
1100 fn analyze_compose_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1101 Ok(vec![])
1103 }
1104
1105 fn analyze_cicd_security(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1106 Ok(vec![])
1108 }
1109
1110 fn collect_source_files(&self, project_root: &Path, language: &str) -> Result<Vec<PathBuf>, SecurityError> {
1112 Ok(vec![])
1114 }
1115
1116 fn analyze_file_with_rules(&self, _file_path: &Path, _rules: &[SecurityRule]) -> Result<Vec<SecurityFinding>, SecurityError> {
1117 Ok(vec![])
1119 }
1120
1121 fn check_insecure_configurations(&self, _project_root: &Path) -> Result<Vec<SecurityFinding>, SecurityError> {
1122 Ok(vec![])
1124 }
1125
1126 fn deduplicate_findings(&self, mut findings: Vec<SecurityFinding>) -> Vec<SecurityFinding> {
1128 use std::collections::HashSet;
1129
1130 let mut seen_secrets: HashSet<String> = HashSet::new();
1131 let mut deduplicated = Vec::new();
1132
1133 findings.sort_by(|a, b| {
1135 let a_priority = self.get_pattern_priority(&a.title);
1137 let b_priority = self.get_pattern_priority(&b.title);
1138
1139 match a_priority.cmp(&b_priority) {
1140 std::cmp::Ordering::Equal => {
1141 a.severity.cmp(&b.severity)
1143 }
1144 other => other
1145 }
1146 });
1147
1148 for finding in findings {
1149 let key = self.generate_finding_key(&finding);
1150
1151 if !seen_secrets.contains(&key) {
1152 seen_secrets.insert(key);
1153 deduplicated.push(finding);
1154 }
1155 }
1156
1157 deduplicated
1158 }
1159
1160 fn generate_finding_key(&self, finding: &SecurityFinding) -> String {
1162 match finding.category {
1163 SecurityCategory::SecretsExposure => {
1164 if let Some(evidence) = &finding.evidence {
1166 if let Some(file_path) = &finding.file_path {
1167 if let Some(secret_value) = self.extract_secret_value(evidence) {
1169 return format!("secret:{}:{}", file_path.display(), secret_value);
1170 }
1171 if let Some(line_num) = finding.line_number {
1173 return format!("secret:{}:{}", file_path.display(), line_num);
1174 }
1175 }
1176 }
1177 format!("secret:{}", finding.title)
1179 }
1180 _ => {
1181 if let Some(file_path) = &finding.file_path {
1183 if let Some(line_num) = finding.line_number {
1184 format!("other:{}:{}:{}", file_path.display(), line_num, finding.title)
1185 } else {
1186 format!("other:{}:{}", file_path.display(), finding.title)
1187 }
1188 } else {
1189 format!("other:{}", finding.title)
1190 }
1191 }
1192 }
1193 }
1194
1195 fn extract_secret_value(&self, evidence: &str) -> Option<String> {
1197 if let Some(pos) = evidence.find('=') {
1199 let value = evidence[pos + 1..].trim();
1200 let value = value.trim_matches('"').trim_matches('\'');
1202 if value.len() > 10 { return Some(value.to_string());
1204 }
1205 }
1206
1207 if let Some(pos) = evidence.find(':') {
1209 let value = evidence[pos + 1..].trim();
1210 let value = value.trim_matches('"').trim_matches('\'');
1211 if value.len() > 10 {
1212 return Some(value.to_string());
1213 }
1214 }
1215
1216 None
1217 }
1218
1219 fn get_pattern_priority(&self, title: &str) -> u8 {
1221 if title.contains("AWS Access Key") { return 1; }
1223 if title.contains("AWS Secret Key") { return 1; }
1224 if title.contains("S3 Secret Key") { return 1; }
1225 if title.contains("GitHub Token") { return 1; }
1226 if title.contains("OpenAI API Key") { return 1; }
1227 if title.contains("Stripe") { return 1; }
1228 if title.contains("RSA Private Key") { return 1; }
1229 if title.contains("SSH Private Key") { return 1; }
1230
1231 if title.contains("JWT Secret") { return 2; }
1233 if title.contains("Database URL") { return 2; }
1234
1235 if title.contains("API Key") { return 3; }
1237
1238 if title.contains("Environment Variable") { return 4; }
1240
1241 if title.contains("Generic Secret") { return 5; }
1243
1244 3
1246 }
1247
1248 fn count_by_severity(&self, findings: &[SecurityFinding]) -> HashMap<SecuritySeverity, usize> {
1249 let mut counts = HashMap::new();
1250 for finding in findings {
1251 *counts.entry(finding.severity.clone()).or_insert(0) += 1;
1252 }
1253 counts
1254 }
1255
1256 fn count_by_category(&self, findings: &[SecurityFinding]) -> HashMap<SecurityCategory, usize> {
1257 let mut counts = HashMap::new();
1258 for finding in findings {
1259 *counts.entry(finding.category.clone()).or_insert(0) += 1;
1260 }
1261 counts
1262 }
1263
1264 fn calculate_security_score(&self, findings: &[SecurityFinding]) -> f32 {
1265 if findings.is_empty() {
1266 return 100.0;
1267 }
1268
1269 let total_penalty = findings.iter().map(|f| match f.severity {
1270 SecuritySeverity::Critical => 25.0,
1271 SecuritySeverity::High => 15.0,
1272 SecuritySeverity::Medium => 8.0,
1273 SecuritySeverity::Low => 3.0,
1274 SecuritySeverity::Info => 1.0,
1275 }).sum::<f32>();
1276
1277 (100.0 - total_penalty).max(0.0)
1278 }
1279
1280 fn determine_risk_level(&self, findings: &[SecurityFinding]) -> SecuritySeverity {
1281 if findings.iter().any(|f| f.severity == SecuritySeverity::Critical) {
1282 SecuritySeverity::Critical
1283 } else if findings.iter().any(|f| f.severity == SecuritySeverity::High) {
1284 SecuritySeverity::High
1285 } else if findings.iter().any(|f| f.severity == SecuritySeverity::Medium) {
1286 SecuritySeverity::Medium
1287 } else if !findings.is_empty() {
1288 SecuritySeverity::Low
1289 } else {
1290 SecuritySeverity::Info
1291 }
1292 }
1293
1294 fn assess_compliance(&self, _findings: &[SecurityFinding], _technologies: &[DetectedTechnology]) -> HashMap<String, ComplianceStatus> {
1295 HashMap::new()
1297 }
1298
1299 fn generate_recommendations(&self, findings: &[SecurityFinding], _technologies: &[DetectedTechnology]) -> Vec<String> {
1300 let mut recommendations = Vec::new();
1301
1302 if findings.iter().any(|f| f.category == SecurityCategory::SecretsExposure) {
1303 recommendations.push("Implement a secure secret management strategy".to_string());
1304 }
1305
1306 if findings.iter().any(|f| f.severity == SecuritySeverity::Critical) {
1307 recommendations.push("Address critical security findings immediately".to_string());
1308 }
1309
1310 recommendations
1313 }
1314}
1315
1316
1317
1318#[cfg(test)]
1319mod tests {
1320 use super::*;
1321
1322 #[test]
1323 fn test_security_score_calculation() {
1324 let analyzer = SecurityAnalyzer::new().unwrap();
1325
1326 let findings = vec![
1327 SecurityFinding {
1328 id: "test-1".to_string(),
1329 title: "Test Critical".to_string(),
1330 description: "Test".to_string(),
1331 severity: SecuritySeverity::Critical,
1332 category: SecurityCategory::SecretsExposure,
1333 file_path: None,
1334 line_number: None,
1335 evidence: None,
1336 remediation: vec![],
1337 references: vec![],
1338 cwe_id: None,
1339 compliance_frameworks: vec![],
1340 }
1341 ];
1342
1343 let score = analyzer.calculate_security_score(&findings);
1344 assert_eq!(score, 75.0); }
1346
1347 #[test]
1348 fn test_secret_pattern_matching() {
1349 let analyzer = SecurityAnalyzer::new().unwrap();
1350
1351 assert!(analyzer.is_likely_placeholder("API_KEY=sk-xxxxxxxxxxxxxxxx"));
1353 assert!(!analyzer.is_likely_placeholder("API_KEY=sk-1234567890abcdef"));
1354 }
1355
1356 #[test]
1357 fn test_sensitive_env_var_detection() {
1358 let analyzer = SecurityAnalyzer::new().unwrap();
1359
1360 assert!(analyzer.is_sensitive_env_var("DATABASE_PASSWORD"));
1361 assert!(analyzer.is_sensitive_env_var("JWT_SECRET"));
1362 assert!(!analyzer.is_sensitive_env_var("PORT"));
1363 assert!(!analyzer.is_sensitive_env_var("NODE_ENV"));
1364 }
1365}