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