libverify_core/controls/
secret_scanning_push_protection.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4pub 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.security_analysis_available {
29 return vec![ControlFinding::indeterminate(
30 self.id(),
31 "Cannot determine push protection status — API token may lack sufficient permissions",
32 vec!["repository".into()],
33 vec![],
34 )];
35 }
36
37 if !posture.secret_scanning_enabled {
38 return vec![ControlFinding::violated(
39 self.id(),
40 "Secret scanning is not enabled — push protection requires secret scanning",
41 vec!["repository".into()],
42 )];
43 }
44
45 if posture.secret_push_protection_enabled {
46 vec![ControlFinding::satisfied(
47 self.id(),
48 "Secret scanning push protection is enabled — credential commits are blocked",
49 vec!["repository:secret-scanning:push-protection".into()],
50 )]
51 } else {
52 vec![ControlFinding::violated(
53 self.id(),
54 "Secret scanning push protection is not enabled — credentials can be pushed to the repository",
55 vec!["repository:secret-scanning:push-protection".into()],
56 )]
57 }
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::control::ControlStatus;
65 use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
66
67 fn posture(secret_scanning: bool, push_protection: bool) -> RepositoryPosture {
68 RepositoryPosture {
69 security_analysis_available: true,
70 secret_scanning_enabled: secret_scanning,
71 secret_push_protection_enabled: push_protection,
72 ..Default::default()
73 }
74 }
75
76 fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
77 EvidenceBundle {
78 repository_posture: state,
79 ..Default::default()
80 }
81 }
82
83 #[test]
84 fn indeterminate_when_security_analysis_unavailable() {
85 let findings = SecretScanningPushProtectionControl.evaluate(&bundle(
86 EvidenceState::complete(RepositoryPosture {
87 security_analysis_available: false,
88 ..Default::default()
89 }),
90 ));
91 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
92 assert!(findings[0].rationale.contains("permissions"));
93 }
94
95 #[test]
96 fn not_applicable_when_posture_not_applicable() {
97 let findings =
98 SecretScanningPushProtectionControl.evaluate(&bundle(EvidenceState::not_applicable()));
99 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
100 }
101
102 #[test]
103 fn indeterminate_when_posture_missing() {
104 let findings =
105 SecretScanningPushProtectionControl.evaluate(&bundle(EvidenceState::missing(vec![
106 EvidenceGap::CollectionFailed {
107 source: "github".to_string(),
108 subject: "posture".to_string(),
109 detail: "API error".to_string(),
110 },
111 ])));
112 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
113 }
114
115 #[test]
116 fn satisfied_when_push_protection_enabled() {
117 let findings = SecretScanningPushProtectionControl
118 .evaluate(&bundle(EvidenceState::complete(posture(true, true))));
119 assert_eq!(findings[0].status, ControlStatus::Satisfied);
120 assert!(findings[0].rationale.contains("push protection is enabled"));
121 }
122
123 #[test]
124 fn violated_when_push_protection_disabled() {
125 let findings = SecretScanningPushProtectionControl
126 .evaluate(&bundle(EvidenceState::complete(posture(true, false))));
127 assert_eq!(findings[0].status, ControlStatus::Violated);
128 assert!(
129 findings[0]
130 .rationale
131 .contains("push protection is not enabled")
132 );
133 }
134
135 #[test]
136 fn violated_when_secret_scanning_disabled() {
137 let findings = SecretScanningPushProtectionControl
138 .evaluate(&bundle(EvidenceState::complete(posture(false, false))));
139 assert_eq!(findings[0].status, ControlStatus::Violated);
140 assert!(
141 findings[0]
142 .rationale
143 .contains("Secret scanning is not enabled")
144 );
145 }
146}