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.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}