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