Skip to main content

libverify_core/controls/
sbom_completeness.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Validates that releases include a Software Bill of Materials (SBOM).
5///
6/// Maps to SOC2 CC7.1 / PI1.4: monitor system components and maintain
7/// processing integrity. SBOMs enable vulnerability tracking and supply
8/// chain transparency for released artifacts.
9///
10/// Evaluation:
11/// - **Satisfied**: release includes an SBOM
12/// - **Violated**: release does not include an SBOM
13pub 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}