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 =
71 SbomAttestationControl.evaluate(&bundle(EvidenceState::not_applicable()));
72 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
73 }
74
75 #[test]
76 fn indeterminate_when_posture_missing() {
77 let findings =
78 SbomAttestationControl.evaluate(&bundle(EvidenceState::missing(vec![
79 EvidenceGap::CollectionFailed {
80 source: "github".to_string(),
81 subject: "posture".to_string(),
82 detail: "API error".to_string(),
83 },
84 ])));
85 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
86 }
87
88 #[test]
89 fn satisfied_when_sbom_present() {
90 let findings =
91 SbomAttestationControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
92 assert_eq!(findings[0].status, ControlStatus::Satisfied);
93 assert!(findings[0].rationale.contains("SBOM"));
94 }
95
96 #[test]
97 fn violated_when_sbom_absent() {
98 let findings =
99 SbomAttestationControl.evaluate(&bundle(EvidenceState::complete(posture(false))));
100 assert_eq!(findings[0].status, ControlStatus::Violated);
101 assert!(findings[0].rationale.contains("does not include"));
102 }
103}