Skip to main content

libverify_core/controls/
security_test_in_ci.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Validates that security testing (SAST/DAST) is integrated into CI.
5///
6/// Maps to UN-R155 Clause 7.2.2.2 (Security testing throughout lifecycle),
7/// NIST 800-53 SA-11 (Developer Testing and Evaluation).
8///
9/// Uses `code_scanning_enabled` as evidence — this is true when CodeQL
10/// or other SAST tools have produced at least one analysis result,
11/// indicating active security testing in the CI pipeline.
12pub struct SecurityTestInCiControl;
13
14impl Control for SecurityTestInCiControl {
15    fn id(&self) -> ControlId {
16        builtin::id(builtin::SECURITY_TEST_IN_CI)
17    }
18
19    fn description(&self) -> &'static str {
20        "Security testing (SAST/DAST) must be integrated into CI pipelines"
21    }
22
23    fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
24        let posture = match ControlFinding::extract_posture(self.id(), evidence) {
25            Ok(p) => p,
26            Err(findings) => return findings,
27        };
28
29        if !posture.security_analysis_available {
30            return vec![ControlFinding::indeterminate(
31                self.id(),
32                "Cannot determine security testing status — API token may lack sufficient permissions",
33                vec!["repository".into()],
34                vec![],
35            )];
36        }
37
38        if posture.code_scanning_enabled {
39            vec![ControlFinding::satisfied(
40                self.id(),
41                "Security testing is active in CI — code scanning analyses detected",
42                vec!["repository:code-scanning:ci".into()],
43            )]
44        } else {
45            vec![ControlFinding::violated(
46                self.id(),
47                "No security testing detected in CI — configure CodeQL, Semgrep, or other SAST tools in GitHub Actions",
48                vec!["repository:code-scanning:ci".into()],
49            )]
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::control::ControlStatus;
58    use crate::evidence::{EvidenceState, RepositoryPosture};
59
60    fn bundle_with(analysis_available: bool, code_scanning: bool) -> EvidenceBundle {
61        EvidenceBundle {
62            repository_posture: EvidenceState::complete(RepositoryPosture {
63                security_analysis_available: analysis_available,
64                code_scanning_enabled: code_scanning,
65                ..Default::default()
66            }),
67            ..Default::default()
68        }
69    }
70
71    #[test]
72    fn satisfied_when_code_scanning_enabled() {
73        let findings = SecurityTestInCiControl.evaluate(&bundle_with(true, true));
74        assert_eq!(findings[0].status, ControlStatus::Satisfied);
75    }
76
77    #[test]
78    fn violated_when_no_code_scanning() {
79        let findings = SecurityTestInCiControl.evaluate(&bundle_with(true, false));
80        assert_eq!(findings[0].status, ControlStatus::Violated);
81        assert!(findings[0].rationale.contains("No security testing"));
82    }
83
84    #[test]
85    fn indeterminate_when_api_unavailable() {
86        let findings = SecurityTestInCiControl.evaluate(&bundle_with(false, false));
87        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
88    }
89
90    #[test]
91    fn indeterminate_when_posture_missing() {
92        let findings = SecurityTestInCiControl.evaluate(&EvidenceBundle {
93            repository_posture: EvidenceState::missing(vec![]),
94            ..Default::default()
95        });
96        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
97    }
98}