uv-sbom 2.2.0

SBOM generation tool for uv projects - Generate CycloneDX SBOMs from uv.lock files
Documentation
//! Vulnerability view structs for read model
//!
//! These structs provide a query-optimized view of vulnerability data
//! with pre-computed categorization and summary information.

/// View representation of a vulnerability report
///
/// Provides pre-categorized vulnerabilities with summary statistics
/// for efficient dashboard and reporting queries.
#[derive(Debug, Clone, Default)]
pub struct VulnerabilityReportView {
    /// Vulnerabilities that require action (CRITICAL, HIGH, MEDIUM)
    pub actionable: Vec<VulnerabilityView>,
    /// Vulnerabilities for information only (LOW, NONE)
    pub informational: Vec<VulnerabilityView>,
    /// Summary statistics
    pub summary: VulnerabilitySummary,
}

impl VulnerabilityReportView {
    /// Returns vulnerability counts broken down by severity level.
    ///
    /// Aggregates counts across both `actionable` and `informational` vectors.
    pub fn counts_by_severity(&self) -> VulnerabilityCountsBySeverity {
        let all = self.actionable.iter().chain(self.informational.iter());
        let mut counts = VulnerabilityCountsBySeverity::default();
        for v in all {
            match v.severity {
                SeverityView::Critical => counts.critical += 1,
                SeverityView::High => counts.high += 1,
                SeverityView::Medium => counts.medium += 1,
                SeverityView::Low => counts.low += 1,
                SeverityView::None => {}
            }
        }
        counts
    }
}

/// Vulnerability counts broken down by severity level
#[derive(Debug, Clone, Default)]
pub struct VulnerabilityCountsBySeverity {
    /// Number of CRITICAL severity vulnerabilities
    pub critical: usize,
    /// Number of HIGH severity vulnerabilities
    pub high: usize,
    /// Number of MEDIUM severity vulnerabilities
    pub medium: usize,
    /// Number of LOW severity vulnerabilities
    pub low: usize,
}

/// View representation of a single vulnerability
#[derive(Debug, Clone)]
pub struct VulnerabilityView {
    /// BOM reference identifier
    pub bom_ref: String,
    /// Vulnerability ID (e.g., CVE-2024-1234)
    pub id: String,
    /// BOM reference of the affected component
    pub affected_component: String,
    /// Name of the affected component
    pub affected_component_name: String,
    /// Version of the affected component
    pub affected_version: String,
    /// CVSS score (0.0 - 10.0)
    pub cvss_score: Option<f32>,
    /// CVSS vector string
    pub cvss_vector: Option<String>,
    /// Severity level
    pub severity: SeverityView,
    /// Version that fixes the vulnerability
    pub fixed_version: Option<String>,
    /// Description of the vulnerability
    pub description: Option<String>,
    /// URL to vulnerability source
    pub source_url: Option<String>,
}

/// Severity level for display purposes
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum SeverityView {
    /// Critical severity (CVSS 9.0-10.0)
    Critical,
    /// High severity (CVSS 7.0-8.9)
    High,
    /// Medium severity (CVSS 4.0-6.9)
    Medium,
    /// Low severity (CVSS 0.1-3.9)
    Low,
    /// No severity / Unknown
    #[default]
    None,
}

impl SeverityView {
    /// Returns the display name of the severity
    pub fn as_str(&self) -> &'static str {
        match self {
            SeverityView::Critical => "CRITICAL",
            SeverityView::High => "HIGH",
            SeverityView::Medium => "MEDIUM",
            SeverityView::Low => "LOW",
            SeverityView::None => "NONE",
        }
    }
}

/// Summary statistics for vulnerabilities
#[derive(Debug, Clone, Default)]
pub struct VulnerabilitySummary {
    /// Total number of vulnerabilities
    pub total_count: usize,
    /// Number of unique affected packages
    pub affected_package_count: usize,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_severity_view_as_str() {
        assert_eq!(SeverityView::Critical.as_str(), "CRITICAL");
        assert_eq!(SeverityView::High.as_str(), "HIGH");
        assert_eq!(SeverityView::Medium.as_str(), "MEDIUM");
        assert_eq!(SeverityView::Low.as_str(), "LOW");
        assert_eq!(SeverityView::None.as_str(), "NONE");
    }

    #[test]
    fn test_severity_view_ordering() {
        assert!(SeverityView::Critical < SeverityView::High);
        assert!(SeverityView::High < SeverityView::Medium);
        assert!(SeverityView::Medium < SeverityView::Low);
        assert!(SeverityView::Low < SeverityView::None);
    }

    #[test]
    fn test_severity_view_default() {
        assert_eq!(SeverityView::default(), SeverityView::None);
    }

    fn make_vuln(severity: SeverityView) -> VulnerabilityView {
        VulnerabilityView {
            bom_ref: String::new(),
            id: String::new(),
            affected_component: String::new(),
            affected_component_name: String::new(),
            affected_version: String::new(),
            cvss_score: None,
            cvss_vector: None,
            severity,
            fixed_version: None,
            description: None,
            source_url: None,
        }
    }

    #[test]
    fn test_counts_by_severity_all_zero() {
        let report = VulnerabilityReportView::default();
        let counts = report.counts_by_severity();
        assert_eq!(counts.critical, 0);
        assert_eq!(counts.high, 0);
        assert_eq!(counts.medium, 0);
        assert_eq!(counts.low, 0);
    }

    #[test]
    fn test_counts_by_severity_only_actionable() {
        let report = VulnerabilityReportView {
            actionable: vec![
                make_vuln(SeverityView::Critical),
                make_vuln(SeverityView::High),
                make_vuln(SeverityView::High),
                make_vuln(SeverityView::Medium),
            ],
            ..Default::default()
        };
        let counts = report.counts_by_severity();
        assert_eq!(counts.critical, 1);
        assert_eq!(counts.high, 2);
        assert_eq!(counts.medium, 1);
        assert_eq!(counts.low, 0);
    }

    #[test]
    fn test_counts_by_severity_only_informational() {
        let report = VulnerabilityReportView {
            informational: vec![
                make_vuln(SeverityView::Low),
                make_vuln(SeverityView::Low),
                make_vuln(SeverityView::None),
            ],
            ..Default::default()
        };
        let counts = report.counts_by_severity();
        assert_eq!(counts.critical, 0);
        assert_eq!(counts.high, 0);
        assert_eq!(counts.medium, 0);
        assert_eq!(counts.low, 2);
    }

    #[test]
    fn test_counts_by_severity_mixed() {
        let report = VulnerabilityReportView {
            actionable: vec![
                make_vuln(SeverityView::Critical),
                make_vuln(SeverityView::Medium),
            ],
            informational: vec![make_vuln(SeverityView::Low), make_vuln(SeverityView::None)],
            ..Default::default()
        };
        let counts = report.counts_by_severity();
        assert_eq!(counts.critical, 1);
        assert_eq!(counts.high, 0);
        assert_eq!(counts.medium, 1);
        assert_eq!(counts.low, 1);
    }
}