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.vulnerability_scanning_enabled {
29 return vec![ControlFinding::violated(
30 self.id(),
31 "Dependency vulnerability scanning is not enabled — \
32 known CVEs in dependencies may go undetected",
33 vec!["repository".to_string()],
34 )];
35 }
36
37 if posture.code_scanning_enabled {
38 vec![ControlFinding::satisfied(
39 self.id(),
40 "Dependency vulnerability scanning and code scanning (SAST) are both enabled",
41 vec!["repository:vulnerability-scanning:sca+sast".to_string()],
42 )]
43 } else {
44 vec![ControlFinding::satisfied(
46 self.id(),
47 "Dependency vulnerability scanning is enabled \
48 (consider enabling code scanning / SAST for source-level coverage)",
49 vec!["repository:vulnerability-scanning:sca-only".to_string()],
50 )]
51 }
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::control::ControlStatus;
59 use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
60
61 fn posture(vuln_scanning: bool) -> RepositoryPosture {
62 RepositoryPosture {
63 vulnerability_scanning_enabled: vuln_scanning,
64 ..Default::default()
65 }
66 }
67
68 fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
69 EvidenceBundle {
70 repository_posture: state,
71 ..Default::default()
72 }
73 }
74
75 #[test]
76 fn not_applicable_when_posture_not_applicable() {
77 let findings =
78 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::not_applicable()));
79 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
80 }
81
82 #[test]
83 fn indeterminate_when_posture_missing() {
84 let findings =
85 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::missing(vec![
86 EvidenceGap::CollectionFailed {
87 source: "github".to_string(),
88 subject: "posture".to_string(),
89 detail: "API error".to_string(),
90 },
91 ])));
92 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
93 }
94
95 #[test]
96 fn satisfied_when_enabled() {
97 let findings =
98 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
99 assert_eq!(findings[0].status, ControlStatus::Satisfied);
100 }
101
102 #[test]
103 fn violated_when_disabled() {
104 let findings =
105 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(false))));
106 assert_eq!(findings[0].status, ControlStatus::Violated);
107 assert!(findings[0].rationale.contains("not enabled"));
108 }
109
110 #[test]
111 fn satisfied_with_code_scanning_has_sast_tier() {
112 let mut p = posture(true);
113 p.code_scanning_enabled = true;
114 let findings = VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(p)));
115 assert_eq!(findings[0].status, ControlStatus::Satisfied);
116 assert!(findings[0].rationale.contains("code scanning"));
117 assert!(findings[0].subjects[0].contains("sca+sast"));
118 }
119
120 #[test]
121 fn satisfied_sca_only_has_sca_tier() {
122 let findings =
123 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
124 assert_eq!(findings[0].status, ControlStatus::Satisfied);
125 assert!(
126 findings[0]
127 .rationale
128 .contains("consider enabling code scanning")
129 );
130 assert!(findings[0].subjects[0].contains("sca-only"));
131 }
132}