Skip to main content

libverify_core/controls/
environment_protection_rules.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Validates that production/release environments have required reviewer protection rules.
5///
6/// Maps to SOC2 CC6.1 / CC8.1: enforce separation of duties for production deployments.
7/// GitHub environment protection rules ensure that deployments to production require
8/// explicit approval from designated reviewers.
9///
10/// Evaluation:
11/// - **Satisfied**: production environment has required reviewer rules
12/// - **Violated**: production environment lacks required reviewer rules
13/// - **Indeterminate**: branch protection is not configured (cannot assess environment rules)
14pub 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}