Skip to main content

libverify_core/controls/
hosted_build_platform.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::{EvidenceBundle, EvidenceState};
3use crate::integrity::hosted_build_severity;
4use crate::verdict::Severity;
5
6/// Verifies that all builds run on hosted infrastructure, not developer workstations.
7pub 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    // --- NotApplicable ---
95
96    #[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    // --- Indeterminate ---
119
120    #[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    // --- Satisfied ---
137
138    #[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    // --- Violated ---
163
164    #[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    // --- Edge cases ---
188
189    #[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}