1use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::RwLock;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum SecuritySeverity {
17 Info,
19 Low,
21 Medium,
23 High,
25 Critical,
27}
28
29impl SecuritySeverity {
30 pub fn as_str(&self) -> &'static str {
31 match self {
32 Self::Info => "info",
33 Self::Low => "low",
34 Self::Medium => "medium",
35 Self::High => "high",
36 Self::Critical => "critical",
37 }
38 }
39
40 pub fn score(&self) -> f32 {
42 match self {
43 Self::Info => 0.0,
44 Self::Low => 3.0,
45 Self::Medium => 5.5,
46 Self::High => 7.5,
47 Self::Critical => 9.5,
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub enum SecurityCategory {
55 HardcodedSecret,
57 Injection,
59 Authentication,
61 Authorization,
63 DataExposure,
65 Cryptography,
67 Configuration,
69 Dependency,
71 Compliance,
73 CodeQuality,
75 Custom(String),
77}
78
79impl SecurityCategory {
80 pub fn as_str(&self) -> &str {
81 match self {
82 Self::HardcodedSecret => "hardcoded_secret",
83 Self::Injection => "injection",
84 Self::Authentication => "authentication",
85 Self::Authorization => "authorization",
86 Self::DataExposure => "data_exposure",
87 Self::Cryptography => "cryptography",
88 Self::Configuration => "configuration",
89 Self::Dependency => "dependency",
90 Self::Compliance => "compliance",
91 Self::CodeQuality => "code_quality",
92 Self::Custom(s) => s,
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct SecurityFinding {
100 pub id: String,
102 pub title: String,
104 pub description: String,
106 pub category: SecurityCategory,
108 pub severity: SecuritySeverity,
110 pub file: Option<PathBuf>,
112 pub line: Option<u32>,
114 pub snippet: Option<String>,
116 pub remediation: Option<String>,
118 pub cwe: Option<String>,
120 pub timestamp: u64,
122}
123
124impl SecurityFinding {
125 pub fn new(title: &str, category: SecurityCategory, severity: SecuritySeverity) -> Self {
126 let id = format!(
127 "SEC-{:x}",
128 SystemTime::now()
129 .duration_since(UNIX_EPOCH)
130 .unwrap_or_default()
131 .as_nanos() as u64
132 );
133 Self {
134 id,
135 title: title.to_string(),
136 description: String::new(),
137 category,
138 severity,
139 file: None,
140 line: None,
141 snippet: None,
142 remediation: None,
143 cwe: None,
144 timestamp: SystemTime::now()
145 .duration_since(UNIX_EPOCH)
146 .unwrap_or_default()
147 .as_secs(),
148 }
149 }
150
151 pub fn with_description(mut self, desc: &str) -> Self {
152 self.description = desc.to_string();
153 self
154 }
155
156 pub fn with_location(mut self, file: PathBuf, line: u32) -> Self {
157 self.file = Some(file);
158 self.line = Some(line);
159 self
160 }
161
162 pub fn with_snippet(mut self, snippet: &str) -> Self {
163 self.snippet = Some(snippet.to_string());
164 self
165 }
166
167 pub fn with_remediation(mut self, remediation: &str) -> Self {
168 self.remediation = Some(remediation.to_string());
169 self
170 }
171
172 pub fn with_cwe(mut self, cwe: &str) -> Self {
173 self.cwe = Some(cwe.to_string());
174 self
175 }
176}
177
178#[derive(Debug, Clone)]
180pub struct SecretPattern {
181 pub name: String,
183 pub pattern: String,
185 pub compiled: Option<regex::Regex>,
187 pub severity: SecuritySeverity,
189 pub description: String,
191}
192
193impl SecretPattern {
194 pub fn new(name: &str, pattern: &str, severity: SecuritySeverity) -> Self {
195 let compiled = regex::RegexBuilder::new(pattern)
196 .size_limit(1 << 20) .build()
198 .ok();
199 Self {
200 name: name.to_string(),
201 pattern: pattern.to_string(),
202 compiled,
203 severity,
204 description: format!("Potential {} detected", name),
205 }
206 }
207}
208
209pub struct SecretScanner {
211 patterns: Vec<SecretPattern>,
213 _skip_files: Vec<String>,
215 findings: Vec<SecurityFinding>,
217}
218
219impl SecretScanner {
220 pub fn new() -> Self {
221 Self {
222 patterns: Self::default_patterns(),
223 _skip_files: vec![
224 ".git".to_string(),
225 "node_modules".to_string(),
226 "target".to_string(),
227 ".env.example".to_string(),
228 ],
229 findings: Vec::new(),
230 }
231 }
232
233 fn default_patterns() -> Vec<SecretPattern> {
234 vec![
235 SecretPattern::new(
236 "AWS Access Key",
237 r"AKIA[0-9A-Z]{16}",
238 SecuritySeverity::Critical,
239 ),
240 SecretPattern::new(
241 "AWS Secret Key",
242 r#"(?i)aws(.{0,20})?['"][0-9a-zA-Z/+]{40}['"]"#,
243 SecuritySeverity::Critical,
244 ),
245 SecretPattern::new(
247 "GitHub Token",
248 r"gh[pousr]_[A-Za-z0-9_]{36,}",
249 SecuritySeverity::Critical,
250 ),
251 SecretPattern::new(
253 "GitHub Fine-Grained Token",
254 r"github_pat_[A-Za-z0-9_]{22,}",
255 SecuritySeverity::Critical,
256 ),
257 SecretPattern::new(
259 "GitLab Token",
260 r"glpat-[A-Za-z0-9_\-]{20,}",
261 SecuritySeverity::Critical,
262 ),
263 SecretPattern::new(
264 "Generic API Key",
265 r#"(?i)(api[_-]?key|apikey)['"]?\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]"#,
266 SecuritySeverity::High,
267 ),
268 SecretPattern::new(
269 "Private Key",
270 r"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
271 SecuritySeverity::Critical,
272 ),
273 SecretPattern::new(
275 "Google API Key",
276 r"AIza[a-zA-Z0-9_\-]{35}",
277 SecuritySeverity::High,
278 ),
279 SecretPattern::new(
281 "Stripe Key",
282 r"(sk_live_|rk_live_|pk_live_)[a-zA-Z0-9]{24,}",
283 SecuritySeverity::Critical,
284 ),
285 SecretPattern::new(
286 "Password in Code",
287 r#"(?i)(password|passwd|pwd)['"]?\s*[:=]\s*['"][^'"]{8,}['"]"#,
288 SecuritySeverity::High,
289 ),
290 SecretPattern::new(
291 "Bearer Token",
292 r"(?i)bearer\s+[a-zA-Z0-9_\-\.]+",
293 SecuritySeverity::High,
294 ),
295 SecretPattern::new(
296 "JWT Token",
297 r"eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*",
298 SecuritySeverity::High,
299 ),
300 SecretPattern::new(
301 "Database URL",
302 r"(?i)(postgres|mysql|mongodb)://[^:]+:[^@]+@",
303 SecuritySeverity::High,
304 ),
305 SecretPattern::new(
308 "Slack Token",
309 r"xox[bpsar]-[0-9A-Za-z\-]{10,}",
310 SecuritySeverity::High,
311 ),
312 SecretPattern::new(
314 "JWT Partial",
315 r"eyJ[a-zA-Z0-9_/+\-]{30,}",
316 SecuritySeverity::Medium,
317 ),
318 SecretPattern::new(
320 "Base64 Secret",
321 r#"(?i)(?:key|token|secret|password|credential|auth)\s*[:=]\s*['"]?[A-Za-z0-9+/=_\-]{40,}['"]?"#,
322 SecuritySeverity::Medium,
323 ),
324 ]
325 }
326
327 pub fn add_pattern(&mut self, pattern: SecretPattern) {
329 self.patterns.push(pattern);
330 }
331
332 pub fn scan_content(&mut self, content: &str, file: Option<&PathBuf>) -> Vec<SecurityFinding> {
334 let mut findings = Vec::new();
335
336 for (line_num, line) in content.lines().enumerate() {
337 let trimmed = line.trim();
339 if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with("/*") {
340 continue;
341 }
342
343 for pattern in &self.patterns {
344 if let Some(ref re) = pattern.compiled {
345 for mat in re.find_iter(line) {
346 let mut finding = SecurityFinding::new(
347 &pattern.name,
348 SecurityCategory::HardcodedSecret,
349 pattern.severity,
350 )
351 .with_description(&pattern.description)
352 .with_cwe("CWE-798");
353
354 if let Some(f) = file {
355 finding = finding.with_location(f.clone(), (line_num + 1) as u32);
356 }
357
358 let masked = Self::mask_secret(mat.as_str());
360 finding = finding.with_snippet(&masked);
361 finding = finding.with_remediation(
362 "Remove hardcoded secret and use environment variables or a secrets manager"
363 );
364
365 findings.push(finding.clone());
366 self.findings.push(finding);
367 }
368 }
369 }
370 }
371
372 findings
373 }
374
375 fn mask_secret(secret: &str) -> String {
377 if secret.len() <= 8 {
378 "*".repeat(secret.len())
379 } else {
380 format!(
381 "{}...{}",
382 secret.chars().take(4).collect::<String>(),
383 "*".repeat(secret.chars().count().saturating_sub(4))
384 )
385 }
386 }
387
388 pub fn findings(&self) -> &[SecurityFinding] {
390 &self.findings
391 }
392
393 pub fn clear(&mut self) {
395 self.findings.clear();
396 }
397}
398
399impl Default for SecretScanner {
400 fn default() -> Self {
401 Self::new()
402 }
403}
404
405#[derive(Debug, Clone)]
407pub struct VulnerabilityPattern {
408 pub id: String,
410 pub name: String,
412 pub language: String,
414 pub pattern: String,
416 pub severity: SecuritySeverity,
418 pub cwe: String,
420 pub description: String,
422 pub remediation: String,
424}
425
426pub struct VulnerabilityDetector {
428 patterns: Vec<VulnerabilityPattern>,
430 findings: Vec<SecurityFinding>,
432}
433
434impl VulnerabilityDetector {
435 pub fn new() -> Self {
436 Self {
437 patterns: Self::default_patterns(),
438 findings: Vec::new(),
439 }
440 }
441
442 fn default_patterns() -> Vec<VulnerabilityPattern> {
443 vec![
444 VulnerabilityPattern {
445 id: "RUST001".to_string(),
446 name: "Unsafe Block".to_string(),
447 language: "rust".to_string(),
448 pattern: r"unsafe\s*\{".to_string(),
449 severity: SecuritySeverity::Medium,
450 cwe: "CWE-242".to_string(),
451 description: "Unsafe block found - requires careful review".to_string(),
452 remediation: "Document safety invariants or use safe alternatives".to_string(),
453 },
454 VulnerabilityPattern {
455 id: "RUST002".to_string(),
456 name: "Unwrap on Result/Option".to_string(),
457 language: "rust".to_string(),
458 pattern: r"\.unwrap\(\)".to_string(),
459 severity: SecuritySeverity::Low,
460 cwe: "CWE-252".to_string(),
461 description: "Unwrap can panic on None/Err".to_string(),
462 remediation: "Use proper error handling with ? or match".to_string(),
463 },
464 VulnerabilityPattern {
465 id: "RUST003".to_string(),
466 name: "SQL Query String Building".to_string(),
467 language: "rust".to_string(),
468 pattern: r#"format!\s*\(\s*["']SELECT|format!\s*\(\s*["']INSERT|format!\s*\(\s*["']UPDATE|format!\s*\(\s*["']DELETE"#.to_string(),
469 severity: SecuritySeverity::High,
470 cwe: "CWE-89".to_string(),
471 description: "Potential SQL injection vulnerability".to_string(),
472 remediation: "Use parameterized queries instead of string formatting".to_string(),
473 },
474 VulnerabilityPattern {
475 id: "RUST004".to_string(),
476 name: "Command Injection".to_string(),
477 language: "rust".to_string(),
478 pattern: r"Command::new\s*\(\s*&?format!".to_string(),
479 severity: SecuritySeverity::Critical,
480 cwe: "CWE-78".to_string(),
481 description: "Potential command injection vulnerability".to_string(),
482 remediation: "Use static command strings or validate/sanitize input".to_string(),
483 },
484 VulnerabilityPattern {
485 id: "RUST005".to_string(),
486 name: "Path Traversal".to_string(),
487 language: "rust".to_string(),
488 pattern: r#"Path::new\s*\(\s*&?format!|PathBuf::from\s*\(\s*&?format!"#.to_string(),
489 severity: SecuritySeverity::High,
490 cwe: "CWE-22".to_string(),
491 description: "Potential path traversal vulnerability".to_string(),
492 remediation: "Validate paths and use canonicalize()".to_string(),
493 },
494 VulnerabilityPattern {
495 id: "JS001".to_string(),
496 name: "eval() Usage".to_string(),
497 language: "javascript".to_string(),
498 pattern: r"\beval\s*\(".to_string(),
499 severity: SecuritySeverity::Critical,
500 cwe: "CWE-95".to_string(),
501 description: "eval() can execute arbitrary code".to_string(),
502 remediation: "Avoid eval() - use safer alternatives".to_string(),
503 },
504 VulnerabilityPattern {
505 id: "JS002".to_string(),
506 name: "innerHTML Assignment".to_string(),
507 language: "javascript".to_string(),
508 pattern: r"\.innerHTML\s*=".to_string(),
509 severity: SecuritySeverity::High,
510 cwe: "CWE-79".to_string(),
511 description: "Potential XSS via innerHTML".to_string(),
512 remediation: "Use textContent or sanitize HTML".to_string(),
513 },
514 VulnerabilityPattern {
515 id: "PY001".to_string(),
516 name: "Python exec/eval".to_string(),
517 language: "python".to_string(),
518 pattern: r"\b(exec|eval)\s*\(".to_string(),
519 severity: SecuritySeverity::Critical,
520 cwe: "CWE-95".to_string(),
521 description: "exec/eval can execute arbitrary code".to_string(),
522 remediation: "Avoid exec/eval with untrusted input".to_string(),
523 },
524 VulnerabilityPattern {
525 id: "PY002".to_string(),
526 name: "Python pickle".to_string(),
527 language: "python".to_string(),
528 pattern: r"pickle\.(load|loads)\s*\(".to_string(),
529 severity: SecuritySeverity::High,
530 cwe: "CWE-502".to_string(),
531 description: "pickle can deserialize malicious code".to_string(),
532 remediation: "Use json or other safe serialization formats".to_string(),
533 },
534 ]
535 }
536
537 pub fn add_pattern(&mut self, pattern: VulnerabilityPattern) {
539 self.patterns.push(pattern);
540 }
541
542 pub fn scan_content(
544 &mut self,
545 content: &str,
546 file: Option<&PathBuf>,
547 language: &str,
548 ) -> Vec<SecurityFinding> {
549 let mut findings = Vec::new();
550
551 let applicable: Vec<_> = self
553 .patterns
554 .iter()
555 .filter(|p| p.language == language || p.language == "*")
556 .collect();
557
558 for (line_num, line) in content.lines().enumerate() {
559 for pattern in &applicable {
560 if let Ok(re) = regex::Regex::new(&pattern.pattern) {
561 if re.is_match(line) {
562 let mut finding = SecurityFinding::new(
563 &pattern.name,
564 SecurityCategory::CodeQuality,
565 pattern.severity,
566 )
567 .with_description(&pattern.description)
568 .with_cwe(&pattern.cwe)
569 .with_remediation(&pattern.remediation)
570 .with_snippet(line.trim());
571
572 if let Some(f) = file {
573 finding = finding.with_location(f.clone(), (line_num + 1) as u32);
574 }
575
576 findings.push(finding.clone());
577 self.findings.push(finding);
578 }
579 }
580 }
581 }
582
583 findings
584 }
585
586 pub fn findings(&self) -> &[SecurityFinding] {
588 &self.findings
589 }
590
591 pub fn clear(&mut self) {
593 self.findings.clear();
594 }
595}
596
597impl Default for VulnerabilityDetector {
598 fn default() -> Self {
599 Self::new()
600 }
601}
602
603#[derive(Debug, Clone)]
605pub struct Dependency {
606 pub name: String,
608 pub version: String,
610 pub source: String,
612 pub vulnerabilities: Vec<KnownVulnerability>,
614}
615
616impl Dependency {
617 pub fn new(name: &str, version: &str, source: &str) -> Self {
618 Self {
619 name: name.to_string(),
620 version: version.to_string(),
621 source: source.to_string(),
622 vulnerabilities: Vec::new(),
623 }
624 }
625
626 pub fn is_vulnerable(&self) -> bool {
627 !self.vulnerabilities.is_empty()
628 }
629
630 pub fn max_severity(&self) -> Option<SecuritySeverity> {
631 self.vulnerabilities.iter().map(|v| v.severity).max()
632 }
633}
634
635#[derive(Debug, Clone)]
637pub struct KnownVulnerability {
638 pub id: String,
640 pub severity: SecuritySeverity,
642 pub description: String,
644 pub fixed_version: Option<String>,
646 pub url: Option<String>,
648}
649
650impl KnownVulnerability {
651 pub fn new(id: &str, severity: SecuritySeverity, description: &str) -> Self {
652 Self {
653 id: id.to_string(),
654 severity,
655 description: description.to_string(),
656 fixed_version: None,
657 url: None,
658 }
659 }
660
661 pub fn with_fixed_version(mut self, version: &str) -> Self {
662 self.fixed_version = Some(version.to_string());
663 self
664 }
665
666 pub fn with_url(mut self, url: &str) -> Self {
667 self.url = Some(url.to_string());
668 self
669 }
670}
671
672pub struct DependencyAuditor {
674 vulnerability_db: HashMap<String, Vec<KnownVulnerability>>,
676 dependencies: Vec<Dependency>,
678 findings: Vec<SecurityFinding>,
680}
681
682impl DependencyAuditor {
683 pub fn new() -> Self {
684 Self {
685 vulnerability_db: Self::default_db(),
686 dependencies: Vec::new(),
687 findings: Vec::new(),
688 }
689 }
690
691 fn default_db() -> HashMap<String, Vec<KnownVulnerability>> {
692 let mut db = HashMap::new();
693
694 db.insert(
696 "lodash".to_string(),
697 vec![KnownVulnerability::new(
698 "CVE-2021-23337",
699 SecuritySeverity::High,
700 "Command Injection in lodash",
701 )
702 .with_fixed_version("4.17.21")],
703 );
704
705 db.insert(
706 "log4j".to_string(),
707 vec![KnownVulnerability::new(
708 "CVE-2021-44228",
709 SecuritySeverity::Critical,
710 "Log4Shell RCE vulnerability",
711 )
712 .with_fixed_version("2.17.0")],
713 );
714
715 db
716 }
717
718 pub fn add_vulnerability(&mut self, package: &str, vuln: KnownVulnerability) {
720 self.vulnerability_db
721 .entry(package.to_string())
722 .or_default()
723 .push(vuln);
724 }
725
726 pub fn audit_dependency(&mut self, name: &str, version: &str, source: &str) -> Dependency {
728 let mut dep = Dependency::new(name, version, source);
729
730 if let Some(vulns) = self.vulnerability_db.get(name) {
732 for vuln in vulns {
733 dep.vulnerabilities.push(vuln.clone());
735
736 let finding = SecurityFinding::new(
737 &format!("Vulnerable dependency: {}", name),
738 SecurityCategory::Dependency,
739 vuln.severity,
740 )
741 .with_description(&vuln.description)
742 .with_remediation(&format!(
743 "Update {} to version {}",
744 name,
745 vuln.fixed_version.as_deref().unwrap_or("latest")
746 ));
747
748 self.findings.push(finding);
749 }
750 }
751
752 self.dependencies.push(dep.clone());
753 dep
754 }
755
756 pub fn vulnerable_dependencies(&self) -> Vec<&Dependency> {
758 self.dependencies
759 .iter()
760 .filter(|d| d.is_vulnerable())
761 .collect()
762 }
763
764 pub fn findings(&self) -> &[SecurityFinding] {
766 &self.findings
767 }
768
769 pub fn clear(&mut self) {
771 self.dependencies.clear();
772 self.findings.clear();
773 }
774}
775
776impl Default for DependencyAuditor {
777 fn default() -> Self {
778 Self::new()
779 }
780}
781
782#[derive(Debug, Clone)]
784pub struct ComplianceRule {
785 pub id: String,
787 pub standard: String,
789 pub description: String,
791 pub pattern: Option<String>,
793 pub severity: SecuritySeverity,
795}
796
797impl ComplianceRule {
798 pub fn new(id: &str, standard: &str, description: &str) -> Self {
799 Self {
800 id: id.to_string(),
801 standard: standard.to_string(),
802 description: description.to_string(),
803 pattern: None,
804 severity: SecuritySeverity::Medium,
805 }
806 }
807
808 pub fn with_pattern(mut self, pattern: &str) -> Self {
809 self.pattern = Some(pattern.to_string());
810 self
811 }
812
813 pub fn with_severity(mut self, severity: SecuritySeverity) -> Self {
814 self.severity = severity;
815 self
816 }
817}
818
819pub struct ComplianceChecker {
821 rules: Vec<ComplianceRule>,
823 findings: Vec<SecurityFinding>,
825}
826
827impl ComplianceChecker {
828 pub fn new() -> Self {
829 Self {
830 rules: Self::default_rules(),
831 findings: Vec::new(),
832 }
833 }
834
835 fn default_rules() -> Vec<ComplianceRule> {
836 vec![
837 ComplianceRule::new("OWASP-A01", "OWASP Top 10", "Broken Access Control")
838 .with_severity(SecuritySeverity::High),
839 ComplianceRule::new("OWASP-A02", "OWASP Top 10", "Cryptographic Failures")
840 .with_pattern(r"(?i)(md5|sha1)\s*\(")
841 .with_severity(SecuritySeverity::High),
842 ComplianceRule::new("OWASP-A03", "OWASP Top 10", "Injection")
843 .with_severity(SecuritySeverity::Critical),
844 ComplianceRule::new("PCI-DSS-6.5.1", "PCI-DSS", "Address injection flaws")
845 .with_severity(SecuritySeverity::High),
846 ComplianceRule::new("HIPAA-164.312", "HIPAA", "Encryption of PHI at rest")
847 .with_severity(SecuritySeverity::High),
848 ]
849 }
850
851 pub fn add_rule(&mut self, rule: ComplianceRule) {
853 self.rules.push(rule);
854 }
855
856 pub fn check_content(&mut self, content: &str, file: Option<&PathBuf>) -> Vec<SecurityFinding> {
858 let mut findings = Vec::new();
859
860 for (line_num, line) in content.lines().enumerate() {
861 for rule in &self.rules {
862 if let Some(pattern) = &rule.pattern {
863 if let Ok(re) = regex::Regex::new(pattern) {
864 if re.is_match(line) {
865 let mut finding = SecurityFinding::new(
866 &format!("{}: {}", rule.id, rule.description),
867 SecurityCategory::Compliance,
868 rule.severity,
869 )
870 .with_description(&format!("Potential {} violation", rule.standard))
871 .with_snippet(line.trim());
872
873 if let Some(f) = file {
874 finding = finding.with_location(f.clone(), (line_num + 1) as u32);
875 }
876
877 findings.push(finding.clone());
878 self.findings.push(finding);
879 }
880 }
881 }
882 }
883 }
884
885 findings
886 }
887
888 pub fn standards(&self) -> Vec<String> {
890 let mut standards: Vec<_> = self.rules.iter().map(|r| r.standard.clone()).collect();
891 standards.sort();
892 standards.dedup();
893 standards
894 }
895
896 pub fn findings(&self) -> &[SecurityFinding] {
898 &self.findings
899 }
900
901 pub fn clear(&mut self) {
903 self.findings.clear();
904 }
905}
906
907impl Default for ComplianceChecker {
908 fn default() -> Self {
909 Self::new()
910 }
911}
912
913#[derive(Debug, Clone)]
915pub struct ScanResult {
916 pub findings: Vec<SecurityFinding>,
918 pub by_severity: HashMap<SecuritySeverity, usize>,
920 pub by_category: HashMap<String, usize>,
922 pub duration_ms: u64,
924 pub files_scanned: usize,
926 pub lines_scanned: usize,
928}
929
930impl ScanResult {
931 pub fn new() -> Self {
932 Self {
933 findings: Vec::new(),
934 by_severity: HashMap::new(),
935 by_category: HashMap::new(),
936 duration_ms: 0,
937 files_scanned: 0,
938 lines_scanned: 0,
939 }
940 }
941
942 pub fn total_findings(&self) -> usize {
944 self.findings.len()
945 }
946
947 pub fn has_critical(&self) -> bool {
949 self.by_severity
950 .get(&SecuritySeverity::Critical)
951 .is_some_and(|&c| c > 0)
952 }
953
954 pub fn has_high(&self) -> bool {
956 self.by_severity
957 .get(&SecuritySeverity::High)
958 .is_some_and(|&c| c > 0)
959 }
960
961 pub fn risk_score(&self) -> f32 {
963 self.findings.iter().map(|f| f.severity.score()).sum()
964 }
965}
966
967impl Default for ScanResult {
968 fn default() -> Self {
969 Self::new()
970 }
971}
972
973pub struct SecurityScanner {
975 secret_scanner: RwLock<SecretScanner>,
977 vuln_detector: RwLock<VulnerabilityDetector>,
979 dep_auditor: RwLock<DependencyAuditor>,
981 compliance_checker: RwLock<ComplianceChecker>,
983 scan_history: RwLock<Vec<ScanResult>>,
985}
986
987impl SecurityScanner {
988 pub fn new() -> Self {
989 Self {
990 secret_scanner: RwLock::new(SecretScanner::new()),
991 vuln_detector: RwLock::new(VulnerabilityDetector::new()),
992 dep_auditor: RwLock::new(DependencyAuditor::new()),
993 compliance_checker: RwLock::new(ComplianceChecker::new()),
994 scan_history: RwLock::new(Vec::new()),
995 }
996 }
997
998 pub fn scan_content(
1000 &self,
1001 content: &str,
1002 file: Option<&PathBuf>,
1003 language: &str,
1004 ) -> ScanResult {
1005 let start = std::time::Instant::now();
1006 let mut result = ScanResult::new();
1007
1008 if let Ok(mut scanner) = self.secret_scanner.write() {
1010 result.findings.extend(scanner.scan_content(content, file));
1011 }
1012
1013 if let Ok(mut detector) = self.vuln_detector.write() {
1015 result
1016 .findings
1017 .extend(detector.scan_content(content, file, language));
1018 }
1019
1020 if let Ok(mut checker) = self.compliance_checker.write() {
1022 result.findings.extend(checker.check_content(content, file));
1023 }
1024
1025 for finding in &result.findings {
1027 *result.by_severity.entry(finding.severity).or_insert(0) += 1;
1028 *result
1029 .by_category
1030 .entry(finding.category.as_str().to_string())
1031 .or_insert(0) += 1;
1032 }
1033
1034 result.duration_ms = start.elapsed().as_millis() as u64;
1035 result.files_scanned = 1;
1036 result.lines_scanned = content.lines().count();
1037
1038 if let Ok(mut history) = self.scan_history.write() {
1040 history.push(result.clone());
1041 if history.len() > 100 {
1042 history.remove(0);
1043 }
1044 }
1045
1046 result
1047 }
1048
1049 pub fn audit_dependency(&self, name: &str, version: &str, source: &str) -> Dependency {
1051 if let Ok(mut auditor) = self.dep_auditor.write() {
1052 auditor.audit_dependency(name, version, source)
1053 } else {
1054 Dependency::new(name, version, source)
1055 }
1056 }
1057
1058 pub fn get_stats(&self) -> ScannerStats {
1060 let history = self.scan_history.read().ok();
1061 let total_scans = history.as_ref().map_or(0, |h| h.len());
1062 let total_findings: usize = history
1063 .as_ref()
1064 .map_or(0, |h| h.iter().map(|r| r.total_findings()).sum());
1065
1066 ScannerStats {
1067 total_scans,
1068 total_findings,
1069 critical_findings: history.as_ref().map_or(0, |h| {
1070 h.iter()
1071 .map(|r| {
1072 r.by_severity
1073 .get(&SecuritySeverity::Critical)
1074 .copied()
1075 .unwrap_or(0)
1076 })
1077 .sum()
1078 }),
1079 high_findings: history.as_ref().map_or(0, |h| {
1080 h.iter()
1081 .map(|r| {
1082 r.by_severity
1083 .get(&SecuritySeverity::High)
1084 .copied()
1085 .unwrap_or(0)
1086 })
1087 .sum()
1088 }),
1089 }
1090 }
1091
1092 pub fn generate_report(&self, result: &ScanResult) -> String {
1094 let mut report = String::new();
1095 report.push_str("# Security Scan Report\n\n");
1096
1097 report.push_str(&format!("- Files scanned: {}\n", result.files_scanned));
1098 report.push_str(&format!("- Lines scanned: {}\n", result.lines_scanned));
1099 report.push_str(&format!("- Scan duration: {}ms\n", result.duration_ms));
1100 report.push_str(&format!("- Total findings: {}\n", result.total_findings()));
1101 report.push_str(&format!("- Risk score: {:.1}\n\n", result.risk_score()));
1102
1103 if result.has_critical() {
1104 report.push_str("## CRITICAL Findings\n");
1105 for finding in result
1106 .findings
1107 .iter()
1108 .filter(|f| f.severity == SecuritySeverity::Critical)
1109 {
1110 report.push_str(&format!(
1111 "- **{}**: {}\n",
1112 finding.title, finding.description
1113 ));
1114 if let Some(file) = &finding.file {
1115 report.push_str(&format!(
1116 " Location: {}:{}\n",
1117 file.display(),
1118 finding.line.unwrap_or(0)
1119 ));
1120 }
1121 }
1122 report.push('\n');
1123 }
1124
1125 if result.has_high() {
1126 report.push_str("## HIGH Findings\n");
1127 for finding in result
1128 .findings
1129 .iter()
1130 .filter(|f| f.severity == SecuritySeverity::High)
1131 {
1132 report.push_str(&format!(
1133 "- **{}**: {}\n",
1134 finding.title, finding.description
1135 ));
1136 }
1137 report.push('\n');
1138 }
1139
1140 report.push_str("## Summary by Category\n");
1141 for (cat, count) in &result.by_category {
1142 report.push_str(&format!("- {}: {}\n", cat, count));
1143 }
1144
1145 report
1146 }
1147}
1148
1149impl Default for SecurityScanner {
1150 fn default() -> Self {
1151 Self::new()
1152 }
1153}
1154
1155#[derive(Debug, Clone)]
1157pub struct ScannerStats {
1158 pub total_scans: usize,
1159 pub total_findings: usize,
1160 pub critical_findings: usize,
1161 pub high_findings: usize,
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166 use super::*;
1167
1168 #[test]
1173 fn test_security_severity_ordering() {
1174 assert!(SecuritySeverity::Critical > SecuritySeverity::High);
1175 assert!(SecuritySeverity::High > SecuritySeverity::Medium);
1176 assert!(SecuritySeverity::Medium > SecuritySeverity::Low);
1177 assert!(SecuritySeverity::Low > SecuritySeverity::Info);
1178 }
1179
1180 #[test]
1181 fn test_security_severity_score() {
1182 assert!(SecuritySeverity::Critical.score() > SecuritySeverity::High.score());
1183 assert!(SecuritySeverity::High.score() > SecuritySeverity::Medium.score());
1184 assert!(SecuritySeverity::Medium.score() > SecuritySeverity::Low.score());
1185 assert!(SecuritySeverity::Low.score() > SecuritySeverity::Info.score());
1186 assert_eq!(SecuritySeverity::Info.score(), 0.0);
1187 }
1188
1189 #[test]
1190 fn test_security_severity_as_str() {
1191 assert_eq!(SecuritySeverity::Info.as_str(), "info");
1192 assert_eq!(SecuritySeverity::Low.as_str(), "low");
1193 assert_eq!(SecuritySeverity::Medium.as_str(), "medium");
1194 assert_eq!(SecuritySeverity::High.as_str(), "high");
1195 assert_eq!(SecuritySeverity::Critical.as_str(), "critical");
1196 }
1197
1198 #[test]
1199 fn test_security_severity_scores_concrete() {
1200 assert_eq!(SecuritySeverity::Info.score(), 0.0);
1201 assert_eq!(SecuritySeverity::Low.score(), 3.0);
1202 assert_eq!(SecuritySeverity::Medium.score(), 5.5);
1203 assert_eq!(SecuritySeverity::High.score(), 7.5);
1204 assert_eq!(SecuritySeverity::Critical.score(), 9.5);
1205 }
1206
1207 #[test]
1212 fn test_security_category_as_str() {
1213 assert_eq!(
1214 SecurityCategory::HardcodedSecret.as_str(),
1215 "hardcoded_secret"
1216 );
1217 assert_eq!(SecurityCategory::Injection.as_str(), "injection");
1218 assert_eq!(SecurityCategory::Authentication.as_str(), "authentication");
1219 assert_eq!(SecurityCategory::Authorization.as_str(), "authorization");
1220 assert_eq!(SecurityCategory::DataExposure.as_str(), "data_exposure");
1221 assert_eq!(SecurityCategory::Cryptography.as_str(), "cryptography");
1222 assert_eq!(SecurityCategory::Configuration.as_str(), "configuration");
1223 assert_eq!(SecurityCategory::Dependency.as_str(), "dependency");
1224 assert_eq!(SecurityCategory::Compliance.as_str(), "compliance");
1225 assert_eq!(SecurityCategory::CodeQuality.as_str(), "code_quality");
1226 }
1227
1228 #[test]
1229 fn test_security_category_custom() {
1230 let cat = SecurityCategory::Custom("my_category".to_string());
1231 assert_eq!(cat.as_str(), "my_category");
1232 }
1233
1234 #[test]
1239 fn test_security_finding_new() {
1240 let finding = SecurityFinding::new(
1241 "Test Finding",
1242 SecurityCategory::HardcodedSecret,
1243 SecuritySeverity::High,
1244 );
1245 assert!(!finding.id.is_empty());
1246 assert!(finding.id.starts_with("SEC-"));
1247 assert!(finding.timestamp > 0);
1248 assert_eq!(finding.title, "Test Finding");
1249 assert_eq!(finding.description, "");
1250 assert!(finding.file.is_none());
1251 assert!(finding.line.is_none());
1252 assert!(finding.snippet.is_none());
1253 assert!(finding.remediation.is_none());
1254 assert!(finding.cwe.is_none());
1255 }
1256
1257 #[test]
1258 fn test_security_finding_with_description() {
1259 let finding = SecurityFinding::new("T", SecurityCategory::Injection, SecuritySeverity::Low)
1260 .with_description("Detailed description here");
1261 assert_eq!(finding.description, "Detailed description here");
1262 }
1263
1264 #[test]
1265 fn test_security_finding_with_location() {
1266 let finding =
1267 SecurityFinding::new("Test", SecurityCategory::Injection, SecuritySeverity::High)
1268 .with_location(PathBuf::from("test.rs"), 42);
1269 assert_eq!(finding.line, Some(42));
1270 assert_eq!(finding.file, Some(PathBuf::from("test.rs")));
1271 }
1272
1273 #[test]
1274 fn test_security_finding_with_snippet() {
1275 let finding = SecurityFinding::new("T", SecurityCategory::Injection, SecuritySeverity::Low)
1276 .with_snippet("let x = dangerous();");
1277 assert_eq!(finding.snippet.as_deref(), Some("let x = dangerous();"));
1278 }
1279
1280 #[test]
1281 fn test_security_finding_with_remediation() {
1282 let finding = SecurityFinding::new("T", SecurityCategory::Injection, SecuritySeverity::Low)
1283 .with_remediation("Use parameterized queries");
1284 assert_eq!(
1285 finding.remediation.as_deref(),
1286 Some("Use parameterized queries")
1287 );
1288 }
1289
1290 #[test]
1291 fn test_security_finding_with_cwe() {
1292 let finding = SecurityFinding::new("T", SecurityCategory::Injection, SecuritySeverity::Low)
1293 .with_cwe("CWE-89");
1294 assert_eq!(finding.cwe.as_deref(), Some("CWE-89"));
1295 }
1296
1297 #[test]
1298 fn test_security_finding_builder_chain() {
1299 let finding = SecurityFinding::new(
1300 "Chain Test",
1301 SecurityCategory::HardcodedSecret,
1302 SecuritySeverity::Critical,
1303 )
1304 .with_description("desc")
1305 .with_location(PathBuf::from("foo.rs"), 10)
1306 .with_snippet("snippet")
1307 .with_remediation("fix")
1308 .with_cwe("CWE-798");
1309
1310 assert_eq!(finding.title, "Chain Test");
1311 assert_eq!(finding.description, "desc");
1312 assert_eq!(finding.line, Some(10));
1313 assert_eq!(finding.snippet.as_deref(), Some("snippet"));
1314 assert_eq!(finding.remediation.as_deref(), Some("fix"));
1315 assert_eq!(finding.cwe.as_deref(), Some("CWE-798"));
1316 assert_eq!(finding.severity, SecuritySeverity::Critical);
1317 assert_eq!(finding.category, SecurityCategory::HardcodedSecret);
1318 }
1319
1320 #[test]
1325 fn test_secret_pattern_new() {
1326 let pattern = SecretPattern::new("Test", r"\d+", SecuritySeverity::Low);
1327 assert_eq!(pattern.name, "Test");
1328 assert_eq!(pattern.severity, SecuritySeverity::Low);
1329 assert!(pattern.compiled.is_some(), "valid regex should compile");
1330 }
1331
1332 #[test]
1333 fn test_secret_pattern_invalid_regex() {
1334 let pattern = SecretPattern::new("Bad", r"[invalid", SecuritySeverity::Low);
1335 assert!(
1336 pattern.compiled.is_none(),
1337 "invalid regex should produce None"
1338 );
1339 }
1340
1341 #[test]
1342 fn test_secret_pattern_description_default() {
1343 let pattern = SecretPattern::new("AWS Key", r"\d+", SecuritySeverity::Critical);
1344 assert!(pattern.description.contains("AWS Key"));
1345 }
1346
1347 #[test]
1352 fn test_secret_scanner_new() {
1353 let scanner = SecretScanner::new();
1354 assert!(scanner.findings().is_empty());
1355 }
1356
1357 #[test]
1358 fn test_secret_scanner_default() {
1359 let scanner = SecretScanner::default();
1360 assert!(scanner.findings().is_empty());
1361 }
1362
1363 #[test]
1364 fn test_secret_scanner_empty_content() {
1365 let mut scanner = SecretScanner::new();
1366 let findings = scanner.scan_content("", None);
1367 assert!(findings.is_empty());
1368 }
1369
1370 #[test]
1371 fn test_secret_scanner_no_secrets() {
1372 let mut scanner = SecretScanner::new();
1373 let content = "let x = 42;\nfn main() {}\n// This is safe code";
1374 let findings = scanner.scan_content(content, None);
1375 assert!(findings.is_empty());
1376 }
1377
1378 #[test]
1379 fn test_secret_scanner_detect_aws_access_key() {
1380 let mut scanner = SecretScanner::new();
1381 let content = "aws_access_key = AKIAIOSFODNN7EXAMPLE";
1383 let findings = scanner.scan_content(content, None);
1384 assert!(!findings.is_empty());
1385 assert!(findings
1386 .iter()
1387 .any(|f| f.severity == SecuritySeverity::Critical));
1388 }
1389
1390 #[test]
1391 fn test_secret_scanner_detect_private_key() {
1392 let mut scanner = SecretScanner::new();
1393 let content = "-----BEGIN RSA PRIVATE KEY-----";
1394 let findings = scanner.scan_content(content, None);
1395 assert!(!findings.is_empty());
1396 }
1397
1398 #[test]
1399 fn test_secret_scanner_detect_ec_private_key() {
1400 let mut scanner = SecretScanner::new();
1401 let content = "-----BEGIN EC PRIVATE KEY-----";
1402 let findings = scanner.scan_content(content, None);
1403 assert!(!findings.is_empty());
1404 }
1405
1406 #[test]
1407 fn test_secret_scanner_detect_openssh_private_key() {
1408 let mut scanner = SecretScanner::new();
1409 let content = "-----BEGIN OPENSSH PRIVATE KEY-----";
1410 let findings = scanner.scan_content(content, None);
1411 assert!(!findings.is_empty());
1412 }
1413
1414 #[test]
1415 fn test_secret_scanner_detect_generic_private_key() {
1416 let mut scanner = SecretScanner::new();
1417 let content = "-----BEGIN PRIVATE KEY-----";
1418 let findings = scanner.scan_content(content, None);
1419 assert!(!findings.is_empty());
1420 }
1421
1422 #[test]
1423 fn test_secret_scanner_detect_github_token_ghp() {
1424 let mut scanner = SecretScanner::new();
1425 let content =
1427 "token = ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ab";
1428 let findings = scanner.scan_content(content, None);
1429 assert!(!findings.is_empty());
1430 assert!(findings
1431 .iter()
1432 .any(|f| f.severity == SecuritySeverity::Critical));
1433 }
1434
1435 #[test]
1436 fn test_secret_scanner_detect_github_token_gho() {
1437 let mut scanner = SecretScanner::new();
1438 let content = "gho_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij";
1439 let findings = scanner.scan_content(content, None);
1440 assert!(!findings.is_empty());
1441 }
1442
1443 #[test]
1444 fn test_secret_scanner_detect_github_token_ghu() {
1445 let mut scanner = SecretScanner::new();
1446 let content = "ghu_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij";
1447 let findings = scanner.scan_content(content, None);
1448 assert!(!findings.is_empty());
1449 }
1450
1451 #[test]
1452 fn test_secret_scanner_detect_github_fine_grained_token() {
1453 let mut scanner = SecretScanner::new();
1454 let content = "github_pat_ABCDEFGHIJKLMNOPQRSTUVWXYZabcde";
1456 let findings = scanner.scan_content(content, None);
1457 assert!(!findings.is_empty());
1458 }
1459
1460 #[test]
1461 fn test_secret_scanner_detect_gitlab_token() {
1462 let mut scanner = SecretScanner::new();
1463 let content = "glpat-ABCDEFGHIJKLMNOPQRSTUVWXYZabcde";
1465 let findings = scanner.scan_content(content, None);
1466 assert!(!findings.is_empty());
1467 }
1468
1469 #[test]
1470 fn test_secret_scanner_detect_google_api_key() {
1471 let mut scanner = SecretScanner::new();
1472 let content = "key = AIzaSyDabcdefghijklmnopqrstuvwxyz012345";
1476 let findings = scanner.scan_content(content, None);
1477 assert!(!findings.is_empty());
1478 }
1479
1480 #[test]
1481 fn test_secret_scanner_detect_stripe_secret_key() {
1482 let mut scanner = SecretScanner::new();
1483 let prefix = "sk_live_";
1485 let suffix = "TESTKEYTESTKEYTESTKEYTESTK";
1486 let content = format!("stripe_key = {}{}", prefix, suffix);
1487 let findings = scanner.scan_content(&content, None);
1488 assert!(!findings.is_empty());
1489 assert!(findings
1490 .iter()
1491 .any(|f| f.severity == SecuritySeverity::Critical));
1492 }
1493
1494 #[test]
1495 fn test_secret_scanner_detect_stripe_restricted_key() {
1496 let mut scanner = SecretScanner::new();
1497 let prefix = "rk_live_";
1499 let suffix = "TESTKEYTESTKEYTESTKEYTESTK";
1500 let content = format!("{}{}", prefix, suffix);
1501 let findings = scanner.scan_content(&content, None);
1502 assert!(!findings.is_empty());
1503 }
1504
1505 #[test]
1506 fn test_secret_scanner_detect_generic_api_key() {
1507 let mut scanner = SecretScanner::new();
1508 let content = r#"api_key = "abcdefghij1234567890extra""#;
1510 let findings = scanner.scan_content(content, None);
1511 assert!(!findings.is_empty());
1512 }
1513
1514 #[test]
1515 fn test_secret_scanner_detect_apikey_variant() {
1516 let mut scanner = SecretScanner::new();
1517 let content = r#"apikey = "abcdefghijklmnopqrstuv""#;
1518 let findings = scanner.scan_content(content, None);
1519 assert!(!findings.is_empty());
1520 }
1521
1522 #[test]
1523 fn test_secret_scanner_detect_password_in_code() {
1524 let mut scanner = SecretScanner::new();
1525 let content = r#"password = "s3cr3tpassword""#;
1526 let findings = scanner.scan_content(content, None);
1527 assert!(!findings.is_empty());
1528 }
1529
1530 #[test]
1531 fn test_secret_scanner_detect_passwd_variant() {
1532 let mut scanner = SecretScanner::new();
1533 let content = r#"passwd = "mysecretpass1234""#;
1534 let findings = scanner.scan_content(content, None);
1535 assert!(!findings.is_empty());
1536 }
1537
1538 #[test]
1539 fn test_secret_scanner_detect_bearer_token() {
1540 let mut scanner = SecretScanner::new();
1541 let content = "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9";
1542 let findings = scanner.scan_content(content, None);
1543 assert!(!findings.is_empty());
1544 }
1545
1546 #[test]
1547 fn test_secret_scanner_detect_jwt_token() {
1548 let mut scanner = SecretScanner::new();
1549 let content =
1551 "token = eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
1552 let findings = scanner.scan_content(content, None);
1553 assert!(!findings.is_empty());
1554 }
1555
1556 #[test]
1557 fn test_secret_scanner_detect_database_url_postgres() {
1558 let mut scanner = SecretScanner::new();
1559 let content = "db_url = postgres://user:password@localhost/mydb";
1560 let findings = scanner.scan_content(content, None);
1561 assert!(!findings.is_empty());
1562 }
1563
1564 #[test]
1565 fn test_secret_scanner_detect_database_url_mysql() {
1566 let mut scanner = SecretScanner::new();
1567 let content = "url = mysql://admin:s3cr3t@db.example.com/prod";
1568 let findings = scanner.scan_content(content, None);
1569 assert!(!findings.is_empty());
1570 }
1571
1572 #[test]
1573 fn test_secret_scanner_detect_database_url_mongodb() {
1574 let mut scanner = SecretScanner::new();
1575 let content = "mongo_uri = mongodb://root:hunter2@mongo.local/admin";
1576 let findings = scanner.scan_content(content, None);
1577 assert!(!findings.is_empty());
1578 }
1579
1580 #[test]
1581 fn test_secret_scanner_detect_slack_bot_token() {
1582 let mut scanner = SecretScanner::new();
1583 let content = "slack_token = xoxb-1234567890-abcdefghij";
1585 let findings = scanner.scan_content(content, None);
1586 assert!(!findings.is_empty());
1587 }
1588
1589 #[test]
1590 fn test_secret_scanner_detect_slack_user_token() {
1591 let mut scanner = SecretScanner::new();
1592 let content = "xoxp-1234567890-0987654321-abcdef";
1593 let findings = scanner.scan_content(content, None);
1594 assert!(!findings.is_empty());
1595 }
1596
1597 #[test]
1598 fn test_secret_scanner_detect_base64_secret() {
1599 let mut scanner = SecretScanner::new();
1600 let content = "secret = ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/==";
1602 let findings = scanner.scan_content(content, None);
1603 assert!(!findings.is_empty());
1604 }
1605
1606 #[test]
1607 fn test_secret_scanner_skip_double_slash_comment() {
1608 let mut scanner = SecretScanner::new();
1609 let content = "// aws_key = AKIAIOSFODNN7EXAMPLE";
1610 let findings = scanner.scan_content(content, None);
1611 assert!(findings.is_empty());
1612 }
1613
1614 #[test]
1615 fn test_secret_scanner_skip_hash_comment() {
1616 let mut scanner = SecretScanner::new();
1617 let content = "# password = supersecretvalue";
1618 let findings = scanner.scan_content(content, None);
1619 assert!(findings.is_empty());
1620 }
1621
1622 #[test]
1623 fn test_secret_scanner_skip_block_comment() {
1624 let mut scanner = SecretScanner::new();
1625 let content = "/* password = supersecretvalue */";
1626 let findings = scanner.scan_content(content, None);
1627 assert!(findings.is_empty());
1628 }
1629
1630 #[test]
1631 fn test_secret_scanner_with_file_path() {
1632 let mut scanner = SecretScanner::new();
1633 let path = PathBuf::from("src/config.rs");
1634 let content = "AKIAIOSFODNN7EXAMPLE";
1635 let findings = scanner.scan_content(content, Some(&path));
1636 assert!(!findings.is_empty());
1637 assert_eq!(findings[0].file, Some(path));
1638 assert_eq!(findings[0].line, Some(1));
1639 }
1640
1641 #[test]
1642 fn test_secret_scanner_line_numbers() {
1643 let mut scanner = SecretScanner::new();
1644 let path = PathBuf::from("foo.rs");
1645 let content = "line1\nline2\nAKIAIOSFODNN7EXAMPLE\nline4";
1646 let findings = scanner.scan_content(content, Some(&path));
1647 assert!(!findings.is_empty());
1648 assert_eq!(findings[0].line, Some(3));
1649 }
1650
1651 #[test]
1652 fn test_secret_scanner_accumulates_findings() {
1653 let mut scanner = SecretScanner::new();
1654 scanner.scan_content("AKIAIOSFODNN7EXAMPLE", None);
1655 scanner.scan_content("-----BEGIN RSA PRIVATE KEY-----", None);
1656 assert!(scanner.findings().len() >= 2);
1657 }
1658
1659 #[test]
1660 fn test_secret_scanner_clear() {
1661 let mut scanner = SecretScanner::new();
1662 scanner.scan_content("AKIAIOSFODNN7EXAMPLE", None);
1663 assert!(!scanner.findings().is_empty());
1664 scanner.clear();
1665 assert!(scanner.findings().is_empty());
1666 }
1667
1668 #[test]
1669 fn test_secret_scanner_masked_snippet() {
1670 let mut scanner = SecretScanner::new();
1671 let content = "AKIAIOSFODNN7EXAMPLE";
1672 let findings = scanner.scan_content(content, None);
1673 assert!(!findings.is_empty());
1674 if let Some(snippet) = &findings[0].snippet {
1676 assert!(snippet.contains("...") || snippet.chars().all(|c| c == '*'));
1677 }
1678 }
1679
1680 #[test]
1681 fn test_secret_scanner_remediation_set() {
1682 let mut scanner = SecretScanner::new();
1683 let findings = scanner.scan_content("AKIAIOSFODNN7EXAMPLE", None);
1684 assert!(!findings.is_empty());
1685 assert!(findings[0].remediation.is_some());
1686 }
1687
1688 #[test]
1689 fn test_secret_scanner_cwe_set() {
1690 let mut scanner = SecretScanner::new();
1691 let findings = scanner.scan_content("AKIAIOSFODNN7EXAMPLE", None);
1692 assert!(!findings.is_empty());
1693 assert_eq!(findings[0].cwe.as_deref(), Some("CWE-798"));
1694 }
1695
1696 #[test]
1697 fn test_secret_scanner_add_custom_pattern() {
1698 let mut scanner = SecretScanner::new();
1699 let custom = SecretPattern::new("Custom Secret", r"MYSECRET\d{6}", SecuritySeverity::High);
1700 scanner.add_pattern(custom);
1701 let content = "key = MYSECRET123456";
1702 let findings = scanner.scan_content(content, None);
1703 assert!(findings.iter().any(|f| f.title == "Custom Secret"));
1704 }
1705
1706 #[test]
1707 fn test_secret_scanner_unicode_content() {
1708 let mut scanner = SecretScanner::new();
1709 let content = "let msg = \"こんにちは世界\"; // no secrets here";
1711 let findings = scanner.scan_content(content, None);
1712 assert!(findings.is_empty());
1714 }
1715
1716 #[test]
1717 fn test_secret_scanner_very_long_line() {
1718 let mut scanner = SecretScanner::new();
1719 let content = "a".repeat(10_000);
1721 let findings = scanner.scan_content(&content, None);
1722 assert!(findings.is_empty());
1723 }
1724
1725 #[test]
1726 fn test_secret_scanner_multiline_content() {
1727 let mut scanner = SecretScanner::new();
1728 let content = "line1\nline2\nline3\nline4\nline5";
1729 let findings = scanner.scan_content(content, None);
1730 assert!(findings.is_empty());
1731 }
1732
1733 #[test]
1738 fn test_vulnerability_detector_new() {
1739 let detector = VulnerabilityDetector::new();
1740 assert!(detector.findings().is_empty());
1741 }
1742
1743 #[test]
1744 fn test_vulnerability_detector_default() {
1745 let detector = VulnerabilityDetector::default();
1746 assert!(detector.findings().is_empty());
1747 }
1748
1749 #[test]
1750 fn test_vulnerability_detector_empty_content() {
1751 let mut detector = VulnerabilityDetector::new();
1752 let findings = detector.scan_content("", None, "rust");
1753 assert!(findings.is_empty());
1754 }
1755
1756 #[test]
1757 fn test_vulnerability_detector_rust_unsafe() {
1758 let mut detector = VulnerabilityDetector::new();
1759 let content = "unsafe { ptr::read(addr) }";
1760 let findings = detector.scan_content(content, None, "rust");
1761 assert!(!findings.is_empty());
1762 assert!(findings.iter().any(|f| f.cwe.as_deref() == Some("CWE-242")));
1763 }
1764
1765 #[test]
1766 fn test_vulnerability_detector_rust_unsafe_severity() {
1767 let mut detector = VulnerabilityDetector::new();
1768 let content = "unsafe { }";
1769 let findings = detector.scan_content(content, None, "rust");
1770 assert!(!findings.is_empty());
1771 assert!(findings
1773 .iter()
1774 .any(|f| f.severity == SecuritySeverity::Medium));
1775 }
1776
1777 #[test]
1778 fn test_vulnerability_detector_rust_unwrap() {
1779 let mut detector = VulnerabilityDetector::new();
1780 let content = "let x = option.unwrap();";
1781 let findings = detector.scan_content(content, None, "rust");
1782 assert!(!findings.is_empty());
1783 assert!(findings.iter().any(|f| f.cwe.as_deref() == Some("CWE-252")));
1784 }
1785
1786 #[test]
1787 fn test_vulnerability_detector_rust_unwrap_severity() {
1788 let mut detector = VulnerabilityDetector::new();
1789 let content = "let x = result.unwrap();";
1790 let findings = detector.scan_content(content, None, "rust");
1791 assert!(!findings.is_empty());
1792 assert!(findings.iter().any(|f| f.severity == SecuritySeverity::Low));
1794 }
1795
1796 #[test]
1797 fn test_vulnerability_detector_rust_sql_injection_select() {
1798 let mut detector = VulnerabilityDetector::new();
1799 let content = r#"let q = format!("SELECT * FROM users WHERE id={}", id);"#;
1800 let findings = detector.scan_content(content, None, "rust");
1801 assert!(!findings.is_empty());
1802 assert!(findings.iter().any(|f| f.cwe.as_deref() == Some("CWE-89")));
1803 }
1804
1805 #[test]
1806 fn test_vulnerability_detector_rust_sql_injection_insert() {
1807 let mut detector = VulnerabilityDetector::new();
1808 let content = r#"let q = format!("INSERT INTO logs VALUES({})", val);"#;
1809 let findings = detector.scan_content(content, None, "rust");
1810 assert!(!findings.is_empty());
1811 }
1812
1813 #[test]
1814 fn test_vulnerability_detector_rust_sql_injection_update() {
1815 let mut detector = VulnerabilityDetector::new();
1816 let content = r#"let q = format!("UPDATE users SET name={}", name);"#;
1817 let findings = detector.scan_content(content, None, "rust");
1818 assert!(!findings.is_empty());
1819 }
1820
1821 #[test]
1822 fn test_vulnerability_detector_rust_sql_injection_delete() {
1823 let mut detector = VulnerabilityDetector::new();
1824 let content = r#"let q = format!("DELETE FROM sessions WHERE id={}", id);"#;
1825 let findings = detector.scan_content(content, None, "rust");
1826 assert!(!findings.is_empty());
1827 }
1828
1829 #[test]
1830 fn test_vulnerability_detector_rust_command_injection() {
1831 let mut detector = VulnerabilityDetector::new();
1832 let content = r#"Command::new(&format!("{}", user_input)).spawn().unwrap();"#;
1833 let findings = detector.scan_content(content, None, "rust");
1834 assert!(!findings.is_empty());
1835 assert!(findings
1836 .iter()
1837 .any(|f| f.severity == SecuritySeverity::Critical));
1838 assert!(findings.iter().any(|f| f.cwe.as_deref() == Some("CWE-78")));
1839 }
1840
1841 #[test]
1842 fn test_vulnerability_detector_rust_path_traversal_path_new() {
1843 let mut detector = VulnerabilityDetector::new();
1844 let content = r#"let p = Path::new(&format!("/uploads/{}", filename));"#;
1845 let findings = detector.scan_content(content, None, "rust");
1846 assert!(!findings.is_empty());
1847 assert!(findings.iter().any(|f| f.cwe.as_deref() == Some("CWE-22")));
1848 }
1849
1850 #[test]
1851 fn test_vulnerability_detector_rust_path_traversal_pathbuf_from() {
1852 let mut detector = VulnerabilityDetector::new();
1853 let content = r#"let p = PathBuf::from(&format!("/data/{}", input));"#;
1854 let findings = detector.scan_content(content, None, "rust");
1855 assert!(!findings.is_empty());
1856 }
1857
1858 #[test]
1859 fn test_vulnerability_detector_js_eval() {
1860 let mut detector = VulnerabilityDetector::new();
1861 let content = "eval(userInput)";
1862 let findings = detector.scan_content(content, None, "javascript");
1863 assert!(!findings.is_empty());
1864 assert!(findings
1865 .iter()
1866 .any(|f| f.severity == SecuritySeverity::Critical));
1867 assert!(findings.iter().any(|f| f.cwe.as_deref() == Some("CWE-95")));
1868 }
1869
1870 #[test]
1871 fn test_vulnerability_detector_js_inner_html() {
1872 let mut detector = VulnerabilityDetector::new();
1873 let content = "element.innerHTML = userInput;";
1874 let findings = detector.scan_content(content, None, "javascript");
1875 assert!(!findings.is_empty());
1876 assert!(findings.iter().any(|f| f.cwe.as_deref() == Some("CWE-79")));
1877 }
1878
1879 #[test]
1880 fn test_vulnerability_detector_python_exec() {
1881 let mut detector = VulnerabilityDetector::new();
1882 let content = "exec(user_code)";
1883 let findings = detector.scan_content(content, None, "python");
1884 assert!(!findings.is_empty());
1885 assert!(findings
1886 .iter()
1887 .any(|f| f.severity == SecuritySeverity::Critical));
1888 }
1889
1890 #[test]
1891 fn test_vulnerability_detector_python_eval() {
1892 let mut detector = VulnerabilityDetector::new();
1893 let content = "result = eval(expression)";
1894 let findings = detector.scan_content(content, None, "python");
1895 assert!(!findings.is_empty());
1896 }
1897
1898 #[test]
1899 fn test_vulnerability_detector_python_pickle_load() {
1900 let mut detector = VulnerabilityDetector::new();
1901 let content = "obj = pickle.load(file_handle)";
1902 let findings = detector.scan_content(content, None, "python");
1903 assert!(!findings.is_empty());
1904 assert!(findings.iter().any(|f| f.cwe.as_deref() == Some("CWE-502")));
1905 }
1906
1907 #[test]
1908 fn test_vulnerability_detector_python_pickle_loads() {
1909 let mut detector = VulnerabilityDetector::new();
1910 let content = "data = pickle.loads(raw_bytes)";
1911 let findings = detector.scan_content(content, None, "python");
1912 assert!(!findings.is_empty());
1913 }
1914
1915 #[test]
1916 fn test_vulnerability_detector_language_filter() {
1917 let mut detector = VulnerabilityDetector::new();
1919 let content = "eval(something)";
1920 let findings = detector.scan_content(content, None, "rust");
1921 assert!(!findings.iter().any(|f| f.cwe.as_deref() == Some("CWE-95")));
1923 }
1924
1925 #[test]
1926 fn test_vulnerability_detector_with_file_location() {
1927 let mut detector = VulnerabilityDetector::new();
1928 let path = PathBuf::from("src/main.rs");
1929 let content = "unsafe { }";
1930 let findings = detector.scan_content(content, Some(&path), "rust");
1931 assert!(!findings.is_empty());
1932 assert_eq!(findings[0].file, Some(path));
1933 assert_eq!(findings[0].line, Some(1));
1934 }
1935
1936 #[test]
1937 fn test_vulnerability_detector_snippet_is_trimmed() {
1938 let mut detector = VulnerabilityDetector::new();
1939 let content = " unsafe { } ";
1940 let findings = detector.scan_content(content, None, "rust");
1941 assert!(!findings.is_empty());
1942 if let Some(snippet) = &findings[0].snippet {
1944 assert_eq!(snippet.trim(), snippet.as_str());
1945 }
1946 }
1947
1948 #[test]
1949 fn test_vulnerability_detector_accumulates_findings() {
1950 let mut detector = VulnerabilityDetector::new();
1951 detector.scan_content("unsafe { }", None, "rust");
1952 detector.scan_content("let x = option.unwrap();", None, "rust");
1953 assert!(detector.findings().len() >= 2);
1954 }
1955
1956 #[test]
1957 fn test_vulnerability_detector_clear() {
1958 let mut detector = VulnerabilityDetector::new();
1959 detector.scan_content("unsafe { }", None, "rust");
1960 assert!(!detector.findings().is_empty());
1961 detector.clear();
1962 assert!(detector.findings().is_empty());
1963 }
1964
1965 #[test]
1966 fn test_vulnerability_detector_add_custom_pattern() {
1967 let mut detector = VulnerabilityDetector::new();
1968 detector.add_pattern(VulnerabilityPattern {
1969 id: "CUSTOM001".to_string(),
1970 name: "Custom Check".to_string(),
1971 language: "rust".to_string(),
1972 pattern: r"todo!\(\)".to_string(),
1973 severity: SecuritySeverity::Info,
1974 cwe: "CWE-0".to_string(),
1975 description: "Incomplete code marker".to_string(),
1976 remediation: "Remove todo!() before production".to_string(),
1977 });
1978 let findings = detector.scan_content("todo!()", None, "rust");
1979 assert!(findings.iter().any(|f| f.title == "Custom Check"));
1980 }
1981
1982 #[test]
1983 fn test_vulnerability_detector_unknown_language_no_matches() {
1984 let mut detector = VulnerabilityDetector::new();
1985 let content = "eval(foo) unsafe { } pickle.loads(x)";
1987 let findings = detector.scan_content(content, None, "cobol");
1988 assert!(findings.is_empty());
1989 }
1990
1991 #[test]
1992 fn test_vulnerability_detector_wildcard_language() {
1993 let mut detector = VulnerabilityDetector::new();
1995 detector.add_pattern(VulnerabilityPattern {
1996 id: "ALL001".to_string(),
1997 name: "Universal Check".to_string(),
1998 language: "*".to_string(),
1999 pattern: r"FORBIDDEN".to_string(),
2000 severity: SecuritySeverity::High,
2001 cwe: "CWE-999".to_string(),
2002 description: "Forbidden token".to_string(),
2003 remediation: "Remove it".to_string(),
2004 });
2005 let findings_rust = detector.scan_content("FORBIDDEN", None, "rust");
2006 assert!(!findings_rust.is_empty());
2007 let findings_js = detector.scan_content("FORBIDDEN", None, "javascript");
2008 assert!(!findings_js.is_empty());
2009 }
2010
2011 #[test]
2016 fn test_known_vulnerability_new() {
2017 let vuln = KnownVulnerability::new("CVE-2021-1234", SecuritySeverity::High, "Test vuln");
2018 assert_eq!(vuln.id, "CVE-2021-1234");
2019 assert_eq!(vuln.severity, SecuritySeverity::High);
2020 assert_eq!(vuln.description, "Test vuln");
2021 assert!(vuln.fixed_version.is_none());
2022 assert!(vuln.url.is_none());
2023 }
2024
2025 #[test]
2026 fn test_known_vulnerability_with_fixed_version() {
2027 let vuln = KnownVulnerability::new("CVE-X", SecuritySeverity::Low, "desc")
2028 .with_fixed_version("2.0.0");
2029 assert_eq!(vuln.fixed_version.as_deref(), Some("2.0.0"));
2030 }
2031
2032 #[test]
2033 fn test_known_vulnerability_with_url() {
2034 let vuln = KnownVulnerability::new("CVE-X", SecuritySeverity::Low, "desc")
2035 .with_url("https://nvd.nist.gov/vuln/detail/CVE-X");
2036 assert_eq!(
2037 vuln.url.as_deref(),
2038 Some("https://nvd.nist.gov/vuln/detail/CVE-X")
2039 );
2040 }
2041
2042 #[test]
2047 fn test_dependency_new() {
2048 let dep = Dependency::new("test-pkg", "1.0.0", "npm");
2049 assert_eq!(dep.name, "test-pkg");
2050 assert_eq!(dep.version, "1.0.0");
2051 assert_eq!(dep.source, "npm");
2052 assert!(!dep.is_vulnerable());
2053 assert!(dep.vulnerabilities.is_empty());
2054 }
2055
2056 #[test]
2057 fn test_dependency_is_vulnerable_with_vuln() {
2058 let mut dep = Dependency::new("pkg", "1.0", "crates.io");
2059 dep.vulnerabilities.push(KnownVulnerability::new(
2060 "CVE-X",
2061 SecuritySeverity::High,
2062 "d",
2063 ));
2064 assert!(dep.is_vulnerable());
2065 }
2066
2067 #[test]
2068 fn test_dependency_max_severity_none() {
2069 let dep = Dependency::new("pkg", "1.0", "crates.io");
2070 assert_eq!(dep.max_severity(), None);
2071 }
2072
2073 #[test]
2074 fn test_dependency_max_severity_single() {
2075 let mut dep = Dependency::new("pkg", "1.0", "crates.io");
2076 dep.vulnerabilities.push(KnownVulnerability::new(
2077 "CVE-X",
2078 SecuritySeverity::High,
2079 "d",
2080 ));
2081 assert_eq!(dep.max_severity(), Some(SecuritySeverity::High));
2082 }
2083
2084 #[test]
2085 fn test_dependency_max_severity_multiple() {
2086 let mut dep = Dependency::new("pkg", "1.0", "crates.io");
2087 dep.vulnerabilities
2088 .push(KnownVulnerability::new("CVE-A", SecuritySeverity::Low, "d"));
2089 dep.vulnerabilities.push(KnownVulnerability::new(
2090 "CVE-B",
2091 SecuritySeverity::Critical,
2092 "e",
2093 ));
2094 dep.vulnerabilities.push(KnownVulnerability::new(
2095 "CVE-C",
2096 SecuritySeverity::High,
2097 "f",
2098 ));
2099 assert_eq!(dep.max_severity(), Some(SecuritySeverity::Critical));
2100 }
2101
2102 #[test]
2107 fn test_dependency_auditor_new() {
2108 let auditor = DependencyAuditor::new();
2109 assert!(auditor.vulnerable_dependencies().is_empty());
2110 assert!(auditor.findings().is_empty());
2111 }
2112
2113 #[test]
2114 fn test_dependency_auditor_default() {
2115 let auditor = DependencyAuditor::default();
2116 assert!(auditor.findings().is_empty());
2117 }
2118
2119 #[test]
2120 fn test_dependency_auditor_known_vuln_lodash() {
2121 let mut auditor = DependencyAuditor::new();
2122 let dep = auditor.audit_dependency("lodash", "4.17.0", "npm");
2123 assert!(dep.is_vulnerable());
2124 assert!(!auditor.findings().is_empty());
2125 assert_eq!(auditor.findings()[0].category, SecurityCategory::Dependency);
2126 }
2127
2128 #[test]
2129 fn test_dependency_auditor_known_vuln_log4j() {
2130 let mut auditor = DependencyAuditor::new();
2131 let dep = auditor.audit_dependency("log4j", "2.14.0", "maven");
2132 assert!(dep.is_vulnerable());
2133 assert!(dep.vulnerabilities.iter().any(|v| v.id == "CVE-2021-44228"));
2134 assert!(dep
2135 .vulnerabilities
2136 .iter()
2137 .any(|v| v.severity == SecuritySeverity::Critical));
2138 }
2139
2140 #[test]
2141 fn test_dependency_auditor_safe_dep() {
2142 let mut auditor = DependencyAuditor::new();
2143 let dep = auditor.audit_dependency("safe-package", "1.0.0", "npm");
2144 assert!(!dep.is_vulnerable());
2145 }
2146
2147 #[test]
2148 fn test_dependency_auditor_vulnerable_dependencies_list() {
2149 let mut auditor = DependencyAuditor::new();
2150 auditor.audit_dependency("safe-package", "1.0.0", "npm");
2151 auditor.audit_dependency("lodash", "4.17.0", "npm");
2152 let vulns = auditor.vulnerable_dependencies();
2153 assert_eq!(vulns.len(), 1);
2154 assert_eq!(vulns[0].name, "lodash");
2155 }
2156
2157 #[test]
2158 fn test_dependency_auditor_add_vulnerability() {
2159 let mut auditor = DependencyAuditor::new();
2160 let vuln = KnownVulnerability::new("CVE-CUSTOM", SecuritySeverity::Medium, "Custom vuln");
2161 auditor.add_vulnerability("my-package", vuln);
2162 let dep = auditor.audit_dependency("my-package", "1.0.0", "custom");
2163 assert!(dep.is_vulnerable());
2164 }
2165
2166 #[test]
2167 fn test_dependency_auditor_finding_remediation_contains_fixed_version() {
2168 let mut auditor = DependencyAuditor::new();
2169 auditor.audit_dependency("lodash", "4.17.0", "npm");
2170 let finding = &auditor.findings()[0];
2171 assert!(finding
2173 .remediation
2174 .as_deref()
2175 .unwrap_or("")
2176 .contains("4.17.21"));
2177 }
2178
2179 #[test]
2180 fn test_dependency_auditor_clear() {
2181 let mut auditor = DependencyAuditor::new();
2182 auditor.audit_dependency("lodash", "4.17.0", "npm");
2183 assert!(!auditor.findings().is_empty());
2184 auditor.clear();
2185 assert!(auditor.findings().is_empty());
2186 assert!(auditor.vulnerable_dependencies().is_empty());
2187 }
2188
2189 #[test]
2194 fn test_compliance_rule_new() {
2195 let rule = ComplianceRule::new("R1", "OWASP", "Test rule");
2196 assert_eq!(rule.id, "R1");
2197 assert_eq!(rule.standard, "OWASP");
2198 assert_eq!(rule.description, "Test rule");
2199 assert!(rule.pattern.is_none());
2200 assert_eq!(rule.severity, SecuritySeverity::Medium);
2201 }
2202
2203 #[test]
2204 fn test_compliance_rule_with_pattern() {
2205 let rule = ComplianceRule::new("R1", "OWASP", "desc").with_pattern(r"md5\(");
2206 assert!(rule.pattern.is_some());
2207 }
2208
2209 #[test]
2210 fn test_compliance_rule_with_severity() {
2211 let rule =
2212 ComplianceRule::new("R1", "PCI", "desc").with_severity(SecuritySeverity::Critical);
2213 assert_eq!(rule.severity, SecuritySeverity::Critical);
2214 }
2215
2216 #[test]
2221 fn test_compliance_checker_new() {
2222 let checker = ComplianceChecker::new();
2223 assert!(!checker.standards().is_empty());
2224 assert!(checker.findings().is_empty());
2225 }
2226
2227 #[test]
2228 fn test_compliance_checker_default() {
2229 let checker = ComplianceChecker::default();
2230 assert!(!checker.standards().is_empty());
2231 }
2232
2233 #[test]
2234 fn test_compliance_checker_standards_sorted_deduped() {
2235 let checker = ComplianceChecker::new();
2236 let standards = checker.standards();
2237 for window in standards.windows(2) {
2239 assert!(window[0] <= window[1]);
2240 }
2241 let mut prev = "";
2243 for s in &standards {
2244 assert_ne!(s, prev, "duplicate standard found: {s}");
2245 prev = s;
2246 }
2247 }
2248
2249 #[test]
2250 fn test_compliance_checker_contains_known_standards() {
2251 let checker = ComplianceChecker::new();
2252 let standards = checker.standards();
2253 assert!(standards.iter().any(|s| s == "OWASP Top 10"));
2254 assert!(standards.iter().any(|s| s == "PCI-DSS"));
2255 assert!(standards.iter().any(|s| s == "HIPAA"));
2256 }
2257
2258 #[test]
2259 fn test_compliance_checker_weak_crypto_md5() {
2260 let mut checker = ComplianceChecker::new();
2261 let content = "hash = md5(password)";
2262 let findings = checker.check_content(content, None);
2263 assert!(!findings.is_empty());
2264 assert!(findings
2265 .iter()
2266 .any(|f| f.category == SecurityCategory::Compliance));
2267 }
2268
2269 #[test]
2270 fn test_compliance_checker_weak_crypto_sha1() {
2271 let mut checker = ComplianceChecker::new();
2272 let content = "digest = sha1(data)";
2273 let findings = checker.check_content(content, None);
2274 assert!(!findings.is_empty());
2275 }
2276
2277 #[test]
2278 fn test_compliance_checker_case_insensitive_md5() {
2279 let mut checker = ComplianceChecker::new();
2280 let content = "hash = MD5(password)";
2281 let findings = checker.check_content(content, None);
2282 assert!(!findings.is_empty());
2283 }
2284
2285 #[test]
2286 fn test_compliance_checker_case_insensitive_sha1() {
2287 let mut checker = ComplianceChecker::new();
2288 let content = "digest = SHA1(data)";
2289 let findings = checker.check_content(content, None);
2290 assert!(!findings.is_empty());
2291 }
2292
2293 #[test]
2294 fn test_compliance_checker_clean_code() {
2295 let mut checker = ComplianceChecker::new();
2296 let content = "digest = sha256(data)";
2298 let findings = checker.check_content(content, None);
2299 assert!(findings.is_empty());
2300 }
2301
2302 #[test]
2303 fn test_compliance_checker_empty_content() {
2304 let mut checker = ComplianceChecker::new();
2305 let findings = checker.check_content("", None);
2306 assert!(findings.is_empty());
2307 }
2308
2309 #[test]
2310 fn test_compliance_checker_with_file_path() {
2311 let mut checker = ComplianceChecker::new();
2312 let path = PathBuf::from("crypto.py");
2313 let content = "hash = md5(data)";
2314 let findings = checker.check_content(content, Some(&path));
2315 assert!(!findings.is_empty());
2316 assert_eq!(findings[0].file, Some(path));
2317 }
2318
2319 #[test]
2320 fn test_compliance_checker_snippet_is_trimmed() {
2321 let mut checker = ComplianceChecker::new();
2322 let content = " hash = md5(data) ";
2323 let findings = checker.check_content(content, None);
2324 assert!(!findings.is_empty());
2325 if let Some(snippet) = &findings[0].snippet {
2326 assert_eq!(snippet.trim(), snippet.as_str());
2327 }
2328 }
2329
2330 #[test]
2331 fn test_compliance_checker_accumulates_findings() {
2332 let mut checker = ComplianceChecker::new();
2333 checker.check_content("md5(x)", None);
2334 checker.check_content("sha1(y)", None);
2335 assert!(checker.findings().len() >= 2);
2336 }
2337
2338 #[test]
2339 fn test_compliance_checker_clear() {
2340 let mut checker = ComplianceChecker::new();
2341 checker.check_content("md5(x)", None);
2342 assert!(!checker.findings().is_empty());
2343 checker.clear();
2344 assert!(checker.findings().is_empty());
2345 }
2346
2347 #[test]
2348 fn test_compliance_checker_add_rule() {
2349 let mut checker = ComplianceChecker::new();
2350 checker.add_rule(
2351 ComplianceRule::new("CUSTOM-001", "CUSTOM_STD", "No goto")
2352 .with_pattern(r"\bgoto\b")
2353 .with_severity(SecuritySeverity::Low),
2354 );
2355 let findings = checker.check_content("goto label;", None);
2356 assert!(findings.iter().any(|f| f.title.contains("CUSTOM-001")));
2357 }
2358
2359 #[test]
2364 fn test_scan_result_new() {
2365 let result = ScanResult::new();
2366 assert_eq!(result.total_findings(), 0);
2367 assert!(!result.has_critical());
2368 assert!(!result.has_high());
2369 assert_eq!(result.risk_score(), 0.0);
2370 assert_eq!(result.files_scanned, 0);
2371 assert_eq!(result.lines_scanned, 0);
2372 }
2373
2374 #[test]
2375 fn test_scan_result_default() {
2376 let result = ScanResult::default();
2377 assert_eq!(result.total_findings(), 0);
2378 }
2379
2380 #[test]
2381 fn test_scan_result_total_findings() {
2382 let mut result = ScanResult::new();
2383 result.findings.push(SecurityFinding::new(
2384 "F1",
2385 SecurityCategory::Injection,
2386 SecuritySeverity::Low,
2387 ));
2388 result.findings.push(SecurityFinding::new(
2389 "F2",
2390 SecurityCategory::Injection,
2391 SecuritySeverity::High,
2392 ));
2393 assert_eq!(result.total_findings(), 2);
2394 }
2395
2396 #[test]
2397 fn test_scan_result_has_critical_true() {
2398 let mut result = ScanResult::new();
2399 result.by_severity.insert(SecuritySeverity::Critical, 1);
2400 assert!(result.has_critical());
2401 }
2402
2403 #[test]
2404 fn test_scan_result_has_critical_zero() {
2405 let mut result = ScanResult::new();
2406 result.by_severity.insert(SecuritySeverity::Critical, 0);
2407 assert!(!result.has_critical());
2408 }
2409
2410 #[test]
2411 fn test_scan_result_has_high_true() {
2412 let mut result = ScanResult::new();
2413 result.by_severity.insert(SecuritySeverity::High, 2);
2414 assert!(result.has_high());
2415 }
2416
2417 #[test]
2418 fn test_scan_result_has_high_zero() {
2419 let mut result = ScanResult::new();
2420 result.by_severity.insert(SecuritySeverity::High, 0);
2421 assert!(!result.has_high());
2422 }
2423
2424 #[test]
2425 fn test_scan_result_risk_score_empty() {
2426 let result = ScanResult::new();
2427 assert_eq!(result.risk_score(), 0.0);
2428 }
2429
2430 #[test]
2431 fn test_scan_result_risk_score_one_critical() {
2432 let mut result = ScanResult::new();
2433 result.findings.push(SecurityFinding::new(
2434 "Test",
2435 SecurityCategory::Injection,
2436 SecuritySeverity::Critical,
2437 ));
2438 assert_eq!(result.risk_score(), 9.5);
2439 }
2440
2441 #[test]
2442 fn test_scan_result_risk_score_additive() {
2443 let mut result = ScanResult::new();
2444 result.findings.push(SecurityFinding::new(
2445 "F1",
2446 SecurityCategory::Injection,
2447 SecuritySeverity::High,
2448 ));
2449 result.findings.push(SecurityFinding::new(
2450 "F2",
2451 SecurityCategory::Injection,
2452 SecuritySeverity::Medium,
2453 ));
2454 assert!((result.risk_score() - 13.0).abs() < f32::EPSILON);
2456 }
2457
2458 #[test]
2463 fn test_security_scanner_new() {
2464 let scanner = SecurityScanner::new();
2465 let stats = scanner.get_stats();
2466 assert_eq!(stats.total_scans, 0);
2467 assert_eq!(stats.total_findings, 0);
2468 assert_eq!(stats.critical_findings, 0);
2469 assert_eq!(stats.high_findings, 0);
2470 }
2471
2472 #[test]
2473 fn test_security_scanner_default() {
2474 let scanner = SecurityScanner::default();
2475 let stats = scanner.get_stats();
2476 assert_eq!(stats.total_scans, 0);
2477 }
2478
2479 #[test]
2480 fn test_security_scanner_scan_clean_code() {
2481 let scanner = SecurityScanner::new();
2482 let result = scanner.scan_content("let x = 1;", None, "rust");
2483 assert_eq!(result.files_scanned, 1);
2484 assert_eq!(result.lines_scanned, 1);
2485 }
2486
2487 #[test]
2488 fn test_security_scanner_scan_empty_content() {
2489 let scanner = SecurityScanner::new();
2490 let result = scanner.scan_content("", None, "rust");
2491 assert_eq!(result.total_findings(), 0);
2492 assert_eq!(result.lines_scanned, 0);
2493 }
2494
2495 #[test]
2496 fn test_security_scanner_scan_detects_aws_secret() {
2497 let scanner = SecurityScanner::new();
2498 let result = scanner.scan_content("api_key = \"AKIAIOSFODNN7EXAMPLE\"", None, "rust");
2499 assert!(result.total_findings() > 0);
2500 assert!(result.by_category.contains_key("hardcoded_secret"));
2501 }
2502
2503 #[test]
2504 fn test_security_scanner_scan_detects_rust_unsafe() {
2505 let scanner = SecurityScanner::new();
2506 let result = scanner.scan_content("unsafe { }", None, "rust");
2507 assert!(result.total_findings() > 0);
2508 assert!(result.by_category.contains_key("code_quality"));
2509 }
2510
2511 #[test]
2512 fn test_security_scanner_scan_detects_weak_crypto() {
2513 let scanner = SecurityScanner::new();
2514 let result = scanner.scan_content("md5(password)", None, "python");
2515 assert!(result.total_findings() > 0);
2516 assert!(result.by_category.contains_key("compliance"));
2517 }
2518
2519 #[test]
2520 fn test_security_scanner_by_severity_populated() {
2521 let scanner = SecurityScanner::new();
2522 let result = scanner.scan_content("unsafe { }", None, "rust");
2524 let total: usize = result.by_severity.values().sum();
2525 assert_eq!(total, result.total_findings());
2526 }
2527
2528 #[test]
2529 fn test_security_scanner_by_category_populated() {
2530 let scanner = SecurityScanner::new();
2531 let result = scanner.scan_content("unsafe { }", None, "rust");
2532 let total: usize = result.by_category.values().sum();
2533 assert_eq!(total, result.total_findings());
2534 }
2535
2536 #[test]
2537 fn test_security_scanner_duration_recorded() {
2538 let scanner = SecurityScanner::new();
2539 let result = scanner.scan_content("let x = 1;", None, "rust");
2540 let _ = result.duration_ms;
2542 }
2543
2544 #[test]
2545 fn test_security_scanner_stats_accumulate_across_scans() {
2546 let scanner = SecurityScanner::new();
2547 scanner.scan_content("let x = 1;", None, "rust");
2548 scanner.scan_content("let y = 2;", None, "rust");
2549 let stats = scanner.get_stats();
2550 assert_eq!(stats.total_scans, 2);
2551 }
2552
2553 #[test]
2554 fn test_security_scanner_stats_count_findings() {
2555 let scanner = SecurityScanner::new();
2556 scanner.scan_content("unsafe { }", None, "rust");
2557 let stats = scanner.get_stats();
2558 assert!(stats.total_findings >= 1);
2559 }
2560
2561 #[test]
2562 fn test_security_scanner_stats_count_critical() {
2563 let scanner = SecurityScanner::new();
2564 scanner.scan_content("AKIAIOSFODNN7EXAMPLE", None, "rust");
2566 let stats = scanner.get_stats();
2567 assert!(stats.critical_findings >= 1);
2568 }
2569
2570 #[test]
2571 fn test_security_scanner_stats_count_high() {
2572 let scanner = SecurityScanner::new();
2573 scanner.scan_content("Authorization: Bearer sometoken123", None, "rust");
2575 let stats = scanner.get_stats();
2576 assert!(stats.high_findings >= 1);
2577 }
2578
2579 #[test]
2580 fn test_security_scanner_audit_dependency_vulnerable() {
2581 let scanner = SecurityScanner::new();
2582 let dep = scanner.audit_dependency("lodash", "4.17.0", "npm");
2583 assert!(dep.is_vulnerable());
2584 }
2585
2586 #[test]
2587 fn test_security_scanner_audit_dependency_safe() {
2588 let scanner = SecurityScanner::new();
2589 let dep = scanner.audit_dependency("totally-safe-pkg", "9.9.9", "npm");
2590 assert!(!dep.is_vulnerable());
2591 }
2592
2593 #[test]
2594 fn test_security_scanner_audit_dependency_log4j() {
2595 let scanner = SecurityScanner::new();
2596 let dep = scanner.audit_dependency("log4j", "2.14.0", "maven");
2597 assert!(dep.is_vulnerable());
2598 assert!(dep
2599 .vulnerabilities
2600 .iter()
2601 .any(|v| v.severity == SecuritySeverity::Critical));
2602 }
2603
2604 #[test]
2605 fn test_security_scanner_report_contains_header() {
2606 let scanner = SecurityScanner::new();
2607 let result = scanner.scan_content("let x = 1;", None, "rust");
2608 let report = scanner.generate_report(&result);
2609 assert!(report.contains("# Security Scan Report"));
2610 }
2611
2612 #[test]
2613 fn test_security_scanner_report_contains_stats() {
2614 let scanner = SecurityScanner::new();
2615 let result = scanner.scan_content("let x = 1;\nlet y = 2;", None, "rust");
2616 let report = scanner.generate_report(&result);
2617 assert!(report.contains("Files scanned: 1"));
2618 assert!(report.contains("Lines scanned: 2"));
2619 assert!(report.contains("Total findings:"));
2620 assert!(report.contains("Risk score:"));
2621 }
2622
2623 #[test]
2624 fn test_security_scanner_report_critical_section() {
2625 let scanner = SecurityScanner::new();
2626 let result = scanner.scan_content("AKIAIOSFODNN7EXAMPLE", None, "rust");
2628 let report = scanner.generate_report(&result);
2629 assert!(report.contains("CRITICAL"));
2630 }
2631
2632 #[test]
2633 fn test_security_scanner_report_high_section() {
2634 let scanner = SecurityScanner::new();
2635 let result = scanner.scan_content("Authorization: Bearer sometoken", None, "rust");
2637 let report = scanner.generate_report(&result);
2638 assert!(report.contains("Summary by Category"));
2640 }
2641
2642 #[test]
2643 fn test_security_scanner_report_summary_by_category() {
2644 let scanner = SecurityScanner::new();
2645 let result = scanner.scan_content("unsafe { }", None, "rust");
2646 let report = scanner.generate_report(&result);
2647 assert!(report.contains("Summary by Category"));
2648 }
2649
2650 #[test]
2651 fn test_security_scanner_report_with_file_location() {
2652 let scanner = SecurityScanner::new();
2653 let path = PathBuf::from("src/secrets.rs");
2654 let result = scanner.scan_content("AKIAIOSFODNN7EXAMPLE", Some(&path), "rust");
2655 let report = scanner.generate_report(&result);
2656 assert!(report.contains("src/secrets.rs"));
2657 }
2658
2659 #[test]
2660 fn test_security_scanner_scan_history_capped() {
2661 let scanner = SecurityScanner::new();
2662 for _ in 0..110 {
2664 scanner.scan_content("let x = 1;", None, "rust");
2665 }
2666 let stats = scanner.get_stats();
2667 assert!(stats.total_scans <= 100);
2668 }
2669
2670 #[test]
2671 fn test_security_scanner_multiline_rust_vuln() {
2672 let scanner = SecurityScanner::new();
2673 let content = "fn safe() {}\nfn danger() { unsafe { *ptr = 0; } }\nfn also_safe() {}";
2674 let result = scanner.scan_content(content, None, "rust");
2675 assert!(result.total_findings() >= 1);
2676 assert_eq!(result.lines_scanned, 3);
2677 }
2678
2679 #[test]
2684 fn test_scan_only_whitespace() {
2685 let mut scanner = SecretScanner::new();
2686 let content = " \n\t\n ";
2687 let findings = scanner.scan_content(content, None);
2688 assert!(findings.is_empty());
2689 }
2690
2691 #[test]
2692 fn test_scan_windows_line_endings() {
2693 let mut scanner = SecretScanner::new();
2694 let content = "let x = 1;\r\nlet y = 2;\r\n";
2696 let findings = scanner.scan_content(content, None);
2697 assert!(findings.is_empty());
2698 }
2699
2700 #[test]
2701 fn test_secret_scanner_short_secret_masking() {
2702 let mut scanner = SecretScanner::new();
2704 let content = "-----BEGIN PRIVATE KEY-----";
2708 let findings = scanner.scan_content(content, None);
2709 if let Some(f) = findings.first() {
2710 assert!(f.snippet.is_some());
2712 }
2713 }
2714
2715 #[test]
2716 fn test_vulnerability_detector_multiple_vulns_same_line() {
2717 let mut detector = VulnerabilityDetector::new();
2718 let content = "unsafe { x.unwrap() }";
2720 let findings = detector.scan_content(content, None, "rust");
2721 assert!(findings.len() >= 2);
2722 }
2723
2724 #[test]
2725 fn test_compliance_checker_multiple_findings_same_line() {
2726 let mut checker = ComplianceChecker::new();
2727 let content = "use_md5_and_sha1()";
2729 let findings = checker.check_content(content, None);
2731 let _ = findings; }
2734
2735 #[test]
2736 fn test_security_finding_ids_are_unique() {
2737 let f1 = SecurityFinding::new("A", SecurityCategory::Injection, SecuritySeverity::Low);
2741 let f2 = SecurityFinding::new("B", SecurityCategory::Injection, SecuritySeverity::Low);
2742 assert!(f1.id.starts_with("SEC-"));
2743 assert!(f2.id.starts_with("SEC-"));
2744 }
2745
2746 #[test]
2747 fn test_scan_result_has_critical_absent_key() {
2748 let result = ScanResult::new();
2749 assert!(!result.has_critical());
2751 }
2752
2753 #[test]
2754 fn test_scan_result_has_high_absent_key() {
2755 let result = ScanResult::new();
2756 assert!(!result.has_high());
2757 }
2758
2759 #[test]
2760 fn test_dependency_auditor_multiple_audits() {
2761 let mut auditor = DependencyAuditor::new();
2762 auditor.audit_dependency("lodash", "4.17.0", "npm");
2763 auditor.audit_dependency("log4j", "2.14.0", "maven");
2764 auditor.audit_dependency("safe-pkg", "1.0", "npm");
2765 assert_eq!(auditor.vulnerable_dependencies().len(), 2);
2766 }
2767
2768 #[test]
2769 fn test_scanner_stats_fields() {
2770 let scanner = SecurityScanner::new();
2771 scanner.scan_content("AKIAIOSFODNN7EXAMPLE", None, "rust");
2773 let stats = scanner.get_stats();
2774 assert_eq!(stats.total_scans, 1);
2775 assert!(stats.total_findings >= 1);
2776 assert!(stats.critical_findings >= 1);
2777 }
2778
2779 #[test]
2780 fn test_vulnerability_pattern_struct_fields() {
2781 let pattern = VulnerabilityPattern {
2782 id: "V1".to_string(),
2783 name: "Test Pattern".to_string(),
2784 language: "rust".to_string(),
2785 pattern: r"test".to_string(),
2786 severity: SecuritySeverity::Low,
2787 cwe: "CWE-1".to_string(),
2788 description: "A test pattern".to_string(),
2789 remediation: "Fix it".to_string(),
2790 };
2791 assert_eq!(pattern.id, "V1");
2792 assert_eq!(pattern.name, "Test Pattern");
2793 assert_eq!(pattern.language, "rust");
2794 assert_eq!(pattern.cwe, "CWE-1");
2795 assert_eq!(pattern.severity, SecuritySeverity::Low);
2796 }
2797
2798 #[test]
2799 fn test_secret_scanner_detects_multiline_secrets() {
2800 let mut scanner = SecretScanner::new();
2801 let content = "line1\nAKIAIOSFODNN7EXAMPLE\nline3";
2802 let findings = scanner.scan_content(content, None);
2803 assert!(!findings.is_empty());
2804 }
2805
2806 #[test]
2807 fn test_compliance_checker_no_match_when_no_pattern() {
2808 let mut checker = ComplianceChecker::new();
2810 checker.add_rule(ComplianceRule::new(
2811 "OWASP-A01",
2812 "OWASP Top 10",
2813 "Broken Access Control",
2814 ));
2815 let findings = checker.check_content("something dangerous", None);
2817 let _ = findings;
2819 }
2820}