libverify_core/controls/
hosted_build_platform.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::{EvidenceBundle, EvidenceState};
3use crate::integrity::hosted_build_severity;
4use crate::verdict::Severity;
5
6pub struct HostedBuildPlatformControl;
8
9impl Control for HostedBuildPlatformControl {
10 fn id(&self) -> ControlId {
11 builtin::id(builtin::HOSTED_BUILD_PLATFORM)
12 }
13
14 fn description(&self) -> &'static str {
15 "Build must run on a hosted platform, not a developer workstation"
16 }
17
18 fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
19 let id = self.id();
20
21 match &evidence.build_platform {
22 EvidenceState::NotApplicable => {
23 vec![ControlFinding::not_applicable(
24 id,
25 "Build platform evidence is not applicable",
26 )]
27 }
28 EvidenceState::Missing { gaps } => {
29 vec![ControlFinding::indeterminate(
30 id,
31 "Build platform evidence could not be collected",
32 Vec::new(),
33 gaps.clone(),
34 )]
35 }
36 EvidenceState::Complete { value } | EvidenceState::Partial { value, .. } => {
37 if value.is_empty() {
38 return vec![ControlFinding::not_applicable(
39 id,
40 "No build platform evidence was present",
41 )];
42 }
43
44 let subjects: Vec<String> = value.iter().map(|p| p.platform.clone()).collect();
45
46 let non_hosted: Vec<&str> = value
47 .iter()
48 .filter(|p| !p.hosted)
49 .map(|p| p.platform.as_str())
50 .collect();
51
52 let finding = match hosted_build_severity(non_hosted.len()) {
53 Severity::Pass => ControlFinding::satisfied(
54 id,
55 format!("All {} build platform(s) are hosted", value.len()),
56 subjects,
57 ),
58 _ => ControlFinding::violated(
59 id,
60 format!("Non-hosted build platform(s): {}", non_hosted.join(", ")),
61 subjects,
62 ),
63 };
64 vec![finding]
65 }
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::control::ControlStatus;
74 use crate::evidence::{BuildPlatformEvidence, EvidenceGap};
75
76 fn make_platform(name: &str, hosted: bool) -> BuildPlatformEvidence {
77 BuildPlatformEvidence {
78 platform: name.to_string(),
79 hosted,
80 ephemeral: true,
81 isolated: true,
82 runner_labels: vec!["ubuntu-latest".to_string()],
83 signing_key_isolated: true,
84 }
85 }
86
87 fn make_bundle(platforms: Vec<BuildPlatformEvidence>) -> EvidenceBundle {
88 EvidenceBundle {
89 build_platform: EvidenceState::complete(platforms),
90 ..Default::default()
91 }
92 }
93
94 #[test]
97 fn not_applicable_when_evidence_state_is_not_applicable() {
98 let evidence = EvidenceBundle {
99 build_platform: EvidenceState::not_applicable(),
100 ..Default::default()
101 };
102 let findings = HostedBuildPlatformControl.evaluate(&evidence);
103 assert_eq!(findings.len(), 1);
104 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
105 assert_eq!(
106 findings[0].control_id,
107 builtin::id(builtin::HOSTED_BUILD_PLATFORM)
108 );
109 }
110
111 #[test]
112 fn not_applicable_when_platform_list_empty() {
113 let findings = HostedBuildPlatformControl.evaluate(&make_bundle(vec![]));
114 assert_eq!(findings.len(), 1);
115 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
116 }
117
118 #[test]
121 fn indeterminate_when_evidence_missing() {
122 let evidence = EvidenceBundle {
123 build_platform: EvidenceState::missing(vec![EvidenceGap::CollectionFailed {
124 source: "github".to_string(),
125 subject: "build-platform".to_string(),
126 detail: "API returned 403".to_string(),
127 }]),
128 ..Default::default()
129 };
130 let findings = HostedBuildPlatformControl.evaluate(&evidence);
131 assert_eq!(findings.len(), 1);
132 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
133 assert_eq!(findings[0].evidence_gaps.len(), 1);
134 }
135
136 #[test]
139 fn satisfied_when_all_platforms_hosted() {
140 let findings = HostedBuildPlatformControl.evaluate(&make_bundle(vec![
141 make_platform("github-actions", true),
142 make_platform("cloud-build", true),
143 ]));
144 assert_eq!(findings.len(), 1);
145 assert_eq!(findings[0].status, ControlStatus::Satisfied);
146 assert_eq!(findings[0].subjects.len(), 2);
147 assert!(
148 findings[0]
149 .rationale
150 .contains("2 build platform(s) are hosted")
151 );
152 }
153
154 #[test]
155 fn satisfied_single_hosted_platform() {
156 let findings = HostedBuildPlatformControl
157 .evaluate(&make_bundle(vec![make_platform("github-actions", true)]));
158 assert_eq!(findings[0].status, ControlStatus::Satisfied);
159 assert_eq!(findings[0].subjects, vec!["github-actions"]);
160 }
161
162 #[test]
165 fn violated_when_any_platform_not_hosted() {
166 let findings = HostedBuildPlatformControl.evaluate(&make_bundle(vec![
167 make_platform("github-actions", true),
168 make_platform("developer-laptop", false),
169 ]));
170 assert_eq!(findings.len(), 1);
171 assert_eq!(findings[0].status, ControlStatus::Violated);
172 assert!(findings[0].rationale.contains("developer-laptop"));
173 assert_eq!(findings[0].subjects.len(), 2);
174 }
175
176 #[test]
177 fn violated_when_all_platforms_not_hosted() {
178 let findings = HostedBuildPlatformControl.evaluate(&make_bundle(vec![
179 make_platform("local-runner-a", false),
180 make_platform("local-runner-b", false),
181 ]));
182 assert_eq!(findings[0].status, ControlStatus::Violated);
183 assert!(findings[0].rationale.contains("local-runner-a"));
184 assert!(findings[0].rationale.contains("local-runner-b"));
185 }
186
187 #[test]
190 fn partial_evidence_with_hosted_platforms_satisfied() {
191 let evidence = EvidenceBundle {
192 build_platform: EvidenceState::partial(
193 vec![make_platform("github-actions", true)],
194 vec![EvidenceGap::Truncated {
195 source: "github".to_string(),
196 subject: "build-platforms".to_string(),
197 }],
198 ),
199 ..Default::default()
200 };
201 let findings = HostedBuildPlatformControl.evaluate(&evidence);
202 assert_eq!(findings[0].status, ControlStatus::Satisfied);
203 }
204
205 #[test]
206 fn partial_evidence_with_non_hosted_platform_violated() {
207 let evidence = EvidenceBundle {
208 build_platform: EvidenceState::partial(
209 vec![make_platform("self-hosted-runner", false)],
210 vec![EvidenceGap::Truncated {
211 source: "github".to_string(),
212 subject: "build-platforms".to_string(),
213 }],
214 ),
215 ..Default::default()
216 };
217 let findings = HostedBuildPlatformControl.evaluate(&evidence);
218 assert_eq!(findings[0].status, ControlStatus::Violated);
219 }
220
221 #[test]
222 fn correct_control_id() {
223 assert_eq!(
224 HostedBuildPlatformControl.id(),
225 builtin::id(builtin::HOSTED_BUILD_PLATFORM)
226 );
227 }
228}