libverify_core/controls/
security_test_in_ci.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4pub 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}