Skip to main content

torsh_package/
vulnerability.rs

1//! Package vulnerability scanning and security auditing
2//!
3//! This module provides comprehensive security scanning capabilities for packages,
4//! including:
5//! - Dependency vulnerability detection
6//! - Known CVE checking
7//! - Security policy enforcement
8//! - Package integrity verification
9//! - Malicious code pattern detection
10//!
11//! # Security Scanning Workflow
12//!
13//! 1. **Dependency Analysis**: Check all dependencies against vulnerability databases
14//! 2. **Pattern Detection**: Scan package contents for suspicious patterns
15//! 3. **Policy Enforcement**: Verify package meets security requirements
16//! 4. **Report Generation**: Generate detailed security audit reports
17//!
18//! # Example
19//!
20//! ```rust,no_run
21//! use torsh_package::vulnerability::{VulnerabilityScanner, ScanPolicy};
22//! use torsh_package::Package;
23//!
24//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! let package = Package::load("model.torshpkg")?;
26//!
27//! let scanner = VulnerabilityScanner::new()
28//!     .with_policy(ScanPolicy::strict());
29//!
30//! let report = scanner.scan(&package)?;
31//!
32//! if report.has_critical_issues() {
33//!     eprintln!("Critical security issues found!");
34//!     for issue in report.critical_issues() {
35//!         eprintln!("  - {}: {}", issue.severity, issue.description);
36//!     }
37//! }
38//! # Ok(())
39//! # }
40//! ```
41
42use crate::package::Package;
43use serde::{Deserialize, Serialize};
44use std::collections::HashMap;
45use torsh_core::error::Result;
46
47/// Vulnerability severity levels
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
49pub enum Severity {
50    /// Low severity - informational
51    Low,
52    /// Medium severity - should be addressed
53    Medium,
54    /// High severity - should be fixed soon
55    High,
56    /// Critical severity - immediate action required
57    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/// Security issue types
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub enum IssueType {
74    /// Known CVE (Common Vulnerabilities and Exposures)
75    KnownCVE,
76    /// Dependency vulnerability
77    DependencyVulnerability,
78    /// Suspicious code pattern
79    SuspiciousPattern,
80    /// Missing security feature
81    MissingSecurityFeature,
82    /// Weak cryptography
83    WeakCryptography,
84    /// Insecure configuration
85    InsecureConfiguration,
86    /// Supply chain risk
87    SupplyChainRisk,
88}
89
90/// A single security issue found during scanning
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SecurityIssue {
93    /// Issue type
94    pub issue_type: IssueType,
95    /// Severity level
96    pub severity: Severity,
97    /// Issue title
98    pub title: String,
99    /// Detailed description
100    pub description: String,
101    /// Affected component (dependency, resource, etc.)
102    pub affected_component: Option<String>,
103    /// CVE ID if applicable
104    pub cve_id: Option<String>,
105    /// Recommended fix
106    pub recommendation: Option<String>,
107    /// Additional metadata
108    pub metadata: HashMap<String, String>,
109}
110
111impl SecurityIssue {
112    /// Create a new security issue
113    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    /// Set affected component
132    pub fn with_affected_component(mut self, component: String) -> Self {
133        self.affected_component = Some(component);
134        self
135    }
136
137    /// Set CVE ID
138    pub fn with_cve_id(mut self, cve_id: String) -> Self {
139        self.cve_id = Some(cve_id);
140        self
141    }
142
143    /// Set recommendation
144    pub fn with_recommendation(mut self, recommendation: String) -> Self {
145        self.recommendation = Some(recommendation);
146        self
147    }
148
149    /// Add metadata
150    pub fn with_metadata(mut self, key: String, value: String) -> Self {
151        self.metadata.insert(key, value);
152        self
153    }
154}
155
156/// Vulnerability scan report
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ScanReport {
159    /// Package name
160    pub package_name: String,
161    /// Package version
162    pub package_version: String,
163    /// Scan timestamp
164    pub scan_timestamp: chrono::DateTime<chrono::Utc>,
165    /// All issues found
166    pub issues: Vec<SecurityIssue>,
167    /// Scan policy used
168    pub policy_name: String,
169    /// Scan duration in milliseconds
170    pub scan_duration_ms: u64,
171    /// Scanner version
172    pub scanner_version: String,
173}
174
175impl ScanReport {
176    /// Get critical issues
177    pub fn critical_issues(&self) -> Vec<&SecurityIssue> {
178        self.issues
179            .iter()
180            .filter(|i| i.severity == Severity::Critical)
181            .collect()
182    }
183
184    /// Get high severity issues
185    pub fn high_issues(&self) -> Vec<&SecurityIssue> {
186        self.issues
187            .iter()
188            .filter(|i| i.severity == Severity::High)
189            .collect()
190    }
191
192    /// Get medium severity issues
193    pub fn medium_issues(&self) -> Vec<&SecurityIssue> {
194        self.issues
195            .iter()
196            .filter(|i| i.severity == Severity::Medium)
197            .collect()
198    }
199
200    /// Get low severity issues
201    pub fn low_issues(&self) -> Vec<&SecurityIssue> {
202        self.issues
203            .iter()
204            .filter(|i| i.severity == Severity::Low)
205            .collect()
206    }
207
208    /// Check if there are critical issues
209    pub fn has_critical_issues(&self) -> bool {
210        self.issues.iter().any(|i| i.severity == Severity::Critical)
211    }
212
213    /// Check if there are high severity issues
214    pub fn has_high_issues(&self) -> bool {
215        self.issues.iter().any(|i| i.severity == Severity::High)
216    }
217
218    /// Get total issue count
219    pub fn total_issues(&self) -> usize {
220        self.issues.len()
221    }
222
223    /// Calculate risk score (0-100)
224    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/// Security scan policy
240#[derive(Debug, Clone)]
241pub struct ScanPolicy {
242    /// Policy name
243    pub name: String,
244    /// Check for known CVEs
245    pub check_cves: bool,
246    /// Scan dependencies
247    pub scan_dependencies: bool,
248    /// Detect suspicious patterns
249    pub detect_patterns: bool,
250    /// Check cryptography
251    pub check_cryptography: bool,
252    /// Verify signatures
253    pub verify_signatures: bool,
254    /// Maximum allowed risk score
255    pub max_risk_score: u32,
256    /// Fail on critical issues
257    pub fail_on_critical: bool,
258}
259
260impl Default for ScanPolicy {
261    fn default() -> Self {
262        Self::standard()
263    }
264}
265
266impl ScanPolicy {
267    /// Create a lenient policy (minimal checks)
268    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    /// Create a standard policy (balanced security)
282    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    /// Create a strict policy (maximum security)
296    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
310/// Vulnerability scanner
311pub struct VulnerabilityScanner {
312    policy: ScanPolicy,
313    cve_database: HashMap<String, Vec<String>>, // package -> CVEs
314}
315
316impl VulnerabilityScanner {
317    /// Create a new vulnerability scanner with default policy
318    pub fn new() -> Self {
319        Self {
320            policy: ScanPolicy::default(),
321            cve_database: Self::load_cve_database(),
322        }
323    }
324
325    /// Set scan policy
326    pub fn with_policy(mut self, policy: ScanPolicy) -> Self {
327        self.policy = policy;
328        self
329    }
330
331    /// Scan a package for vulnerabilities
332    pub fn scan(&self, package: &Package) -> Result<ScanReport> {
333        let start_time = std::time::Instant::now();
334        let mut issues = Vec::new();
335
336        // Check package signature if policy requires
337        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        // Scan dependencies for vulnerabilities
352        if self.policy.scan_dependencies {
353            issues.extend(self.scan_dependencies(package));
354        }
355
356        // Check for known CVEs
357        if self.policy.check_cves {
358            issues.extend(self.check_cves(package));
359        }
360
361        // Detect suspicious patterns
362        if self.policy.detect_patterns {
363            issues.extend(self.detect_patterns(package));
364        }
365
366        // Check cryptography
367        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    /// Scan dependencies for known vulnerabilities
385    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            // Check against known vulnerable versions
390            // In production, this would query a real vulnerability database
391            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    /// Check for known CVEs
415    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    /// Detect suspicious code patterns
440    fn detect_patterns(&self, package: &Package) -> Vec<SecurityIssue> {
441        let mut issues = Vec::new();
442
443        // Patterns that might indicate malicious code
444        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    /// Check cryptography usage
480    fn check_cryptography(&self, _package: &Package) -> Vec<SecurityIssue> {
481        let issues = Vec::new();
482
483        // Check for weak cryptographic algorithms
484        // This is a simplified check - production systems would be more sophisticated
485
486        // Example: Check if package uses weak hashing
487        // In a real implementation, this would scan the actual code
488
489        issues
490    }
491
492    /// Load CVE database (in production, this would load from a real database)
493    fn load_cve_database() -> HashMap<String, Vec<String>> {
494        let mut db = HashMap::new();
495
496        // Mock CVE data for demonstration
497        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        // No issues = 0 score
554        assert_eq!(report.risk_score(), 0);
555
556        // Add critical issue
557        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        // Add high issue
566        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        // Add resource with suspicious pattern
642        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}