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.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            // SCA enabled but no SAST — still satisfied, note the gap
54            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 =
88            VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(RepositoryPosture {
89                security_analysis_available: false,
90                ..Default::default()
91            })));
92        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
93        assert!(findings[0].rationale.contains("permissions"));
94    }
95
96    #[test]
97    fn not_applicable_when_posture_not_applicable() {
98        let findings =
99            VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::not_applicable()));
100        assert_eq!(findings[0].status, ControlStatus::NotApplicable);
101    }
102
103    #[test]
104    fn indeterminate_when_posture_missing() {
105        let findings =
106            VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::missing(vec![
107                EvidenceGap::CollectionFailed {
108                    source: "github".to_string(),
109                    subject: "posture".to_string(),
110                    detail: "API error".to_string(),
111                },
112            ])));
113        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
114    }
115
116    #[test]
117    fn satisfied_when_enabled() {
118        let findings =
119            VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
120        assert_eq!(findings[0].status, ControlStatus::Satisfied);
121    }
122
123    #[test]
124    fn violated_when_disabled() {
125        let findings =
126            VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(false))));
127        assert_eq!(findings[0].status, ControlStatus::Violated);
128        assert!(findings[0].rationale.contains("not enabled"));
129    }
130
131    #[test]
132    fn satisfied_with_code_scanning_has_sast_tier() {
133        let mut p = posture(true);
134        p.code_scanning_enabled = true;
135        let findings = VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(p)));
136        assert_eq!(findings[0].status, ControlStatus::Satisfied);
137        assert!(findings[0].rationale.contains("code scanning"));
138        assert!(findings[0].subjects[0].contains("sca+sast"));
139    }
140
141    #[test]
142    fn satisfied_sca_only_has_sca_tier() {
143        let findings =
144            VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
145        assert_eq!(findings[0].status, ControlStatus::Satisfied);
146        assert!(
147            findings[0]
148                .rationale
149                .contains("consider enabling code scanning")
150        );
151        assert!(findings[0].subjects[0].contains("sca-only"));
152    }
153}