Skip to main content

libverify_core/controls/
vulnerability_scanning.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Validates that dependency vulnerability scanning is enabled on the repository.
5///
6/// Maps to SOC2 CC7.1: detect vulnerabilities in third-party components.
7/// ASPM signal — continuous vulnerability scanning ensures known CVEs in
8/// dependencies are flagged before they reach production.
9///
10/// Evaluates both dependency scanning (SCA) and code scanning (SAST) when available.
11pub 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            // SCA enabled but no SAST — still satisfied, note the gap
45            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}