sbom-tools 0.1.22

Semantic SBOM diff and analysis tool
Documentation
//! Executive Order 14028 Section 4 checks.

use super::*;

impl ComplianceChecker {
    /// Executive Order 14028 Section 4 checks
    pub(crate) fn check_eo14028(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        use crate::model::ExternalRefType;

        // Sec 4(e) — Machine-readable format
        let format_ok = match sbom.document.format {
            crate::model::SbomFormat::CycloneDx => {
                let v = &sbom.document.spec_version;
                !(v.starts_with("1.0")
                    || v.starts_with("1.1")
                    || v.starts_with("1.2")
                    || v.starts_with("1.3"))
            }
            crate::model::SbomFormat::Spdx => {
                let v = &sbom.document.spec_version;
                v.starts_with("2.3") || v.starts_with("3.")
            }
        };
        if !format_ok {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::FormatSpecific,
                message: format!(
                    "SBOM format {} {} does not meet EO 14028 machine-readable requirements; \
                    use CycloneDX 1.4+, SPDX 2.3+, or SPDX 3.0+",
                    sbom.document.format, sbom.document.spec_version
                ),
                element: None,
                requirement: "EO 14028 Sec 4(e): Machine-readable SBOM format".to_string(),
                rule_id: "SBOM-EO14028-FORMAT",
                standard_refs: Vec::new(),
            });
        }

        // Sec 4(e) — Automated generation: tool creator should be present
        let has_tool = sbom
            .document
            .creators
            .iter()
            .any(|c| c.creator_type == crate::model::CreatorType::Tool);
        if !has_tool {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::DocumentMetadata,
                message: "SBOM should be auto-generated by a tool; no tool creator identified"
                    .to_string(),
                element: None,
                requirement: "EO 14028 Sec 4(e): Automated SBOM generation".to_string(),
                rule_id: "SBOM-EO14028-AUTOGEN",
                standard_refs: Vec::new(),
            });
        }

        // Sec 4(e) — Creator identification
        if sbom.document.creators.is_empty() {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::DocumentMetadata,
                message: "SBOM must identify its creator (vendor or tool)".to_string(),
                element: None,
                requirement: "EO 14028 Sec 4(e): SBOM creator identification".to_string(),
                rule_id: "SBOM-EO14028-CREATOR",
                standard_refs: Vec::new(),
            });
        }

        // Sec 4(e) — Component identification with unique identifiers
        let total = sbom.components.len();
        let without_id = sbom
            .components
            .values()
            .filter(|c| {
                c.identifiers.purl.is_none()
                    && c.identifiers.cpe.is_empty()
                    && c.identifiers.swid.is_none()
            })
            .count();
        if without_id > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::ComponentIdentification,
                message: format!(
                    "{without_id}/{total} components missing unique identifier (PURL/CPE/SWID)"
                ),
                element: None,
                requirement: "EO 14028 Sec 4(e): Component unique identification".to_string(),
                rule_id: "SBOM-EO14028-IDENTIFIER",
                standard_refs: Vec::new(),
            });
        }

        // Sec 4(e) — Dependency relationships
        if sbom.components.len() > 1 && sbom.edges.is_empty() {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::DependencyInfo,
                message: "SBOM with multiple components must include dependency relationships"
                    .to_string(),
                element: None,
                requirement: "EO 14028 Sec 4(e): Dependency relationships".to_string(),
                rule_id: "SBOM-EO14028-DEPENDENCY",
                standard_refs: Vec::new(),
            });
        }

        // Sec 4(e) — Version information
        let without_version = sbom
            .components
            .values()
            .filter(|c| c.version.is_none())
            .count();
        if without_version > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::ComponentIdentification,
                message: format!(
                    "{without_version}/{total} components missing version information"
                ),
                element: None,
                requirement: "EO 14028 Sec 4(e): Component version".to_string(),
                rule_id: "SBOM-EO14028-VERSION",
                standard_refs: Vec::new(),
            });
        }

        // Sec 4(e) — Cryptographic hashes for integrity
        let without_hash = sbom
            .components
            .values()
            .filter(|c| c.hashes.is_empty())
            .count();
        if without_hash > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::IntegrityInfo,
                message: format!("{without_hash}/{total} components missing cryptographic hashes"),
                element: None,
                requirement: "EO 14028 Sec 4(e): Component integrity verification".to_string(),
                rule_id: "SBOM-EO14028-INTEGRITY",
                standard_refs: Vec::new(),
            });
        }

        // Sec 4(g) — Vulnerability disclosure
        let has_security_ref = sbom.document.security_contact.is_some()
            || sbom.document.vulnerability_disclosure_url.is_some()
            || sbom.components.values().any(|comp| {
                comp.external_refs.iter().any(|r| {
                    matches!(
                        r.ref_type,
                        ExternalRefType::SecurityContact | ExternalRefType::Advisories
                    )
                })
            });
        if !has_security_ref {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::SecurityInfo,
                message: "No security contact or vulnerability disclosure reference found"
                    .to_string(),
                element: None,
                requirement: "EO 14028 Sec 4(g): Vulnerability disclosure process".to_string(),
                rule_id: "SBOM-EO14028-DISCLOSURE",
                standard_refs: Vec::new(),
            });
        }

        // Sec 4(e) — Supplier identification
        let without_supplier = sbom
            .components
            .values()
            .filter(|c| c.supplier.is_none() && c.author.is_none())
            .count();
        if without_supplier > 0 {
            let pct = (without_supplier * 100) / total.max(1);
            if pct > 30 {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::SupplierInfo,
                    message: format!(
                        "{without_supplier}/{total} components ({pct}%) missing supplier information"
                    ),
                    element: None,
                    requirement: "EO 14028 Sec 4(e): Supplier identification".to_string(),
                    rule_id: "SBOM-EO14028-SUPPLIER",
                    standard_refs: Vec::new(),
                });
            }
        }
    }
}