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.secret_scanning_enabled {
32 return vec![ControlFinding::violated(
33 self.id(),
34 "Secret scanning is not enabled — credentials may be exposed in source code",
35 vec!["repository".to_string()],
36 )];
37 }
38
39 if posture.secret_push_protection_enabled {
40 vec![ControlFinding::satisfied(
41 self.id(),
42 "Secret scanning with push protection is enabled",
43 vec!["repository:secret-scanning:prevention".to_string()],
44 )]
45 } else {
46 vec![ControlFinding::satisfied(
50 self.id(),
51 "Secret scanning is enabled (detection only — \
52 consider enabling push protection for prevention)",
53 vec!["repository:secret-scanning:detection".to_string()],
54 )]
55 }
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::control::ControlStatus;
63 use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
64
65 fn posture(secret_scanning: bool) -> RepositoryPosture {
66 RepositoryPosture {
67 secret_scanning_enabled: secret_scanning,
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 = SecretScanningControl.evaluate(&bundle(EvidenceState::not_applicable()));
82 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
83 }
84
85 #[test]
86 fn indeterminate_when_posture_missing() {
87 let findings = SecretScanningControl.evaluate(&bundle(EvidenceState::missing(vec![
88 EvidenceGap::CollectionFailed {
89 source: "github".to_string(),
90 subject: "posture".to_string(),
91 detail: "API error".to_string(),
92 },
93 ])));
94 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
95 }
96
97 #[test]
98 fn satisfied_when_enabled() {
99 let findings =
100 SecretScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
101 assert_eq!(findings[0].status, ControlStatus::Satisfied);
102 }
103
104 #[test]
105 fn violated_when_disabled() {
106 let findings =
107 SecretScanningControl.evaluate(&bundle(EvidenceState::complete(posture(false))));
108 assert_eq!(findings[0].status, ControlStatus::Violated);
109 assert!(findings[0].rationale.contains("not enabled"));
110 }
111
112 #[test]
113 fn satisfied_with_push_protection_has_prevention_tier() {
114 let mut p = posture(true);
115 p.secret_push_protection_enabled = true;
116 let findings = SecretScanningControl.evaluate(&bundle(EvidenceState::complete(p)));
117 assert_eq!(findings[0].status, ControlStatus::Satisfied);
118 assert!(findings[0].rationale.contains("push protection"));
119 assert!(findings[0].subjects[0].contains("prevention"));
120 }
121
122 #[test]
123 fn satisfied_detection_only_has_detection_tier() {
124 let findings =
125 SecretScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
126 assert_eq!(findings[0].status, ControlStatus::Satisfied);
127 assert!(findings[0].rationale.contains("detection only"));
128 assert!(findings[0].subjects[0].contains("detection"));
129 }
130}