use super::super::vulnerability::{PackageVulnerabilities, Severity, Vulnerability};
use super::cve_filter::CveFilter;
use crate::config::IgnoreCve;
#[derive(Debug, Clone, PartialEq)]
pub enum ThresholdConfig {
None,
Severity(Severity),
Cvss(f32),
}
impl ThresholdConfig {
pub fn is_above_threshold(&self, vuln: &Vulnerability) -> bool {
match self {
ThresholdConfig::None => true,
ThresholdConfig::Severity(min_severity) => vuln.severity() >= *min_severity,
ThresholdConfig::Cvss(min_cvss) => {
match vuln.cvss_score() {
Some(score) => score.value() >= *min_cvss,
None => false,
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct VulnerabilityCheckResult {
pub above_threshold: Vec<PackageVulnerabilities>,
pub below_threshold: Vec<PackageVulnerabilities>,
pub threshold_exceeded: bool,
}
impl VulnerabilityCheckResult {
pub fn actionable_count(&self) -> usize {
self.above_threshold
.iter()
.map(|pv| pv.vulnerabilities().len())
.sum()
}
pub fn informational_count(&self) -> usize {
self.below_threshold
.iter()
.map(|pv| pv.vulnerabilities().len())
.sum()
}
}
pub struct VulnerabilityChecker;
impl VulnerabilityChecker {
pub fn check(
vulnerabilities: Vec<PackageVulnerabilities>,
threshold: ThresholdConfig,
ignore_cves: &[IgnoreCve],
) -> VulnerabilityCheckResult {
let filtered = CveFilter::apply(vulnerabilities, ignore_cves);
let mut above_threshold = Vec::new();
let mut below_threshold = Vec::new();
for pkg_vulns in filtered {
let mut above = Vec::new();
let mut below = Vec::new();
for vuln in pkg_vulns.vulnerabilities() {
if threshold.is_above_threshold(vuln) {
above.push(vuln.clone());
} else {
below.push(vuln.clone());
}
}
if !above.is_empty() {
above_threshold.push(PackageVulnerabilities::new(
pkg_vulns.package_name().to_string(),
pkg_vulns.current_version().to_string(),
above,
));
}
if !below.is_empty() {
below_threshold.push(PackageVulnerabilities::new(
pkg_vulns.package_name().to_string(),
pkg_vulns.current_version().to_string(),
below,
));
}
}
let threshold_exceeded = !above_threshold.is_empty();
VulnerabilityCheckResult {
above_threshold,
below_threshold,
threshold_exceeded,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sbom_generation::domain::vulnerability::CvssScore;
fn create_vulnerability(id: &str, cvss: Option<f32>, severity: Severity) -> Vulnerability {
let cvss_score = cvss.map(|s| CvssScore::new(s).unwrap());
Vulnerability::new(id.to_string(), cvss_score, severity, None, None).unwrap()
}
fn create_package_vulnerabilities(
name: &str,
vulnerabilities: Vec<Vulnerability>,
) -> PackageVulnerabilities {
PackageVulnerabilities::new(name.to_string(), "1.0.0".to_string(), vulnerabilities)
}
#[test]
fn test_threshold_none_all_above() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(3.0), Severity::Low);
let vuln2 = create_vulnerability("CVE-2024-002", Some(9.8), Severity::Critical);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln1, vuln2]);
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::None, &[]);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 2);
assert!(result.below_threshold.is_empty());
}
#[test]
fn test_threshold_severity_high() {
let vuln_low = create_vulnerability("CVE-2024-001", Some(3.0), Severity::Low);
let vuln_medium = create_vulnerability("CVE-2024-002", Some(5.0), Severity::Medium);
let vuln_high = create_vulnerability("CVE-2024-003", Some(7.5), Severity::High);
let vuln_critical = create_vulnerability("CVE-2024-004", Some(9.8), Severity::Critical);
let pkg = create_package_vulnerabilities(
"test-pkg",
vec![vuln_low, vuln_medium, vuln_high, vuln_critical],
);
let result =
VulnerabilityChecker::check(vec![pkg], ThresholdConfig::Severity(Severity::High), &[]);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 2); assert_eq!(result.below_threshold.len(), 1);
assert_eq!(result.below_threshold[0].vulnerabilities().len(), 2); }
#[test]
fn test_threshold_severity_critical_only() {
let vuln_high = create_vulnerability("CVE-2024-001", Some(8.0), Severity::High);
let vuln_critical = create_vulnerability("CVE-2024-002", Some(9.8), Severity::Critical);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln_high, vuln_critical]);
let result = VulnerabilityChecker::check(
vec![pkg],
ThresholdConfig::Severity(Severity::Critical),
&[],
);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 1); assert_eq!(result.below_threshold.len(), 1);
assert_eq!(result.below_threshold[0].vulnerabilities().len(), 1); }
#[test]
fn test_threshold_cvss() {
let vuln_low = create_vulnerability("CVE-2024-001", Some(3.0), Severity::Low);
let vuln_high = create_vulnerability("CVE-2024-002", Some(7.5), Severity::High);
let vuln_critical = create_vulnerability("CVE-2024-003", Some(9.8), Severity::Critical);
let pkg =
create_package_vulnerabilities("test-pkg", vec![vuln_low, vuln_high, vuln_critical]);
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::Cvss(7.0), &[]);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 2); assert_eq!(result.below_threshold.len(), 1);
assert_eq!(result.below_threshold[0].vulnerabilities().len(), 1); }
#[test]
fn test_threshold_cvss_na_excluded() {
let vuln_with_cvss = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln_without_cvss = create_vulnerability("CVE-2024-002", None, Severity::High);
let pkg =
create_package_vulnerabilities("test-pkg", vec![vuln_with_cvss, vuln_without_cvss]);
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::Cvss(7.0), &[]);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 1); assert_eq!(result.below_threshold.len(), 1);
assert_eq!(result.below_threshold[0].vulnerabilities().len(), 1); }
#[test]
fn test_no_vulnerabilities_above_threshold() {
let vuln_low = create_vulnerability("CVE-2024-001", Some(3.0), Severity::Low);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln_low]);
let result =
VulnerabilityChecker::check(vec![pkg], ThresholdConfig::Severity(Severity::High), &[]);
assert!(!result.threshold_exceeded);
assert!(result.above_threshold.is_empty());
assert_eq!(result.below_threshold.len(), 1);
}
#[test]
fn test_empty_input() {
let result = VulnerabilityChecker::check(vec![], ThresholdConfig::None, &[]);
assert!(!result.threshold_exceeded);
assert!(result.above_threshold.is_empty());
assert!(result.below_threshold.is_empty());
}
#[test]
fn test_multiple_packages() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln2 = create_vulnerability("CVE-2024-002", Some(3.0), Severity::Low);
let pkg1 = create_package_vulnerabilities("pkg-1", vec![vuln1]);
let pkg2 = create_package_vulnerabilities("pkg-2", vec![vuln2]);
let result = VulnerabilityChecker::check(
vec![pkg1, pkg2],
ThresholdConfig::Severity(Severity::High),
&[],
);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].package_name(), "pkg-1");
assert_eq!(result.below_threshold.len(), 1);
assert_eq!(result.below_threshold[0].package_name(), "pkg-2");
}
#[test]
fn test_threshold_cvss_boundary() {
let vuln_at_threshold = create_vulnerability("CVE-2024-001", Some(7.0), Severity::High);
let vuln_below_threshold =
create_vulnerability("CVE-2024-002", Some(6.9), Severity::Medium);
let pkg = create_package_vulnerabilities(
"test-pkg",
vec![vuln_at_threshold, vuln_below_threshold],
);
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::Cvss(7.0), &[]);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 1); assert_eq!(result.below_threshold[0].vulnerabilities().len(), 1); }
#[test]
fn test_actionable_count_single_package() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln2 = create_vulnerability("CVE-2024-002", Some(8.0), Severity::High);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln1, vuln2]);
let result = VulnerabilityCheckResult {
above_threshold: vec![pkg],
below_threshold: vec![],
threshold_exceeded: true,
};
assert_eq!(result.actionable_count(), 2);
}
#[test]
fn test_actionable_count_multiple_packages() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln2 = create_vulnerability("CVE-2024-002", Some(8.0), Severity::High);
let vuln3 = create_vulnerability("CVE-2024-003", Some(7.5), Severity::High);
let pkg1 = create_package_vulnerabilities("pkg-1", vec![vuln1, vuln2]);
let pkg2 = create_package_vulnerabilities("pkg-2", vec![vuln3]);
let result = VulnerabilityCheckResult {
above_threshold: vec![pkg1, pkg2],
below_threshold: vec![],
threshold_exceeded: true,
};
assert_eq!(result.actionable_count(), 3);
}
#[test]
fn test_actionable_count_empty() {
let result = VulnerabilityCheckResult {
above_threshold: vec![],
below_threshold: vec![],
threshold_exceeded: false,
};
assert_eq!(result.actionable_count(), 0);
}
#[test]
fn test_informational_count_single_package() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(3.0), Severity::Low);
let vuln2 = create_vulnerability("CVE-2024-002", Some(2.0), Severity::Low);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln1, vuln2]);
let result = VulnerabilityCheckResult {
above_threshold: vec![],
below_threshold: vec![pkg],
threshold_exceeded: false,
};
assert_eq!(result.informational_count(), 2);
}
#[test]
fn test_informational_count_multiple_packages() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(3.0), Severity::Low);
let vuln2 = create_vulnerability("CVE-2024-002", Some(5.0), Severity::Medium);
let vuln3 = create_vulnerability("CVE-2024-003", Some(4.0), Severity::Medium);
let pkg1 = create_package_vulnerabilities("pkg-1", vec![vuln1]);
let pkg2 = create_package_vulnerabilities("pkg-2", vec![vuln2, vuln3]);
let result = VulnerabilityCheckResult {
above_threshold: vec![],
below_threshold: vec![pkg1, pkg2],
threshold_exceeded: false,
};
assert_eq!(result.informational_count(), 3);
}
#[test]
fn test_actionable_package_count() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln2 = create_vulnerability("CVE-2024-002", Some(8.0), Severity::High);
let pkg1 = create_package_vulnerabilities("pkg-1", vec![vuln1]);
let pkg2 = create_package_vulnerabilities("pkg-2", vec![vuln2]);
let result = VulnerabilityCheckResult {
above_threshold: vec![pkg1, pkg2],
below_threshold: vec![],
threshold_exceeded: true,
};
assert_eq!(result.above_threshold.len(), 2);
}
#[test]
fn test_actionable_package_count_empty() {
let result = VulnerabilityCheckResult {
above_threshold: vec![],
below_threshold: vec![],
threshold_exceeded: false,
};
assert_eq!(result.above_threshold.len(), 0);
}
#[test]
fn test_informational_package_count() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(3.0), Severity::Low);
let vuln2 = create_vulnerability("CVE-2024-002", Some(4.0), Severity::Medium);
let vuln3 = create_vulnerability("CVE-2024-003", Some(2.0), Severity::Low);
let pkg1 = create_package_vulnerabilities("pkg-1", vec![vuln1]);
let pkg2 = create_package_vulnerabilities("pkg-2", vec![vuln2]);
let pkg3 = create_package_vulnerabilities("pkg-3", vec![vuln3]);
let result = VulnerabilityCheckResult {
above_threshold: vec![],
below_threshold: vec![pkg1, pkg2, pkg3],
threshold_exceeded: false,
};
assert_eq!(result.below_threshold.len(), 3);
}
#[test]
fn test_semantic_methods_with_mixed_result() {
let vuln_critical = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln_high = create_vulnerability("CVE-2024-002", Some(8.0), Severity::High);
let vuln_low = create_vulnerability("CVE-2024-003", Some(3.0), Severity::Low);
let vuln_medium = create_vulnerability("CVE-2024-004", Some(5.0), Severity::Medium);
let above_pkg =
create_package_vulnerabilities("critical-pkg", vec![vuln_critical, vuln_high]);
let below_pkg1 = create_package_vulnerabilities("low-pkg", vec![vuln_low]);
let below_pkg2 = create_package_vulnerabilities("medium-pkg", vec![vuln_medium]);
let result = VulnerabilityCheckResult {
above_threshold: vec![above_pkg],
below_threshold: vec![below_pkg1, below_pkg2],
threshold_exceeded: true,
};
assert!(!result.above_threshold.is_empty());
assert!(!result.below_threshold.is_empty());
assert_eq!(result.actionable_count(), 2);
assert_eq!(result.informational_count(), 2);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.below_threshold.len(), 2);
}
#[test]
fn test_ignore_single_cve() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln2 = create_vulnerability("CVE-2024-002", Some(7.5), Severity::High);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln1, vuln2]);
let ignore = vec![IgnoreCve {
id: "CVE-2024-001".to_string(),
reason: Some("False positive".to_string()),
}];
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::None, &ignore);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 1);
assert_eq!(
result.above_threshold[0].vulnerabilities()[0].id(),
"CVE-2024-002"
);
}
#[test]
fn test_ignore_multiple_cves() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln2 = create_vulnerability("CVE-2024-002", Some(7.5), Severity::High);
let vuln3 = create_vulnerability("CVE-2024-003", Some(3.0), Severity::Low);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln1, vuln2, vuln3]);
let ignore = vec![
IgnoreCve {
id: "CVE-2024-001".to_string(),
reason: None,
},
IgnoreCve {
id: "CVE-2024-002".to_string(),
reason: Some("Accepted risk".to_string()),
},
];
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::None, &ignore);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 1);
assert_eq!(
result.above_threshold[0].vulnerabilities()[0].id(),
"CVE-2024-003"
);
}
#[test]
fn test_ignore_cve_with_reason() {
let vuln = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln]);
let ignore = vec![IgnoreCve {
id: "CVE-2024-001".to_string(),
reason: Some("Code path not reachable".to_string()),
}];
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::None, &ignore);
assert!(!result.threshold_exceeded);
assert!(result.above_threshold.is_empty());
assert!(result.below_threshold.is_empty());
}
#[test]
fn test_ignore_cve_no_match() {
let vuln = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln]);
let ignore = vec![IgnoreCve {
id: "CVE-2024-999".to_string(),
reason: None,
}];
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::None, &ignore);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 1);
}
#[test]
fn test_ignore_cves_empty_list() {
let vuln = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln]);
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::None, &[]);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
}
#[test]
fn test_ignore_all_cves_in_package_removes_package() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln2 = create_vulnerability("CVE-2024-002", Some(7.5), Severity::High);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln1, vuln2]);
let ignore = vec![
IgnoreCve {
id: "CVE-2024-001".to_string(),
reason: None,
},
IgnoreCve {
id: "CVE-2024-002".to_string(),
reason: None,
},
];
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::None, &ignore);
assert!(!result.threshold_exceeded);
assert!(result.above_threshold.is_empty());
assert!(result.below_threshold.is_empty());
}
#[test]
fn test_ignore_cve_case_sensitive() {
let vuln = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln]);
let ignore = vec![IgnoreCve {
id: "cve-2024-001".to_string(),
reason: None,
}];
let result = VulnerabilityChecker::check(vec![pkg], ThresholdConfig::None, &ignore);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
}
#[test]
fn test_ignore_cve_does_not_trigger_threshold() {
let vuln_critical = create_vulnerability("CVE-2024-001", Some(9.8), Severity::Critical);
let vuln_low = create_vulnerability("CVE-2024-002", Some(3.0), Severity::Low);
let pkg = create_package_vulnerabilities("test-pkg", vec![vuln_critical, vuln_low]);
let ignore = vec![IgnoreCve {
id: "CVE-2024-001".to_string(),
reason: Some("False positive".to_string()),
}];
let result = VulnerabilityChecker::check(
vec![pkg],
ThresholdConfig::Severity(Severity::High),
&ignore,
);
assert!(!result.threshold_exceeded);
assert!(result.above_threshold.is_empty());
assert_eq!(result.below_threshold.len(), 1);
}
#[test]
fn test_ignore_cve_across_multiple_packages() {
let vuln1 = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln2 = create_vulnerability("CVE-2024-001", Some(9.0), Severity::Critical);
let vuln3 = create_vulnerability("CVE-2024-002", Some(7.5), Severity::High);
let pkg1 = create_package_vulnerabilities("pkg-1", vec![vuln1]);
let pkg2 = create_package_vulnerabilities("pkg-2", vec![vuln2, vuln3]);
let ignore = vec![IgnoreCve {
id: "CVE-2024-001".to_string(),
reason: None,
}];
let result = VulnerabilityChecker::check(vec![pkg1, pkg2], ThresholdConfig::None, &ignore);
assert!(result.threshold_exceeded);
assert_eq!(result.above_threshold.len(), 1);
assert_eq!(result.above_threshold[0].package_name(), "pkg-2");
assert_eq!(result.above_threshold[0].vulnerabilities().len(), 1);
assert_eq!(
result.above_threshold[0].vulnerabilities()[0].id(),
"CVE-2024-002"
);
}
}