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