1use crate::error::Result;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::process::Command;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SecurityScanConfig {
18 pub enabled: bool,
20
21 pub scan_frequency_hours: u32,
23
24 pub enable_dependency_scan: bool,
26
27 pub enable_secrets_scan: bool,
29
30 pub enable_sast: bool,
32
33 pub enable_license_check: bool,
35
36 pub fail_on_high_severity: bool,
38
39 pub fail_on_medium_severity: bool,
41}
42
43impl Default for SecurityScanConfig {
44 fn default() -> Self {
45 Self {
46 enabled: true,
47 scan_frequency_hours: 24,
48 enable_dependency_scan: true,
49 enable_secrets_scan: true,
50 enable_sast: true,
51 enable_license_check: true,
52 fail_on_high_severity: true,
53 fail_on_medium_severity: false,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SecurityScanResult {
61 pub timestamp: DateTime<Utc>,
63
64 pub status: ScanStatus,
66
67 pub findings: HashMap<String, Vec<SecurityFinding>>,
69
70 pub summary: ScanSummary,
72
73 pub recommendations: Vec<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78pub enum ScanStatus {
79 Pass,
80 Warning,
81 Fail,
82 Error,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SecurityFinding {
87 pub id: String,
89
90 pub title: String,
92
93 pub description: String,
95
96 pub severity: Severity,
98
99 pub category: FindingCategory,
101
102 pub component: String,
104
105 pub fix: Option<String>,
107
108 pub cve: Option<String>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
113pub enum Severity {
114 Critical,
115 High,
116 Medium,
117 Low,
118 Info,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub enum FindingCategory {
123 Dependency,
124 SecretLeak,
125 CodeVulnerability,
126 LicenseIssue,
127 ConfigurationIssue,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ScanSummary {
132 pub total_findings: usize,
133 pub critical: usize,
134 pub high: usize,
135 pub medium: usize,
136 pub low: usize,
137 pub info: usize,
138}
139
140pub struct SecurityScanner {
142 config: SecurityScanConfig,
143 last_scan: Option<DateTime<Utc>>,
144 last_result: Option<SecurityScanResult>,
145}
146
147impl SecurityScanner {
148 pub fn new(config: SecurityScanConfig) -> Self {
150 Self {
151 config,
152 last_scan: None,
153 last_result: None,
154 }
155 }
156
157 pub fn run_full_scan(&mut self) -> Result<SecurityScanResult> {
159 let mut all_findings: HashMap<String, Vec<SecurityFinding>> = HashMap::new();
160 let mut recommendations = Vec::new();
161
162 if self.config.enable_dependency_scan {
164 match self.scan_dependencies() {
165 Ok(findings) => {
166 if !findings.is_empty() {
167 all_findings.insert("dependencies".to_string(), findings);
168 recommendations.push(
169 "Run 'cargo update' to update vulnerable dependencies".to_string(),
170 );
171 }
172 }
173 Err(e) => {
174 eprintln!("Dependency scan failed: {}", e);
175 }
176 }
177 }
178
179 if self.config.enable_secrets_scan {
181 match self.scan_secrets() {
182 Ok(findings) => {
183 if !findings.is_empty() {
184 all_findings.insert("secrets".to_string(), findings);
185 recommendations.push(
186 "Remove hardcoded secrets and use environment variables".to_string(),
187 );
188 }
189 }
190 Err(e) => {
191 eprintln!("Secrets scan failed: {}", e);
192 }
193 }
194 }
195
196 if self.config.enable_sast {
198 match self.run_static_analysis() {
199 Ok(findings) => {
200 if !findings.is_empty() {
201 all_findings.insert("code_analysis".to_string(), findings);
202 recommendations.push("Review and fix code quality issues".to_string());
203 }
204 }
205 Err(e) => {
206 eprintln!("SAST failed: {}", e);
207 }
208 }
209 }
210
211 if self.config.enable_license_check {
213 match self.check_licenses() {
214 Ok(findings) => {
215 if !findings.is_empty() {
216 all_findings.insert("licenses".to_string(), findings);
217 recommendations
218 .push("Review dependency licenses for compliance".to_string());
219 }
220 }
221 Err(e) => {
222 eprintln!("License check failed: {}", e);
223 }
224 }
225 }
226
227 let summary = self.calculate_summary(&all_findings);
229
230 let status = self.determine_status(&summary);
232
233 let result = SecurityScanResult {
234 timestamp: Utc::now(),
235 status,
236 findings: all_findings,
237 summary,
238 recommendations,
239 };
240
241 self.last_scan = Some(Utc::now());
242 self.last_result = Some(result.clone());
243
244 Ok(result)
245 }
246
247 fn scan_dependencies(&self) -> Result<Vec<SecurityFinding>> {
249 let mut findings = Vec::new();
250
251 let output = Command::new("cargo").args(["audit", "--json"]).output();
253
254 match output {
255 Ok(output) if output.status.success() => {
256 if let Ok(output_str) = String::from_utf8(output.stdout) {
258 if output_str.contains("Crate:") || output_str.contains("ID:") {
260 findings.push(SecurityFinding {
261 id: "DEP-001".to_string(),
262 title: "Vulnerable dependency detected".to_string(),
263 description: "cargo audit found vulnerabilities".to_string(),
264 severity: Severity::High,
265 category: FindingCategory::Dependency,
266 component: "dependencies".to_string(),
267 fix: Some("Run 'cargo update' and review audit output".to_string()),
268 cve: None,
269 });
270 }
271 }
272 }
273 Ok(_) => {
274 findings.push(SecurityFinding {
276 id: "DEP-002".to_string(),
277 title: "Dependency vulnerabilities found".to_string(),
278 description: "cargo audit reported vulnerabilities".to_string(),
279 severity: Severity::High,
280 category: FindingCategory::Dependency,
281 component: "Cargo dependencies".to_string(),
282 fix: Some("Review 'cargo audit' output and update dependencies".to_string()),
283 cve: None,
284 });
285 }
286 Err(_) => {
287 eprintln!("cargo audit not available - install with: cargo install cargo-audit");
289 }
290 }
291
292 Ok(findings)
293 }
294
295 fn scan_secrets(&self) -> Result<Vec<SecurityFinding>> {
297 let mut findings = Vec::new();
298
299 let secret_patterns = vec![
301 (
302 r"(?i)(api[_-]?key|apikey)\s*[:=]\s*[a-zA-Z0-9]{20,}",
303 "API Key",
304 ),
305 (
306 r"(?i)(password|passwd|pwd)\s*[:=]\s*[\w@#$%^&*]{8,}",
307 "Password",
308 ),
309 (
310 r"(?i)(secret[_-]?key)\s*[:=]\s*[a-zA-Z0-9]{20,}",
311 "Secret Key",
312 ),
313 (
314 r"(?i)(aws[_-]?access[_-]?key[_-]?id)\s*[:=]\s*[A-Z0-9]{20}",
315 "AWS Access Key",
316 ),
317 (r"(?i)(private[_-]?key)\s*[:=]", "Private Key"),
318 ];
319
320 let files_to_check = vec![".env", ".env.example", "config.toml", "Cargo.toml"];
322
323 for file in files_to_check {
324 if let Ok(content) = std::fs::read_to_string(file) {
325 for (pattern, secret_type) in &secret_patterns {
326 if content.contains("password")
327 || content.contains("secret")
328 || content.contains("key")
329 {
330 findings.push(SecurityFinding {
331 id: format!("SEC-{:03}", findings.len() + 1),
332 title: format!("Potential {secret_type} found"),
333 description: format!("Potential hardcoded {secret_type} detected in {file}"),
334 severity: Severity::High,
335 category: FindingCategory::SecretLeak,
336 component: file.to_string(),
337 fix: Some("Remove hardcoded secrets, use environment variables or secret management".to_string()),
338 cve: None,
339 });
340 }
341 }
342 }
343 }
344
345 Ok(findings)
346 }
347
348 fn run_static_analysis(&self) -> Result<Vec<SecurityFinding>> {
350 let mut findings = Vec::new();
351
352 let output = Command::new("cargo")
354 .args(["clippy", "--", "-W", "clippy::all"])
355 .output();
356
357 match output {
358 Ok(output) if !output.status.success() => {
359 let stderr = String::from_utf8_lossy(&output.stderr);
360 if stderr.contains("warning:") || stderr.contains("error:") {
361 findings.push(SecurityFinding {
362 id: "SAST-001".to_string(),
363 title: "Code quality issues found".to_string(),
364 description: "Clippy found potential code issues".to_string(),
365 severity: Severity::Medium,
366 category: FindingCategory::CodeVulnerability,
367 component: "source code".to_string(),
368 fix: Some("Run 'cargo clippy' and address warnings".to_string()),
369 cve: None,
370 });
371 }
372 }
373 _ => {}
374 }
375
376 Ok(findings)
377 }
378
379 fn check_licenses(&self) -> Result<Vec<SecurityFinding>> {
381 let findings = Vec::new();
382
383 let restricted_licenses = vec!["GPL-3.0", "AGPL-3.0", "SSPL"];
385
386 Ok(findings)
390 }
391
392 fn calculate_summary(&self, findings: &HashMap<String, Vec<SecurityFinding>>) -> ScanSummary {
393 let mut summary = ScanSummary {
394 total_findings: 0,
395 critical: 0,
396 high: 0,
397 medium: 0,
398 low: 0,
399 info: 0,
400 };
401
402 for findings_vec in findings.values() {
403 for finding in findings_vec {
404 summary.total_findings += 1;
405 match finding.severity {
406 Severity::Critical => summary.critical += 1,
407 Severity::High => summary.high += 1,
408 Severity::Medium => summary.medium += 1,
409 Severity::Low => summary.low += 1,
410 Severity::Info => summary.info += 1,
411 }
412 }
413 }
414
415 summary
416 }
417
418 fn determine_status(&self, summary: &ScanSummary) -> ScanStatus {
419 if summary.critical > 0 {
420 return ScanStatus::Fail;
421 }
422
423 if self.config.fail_on_high_severity && summary.high > 0 {
424 return ScanStatus::Fail;
425 }
426
427 if self.config.fail_on_medium_severity && summary.medium > 0 {
428 return ScanStatus::Fail;
429 }
430
431 if summary.high > 0 || summary.medium > 0 {
432 return ScanStatus::Warning;
433 }
434
435 ScanStatus::Pass
436 }
437
438 pub fn get_last_result(&self) -> Option<&SecurityScanResult> {
440 self.last_result.as_ref()
441 }
442
443 pub fn should_scan(&self) -> bool {
445 if !self.config.enabled {
446 return false;
447 }
448
449 match self.last_scan {
450 None => true,
451 Some(last) => {
452 let elapsed = Utc::now() - last;
453 elapsed.num_hours() >= self.config.scan_frequency_hours as i64
454 }
455 }
456 }
457}
458
459pub struct CiCdIntegration;
461
462impl CiCdIntegration {
463 pub fn generate_github_actions_workflow() -> String {
465 r#"name: Security Scan
466
467on:
468 push:
469 branches: [ main, develop ]
470 pull_request:
471 branches: [ main ]
472 schedule:
473 - cron: '0 0 * * *' # Daily
474
475jobs:
476 security-scan:
477 runs-on: ubuntu-latest
478 steps:
479 - uses: actions/checkout@v3
480
481 - name: Install Rust
482 uses: actions-rs/toolchain@v1
483 with:
484 toolchain: stable
485 components: clippy
486
487 - name: Install cargo-audit
488 run: cargo install cargo-audit
489
490 - name: Dependency Audit
491 run: cargo audit
492
493 - name: Security Clippy
494 run: cargo clippy -- -D warnings
495
496 - name: Run Tests
497 run: cargo test --lib security
498
499 - name: Secret Scanning
500 uses: trufflesecurity/trufflehog@main
501 with:
502 path: ./
503 base: main
504 head: HEAD
505"#
506 .to_string()
507 }
508
509 pub fn generate_gitlab_ci_config() -> String {
511 r#"security-scan:
512 stage: test
513 image: rust:latest
514 script:
515 - cargo install cargo-audit
516 - cargo audit
517 - cargo clippy -- -D warnings
518 - cargo test --lib security
519 allow_failure: false
520"#
521 .to_string()
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
530 fn test_scanner_creation() {
531 let scanner = SecurityScanner::new(SecurityScanConfig::default());
532 assert!(scanner.last_result.is_none());
533 assert!(scanner.should_scan());
534 }
535
536 #[test]
537 fn test_scan_summary_calculation() {
538 let scanner = SecurityScanner::new(SecurityScanConfig::default());
539 let mut findings = HashMap::new();
540
541 findings.insert(
542 "test".to_string(),
543 vec![
544 SecurityFinding {
545 id: "1".to_string(),
546 title: "Test".to_string(),
547 description: "Test".to_string(),
548 severity: Severity::Critical,
549 category: FindingCategory::Dependency,
550 component: "test".to_string(),
551 fix: None,
552 cve: None,
553 },
554 SecurityFinding {
555 id: "2".to_string(),
556 title: "Test2".to_string(),
557 description: "Test2".to_string(),
558 severity: Severity::High,
559 category: FindingCategory::Dependency,
560 component: "test".to_string(),
561 fix: None,
562 cve: None,
563 },
564 ],
565 );
566
567 let summary = scanner.calculate_summary(&findings);
568 assert_eq!(summary.total_findings, 2);
569 assert_eq!(summary.critical, 1);
570 assert_eq!(summary.high, 1);
571 }
572
573 #[test]
574 fn test_status_determination() {
575 let scanner = SecurityScanner::new(SecurityScanConfig::default());
576
577 let summary_critical = ScanSummary {
578 total_findings: 1,
579 critical: 1,
580 high: 0,
581 medium: 0,
582 low: 0,
583 info: 0,
584 };
585 assert_eq!(
586 scanner.determine_status(&summary_critical),
587 ScanStatus::Fail
588 );
589
590 let summary_clean = ScanSummary {
591 total_findings: 0,
592 critical: 0,
593 high: 0,
594 medium: 0,
595 low: 0,
596 info: 0,
597 };
598 assert_eq!(scanner.determine_status(&summary_clean), ScanStatus::Pass);
599 }
600
601 #[test]
602 fn test_github_actions_workflow_generation() {
603 let workflow = CiCdIntegration::generate_github_actions_workflow();
604 assert!(workflow.contains("cargo audit"));
605 assert!(workflow.contains("cargo clippy"));
606 assert!(workflow.contains("Security Scan"));
607 }
608
609 #[test]
610 fn test_default_security_scan_config() {
611 let config = SecurityScanConfig::default();
612 assert!(config.enabled);
613 assert_eq!(config.scan_frequency_hours, 24);
614 assert!(config.enable_dependency_scan);
615 assert!(config.enable_secrets_scan);
616 assert!(config.enable_sast);
617 assert!(config.enable_license_check);
618 assert!(config.fail_on_high_severity);
619 assert!(!config.fail_on_medium_severity);
620 }
621
622 #[test]
623 fn test_security_scan_config_serde() {
624 let config = SecurityScanConfig::default();
625 let json = serde_json::to_string(&config).unwrap();
626 let parsed: SecurityScanConfig = serde_json::from_str(&json).unwrap();
627 assert_eq!(parsed.enabled, config.enabled);
628 assert_eq!(parsed.scan_frequency_hours, config.scan_frequency_hours);
629 }
630
631 #[test]
632 fn test_scan_status_equality() {
633 assert_eq!(ScanStatus::Pass, ScanStatus::Pass);
634 assert_eq!(ScanStatus::Fail, ScanStatus::Fail);
635 assert_eq!(ScanStatus::Warning, ScanStatus::Warning);
636 assert_eq!(ScanStatus::Error, ScanStatus::Error);
637 assert_ne!(ScanStatus::Pass, ScanStatus::Fail);
638 }
639
640 #[test]
641 fn test_severity_ordering() {
642 assert!(Severity::Critical < Severity::High);
643 assert!(Severity::High < Severity::Medium);
644 assert!(Severity::Medium < Severity::Low);
645 assert!(Severity::Low < Severity::Info);
646 }
647
648 #[test]
649 fn test_scan_summary_all_severities() {
650 let scanner = SecurityScanner::new(SecurityScanConfig::default());
651 let mut findings = HashMap::new();
652
653 findings.insert(
654 "test".to_string(),
655 vec![
656 SecurityFinding {
657 id: "1".to_string(),
658 title: "Critical".to_string(),
659 description: "Critical finding".to_string(),
660 severity: Severity::Critical,
661 category: FindingCategory::Dependency,
662 component: "test".to_string(),
663 fix: None,
664 cve: None,
665 },
666 SecurityFinding {
667 id: "2".to_string(),
668 title: "High".to_string(),
669 description: "High finding".to_string(),
670 severity: Severity::High,
671 category: FindingCategory::SecretLeak,
672 component: "test".to_string(),
673 fix: Some("Fix it".to_string()),
674 cve: Some("CVE-2021-1234".to_string()),
675 },
676 SecurityFinding {
677 id: "3".to_string(),
678 title: "Medium".to_string(),
679 description: "Medium finding".to_string(),
680 severity: Severity::Medium,
681 category: FindingCategory::CodeVulnerability,
682 component: "test".to_string(),
683 fix: None,
684 cve: None,
685 },
686 SecurityFinding {
687 id: "4".to_string(),
688 title: "Low".to_string(),
689 description: "Low finding".to_string(),
690 severity: Severity::Low,
691 category: FindingCategory::LicenseIssue,
692 component: "test".to_string(),
693 fix: None,
694 cve: None,
695 },
696 SecurityFinding {
697 id: "5".to_string(),
698 title: "Info".to_string(),
699 description: "Info finding".to_string(),
700 severity: Severity::Info,
701 category: FindingCategory::ConfigurationIssue,
702 component: "test".to_string(),
703 fix: None,
704 cve: None,
705 },
706 ],
707 );
708
709 let summary = scanner.calculate_summary(&findings);
710 assert_eq!(summary.total_findings, 5);
711 assert_eq!(summary.critical, 1);
712 assert_eq!(summary.high, 1);
713 assert_eq!(summary.medium, 1);
714 assert_eq!(summary.low, 1);
715 assert_eq!(summary.info, 1);
716 }
717
718 #[test]
719 fn test_status_high_severity_fail() {
720 let scanner = SecurityScanner::new(SecurityScanConfig {
721 fail_on_high_severity: true,
722 ..Default::default()
723 });
724
725 let summary = ScanSummary {
726 total_findings: 1,
727 critical: 0,
728 high: 1,
729 medium: 0,
730 low: 0,
731 info: 0,
732 };
733 assert_eq!(scanner.determine_status(&summary), ScanStatus::Fail);
734 }
735
736 #[test]
737 fn test_status_high_severity_warning() {
738 let scanner = SecurityScanner::new(SecurityScanConfig {
739 fail_on_high_severity: false,
740 ..Default::default()
741 });
742
743 let summary = ScanSummary {
744 total_findings: 1,
745 critical: 0,
746 high: 1,
747 medium: 0,
748 low: 0,
749 info: 0,
750 };
751 assert_eq!(scanner.determine_status(&summary), ScanStatus::Warning);
752 }
753
754 #[test]
755 fn test_status_medium_severity_fail() {
756 let scanner = SecurityScanner::new(SecurityScanConfig {
757 fail_on_high_severity: false,
758 fail_on_medium_severity: true,
759 ..Default::default()
760 });
761
762 let summary = ScanSummary {
763 total_findings: 1,
764 critical: 0,
765 high: 0,
766 medium: 1,
767 low: 0,
768 info: 0,
769 };
770 assert_eq!(scanner.determine_status(&summary), ScanStatus::Fail);
771 }
772
773 #[test]
774 fn test_status_medium_severity_warning() {
775 let scanner = SecurityScanner::new(SecurityScanConfig {
776 fail_on_high_severity: false,
777 fail_on_medium_severity: false,
778 ..Default::default()
779 });
780
781 let summary = ScanSummary {
782 total_findings: 1,
783 critical: 0,
784 high: 0,
785 medium: 1,
786 low: 0,
787 info: 0,
788 };
789 assert_eq!(scanner.determine_status(&summary), ScanStatus::Warning);
790 }
791
792 #[test]
793 fn test_should_scan_disabled() {
794 let scanner = SecurityScanner::new(SecurityScanConfig {
795 enabled: false,
796 ..Default::default()
797 });
798 assert!(!scanner.should_scan());
799 }
800
801 #[test]
802 fn test_get_last_result_none() {
803 let scanner = SecurityScanner::new(SecurityScanConfig::default());
804 assert!(scanner.get_last_result().is_none());
805 }
806
807 #[test]
808 fn test_gitlab_ci_config_generation() {
809 let config = CiCdIntegration::generate_gitlab_ci_config();
810 assert!(config.contains("cargo audit"));
811 assert!(config.contains("cargo clippy"));
812 assert!(config.contains("security-scan"));
813 }
814
815 #[test]
816 fn test_finding_category_variants() {
817 let categories = vec![
818 FindingCategory::Dependency,
819 FindingCategory::SecretLeak,
820 FindingCategory::CodeVulnerability,
821 FindingCategory::LicenseIssue,
822 FindingCategory::ConfigurationIssue,
823 ];
824
825 for category in categories {
826 let json = serde_json::to_string(&category).unwrap();
828 assert!(!json.is_empty());
829 }
830 }
831
832 #[test]
833 fn test_security_scan_result_serde() {
834 let result = SecurityScanResult {
835 timestamp: Utc::now(),
836 status: ScanStatus::Pass,
837 findings: HashMap::new(),
838 summary: ScanSummary {
839 total_findings: 0,
840 critical: 0,
841 high: 0,
842 medium: 0,
843 low: 0,
844 info: 0,
845 },
846 recommendations: vec!["Test recommendation".to_string()],
847 };
848
849 let json = serde_json::to_string(&result).unwrap();
850 let parsed: SecurityScanResult = serde_json::from_str(&json).unwrap();
851 assert_eq!(parsed.status, result.status);
852 assert_eq!(parsed.summary.total_findings, 0);
853 }
854
855 #[test]
856 fn test_security_finding_serde() {
857 let finding = SecurityFinding {
858 id: "TEST-001".to_string(),
859 title: "Test Finding".to_string(),
860 description: "A test finding".to_string(),
861 severity: Severity::High,
862 category: FindingCategory::Dependency,
863 component: "test-component".to_string(),
864 fix: Some("Apply fix".to_string()),
865 cve: Some("CVE-2021-12345".to_string()),
866 };
867
868 let json = serde_json::to_string(&finding).unwrap();
869 let parsed: SecurityFinding = serde_json::from_str(&json).unwrap();
870 assert_eq!(parsed.id, finding.id);
871 assert_eq!(parsed.title, finding.title);
872 assert_eq!(parsed.cve, finding.cve);
873 }
874
875 #[test]
876 fn test_scan_status_serde() {
877 let statuses = vec![
878 ScanStatus::Pass,
879 ScanStatus::Warning,
880 ScanStatus::Fail,
881 ScanStatus::Error,
882 ];
883
884 for status in statuses {
885 let json = serde_json::to_string(&status).unwrap();
886 let parsed: ScanStatus = serde_json::from_str(&json).unwrap();
887 assert_eq!(parsed, status);
888 }
889 }
890
891 #[test]
892 fn test_empty_findings_summary() {
893 let scanner = SecurityScanner::new(SecurityScanConfig::default());
894 let findings: HashMap<String, Vec<SecurityFinding>> = HashMap::new();
895
896 let summary = scanner.calculate_summary(&findings);
897 assert_eq!(summary.total_findings, 0);
898 assert_eq!(summary.critical, 0);
899 assert_eq!(summary.high, 0);
900 assert_eq!(summary.medium, 0);
901 assert_eq!(summary.low, 0);
902 assert_eq!(summary.info, 0);
903 }
904
905 #[test]
906 fn test_multiple_categories_summary() {
907 let scanner = SecurityScanner::new(SecurityScanConfig::default());
908 let mut findings = HashMap::new();
909
910 findings.insert(
911 "dependencies".to_string(),
912 vec![SecurityFinding {
913 id: "DEP-001".to_string(),
914 title: "Dependency issue".to_string(),
915 description: "A dependency issue".to_string(),
916 severity: Severity::High,
917 category: FindingCategory::Dependency,
918 component: "deps".to_string(),
919 fix: None,
920 cve: None,
921 }],
922 );
923
924 findings.insert(
925 "secrets".to_string(),
926 vec![SecurityFinding {
927 id: "SEC-001".to_string(),
928 title: "Secret issue".to_string(),
929 description: "A secret issue".to_string(),
930 severity: Severity::Critical,
931 category: FindingCategory::SecretLeak,
932 component: "secrets".to_string(),
933 fix: None,
934 cve: None,
935 }],
936 );
937
938 let summary = scanner.calculate_summary(&findings);
939 assert_eq!(summary.total_findings, 2);
940 assert_eq!(summary.critical, 1);
941 assert_eq!(summary.high, 1);
942 }
943}