libverify_core/controls/
environment_protection_rules.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4pub struct EnvironmentProtectionRulesControl;
15
16impl Control for EnvironmentProtectionRulesControl {
17 fn id(&self) -> ControlId {
18 builtin::id(builtin::ENVIRONMENT_PROTECTION_RULES)
19 }
20
21 fn description(&self) -> &'static str {
22 "Production environments must have required reviewer protection rules"
23 }
24
25 fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
26 let posture = match ControlFinding::extract_posture(self.id(), evidence) {
27 Ok(p) => p,
28 Err(findings) => return findings,
29 };
30
31 if !posture.default_branch_protected {
32 return vec![ControlFinding::indeterminate(
33 self.id(),
34 "Branch protection is not configured — \
35 cannot assess environment protection rules without baseline branch controls",
36 vec!["repository:branch-protection:missing".to_string()],
37 vec![],
38 )];
39 }
40
41 if posture.production_environment_protected {
42 vec![ControlFinding::satisfied(
43 self.id(),
44 "Production environment has required reviewer protection rules configured",
45 vec!["repository:environment:production:protected".to_string()],
46 )]
47 } else {
48 vec![ControlFinding::violated(
49 self.id(),
50 "Production environment lacks required reviewer protection rules — \
51 deployments can proceed without approval",
52 vec!["repository:environment:production:unprotected".to_string()],
53 )]
54 }
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::control::ControlStatus;
62 use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
63
64 fn posture(branch_protected: bool, env_protected: bool) -> RepositoryPosture {
65 RepositoryPosture {
66 default_branch_protected: branch_protected,
67 production_environment_protected: env_protected,
68 ..Default::default()
69 }
70 }
71
72 fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
73 EvidenceBundle {
74 repository_posture: state,
75 ..Default::default()
76 }
77 }
78
79 #[test]
80 fn not_applicable_when_posture_not_applicable() {
81 let findings =
82 EnvironmentProtectionRulesControl.evaluate(&bundle(EvidenceState::not_applicable()));
83 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
84 }
85
86 #[test]
87 fn indeterminate_when_posture_missing() {
88 let findings =
89 EnvironmentProtectionRulesControl.evaluate(&bundle(EvidenceState::missing(vec![
90 EvidenceGap::CollectionFailed {
91 source: "github".to_string(),
92 subject: "posture".to_string(),
93 detail: "API error".to_string(),
94 },
95 ])));
96 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
97 }
98
99 #[test]
100 fn indeterminate_when_branch_protection_missing() {
101 let findings = EnvironmentProtectionRulesControl
102 .evaluate(&bundle(EvidenceState::complete(posture(false, false))));
103 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
104 assert!(findings[0].rationale.contains("Branch protection"));
105 }
106
107 #[test]
108 fn satisfied_when_environment_protected() {
109 let findings = EnvironmentProtectionRulesControl
110 .evaluate(&bundle(EvidenceState::complete(posture(true, true))));
111 assert_eq!(findings[0].status, ControlStatus::Satisfied);
112 assert!(findings[0].rationale.contains("required reviewer"));
113 }
114
115 #[test]
116 fn violated_when_environment_not_protected() {
117 let findings = EnvironmentProtectionRulesControl
118 .evaluate(&bundle(EvidenceState::complete(posture(true, false))));
119 assert_eq!(findings[0].status, ControlStatus::Violated);
120 assert!(findings[0].rationale.contains("lacks required reviewer"));
121 }
122}