libverify_core/controls/
workflow_permissions_restricted.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4pub struct WorkflowPermissionsRestrictedControl;
12
13impl Control for WorkflowPermissionsRestrictedControl {
14 fn id(&self) -> ControlId {
15 builtin::id(builtin::WORKFLOW_PERMISSIONS_RESTRICTED)
16 }
17
18 fn description(&self) -> &'static str {
19 "Default workflow permissions must be restricted to read-only"
20 }
21
22 fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
23 let posture = match ControlFinding::extract_posture(self.id(), evidence) {
24 Ok(p) => p,
25 Err(findings) => return findings,
26 };
27
28 if posture.default_workflow_permissions.is_empty() {
29 return vec![ControlFinding::indeterminate(
30 self.id(),
31 "Cannot determine default workflow permissions — API token may lack admin permissions",
32 vec!["repository:workflow-permissions".into()],
33 vec![],
34 )];
35 }
36
37 if posture.default_workflow_permissions == "read" {
38 vec![ControlFinding::satisfied(
39 self.id(),
40 "Default workflow permissions are set to read-only",
41 vec!["repository:workflow-permissions".into()],
42 )]
43 } else {
44 vec![ControlFinding::violated(
45 self.id(),
46 format!(
47 "Default workflow permissions are '{}' — should be 'read' for least privilege",
48 posture.default_workflow_permissions
49 ),
50 vec!["repository:workflow-permissions".into()],
51 )]
52 }
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use crate::control::ControlStatus;
60 use crate::evidence::{EvidenceState, RepositoryPosture};
61
62 fn bundle_with_perms(perms: &str) -> EvidenceBundle {
63 EvidenceBundle {
64 repository_posture: EvidenceState::complete(RepositoryPosture {
65 default_workflow_permissions: perms.to_string(),
66 ..Default::default()
67 }),
68 ..Default::default()
69 }
70 }
71
72 #[test]
73 fn satisfied_when_read_only() {
74 let findings = WorkflowPermissionsRestrictedControl.evaluate(&bundle_with_perms("read"));
75 assert_eq!(findings[0].status, ControlStatus::Satisfied);
76 }
77
78 #[test]
79 fn violated_when_write() {
80 let findings = WorkflowPermissionsRestrictedControl.evaluate(&bundle_with_perms("write"));
81 assert_eq!(findings[0].status, ControlStatus::Violated);
82 assert!(findings[0].rationale.contains("write"));
83 }
84
85 #[test]
86 fn indeterminate_when_empty() {
87 let findings = WorkflowPermissionsRestrictedControl.evaluate(&bundle_with_perms(""));
88 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
89 }
90
91 #[test]
92 fn indeterminate_when_posture_missing() {
93 let findings = WorkflowPermissionsRestrictedControl.evaluate(&EvidenceBundle {
94 repository_posture: EvidenceState::missing(vec![]),
95 ..Default::default()
96 });
97 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
98 }
99
100 #[test]
101 fn not_applicable_when_posture_not_applicable() {
102 let findings = WorkflowPermissionsRestrictedControl.evaluate(&EvidenceBundle {
103 repository_posture: EvidenceState::not_applicable(),
104 ..Default::default()
105 });
106 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
107 }
108}