uv-sbom 2.0.1

SBOM generation tool for uv projects - Generate CycloneDX SBOMs from uv.lock files
Documentation
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;

/// Builds vulnerability report view from vulnerability check result
///
/// Converts above_threshold to actionable and below_threshold to informational.
/// Uses existing VulnerabilityCheckResult semantic methods.
pub(super) fn build_vulnerabilities(
    result: &VulnerabilityCheckResult,
    components: &[ComponentView],
) -> VulnerabilityReportView {
    // Convert above_threshold to actionable vulnerabilities
    let actionable: Vec<VulnerabilityView> = result
        .above_threshold
        .iter()
        .flat_map(|pkg| build_vulnerability_views_for_package(pkg, components))
        .collect();

    // Convert below_threshold to informational vulnerabilities
    let informational: Vec<VulnerabilityView> = result
        .below_threshold
        .iter()
        .flat_map(|pkg| build_vulnerability_views_for_package(pkg, components))
        .collect();

    // Calculate unique affected packages
    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(),
        actionable_count: result.actionable_count(),
        informational_count: result.informational_count(),
        affected_package_count: affected_packages.len(),
    };

    VulnerabilityReportView {
        actionable,
        informational,
        threshold_exceeded: result.threshold_exceeded,
        summary,
    }
}

/// Builds vulnerability views for all vulnerabilities in a package
fn build_vulnerability_views_for_package(
    package: &PackageVulnerabilities,
    components: &[ComponentView],
) -> Vec<VulnerabilityView> {
    package
        .vulnerabilities()
        .iter()
        .map(|vuln| build_vulnerability_view(vuln, package, components))
        .collect()
}

/// Converts domain vulnerability to view
pub(super) fn build_vulnerability_view(
    vuln: &Vulnerability,
    package: &PackageVulnerabilities,
    components: &[ComponentView],
) -> VulnerabilityView {
    // Find the component bom-ref for this package
    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()));

    // Generate vulnerability bom-ref
    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, // OSV API doesn't provide vector in our current implementation
        severity: map_severity(&vuln.severity()),
        fixed_version: vuln.fixed_version().map(|s| s.to_string()),
        description: None, // Summary is not exposed in Vulnerability, could be added later
        source_url: None,  // Not available in current domain model
    }
}

/// Converts domain Severity to SeverityView
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,
    }
}