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 = 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}