use super::*;
impl ComplianceChecker {
pub(crate) fn check_eo14028(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
use crate::model::ExternalRefType;
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(),
});
}
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(),
});
}
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(),
});
}
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(),
});
}
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(),
});
}
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(),
});
}
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(),
});
}
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(),
});
}
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(),
});
}
}
}
}