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 codeowners_entries: vec![],
64 secret_scanning_enabled: false,
65 secret_push_protection_enabled: false,
66 vulnerability_scanning_enabled: vuln_scanning,
67 code_scanning_enabled: false,
68 security_policy_present: false,
69 security_policy_has_disclosure: false,
70 default_branch_protected: false,
71 }
72 }
73
74 fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
75 EvidenceBundle {
76 repository_posture: state,
77 ..Default::default()
78 }
79 }
80
81 #[test]
82 fn not_applicable_when_posture_not_applicable() {
83 let findings =
84 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::not_applicable()));
85 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
86 }
87
88 #[test]
89 fn indeterminate_when_posture_missing() {
90 let findings =
91 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::missing(vec![
92 EvidenceGap::CollectionFailed {
93 source: "github".to_string(),
94 subject: "posture".to_string(),
95 detail: "API error".to_string(),
96 },
97 ])));
98 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
99 }
100
101 #[test]
102 fn satisfied_when_enabled() {
103 let findings =
104 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
105 assert_eq!(findings[0].status, ControlStatus::Satisfied);
106 }
107
108 #[test]
109 fn violated_when_disabled() {
110 let findings =
111 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(false))));
112 assert_eq!(findings[0].status, ControlStatus::Violated);
113 assert!(findings[0].rationale.contains("not enabled"));
114 }
115
116 #[test]
117 fn satisfied_with_code_scanning_has_sast_tier() {
118 let mut p = posture(true);
119 p.code_scanning_enabled = true;
120 let findings = VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(p)));
121 assert_eq!(findings[0].status, ControlStatus::Satisfied);
122 assert!(findings[0].rationale.contains("code scanning"));
123 assert!(findings[0].subjects[0].contains("sca+sast"));
124 }
125
126 #[test]
127 fn satisfied_sca_only_has_sca_tier() {
128 let findings =
129 VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
130 assert_eq!(findings[0].status, ControlStatus::Satisfied);
131 assert!(
132 findings[0]
133 .rationale
134 .contains("consider enabling code scanning")
135 );
136 assert!(findings[0].subjects[0].contains("sca-only"));
137 }
138}