use super::*;
impl ComplianceChecker {
pub(crate) fn check_nist_ssdf(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
use crate::model::ExternalRefType;
if sbom.document.creators.is_empty() {
violations.push(Violation {
severity: ViolationSeverity::Error,
category: ViolationCategory::DocumentMetadata,
message:
"SBOM must identify its creator (tool or organization) for provenance tracking"
.to_string(),
element: None,
requirement: "NIST SSDF PS.1: Provenance — creator identification".to_string(),
rule_id: "SBOM-SSDF-PS1",
standard_refs: Vec::new(),
});
}
let has_tool_creator = sbom
.document
.creators
.iter()
.any(|c| c.creator_type == crate::model::CreatorType::Tool);
if !has_tool_creator {
violations.push(Violation {
severity: ViolationSeverity::Warning,
category: ViolationCategory::DocumentMetadata,
message: "SBOM should identify the generation tool for automated provenance"
.to_string(),
element: None,
requirement: "NIST SSDF PS.1: Provenance — tool identification".to_string(),
rule_id: "SBOM-SSDF-PS1",
standard_refs: Vec::new(),
});
}
let total = sbom.components.len();
let without_hash = sbom
.components
.values()
.filter(|c| c.hashes.is_empty())
.count();
if without_hash > 0 {
let pct = (without_hash * 100) / total.max(1);
violations.push(Violation {
severity: if pct > 50 {
ViolationSeverity::Error
} else {
ViolationSeverity::Warning
},
category: ViolationCategory::IntegrityInfo,
message: format!(
"{without_hash}/{total} components ({pct}%) missing cryptographic hashes for build integrity"
),
element: None,
requirement: "NIST SSDF PS.2: Build integrity — component hashes".to_string(),
rule_id: "SBOM-SSDF-PS2",
standard_refs: Vec::new(),
});
}
let has_vcs_ref = sbom.components.values().any(|comp| {
comp.external_refs
.iter()
.any(|r| matches!(r.ref_type, ExternalRefType::Vcs))
});
if !has_vcs_ref {
violations.push(Violation {
severity: ViolationSeverity::Warning,
category: ViolationCategory::ComponentIdentification,
message: "No components reference a VCS repository; include source repository links for traceability"
.to_string(),
element: None,
requirement: "NIST SSDF PO.1: Source code provenance — VCS references".to_string(),
rule_id: "SBOM-SSDF-PO1",
standard_refs: Vec::new(),
});
}
let has_build_ref = sbom.components.values().any(|comp| {
comp.external_refs.iter().any(|r| {
matches!(
r.ref_type,
ExternalRefType::BuildMeta | ExternalRefType::BuildSystem
)
})
});
if !has_build_ref {
violations.push(Violation {
severity: ViolationSeverity::Info,
category: ViolationCategory::DocumentMetadata,
message: "No build metadata references found; include build system information for reproducibility"
.to_string(),
element: None,
requirement: "NIST SSDF PO.3: Build provenance — build metadata".to_string(),
rule_id: "SBOM-SSDF-PO3",
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: "NIST SSDF PW.4: Dependency management — relationships".to_string(),
rule_id: "SBOM-SSDF-PW4",
standard_refs: Vec::new(),
});
}
let has_vuln_info = sbom
.components
.values()
.any(|c| !c.vulnerabilities.is_empty());
let has_security_ref = sbom.components.values().any(|comp| {
comp.external_refs.iter().any(|r| {
matches!(
r.ref_type,
ExternalRefType::Advisories
| ExternalRefType::SecurityContact
| ExternalRefType::VulnerabilityAssertion
)
})
});
if !has_vuln_info && !has_security_ref {
violations.push(Violation {
severity: ViolationSeverity::Info,
category: ViolationCategory::SecurityInfo,
message: "No vulnerability or security advisory references found; \
include vulnerability data or security contact for incident response"
.to_string(),
element: None,
requirement: "NIST SSDF PW.6: Vulnerability information".to_string(),
rule_id: "SBOM-SSDF-PW6",
standard_refs: Vec::new(),
});
}
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::Warning,
category: ViolationCategory::ComponentIdentification,
message: format!(
"{without_id}/{total} components missing unique identifier (PURL/CPE/SWID)"
),
element: None,
requirement: "NIST SSDF RV.1: Component identification — unique identifiers"
.to_string(),
rule_id: "SBOM-SSDF-RV1",
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 {
violations.push(Violation {
severity: ViolationSeverity::Warning,
category: ViolationCategory::SupplierInfo,
message: format!(
"{without_supplier}/{total} components missing supplier/author information"
),
element: None,
requirement: "NIST SSDF PS.3: Supplier identification".to_string(),
rule_id: "SBOM-SSDF-PS3",
standard_refs: Vec::new(),
});
}
}
}