1use crate::package::Package;
43use serde::{Deserialize, Serialize};
44use std::collections::HashMap;
45use torsh_core::error::Result;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
49pub enum Severity {
50 Low,
52 Medium,
54 High,
56 Critical,
58}
59
60impl std::fmt::Display for Severity {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Severity::Low => write!(f, "LOW"),
64 Severity::Medium => write!(f, "MEDIUM"),
65 Severity::High => write!(f, "HIGH"),
66 Severity::Critical => write!(f, "CRITICAL"),
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub enum IssueType {
74 KnownCVE,
76 DependencyVulnerability,
78 SuspiciousPattern,
80 MissingSecurityFeature,
82 WeakCryptography,
84 InsecureConfiguration,
86 SupplyChainRisk,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SecurityIssue {
93 pub issue_type: IssueType,
95 pub severity: Severity,
97 pub title: String,
99 pub description: String,
101 pub affected_component: Option<String>,
103 pub cve_id: Option<String>,
105 pub recommendation: Option<String>,
107 pub metadata: HashMap<String, String>,
109}
110
111impl SecurityIssue {
112 pub fn new(
114 issue_type: IssueType,
115 severity: Severity,
116 title: String,
117 description: String,
118 ) -> Self {
119 Self {
120 issue_type,
121 severity,
122 title,
123 description,
124 affected_component: None,
125 cve_id: None,
126 recommendation: None,
127 metadata: HashMap::new(),
128 }
129 }
130
131 pub fn with_affected_component(mut self, component: String) -> Self {
133 self.affected_component = Some(component);
134 self
135 }
136
137 pub fn with_cve_id(mut self, cve_id: String) -> Self {
139 self.cve_id = Some(cve_id);
140 self
141 }
142
143 pub fn with_recommendation(mut self, recommendation: String) -> Self {
145 self.recommendation = Some(recommendation);
146 self
147 }
148
149 pub fn with_metadata(mut self, key: String, value: String) -> Self {
151 self.metadata.insert(key, value);
152 self
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ScanReport {
159 pub package_name: String,
161 pub package_version: String,
163 pub scan_timestamp: chrono::DateTime<chrono::Utc>,
165 pub issues: Vec<SecurityIssue>,
167 pub policy_name: String,
169 pub scan_duration_ms: u64,
171 pub scanner_version: String,
173}
174
175impl ScanReport {
176 pub fn critical_issues(&self) -> Vec<&SecurityIssue> {
178 self.issues
179 .iter()
180 .filter(|i| i.severity == Severity::Critical)
181 .collect()
182 }
183
184 pub fn high_issues(&self) -> Vec<&SecurityIssue> {
186 self.issues
187 .iter()
188 .filter(|i| i.severity == Severity::High)
189 .collect()
190 }
191
192 pub fn medium_issues(&self) -> Vec<&SecurityIssue> {
194 self.issues
195 .iter()
196 .filter(|i| i.severity == Severity::Medium)
197 .collect()
198 }
199
200 pub fn low_issues(&self) -> Vec<&SecurityIssue> {
202 self.issues
203 .iter()
204 .filter(|i| i.severity == Severity::Low)
205 .collect()
206 }
207
208 pub fn has_critical_issues(&self) -> bool {
210 self.issues.iter().any(|i| i.severity == Severity::Critical)
211 }
212
213 pub fn has_high_issues(&self) -> bool {
215 self.issues.iter().any(|i| i.severity == Severity::High)
216 }
217
218 pub fn total_issues(&self) -> usize {
220 self.issues.len()
221 }
222
223 pub fn risk_score(&self) -> u32 {
225 let critical_weight = 25;
226 let high_weight = 10;
227 let medium_weight = 3;
228 let low_weight = 1;
229
230 let score = self.critical_issues().len() * critical_weight
231 + self.high_issues().len() * high_weight
232 + self.medium_issues().len() * medium_weight
233 + self.low_issues().len() * low_weight;
234
235 std::cmp::min(score as u32, 100)
236 }
237}
238
239#[derive(Debug, Clone)]
241pub struct ScanPolicy {
242 pub name: String,
244 pub check_cves: bool,
246 pub scan_dependencies: bool,
248 pub detect_patterns: bool,
250 pub check_cryptography: bool,
252 pub verify_signatures: bool,
254 pub max_risk_score: u32,
256 pub fail_on_critical: bool,
258}
259
260impl Default for ScanPolicy {
261 fn default() -> Self {
262 Self::standard()
263 }
264}
265
266impl ScanPolicy {
267 pub fn lenient() -> Self {
269 Self {
270 name: "lenient".to_string(),
271 check_cves: false,
272 scan_dependencies: true,
273 detect_patterns: false,
274 check_cryptography: false,
275 verify_signatures: false,
276 max_risk_score: 75,
277 fail_on_critical: false,
278 }
279 }
280
281 pub fn standard() -> Self {
283 Self {
284 name: "standard".to_string(),
285 check_cves: true,
286 scan_dependencies: true,
287 detect_patterns: true,
288 check_cryptography: true,
289 verify_signatures: true,
290 max_risk_score: 50,
291 fail_on_critical: false,
292 }
293 }
294
295 pub fn strict() -> Self {
297 Self {
298 name: "strict".to_string(),
299 check_cves: true,
300 scan_dependencies: true,
301 detect_patterns: true,
302 check_cryptography: true,
303 verify_signatures: true,
304 max_risk_score: 20,
305 fail_on_critical: true,
306 }
307 }
308}
309
310pub struct VulnerabilityScanner {
312 policy: ScanPolicy,
313 cve_database: HashMap<String, Vec<String>>, }
315
316impl VulnerabilityScanner {
317 pub fn new() -> Self {
319 Self {
320 policy: ScanPolicy::default(),
321 cve_database: Self::load_cve_database(),
322 }
323 }
324
325 pub fn with_policy(mut self, policy: ScanPolicy) -> Self {
327 self.policy = policy;
328 self
329 }
330
331 pub fn scan(&self, package: &Package) -> Result<ScanReport> {
333 let start_time = std::time::Instant::now();
334 let mut issues = Vec::new();
335
336 if self.policy.verify_signatures {
338 if let Err(e) = package.verify() {
339 issues.push(
340 SecurityIssue::new(
341 IssueType::MissingSecurityFeature,
342 Severity::High,
343 "Package signature verification failed".to_string(),
344 format!("Package signature could not be verified: {}", e),
345 )
346 .with_recommendation("Sign the package with a trusted key".to_string()),
347 );
348 }
349 }
350
351 if self.policy.scan_dependencies {
353 issues.extend(self.scan_dependencies(package));
354 }
355
356 if self.policy.check_cves {
358 issues.extend(self.check_cves(package));
359 }
360
361 if self.policy.detect_patterns {
363 issues.extend(self.detect_patterns(package));
364 }
365
366 if self.policy.check_cryptography {
368 issues.extend(self.check_cryptography(package));
369 }
370
371 let scan_duration_ms = start_time.elapsed().as_millis() as u64;
372
373 Ok(ScanReport {
374 package_name: package.name().to_string(),
375 package_version: package.get_version().to_string(),
376 scan_timestamp: chrono::Utc::now(),
377 issues,
378 policy_name: self.policy.name.clone(),
379 scan_duration_ms,
380 scanner_version: env!("CARGO_PKG_VERSION").to_string(),
381 })
382 }
383
384 fn scan_dependencies(&self, package: &Package) -> Vec<SecurityIssue> {
386 let mut issues = Vec::new();
387
388 for (dep_name, dep_version) in &package.metadata().dependencies {
389 if dep_name.contains("vulnerable") {
392 issues.push(
393 SecurityIssue::new(
394 IssueType::DependencyVulnerability,
395 Severity::High,
396 format!("Vulnerable dependency: {}", dep_name),
397 format!(
398 "Dependency {} version {} has known vulnerabilities",
399 dep_name, dep_version
400 ),
401 )
402 .with_affected_component(dep_name.clone())
403 .with_recommendation(format!(
404 "Update {} to the latest secure version",
405 dep_name
406 )),
407 );
408 }
409 }
410
411 issues
412 }
413
414 fn check_cves(&self, package: &Package) -> Vec<SecurityIssue> {
416 let mut issues = Vec::new();
417
418 for (dep_name, _dep_version) in &package.metadata().dependencies {
419 if let Some(cves) = self.cve_database.get(dep_name) {
420 for cve in cves {
421 issues.push(
422 SecurityIssue::new(
423 IssueType::KnownCVE,
424 Severity::Critical,
425 format!("Known CVE in dependency: {}", dep_name),
426 format!("Dependency {} is affected by {}", dep_name, cve),
427 )
428 .with_affected_component(dep_name.clone())
429 .with_cve_id(cve.clone())
430 .with_recommendation("Update to a patched version".to_string()),
431 );
432 }
433 }
434 }
435
436 issues
437 }
438
439 fn detect_patterns(&self, package: &Package) -> Vec<SecurityIssue> {
441 let mut issues = Vec::new();
442
443 let suspicious_patterns = vec![
445 ("eval(", "Dynamic code evaluation"),
446 ("exec(", "Command execution"),
447 ("__import__", "Dynamic imports"),
448 ("os.system", "System command execution"),
449 ("subprocess", "Subprocess execution"),
450 ("/etc/passwd", "System file access"),
451 ("rm -rf", "Destructive command"),
452 ];
453
454 for (_, resource) in package.resources() {
455 let content = String::from_utf8_lossy(&resource.data);
456
457 for (pattern, description) in &suspicious_patterns {
458 if content.contains(pattern) {
459 issues.push(
460 SecurityIssue::new(
461 IssueType::SuspiciousPattern,
462 Severity::Medium,
463 format!("Suspicious pattern detected: {}", pattern),
464 format!(
465 "Resource contains potentially dangerous pattern: {}",
466 description
467 ),
468 )
469 .with_affected_component(resource.name.clone())
470 .with_recommendation("Review the code and ensure it's safe".to_string()),
471 );
472 }
473 }
474 }
475
476 issues
477 }
478
479 fn check_cryptography(&self, _package: &Package) -> Vec<SecurityIssue> {
481 let issues = Vec::new();
482
483 issues
490 }
491
492 fn load_cve_database() -> HashMap<String, Vec<String>> {
494 let mut db = HashMap::new();
495
496 db.insert(
498 "example-vulnerable-lib".to_string(),
499 vec!["CVE-2024-1234".to_string(), "CVE-2024-5678".to_string()],
500 );
501
502 db
503 }
504}
505
506impl Default for VulnerabilityScanner {
507 fn default() -> Self {
508 Self::new()
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use crate::resources::{Resource, ResourceType};
516
517 #[test]
518 fn test_severity_ordering() {
519 assert!(Severity::Critical > Severity::High);
520 assert!(Severity::High > Severity::Medium);
521 assert!(Severity::Medium > Severity::Low);
522 }
523
524 #[test]
525 fn test_security_issue_creation() {
526 let issue = SecurityIssue::new(
527 IssueType::KnownCVE,
528 Severity::High,
529 "Test Issue".to_string(),
530 "Test Description".to_string(),
531 )
532 .with_cve_id("CVE-2024-1234".to_string())
533 .with_affected_component("test-component".to_string())
534 .with_recommendation("Fix it".to_string());
535
536 assert_eq!(issue.severity, Severity::High);
537 assert_eq!(issue.cve_id, Some("CVE-2024-1234".to_string()));
538 assert!(issue.recommendation.is_some());
539 }
540
541 #[test]
542 fn test_scan_report_risk_score() {
543 let mut report = ScanReport {
544 package_name: "test".to_string(),
545 package_version: "1.0.0".to_string(),
546 scan_timestamp: chrono::Utc::now(),
547 issues: vec![],
548 policy_name: "test".to_string(),
549 scan_duration_ms: 100,
550 scanner_version: "1.0.0".to_string(),
551 };
552
553 assert_eq!(report.risk_score(), 0);
555
556 report.issues.push(SecurityIssue::new(
558 IssueType::KnownCVE,
559 Severity::Critical,
560 "Critical".to_string(),
561 "Test".to_string(),
562 ));
563 assert_eq!(report.risk_score(), 25);
564
565 report.issues.push(SecurityIssue::new(
567 IssueType::DependencyVulnerability,
568 Severity::High,
569 "High".to_string(),
570 "Test".to_string(),
571 ));
572 assert_eq!(report.risk_score(), 35);
573 }
574
575 #[test]
576 fn test_scan_report_filtering() {
577 let report = ScanReport {
578 package_name: "test".to_string(),
579 package_version: "1.0.0".to_string(),
580 scan_timestamp: chrono::Utc::now(),
581 issues: vec![
582 SecurityIssue::new(
583 IssueType::KnownCVE,
584 Severity::Critical,
585 "Critical".to_string(),
586 "Test".to_string(),
587 ),
588 SecurityIssue::new(
589 IssueType::DependencyVulnerability,
590 Severity::High,
591 "High".to_string(),
592 "Test".to_string(),
593 ),
594 SecurityIssue::new(
595 IssueType::SuspiciousPattern,
596 Severity::Medium,
597 "Medium".to_string(),
598 "Test".to_string(),
599 ),
600 ],
601 policy_name: "test".to_string(),
602 scan_duration_ms: 100,
603 scanner_version: "1.0.0".to_string(),
604 };
605
606 assert_eq!(report.critical_issues().len(), 1);
607 assert_eq!(report.high_issues().len(), 1);
608 assert_eq!(report.medium_issues().len(), 1);
609 assert_eq!(report.total_issues(), 3);
610 assert!(report.has_critical_issues());
611 assert!(report.has_high_issues());
612 }
613
614 #[test]
615 fn test_scan_policy_presets() {
616 let lenient = ScanPolicy::lenient();
617 let standard = ScanPolicy::standard();
618 let strict = ScanPolicy::strict();
619
620 assert!(!lenient.fail_on_critical);
621 assert!(standard.scan_dependencies);
622 assert!(strict.fail_on_critical);
623 assert!(strict.max_risk_score < standard.max_risk_score);
624 }
625
626 #[test]
627 fn test_vulnerability_scanner_basic() {
628 let scanner = VulnerabilityScanner::new();
629 let package = Package::new("test-package".to_string(), "1.0.0".to_string());
630
631 let report = scanner.scan(&package).unwrap();
632 assert_eq!(report.package_name, "test-package");
633 assert_eq!(report.package_version, "1.0.0");
634 }
635
636 #[test]
637 fn test_pattern_detection() {
638 let scanner = VulnerabilityScanner::new();
639 let mut package = Package::new("test-package".to_string(), "1.0.0".to_string());
640
641 let suspicious_code = b"import os\nos.system('rm -rf /')";
643 let resource = Resource::new(
644 "suspicious.py".to_string(),
645 ResourceType::Source,
646 suspicious_code.to_vec(),
647 );
648 package.add_resource(resource);
649
650 let report = scanner.scan(&package).unwrap();
651 assert!(report.total_issues() > 0);
652 assert!(report
653 .issues
654 .iter()
655 .any(|i| i.issue_type == IssueType::SuspiciousPattern));
656 }
657
658 #[test]
659 fn test_scanner_with_policy() {
660 let scanner = VulnerabilityScanner::new().with_policy(ScanPolicy::strict());
661 let package = Package::new("test".to_string(), "1.0.0".to_string());
662
663 let report = scanner.scan(&package).unwrap();
664 assert_eq!(report.policy_name, "strict");
665 }
666}