use super::super::component_view::ComponentView;
use super::super::vulnerability_view::{
SeverityView, VulnerabilityReportView, VulnerabilitySummary, VulnerabilityView,
};
use crate::sbom_generation::domain::services::VulnerabilityCheckResult;
use crate::sbom_generation::domain::vulnerability::{
PackageVulnerabilities, Severity, Vulnerability,
};
use std::collections::HashSet;
pub(super) fn build_vulnerabilities(
result: &VulnerabilityCheckResult,
components: &[ComponentView],
) -> VulnerabilityReportView {
let actionable: Vec<VulnerabilityView> = result
.above_threshold
.iter()
.flat_map(|pkg| build_vulnerability_views_for_package(pkg, components))
.collect();
let informational: Vec<VulnerabilityView> = result
.below_threshold
.iter()
.flat_map(|pkg| build_vulnerability_views_for_package(pkg, components))
.collect();
let affected_packages: HashSet<&str> = result
.above_threshold
.iter()
.chain(result.below_threshold.iter())
.map(|pkg| pkg.package_name())
.collect();
let summary = VulnerabilitySummary {
total_count: result.actionable_count() + result.informational_count(),
affected_package_count: affected_packages.len(),
};
VulnerabilityReportView {
actionable,
informational,
summary,
}
}
fn build_vulnerability_views_for_package(
package: &PackageVulnerabilities,
components: &[ComponentView],
) -> Vec<VulnerabilityView> {
package
.vulnerabilities()
.iter()
.map(|vuln| build_vulnerability_view(vuln, package, components))
.collect()
}
pub(super) fn build_vulnerability_view(
vuln: &Vulnerability,
package: &PackageVulnerabilities,
components: &[ComponentView],
) -> VulnerabilityView {
let component = components
.iter()
.find(|c| c.name == package.package_name() && c.version == package.current_version());
let affected_component = component
.map(|c| c.bom_ref.clone())
.unwrap_or_else(|| format!("{}-{}", package.package_name(), package.current_version()));
let bom_ref = format!("{}-{}", vuln.id(), affected_component);
VulnerabilityView {
bom_ref,
id: vuln.id().to_string(),
affected_component,
affected_component_name: package.package_name().to_string(),
affected_version: package.current_version().to_string(),
cvss_score: vuln.cvss_score().map(|s| s.value()),
cvss_vector: None, severity: map_severity(&vuln.severity()),
fixed_version: vuln.fixed_version().map(|s| s.to_string()),
description: None, source_url: None, }
}
pub(super) fn map_severity(severity: &Severity) -> SeverityView {
match severity {
Severity::Critical => SeverityView::Critical,
Severity::High => SeverityView::High,
Severity::Medium => SeverityView::Medium,
Severity::Low => SeverityView::Low,
Severity::None => SeverityView::None,
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers as th;
use super::*;
use crate::application::read_models::component_view::ComponentView;
use crate::sbom_generation::domain::services::VulnerabilityCheckResult;
use crate::sbom_generation::domain::vulnerability::Severity;
#[test]
fn test_map_severity_all_levels() {
assert_eq!(map_severity(&Severity::Critical), SeverityView::Critical);
assert_eq!(map_severity(&Severity::High), SeverityView::High);
assert_eq!(map_severity(&Severity::Medium), SeverityView::Medium);
assert_eq!(map_severity(&Severity::Low), SeverityView::Low);
assert_eq!(map_severity(&Severity::None), SeverityView::None);
}
#[test]
fn test_build_vulnerability_view_basic() {
let vuln = th::vulnerability("CVE-2024-1234", Some(9.8), Severity::Critical);
let pkg = th::package_vulnerabilities("requests", "2.31.0", vec![vuln.clone()]);
let components = vec![ComponentView {
bom_ref: "requests-2.31.0".to_string(),
name: "requests".to_string(),
version: "2.31.0".to_string(),
purl: "pkg:pypi/requests@2.31.0".to_string(),
license: None,
description: None,
sha256_hash: None,
is_direct_dependency: true,
}];
let view = build_vulnerability_view(&vuln, &pkg, &components);
assert_eq!(view.id, "CVE-2024-1234");
assert_eq!(view.affected_component, "requests-2.31.0");
assert_eq!(view.affected_component_name, "requests");
assert_eq!(view.affected_version, "2.31.0");
assert_eq!(view.cvss_score, Some(9.8));
assert_eq!(view.severity, SeverityView::Critical);
assert_eq!(view.bom_ref, "CVE-2024-1234-requests-2.31.0");
}
#[test]
fn test_build_vulnerability_view_with_fixed_version() {
let vuln = th::vulnerability_with_fix("CVE-2024-5678", Some(7.5), Severity::High, "3.0.0");
let pkg = th::package_vulnerabilities("requests", "2.31.0", vec![vuln.clone()]);
let components = vec![];
let view = build_vulnerability_view(&vuln, &pkg, &components);
assert_eq!(view.fixed_version, Some("3.0.0".to_string()));
}
#[test]
fn test_build_vulnerability_view_without_cvss() {
let vuln = th::vulnerability("GHSA-xxxx-yyyy-zzzz", None, Severity::High);
let pkg = th::package_vulnerabilities("requests", "2.31.0", vec![vuln.clone()]);
let components = vec![];
let view = build_vulnerability_view(&vuln, &pkg, &components);
assert_eq!(view.cvss_score, None);
assert_eq!(view.severity, SeverityView::High);
}
#[test]
fn test_build_vulnerability_view_component_not_found() {
let vuln = th::vulnerability("CVE-2024-1234", Some(9.8), Severity::Critical);
let pkg = th::package_vulnerabilities("unknown-pkg", "1.0.0", vec![vuln.clone()]);
let components = vec![];
let view = build_vulnerability_view(&vuln, &pkg, &components);
assert_eq!(view.affected_component, "unknown-pkg-1.0.0");
assert_eq!(view.bom_ref, "CVE-2024-1234-unknown-pkg-1.0.0");
}
#[test]
fn test_build_vulnerabilities_actionable_and_informational() {
let vuln_critical = th::vulnerability("CVE-2024-001", Some(9.8), Severity::Critical);
let vuln_low = th::vulnerability("CVE-2024-002", Some(2.0), Severity::Low);
let above_pkg = th::package_vulnerabilities("critical-pkg", "1.0.0", vec![vuln_critical]);
let below_pkg = th::package_vulnerabilities("low-pkg", "1.0.0", vec![vuln_low]);
let result = VulnerabilityCheckResult {
above_threshold: vec![above_pkg],
below_threshold: vec![below_pkg],
threshold_exceeded: true,
};
let components = vec![];
let report = build_vulnerabilities(&result, &components);
assert_eq!(report.actionable.len(), 1);
assert_eq!(report.actionable[0].id, "CVE-2024-001");
assert_eq!(report.informational.len(), 1);
assert_eq!(report.informational[0].id, "CVE-2024-002");
}
#[test]
fn test_build_vulnerabilities_summary_statistics() {
let vuln1 = th::vulnerability("CVE-2024-001", Some(9.8), Severity::Critical);
let vuln2 = th::vulnerability("CVE-2024-002", Some(8.0), Severity::High);
let vuln3 = th::vulnerability("CVE-2024-003", Some(3.0), Severity::Low);
let above_pkg = th::package_vulnerabilities("critical-pkg", "1.0.0", vec![vuln1, vuln2]);
let below_pkg = th::package_vulnerabilities("low-pkg", "1.0.0", vec![vuln3]);
let result = VulnerabilityCheckResult {
above_threshold: vec![above_pkg],
below_threshold: vec![below_pkg],
threshold_exceeded: true,
};
let components = vec![];
let report = build_vulnerabilities(&result, &components);
assert_eq!(report.summary.total_count, 3);
assert_eq!(report.actionable.len(), 2);
assert_eq!(report.informational.len(), 1);
assert_eq!(report.summary.affected_package_count, 2);
}
#[test]
fn test_build_vulnerabilities_empty_result() {
let result = VulnerabilityCheckResult {
above_threshold: vec![],
below_threshold: vec![],
threshold_exceeded: false,
};
let components = vec![];
let report = build_vulnerabilities(&result, &components);
assert!(report.actionable.is_empty());
assert!(report.informational.is_empty());
assert_eq!(report.summary.total_count, 0);
assert_eq!(report.summary.affected_package_count, 0);
}
#[test]
fn test_build_vulnerabilities_multiple_vulns_per_package() {
let vuln1 = th::vulnerability("CVE-2024-001", Some(9.8), Severity::Critical);
let vuln2 = th::vulnerability("CVE-2024-002", Some(8.5), Severity::High);
let vuln3 = th::vulnerability("CVE-2024-003", Some(7.0), Severity::High);
let pkg = th::package_vulnerabilities("multi-vuln-pkg", "1.0.0", vec![vuln1, vuln2, vuln3]);
let result = VulnerabilityCheckResult {
above_threshold: vec![pkg],
below_threshold: vec![],
threshold_exceeded: true,
};
let components = vec![];
let report = build_vulnerabilities(&result, &components);
assert_eq!(report.actionable.len(), 3);
for vuln_view in &report.actionable {
assert_eq!(vuln_view.affected_component_name, "multi-vuln-pkg");
assert_eq!(vuln_view.affected_version, "1.0.0");
}
}
}