use crate::package::Package;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use torsh_core::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Low => write!(f, "LOW"),
Severity::Medium => write!(f, "MEDIUM"),
Severity::High => write!(f, "HIGH"),
Severity::Critical => write!(f, "CRITICAL"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum IssueType {
KnownCVE,
DependencyVulnerability,
SuspiciousPattern,
MissingSecurityFeature,
WeakCryptography,
InsecureConfiguration,
SupplyChainRisk,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityIssue {
pub issue_type: IssueType,
pub severity: Severity,
pub title: String,
pub description: String,
pub affected_component: Option<String>,
pub cve_id: Option<String>,
pub recommendation: Option<String>,
pub metadata: HashMap<String, String>,
}
impl SecurityIssue {
pub fn new(
issue_type: IssueType,
severity: Severity,
title: String,
description: String,
) -> Self {
Self {
issue_type,
severity,
title,
description,
affected_component: None,
cve_id: None,
recommendation: None,
metadata: HashMap::new(),
}
}
pub fn with_affected_component(mut self, component: String) -> Self {
self.affected_component = Some(component);
self
}
pub fn with_cve_id(mut self, cve_id: String) -> Self {
self.cve_id = Some(cve_id);
self
}
pub fn with_recommendation(mut self, recommendation: String) -> Self {
self.recommendation = Some(recommendation);
self
}
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanReport {
pub package_name: String,
pub package_version: String,
pub scan_timestamp: chrono::DateTime<chrono::Utc>,
pub issues: Vec<SecurityIssue>,
pub policy_name: String,
pub scan_duration_ms: u64,
pub scanner_version: String,
}
impl ScanReport {
pub fn critical_issues(&self) -> Vec<&SecurityIssue> {
self.issues
.iter()
.filter(|i| i.severity == Severity::Critical)
.collect()
}
pub fn high_issues(&self) -> Vec<&SecurityIssue> {
self.issues
.iter()
.filter(|i| i.severity == Severity::High)
.collect()
}
pub fn medium_issues(&self) -> Vec<&SecurityIssue> {
self.issues
.iter()
.filter(|i| i.severity == Severity::Medium)
.collect()
}
pub fn low_issues(&self) -> Vec<&SecurityIssue> {
self.issues
.iter()
.filter(|i| i.severity == Severity::Low)
.collect()
}
pub fn has_critical_issues(&self) -> bool {
self.issues.iter().any(|i| i.severity == Severity::Critical)
}
pub fn has_high_issues(&self) -> bool {
self.issues.iter().any(|i| i.severity == Severity::High)
}
pub fn total_issues(&self) -> usize {
self.issues.len()
}
pub fn risk_score(&self) -> u32 {
let critical_weight = 25;
let high_weight = 10;
let medium_weight = 3;
let low_weight = 1;
let score = self.critical_issues().len() * critical_weight
+ self.high_issues().len() * high_weight
+ self.medium_issues().len() * medium_weight
+ self.low_issues().len() * low_weight;
std::cmp::min(score as u32, 100)
}
}
#[derive(Debug, Clone)]
pub struct ScanPolicy {
pub name: String,
pub check_cves: bool,
pub scan_dependencies: bool,
pub detect_patterns: bool,
pub check_cryptography: bool,
pub verify_signatures: bool,
pub max_risk_score: u32,
pub fail_on_critical: bool,
}
impl Default for ScanPolicy {
fn default() -> Self {
Self::standard()
}
}
impl ScanPolicy {
pub fn lenient() -> Self {
Self {
name: "lenient".to_string(),
check_cves: false,
scan_dependencies: true,
detect_patterns: false,
check_cryptography: false,
verify_signatures: false,
max_risk_score: 75,
fail_on_critical: false,
}
}
pub fn standard() -> Self {
Self {
name: "standard".to_string(),
check_cves: true,
scan_dependencies: true,
detect_patterns: true,
check_cryptography: true,
verify_signatures: true,
max_risk_score: 50,
fail_on_critical: false,
}
}
pub fn strict() -> Self {
Self {
name: "strict".to_string(),
check_cves: true,
scan_dependencies: true,
detect_patterns: true,
check_cryptography: true,
verify_signatures: true,
max_risk_score: 20,
fail_on_critical: true,
}
}
}
pub struct VulnerabilityScanner {
policy: ScanPolicy,
cve_database: HashMap<String, Vec<String>>, }
impl VulnerabilityScanner {
pub fn new() -> Self {
Self {
policy: ScanPolicy::default(),
cve_database: Self::load_cve_database(),
}
}
pub fn with_policy(mut self, policy: ScanPolicy) -> Self {
self.policy = policy;
self
}
pub fn scan(&self, package: &Package) -> Result<ScanReport> {
let start_time = std::time::Instant::now();
let mut issues = Vec::new();
if self.policy.verify_signatures {
if let Err(e) = package.verify() {
issues.push(
SecurityIssue::new(
IssueType::MissingSecurityFeature,
Severity::High,
"Package signature verification failed".to_string(),
format!("Package signature could not be verified: {}", e),
)
.with_recommendation("Sign the package with a trusted key".to_string()),
);
}
}
if self.policy.scan_dependencies {
issues.extend(self.scan_dependencies(package));
}
if self.policy.check_cves {
issues.extend(self.check_cves(package));
}
if self.policy.detect_patterns {
issues.extend(self.detect_patterns(package));
}
if self.policy.check_cryptography {
issues.extend(self.check_cryptography(package));
}
let scan_duration_ms = start_time.elapsed().as_millis() as u64;
Ok(ScanReport {
package_name: package.name().to_string(),
package_version: package.get_version().to_string(),
scan_timestamp: chrono::Utc::now(),
issues,
policy_name: self.policy.name.clone(),
scan_duration_ms,
scanner_version: env!("CARGO_PKG_VERSION").to_string(),
})
}
fn scan_dependencies(&self, package: &Package) -> Vec<SecurityIssue> {
let mut issues = Vec::new();
for (dep_name, dep_version) in &package.metadata().dependencies {
if dep_name.contains("vulnerable") {
issues.push(
SecurityIssue::new(
IssueType::DependencyVulnerability,
Severity::High,
format!("Vulnerable dependency: {}", dep_name),
format!(
"Dependency {} version {} has known vulnerabilities",
dep_name, dep_version
),
)
.with_affected_component(dep_name.clone())
.with_recommendation(format!(
"Update {} to the latest secure version",
dep_name
)),
);
}
}
issues
}
fn check_cves(&self, package: &Package) -> Vec<SecurityIssue> {
let mut issues = Vec::new();
for (dep_name, _dep_version) in &package.metadata().dependencies {
if let Some(cves) = self.cve_database.get(dep_name) {
for cve in cves {
issues.push(
SecurityIssue::new(
IssueType::KnownCVE,
Severity::Critical,
format!("Known CVE in dependency: {}", dep_name),
format!("Dependency {} is affected by {}", dep_name, cve),
)
.with_affected_component(dep_name.clone())
.with_cve_id(cve.clone())
.with_recommendation("Update to a patched version".to_string()),
);
}
}
}
issues
}
fn detect_patterns(&self, package: &Package) -> Vec<SecurityIssue> {
let mut issues = Vec::new();
let suspicious_patterns = vec![
("eval(", "Dynamic code evaluation"),
("exec(", "Command execution"),
("__import__", "Dynamic imports"),
("os.system", "System command execution"),
("subprocess", "Subprocess execution"),
("/etc/passwd", "System file access"),
("rm -rf", "Destructive command"),
];
for (_, resource) in package.resources() {
let content = String::from_utf8_lossy(&resource.data);
for (pattern, description) in &suspicious_patterns {
if content.contains(pattern) {
issues.push(
SecurityIssue::new(
IssueType::SuspiciousPattern,
Severity::Medium,
format!("Suspicious pattern detected: {}", pattern),
format!(
"Resource contains potentially dangerous pattern: {}",
description
),
)
.with_affected_component(resource.name.clone())
.with_recommendation("Review the code and ensure it's safe".to_string()),
);
}
}
}
issues
}
fn check_cryptography(&self, _package: &Package) -> Vec<SecurityIssue> {
let issues = Vec::new();
issues
}
fn load_cve_database() -> HashMap<String, Vec<String>> {
let mut db = HashMap::new();
db.insert(
"example-vulnerable-lib".to_string(),
vec!["CVE-2024-1234".to_string(), "CVE-2024-5678".to_string()],
);
db
}
}
impl Default for VulnerabilityScanner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resources::{Resource, ResourceType};
#[test]
fn test_severity_ordering() {
assert!(Severity::Critical > Severity::High);
assert!(Severity::High > Severity::Medium);
assert!(Severity::Medium > Severity::Low);
}
#[test]
fn test_security_issue_creation() {
let issue = SecurityIssue::new(
IssueType::KnownCVE,
Severity::High,
"Test Issue".to_string(),
"Test Description".to_string(),
)
.with_cve_id("CVE-2024-1234".to_string())
.with_affected_component("test-component".to_string())
.with_recommendation("Fix it".to_string());
assert_eq!(issue.severity, Severity::High);
assert_eq!(issue.cve_id, Some("CVE-2024-1234".to_string()));
assert!(issue.recommendation.is_some());
}
#[test]
fn test_scan_report_risk_score() {
let mut report = ScanReport {
package_name: "test".to_string(),
package_version: "1.0.0".to_string(),
scan_timestamp: chrono::Utc::now(),
issues: vec![],
policy_name: "test".to_string(),
scan_duration_ms: 100,
scanner_version: "1.0.0".to_string(),
};
assert_eq!(report.risk_score(), 0);
report.issues.push(SecurityIssue::new(
IssueType::KnownCVE,
Severity::Critical,
"Critical".to_string(),
"Test".to_string(),
));
assert_eq!(report.risk_score(), 25);
report.issues.push(SecurityIssue::new(
IssueType::DependencyVulnerability,
Severity::High,
"High".to_string(),
"Test".to_string(),
));
assert_eq!(report.risk_score(), 35);
}
#[test]
fn test_scan_report_filtering() {
let report = ScanReport {
package_name: "test".to_string(),
package_version: "1.0.0".to_string(),
scan_timestamp: chrono::Utc::now(),
issues: vec![
SecurityIssue::new(
IssueType::KnownCVE,
Severity::Critical,
"Critical".to_string(),
"Test".to_string(),
),
SecurityIssue::new(
IssueType::DependencyVulnerability,
Severity::High,
"High".to_string(),
"Test".to_string(),
),
SecurityIssue::new(
IssueType::SuspiciousPattern,
Severity::Medium,
"Medium".to_string(),
"Test".to_string(),
),
],
policy_name: "test".to_string(),
scan_duration_ms: 100,
scanner_version: "1.0.0".to_string(),
};
assert_eq!(report.critical_issues().len(), 1);
assert_eq!(report.high_issues().len(), 1);
assert_eq!(report.medium_issues().len(), 1);
assert_eq!(report.total_issues(), 3);
assert!(report.has_critical_issues());
assert!(report.has_high_issues());
}
#[test]
fn test_scan_policy_presets() {
let lenient = ScanPolicy::lenient();
let standard = ScanPolicy::standard();
let strict = ScanPolicy::strict();
assert!(!lenient.fail_on_critical);
assert!(standard.scan_dependencies);
assert!(strict.fail_on_critical);
assert!(strict.max_risk_score < standard.max_risk_score);
}
#[test]
fn test_vulnerability_scanner_basic() {
let scanner = VulnerabilityScanner::new();
let package = Package::new("test-package".to_string(), "1.0.0".to_string());
let report = scanner.scan(&package).unwrap();
assert_eq!(report.package_name, "test-package");
assert_eq!(report.package_version, "1.0.0");
}
#[test]
fn test_pattern_detection() {
let scanner = VulnerabilityScanner::new();
let mut package = Package::new("test-package".to_string(), "1.0.0".to_string());
let suspicious_code = b"import os\nos.system('rm -rf /')";
let resource = Resource::new(
"suspicious.py".to_string(),
ResourceType::Source,
suspicious_code.to_vec(),
);
package.add_resource(resource);
let report = scanner.scan(&package).unwrap();
assert!(report.total_issues() > 0);
assert!(report
.issues
.iter()
.any(|i| i.issue_type == IssueType::SuspiciousPattern));
}
#[test]
fn test_scanner_with_policy() {
let scanner = VulnerabilityScanner::new().with_policy(ScanPolicy::strict());
let package = Package::new("test".to_string(), "1.0.0".to_string());
let report = scanner.scan(&package).unwrap();
assert_eq!(report.policy_name, "strict");
}
}