libverify_core/controls/
release_asset_attestation.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4pub 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}