libverify_core/controls/
secret_scanning.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4pub struct SecretScanningControl;
15
16impl Control for SecretScanningControl {
17 fn id(&self) -> ControlId {
18 builtin::id(builtin::SECRET_SCANNING)
19 }
20
21 fn description(&self) -> &'static str {
22 "Secret scanning must be enabled to prevent credential leakage"
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.security_analysis_available {
32 return vec![ControlFinding::indeterminate(
33 self.id(),
34 "Cannot determine secret scanning status — API token may lack sufficient permissions",
35 vec!["repository".to_string()],
36 vec![],
37 )];
38 }
39
40 if !posture.secret_scanning_enabled {
41 return vec![ControlFinding::violated(
42 self.id(),
43 "Secret scanning is not enabled — credentials may be exposed in source code",
44 vec!["repository".to_string()],
45 )];
46 }
47
48 if posture.secret_push_protection_enabled {
49 vec![ControlFinding::satisfied(
50 self.id(),
51 "Secret scanning with push protection is enabled",
52 vec!["repository:secret-scanning:prevention".to_string()],
53 )]
54 } else {
55 vec![ControlFinding::satisfied(
59 self.id(),
60 "Secret scanning is enabled (detection only — \
61 consider enabling push protection for prevention)",
62 vec!["repository:secret-scanning:detection".to_string()],
63 )]
64 }
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::control::ControlStatus;
72 use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
73
74 fn posture(secret_scanning: bool) -> RepositoryPosture {
75 RepositoryPosture {
76 security_analysis_available: true,
77 secret_scanning_enabled: secret_scanning,
78 ..Default::default()
79 }
80 }
81
82 fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
83 EvidenceBundle {
84 repository_posture: state,
85 ..Default::default()
86 }
87 }
88
89 #[test]
90 fn indeterminate_when_security_analysis_unavailable() {
91 let findings =
92 SecretScanningControl.evaluate(&bundle(EvidenceState::complete(RepositoryPosture {
93 security_analysis_available: false,
94 ..Default::default()
95 })));
96 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
97 assert!(findings[0].rationale.contains("permissions"));
98 }
99
100 #[test]
101 fn not_applicable_when_posture_not_applicable() {
102 let findings = SecretScanningControl.evaluate(&bundle(EvidenceState::not_applicable()));
103 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
104 }
105
106 #[test]
107 fn indeterminate_when_posture_missing() {
108 let findings = SecretScanningControl.evaluate(&bundle(EvidenceState::missing(vec![
109 EvidenceGap::CollectionFailed {
110 source: "github".to_string(),
111 subject: "posture".to_string(),
112 detail: "API error".to_string(),
113 },
114 ])));
115 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
116 }
117
118 #[test]
119 fn satisfied_when_enabled() {
120 let findings =
121 SecretScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
122 assert_eq!(findings[0].status, ControlStatus::Satisfied);
123 }
124
125 #[test]
126 fn violated_when_disabled() {
127 let findings =
128 SecretScanningControl.evaluate(&bundle(EvidenceState::complete(posture(false))));
129 assert_eq!(findings[0].status, ControlStatus::Violated);
130 assert!(findings[0].rationale.contains("not enabled"));
131 }
132
133 #[test]
134 fn satisfied_with_push_protection_has_prevention_tier() {
135 let mut p = posture(true);
136 p.secret_push_protection_enabled = true;
137 let findings = SecretScanningControl.evaluate(&bundle(EvidenceState::complete(p)));
138 assert_eq!(findings[0].status, ControlStatus::Satisfied);
139 assert!(findings[0].rationale.contains("push protection"));
140 assert!(findings[0].subjects[0].contains("prevention"));
141 }
142
143 #[test]
144 fn satisfied_detection_only_has_detection_tier() {
145 let findings =
146 SecretScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
147 assert_eq!(findings[0].status, ControlStatus::Satisfied);
148 assert!(findings[0].rationale.contains("detection only"));
149 assert!(findings[0].subjects[0].contains("detection"));
150 }
151}