libverify-core 0.11.0

Platform-agnostic SDLC verification engine — evidence model, controls, assessment
Documentation
use crate::control::{Control, ControlFinding, ControlId, builtin};
use crate::evidence::EvidenceBundle;

/// Validates that dependency vulnerability scanning is enabled on the repository.
///
/// Maps to SOC2 CC7.1: detect vulnerabilities in third-party components.
/// ASPM signal — continuous vulnerability scanning ensures known CVEs in
/// dependencies are flagged before they reach production.
///
/// Evaluates both dependency scanning (SCA) and code scanning (SAST) when available.
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 {
            // SCA enabled but no SAST — still satisfied, note the gap
            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"));
    }
}