Skip to main content

libverify_core/controls/
release_asset_attestation.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Validates that the latest release assets have build provenance attestations
5/// (GitHub Attestations / Sigstore).
6///
7/// Maps to SOC2 PI1.4: processing integrity through artifact provenance.
8/// Build provenance attestations bind release binaries to the source commit
9/// and CI workflow that produced them, enabling consumers to verify that
10/// artifacts were not tampered with after build.
11///
12/// Evaluation tiers:
13/// - **Satisfied**: release assets have attestations
14/// - **Violated**: release assets exist but lack attestations
15/// - **NotApplicable**: no release exists
16pub struct ReleaseAssetAttestationControl;
17
18impl Control for ReleaseAssetAttestationControl {
19    fn id(&self) -> ControlId {
20        builtin::id(builtin::RELEASE_ASSET_ATTESTATION)
21    }
22
23    fn description(&self) -> &'static str {
24        "Latest release assets must have build provenance attestations (Sigstore)"
25    }
26
27    fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
28        let posture = match ControlFinding::extract_posture(self.id(), evidence) {
29            Ok(p) => p,
30            Err(findings) => return findings,
31        };
32
33        if posture.release_assets_attested {
34            vec![ControlFinding::satisfied(
35                self.id(),
36                "Latest release assets have build provenance attestations",
37                vec!["release:attestation".to_string()],
38            )]
39        } else {
40            vec![ControlFinding::violated(
41                self.id(),
42                "Latest release assets lack build provenance attestations — \
43                 consumers cannot verify artifact integrity",
44                vec!["release".to_string()],
45            )]
46        }
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::control::ControlStatus;
54    use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
55
56    fn posture(attested: bool) -> RepositoryPosture {
57        RepositoryPosture {
58            release_assets_attested: attested,
59            ..Default::default()
60        }
61    }
62
63    fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
64        EvidenceBundle {
65            repository_posture: state,
66            ..Default::default()
67        }
68    }
69
70    #[test]
71    fn not_applicable_when_posture_not_applicable() {
72        let findings =
73            ReleaseAssetAttestationControl.evaluate(&bundle(EvidenceState::not_applicable()));
74        assert_eq!(findings[0].status, ControlStatus::NotApplicable);
75    }
76
77    #[test]
78    fn indeterminate_when_posture_missing() {
79        let findings =
80            ReleaseAssetAttestationControl.evaluate(&bundle(EvidenceState::missing(vec![
81                EvidenceGap::CollectionFailed {
82                    source: "github".to_string(),
83                    subject: "posture".to_string(),
84                    detail: "API error".to_string(),
85                },
86            ])));
87        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
88    }
89
90    #[test]
91    fn satisfied_when_attested() {
92        let findings = ReleaseAssetAttestationControl
93            .evaluate(&bundle(EvidenceState::complete(posture(true))));
94        assert_eq!(findings[0].status, ControlStatus::Satisfied);
95        assert!(findings[0].rationale.contains("attestations"));
96    }
97
98    #[test]
99    fn violated_when_not_attested() {
100        let findings = ReleaseAssetAttestationControl
101            .evaluate(&bundle(EvidenceState::complete(posture(false))));
102        assert_eq!(findings[0].status, ControlStatus::Violated);
103        assert!(findings[0].rationale.contains("lack"));
104    }
105}