use crate::control::{Control, ControlFinding, ControlId, builtin};
use crate::evidence::EvidenceBundle;
pub struct VulnerabilityScanningControl;
impl Control for VulnerabilityScanningControl {
fn id(&self) -> ControlId {
builtin::id(builtin::VULNERABILITY_SCANNING)
}
fn description(&self) -> &'static str {
"Dependency vulnerability scanning must be enabled to detect known CVEs"
}
fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
let posture = match ControlFinding::extract_posture(self.id(), evidence) {
Ok(p) => p,
Err(findings) => return findings,
};
if !posture.security_analysis_available {
return vec![ControlFinding::indeterminate(
self.id(),
"Cannot determine vulnerability scanning status — API token may lack sufficient permissions",
vec!["repository".to_string()],
vec![],
)];
}
if !posture.vulnerability_scanning_enabled {
return vec![ControlFinding::violated(
self.id(),
"Dependency vulnerability scanning is not enabled — \
known CVEs in dependencies may go undetected",
vec!["repository".to_string()],
)];
}
if posture.code_scanning_enabled {
vec![ControlFinding::satisfied(
self.id(),
"Dependency vulnerability scanning and code scanning (SAST) are both enabled",
vec!["repository:vulnerability-scanning:sca+sast".to_string()],
)]
} else {
vec![ControlFinding::satisfied(
self.id(),
"Dependency vulnerability scanning is enabled \
(consider enabling code scanning / SAST for source-level coverage)",
vec!["repository:vulnerability-scanning:sca-only".to_string()],
)]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::control::ControlStatus;
use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
fn posture(vuln_scanning: bool) -> RepositoryPosture {
RepositoryPosture {
security_analysis_available: true,
vulnerability_scanning_enabled: vuln_scanning,
..Default::default()
}
}
fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
EvidenceBundle {
repository_posture: state,
..Default::default()
}
}
#[test]
fn indeterminate_when_security_analysis_unavailable() {
let findings = VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(
RepositoryPosture {
security_analysis_available: false,
..Default::default()
},
)));
assert_eq!(findings[0].status, ControlStatus::Indeterminate);
assert!(findings[0].rationale.contains("permissions"));
}
#[test]
fn not_applicable_when_posture_not_applicable() {
let findings =
VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::not_applicable()));
assert_eq!(findings[0].status, ControlStatus::NotApplicable);
}
#[test]
fn indeterminate_when_posture_missing() {
let findings =
VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::missing(vec![
EvidenceGap::CollectionFailed {
source: "github".to_string(),
subject: "posture".to_string(),
detail: "API error".to_string(),
},
])));
assert_eq!(findings[0].status, ControlStatus::Indeterminate);
}
#[test]
fn satisfied_when_enabled() {
let findings =
VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
assert_eq!(findings[0].status, ControlStatus::Satisfied);
}
#[test]
fn violated_when_disabled() {
let findings =
VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(false))));
assert_eq!(findings[0].status, ControlStatus::Violated);
assert!(findings[0].rationale.contains("not enabled"));
}
#[test]
fn satisfied_with_code_scanning_has_sast_tier() {
let mut p = posture(true);
p.code_scanning_enabled = true;
let findings = VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(p)));
assert_eq!(findings[0].status, ControlStatus::Satisfied);
assert!(findings[0].rationale.contains("code scanning"));
assert!(findings[0].subjects[0].contains("sca+sast"));
}
#[test]
fn satisfied_sca_only_has_sca_tier() {
let findings =
VulnerabilityScanningControl.evaluate(&bundle(EvidenceState::complete(posture(true))));
assert_eq!(findings[0].status, ControlStatus::Satisfied);
assert!(
findings[0]
.rationale
.contains("consider enabling code scanning")
);
assert!(findings[0].subjects[0].contains("sca-only"));
}
}