Skip to main content

libverify_core/controls/
secret_scanning_push_protection.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Validates that secret scanning push protection is enabled.
5///
6/// Maps to PCI DSS Req 3.5.1, NIST SI-7, SOC2 CC6.1 / CC6.6.
7/// Push protection actively blocks credential commits at push time,
8/// going beyond detection-only secret scanning.
9///
10/// Requires secret scanning to be enabled as a prerequisite.
11pub struct SecretScanningPushProtectionControl;
12
13impl Control for SecretScanningPushProtectionControl {
14    fn id(&self) -> ControlId {
15        builtin::id(builtin::SECRET_SCANNING_PUSH_PROTECTION)
16    }
17
18    fn description(&self) -> &'static str {
19        "Secret scanning push protection must be enabled to block credential commits"
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.secret_scanning_enabled {
29            return vec![ControlFinding::violated(
30                self.id(),
31                "Secret scanning is not enabled — push protection requires secret scanning",
32                vec!["repository".into()],
33            )];
34        }
35
36        if posture.secret_push_protection_enabled {
37            vec![ControlFinding::satisfied(
38                self.id(),
39                "Secret scanning push protection is enabled — credential commits are blocked",
40                vec!["repository:secret-scanning:push-protection".into()],
41            )]
42        } else {
43            vec![ControlFinding::violated(
44                self.id(),
45                "Secret scanning push protection is not enabled — credentials can be pushed to the repository",
46                vec!["repository:secret-scanning:push-protection".into()],
47            )]
48        }
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::control::ControlStatus;
56    use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
57
58    fn posture(secret_scanning: bool, push_protection: bool) -> RepositoryPosture {
59        RepositoryPosture {
60            secret_scanning_enabled: secret_scanning,
61            secret_push_protection_enabled: push_protection,
62            ..Default::default()
63        }
64    }
65
66    fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
67        EvidenceBundle {
68            repository_posture: state,
69            ..Default::default()
70        }
71    }
72
73    #[test]
74    fn not_applicable_when_posture_not_applicable() {
75        let findings =
76            SecretScanningPushProtectionControl.evaluate(&bundle(EvidenceState::not_applicable()));
77        assert_eq!(findings[0].status, ControlStatus::NotApplicable);
78    }
79
80    #[test]
81    fn indeterminate_when_posture_missing() {
82        let findings =
83            SecretScanningPushProtectionControl.evaluate(&bundle(EvidenceState::missing(vec![
84                EvidenceGap::CollectionFailed {
85                    source: "github".to_string(),
86                    subject: "posture".to_string(),
87                    detail: "API error".to_string(),
88                },
89            ])));
90        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
91    }
92
93    #[test]
94    fn satisfied_when_push_protection_enabled() {
95        let findings = SecretScanningPushProtectionControl
96            .evaluate(&bundle(EvidenceState::complete(posture(true, true))));
97        assert_eq!(findings[0].status, ControlStatus::Satisfied);
98        assert!(findings[0].rationale.contains("push protection is enabled"));
99    }
100
101    #[test]
102    fn violated_when_push_protection_disabled() {
103        let findings = SecretScanningPushProtectionControl
104            .evaluate(&bundle(EvidenceState::complete(posture(true, false))));
105        assert_eq!(findings[0].status, ControlStatus::Violated);
106        assert!(findings[0]
107            .rationale
108            .contains("push protection is not enabled"));
109    }
110
111    #[test]
112    fn violated_when_secret_scanning_disabled() {
113        let findings = SecretScanningPushProtectionControl
114            .evaluate(&bundle(EvidenceState::complete(posture(false, false))));
115        assert_eq!(findings[0].status, ControlStatus::Violated);
116        assert!(findings[0]
117            .rationale
118            .contains("Secret scanning is not enabled"));
119    }
120}