libverify_core/controls/
sbom_attestation.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4pub struct SbomAttestationControl;
16
17impl Control for SbomAttestationControl {
18 fn id(&self) -> ControlId {
19 builtin::id(builtin::SBOM_ATTESTATION)
20 }
21
22 fn description(&self) -> &'static str {
23 "Latest release must include an SBOM artifact (SPDX or CycloneDX)"
24 }
25
26 fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
27 let posture = match ControlFinding::extract_posture(self.id(), evidence) {
28 Ok(p) => p,
29 Err(findings) => return findings,
30 };
31
32 if posture.release_has_sbom {
33 vec![ControlFinding::satisfied(
34 self.id(),
35 "Latest release includes an SBOM artifact",
36 vec!["release:sbom".to_string()],
37 )]
38 } else {
39 vec![ControlFinding::violated(
40 self.id(),
41 "Latest release does not include an SBOM artifact (SPDX or CycloneDX)",
42 vec!["release".to_string()],
43 )]
44 }
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51 use crate::control::ControlStatus;
52 use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
53
54 fn posture(has_sbom: bool) -> RepositoryPosture {
55 RepositoryPosture {
56 release_has_sbom: has_sbom,
57 ..Default::default()
58 }
59 }
60
61 fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
62 EvidenceBundle {
63 repository_posture: state,
64 ..Default::default()
65 }
66 }
67
68 #[test]
69 fn not_applicable_when_posture_not_applicable() {
70 let findings = SbomAttestationControl.evaluate(&bundle(EvidenceState::not_applicable()));
71 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
72 }
73
74 #[test]
75 fn indeterminate_when_posture_missing() {
76 let findings = SbomAttestationControl.evaluate(&bundle(EvidenceState::missing(vec![
77 EvidenceGap::CollectionFailed {
78 source: "github".to_string(),
79 subject: "posture".to_string(),
80 detail: "API error".to_string(),
81 },
82 ])));
83 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
84 }
85
86 #[test]
87 fn satisfied_when_sbom_present() {
88 let findings =
89 SbomAttestationControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
90 assert_eq!(findings[0].status, ControlStatus::Satisfied);
91 assert!(findings[0].rationale.contains("SBOM"));
92 }
93
94 #[test]
95 fn violated_when_sbom_absent() {
96 let findings =
97 SbomAttestationControl.evaluate(&bundle(EvidenceState::complete(posture(false))));
98 assert_eq!(findings[0].status, ControlStatus::Violated);
99 assert!(findings[0].rationale.contains("does not include"));
100 }
101}