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