uv-sbom 2.2.0

SBOM generation tool for uv projects - Generate CycloneDX SBOMs from uv.lock files
Documentation
use crate::application::read_models::{
    ComponentView, LicenseComplianceView, VulnerabilityReportView,
};
use crate::i18n::Messages;

/// Renders the executive summary section
pub(in super::super) fn render(
    messages: &'static Messages,
    output: &mut String,
    components: &[ComponentView],
    vulnerabilities: Option<&VulnerabilityReportView>,
    license_compliance: Option<&LicenseComplianceView>,
) {
    output.push_str(messages.section_summary);
    output.push_str("\n\n");

    // Table header
    output.push_str(&format!(
        "| {} | {} | {} |\n",
        messages.col_item, messages.col_count, messages.col_status
    ));
    output.push_str(&super::super::table::make_separator(&[
        messages.col_item,
        messages.col_count,
        messages.col_status,
    ]));

    // Package count rows
    let direct_count = components.iter().filter(|c| c.is_direct_dependency).count();
    let transitive_count = components
        .iter()
        .filter(|c| !c.is_direct_dependency)
        .count();
    output.push_str(&format!(
        "| {} | {} | ✅ |\n",
        messages.label_direct_deps, direct_count
    ));
    output.push_str(&format!(
        "| {} | {} | ✅ |\n",
        messages.label_transitive_deps, transitive_count
    ));

    // Vulnerability rows
    let mut has_critical = false;
    let mut has_warning = false;
    if let Some(vuln_report) = vulnerabilities {
        let counts = vuln_report.counts_by_severity();
        let critical_status = if counts.critical > 0 {
            has_critical = true;
            ""
        } else {
            ""
        };
        let high_status = if counts.high > 0 {
            has_warning = true;
            "⚠️"
        } else {
            ""
        };
        let medium_status = if counts.medium > 0 {
            has_warning = true;
            "⚠️"
        } else {
            ""
        };
        let low_status = if counts.low > 0 {
            has_warning = true;
            "⚠️"
        } else {
            ""
        };
        output.push_str(&format!(
            "| {} | {} | {} |\n",
            messages.label_vuln_critical, counts.critical, critical_status
        ));
        output.push_str(&format!(
            "| {} | {} | {} |\n",
            messages.label_vuln_high, counts.high, high_status
        ));
        output.push_str(&format!(
            "| {} | {} | {} |\n",
            messages.label_vuln_medium, counts.medium, medium_status
        ));
        output.push_str(&format!(
            "| {} | {} | {} |\n",
            messages.label_vuln_low, counts.low, low_status
        ));
    } else {
        output.push_str(&format!("\n{}\n", messages.label_vuln_check_skipped));
    }

    // License violations row
    let violation_count = license_compliance
        .map(|lc| lc.summary.violation_count)
        .unwrap_or(0);
    let license_status = if violation_count > 0 {
        has_critical = true;
        ""
    } else {
        ""
    };
    output.push_str(&format!(
        "| {} | {} | {} |\n",
        messages.label_license_violations, violation_count, license_status
    ));

    // Overall line
    output.push('\n');
    let overall = if has_critical {
        messages.overall_action_required
    } else if has_warning {
        messages.overall_attention_recommended
    } else {
        messages.overall_no_issues
    };
    output.push_str(overall);
    output.push_str("\n\n");
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::application::read_models::{
        LicenseComplianceSummary, LicenseComplianceView, SeverityView, VulnerabilityReportView,
        VulnerabilityView,
    };
    use crate::i18n::{Locale, Messages};

    fn make_component(is_direct: bool) -> ComponentView {
        ComponentView {
            bom_ref: String::new(),
            name: String::new(),
            version: String::new(),
            purl: String::new(),
            license: None,
            description: None,
            sha256_hash: None,
            is_direct_dependency: is_direct,
        }
    }

    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,
        }
    }

    fn make_license_compliance(violation_count: usize) -> LicenseComplianceView {
        LicenseComplianceView {
            violations: vec![],
            warnings: vec![],
            has_violations: violation_count > 0,
            summary: LicenseComplianceSummary {
                violation_count,
                warning_count: 0,
            },
        }
    }

    fn render_summary(
        locale: Locale,
        components: &[ComponentView],
        vulnerabilities: Option<&VulnerabilityReportView>,
        license_compliance: Option<&LicenseComplianceView>,
    ) -> String {
        let messages = Messages::for_locale(locale);
        let mut output = String::new();
        render(
            messages,
            &mut output,
            components,
            vulnerabilities,
            license_compliance,
        );
        output
    }

    #[test]
    fn test_summary_section_header_en() {
        let output = render_summary(Locale::En, &[], None, None);
        assert!(output.starts_with("## Summary\n\n"));
    }

    #[test]
    fn test_summary_section_header_ja() {
        let output = render_summary(Locale::Ja, &[], None, None);
        assert!(output.starts_with("## サマリー\n\n"));
    }

    #[test]
    fn test_summary_package_counts_en() {
        let components = vec![
            make_component(true),
            make_component(true),
            make_component(false),
        ];
        let output = render_summary(Locale::En, &components, None, None);
        assert!(output.contains("| Direct dependencies | 2 | ✅ |"));
        assert!(output.contains("| Transitive dependencies | 1 | ✅ |"));
    }

    #[test]
    fn test_summary_package_counts_ja() {
        let components = vec![
            make_component(true),
            make_component(false),
            make_component(false),
        ];
        let output = render_summary(Locale::Ja, &components, None, None);
        assert!(output.contains("| 直接依存パッケージ | 1 | ✅ |"));
        assert!(output.contains("| 間接依存パッケージ | 2 | ✅ |"));
    }

    #[test]
    fn test_vuln_check_skipped_en() {
        let output = render_summary(Locale::En, &[], None, None);
        assert!(output.contains("_Vulnerability check skipped._"));
        assert!(!output.contains("Vulnerabilities (CRITICAL)"));
    }

    #[test]
    fn test_vuln_check_skipped_ja() {
        let output = render_summary(Locale::Ja, &[], None, None);
        assert!(output.contains("_脆弱性チェックはスキップされました。_"));
        assert!(!output.contains("脆弱性 (CRITICAL)"));
    }

    #[test]
    fn test_vuln_rows_present_when_check_enabled() {
        let report = VulnerabilityReportView::default();
        let output = render_summary(Locale::En, &[], Some(&report), None);
        assert!(output.contains("| Vulnerabilities (CRITICAL) | 0 | ✅ |"));
        assert!(output.contains("| Vulnerabilities (HIGH) | 0 | ✅ |"));
        assert!(output.contains("| Vulnerabilities (MEDIUM) | 0 | ✅ |"));
        assert!(output.contains("| Vulnerabilities (LOW) | 0 | ✅ |"));
        assert!(!output.contains("_Vulnerability check skipped._"));
    }

    #[test]
    fn test_vuln_critical_status_is_error() {
        let report = VulnerabilityReportView {
            actionable: vec![make_vuln(SeverityView::Critical)],
            ..Default::default()
        };
        let output = render_summary(Locale::En, &[], Some(&report), None);
        assert!(output.contains("| Vulnerabilities (CRITICAL) | 1 | ❌ |"));
        assert!(output.contains("**Overall: Action required**"));
    }

    #[test]
    fn test_vuln_high_status_is_warning() {
        let report = VulnerabilityReportView {
            actionable: vec![make_vuln(SeverityView::High)],
            ..Default::default()
        };
        let output = render_summary(Locale::En, &[], Some(&report), None);
        assert!(output.contains("| Vulnerabilities (HIGH) | 1 | ⚠️ |"));
        assert!(output.contains("**Overall: Attention recommended**"));
    }

    #[test]
    fn test_vuln_medium_status_is_warning() {
        let report = VulnerabilityReportView {
            actionable: vec![make_vuln(SeverityView::Medium)],
            ..Default::default()
        };
        let output = render_summary(Locale::En, &[], Some(&report), None);
        assert!(output.contains("| Vulnerabilities (MEDIUM) | 1 | ⚠️ |"));
        assert!(output.contains("**Overall: Attention recommended**"));
    }

    #[test]
    fn test_vuln_low_status_is_warning() {
        let report = VulnerabilityReportView {
            informational: vec![make_vuln(SeverityView::Low)],
            ..Default::default()
        };
        let output = render_summary(Locale::En, &[], Some(&report), None);
        assert!(output.contains("| Vulnerabilities (LOW) | 1 | ⚠️ |"));
        assert!(output.contains("**Overall: Attention recommended**"));
    }

    #[test]
    fn test_license_violation_status_is_error() {
        let license = make_license_compliance(2);
        let output = render_summary(Locale::En, &[], None, Some(&license));
        assert!(output.contains("| License violations | 2 | ❌ |"));
        assert!(output.contains("**Overall: Action required**"));
    }

    #[test]
    fn test_license_no_violation_status_is_ok() {
        let license = make_license_compliance(0);
        let output = render_summary(Locale::En, &[], None, Some(&license));
        assert!(output.contains("| License violations | 0 | ✅ |"));
    }

    #[test]
    fn test_overall_no_issues_en() {
        let report = VulnerabilityReportView::default();
        let license = make_license_compliance(0);
        let output = render_summary(Locale::En, &[], Some(&report), Some(&license));
        assert!(output.contains("**Overall: No issues found** ✅"));
    }

    #[test]
    fn test_overall_no_issues_ja() {
        let report = VulnerabilityReportView::default();
        let license = make_license_compliance(0);
        let output = render_summary(Locale::Ja, &[], Some(&report), Some(&license));
        assert!(output.contains("**総合判定: 問題なし** ✅"));
    }

    #[test]
    fn test_overall_action_required_ja() {
        let report = VulnerabilityReportView {
            actionable: vec![make_vuln(SeverityView::Critical)],
            ..Default::default()
        };
        let output = render_summary(Locale::Ja, &[], Some(&report), None);
        assert!(output.contains("**総合判定: 対応が必要です**"));
    }

    #[test]
    fn test_overall_attention_recommended_ja() {
        let report = VulnerabilityReportView {
            actionable: vec![make_vuln(SeverityView::High)],
            ..Default::default()
        };
        let output = render_summary(Locale::Ja, &[], Some(&report), None);
        assert!(output.contains("**総合判定: 注意が必要です**"));
    }

    #[test]
    fn test_license_compliance_none_shows_zero_violations() {
        let output = render_summary(Locale::En, &[], None, None);
        assert!(output.contains("| License violations | 0 | ✅ |"));
    }

    #[test]
    fn test_critical_overrides_warning_in_overall() {
        let report = VulnerabilityReportView {
            actionable: vec![
                make_vuln(SeverityView::Critical),
                make_vuln(SeverityView::High),
            ],
            ..Default::default()
        };
        let output = render_summary(Locale::En, &[], Some(&report), None);
        assert!(output.contains("**Overall: Action required**"));
        assert!(!output.contains("**Overall: Attention recommended**"));
    }
}