libverify_core/controls/
security_policy.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4pub struct SecurityPolicyControl;
16
17impl Control for SecurityPolicyControl {
18 fn id(&self) -> ControlId {
19 builtin::id(builtin::SECURITY_POLICY)
20 }
21
22 fn description(&self) -> &'static str {
23 "A security policy (SECURITY.md) with responsible disclosure process must exist"
24 }
25
26 fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
27 let posture = match ControlFinding::extract_posture(self.id(), evidence) {
28 Ok(p) => p,
29 Err(findings) => return findings,
30 };
31
32 if !posture.security_policy_present {
33 return vec![ControlFinding::violated(
34 self.id(),
35 "No SECURITY.md or security policy file found",
36 vec!["SECURITY.md".to_string()],
37 )];
38 }
39
40 if posture.security_policy_has_disclosure {
41 vec![ControlFinding::satisfied(
42 self.id(),
43 "Security policy exists with responsible disclosure process",
44 vec!["SECURITY.md".to_string()],
45 )]
46 } else {
47 vec![ControlFinding::violated(
48 self.id(),
49 "Security policy exists but lacks a responsible disclosure process",
50 vec!["SECURITY.md".to_string()],
51 )]
52 }
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use crate::control::ControlStatus;
60 use crate::evidence::{EvidenceGap, EvidenceState, RepositoryPosture};
61
62 fn posture(present: bool, disclosure: bool) -> RepositoryPosture {
63 RepositoryPosture {
64 codeowners_entries: vec![],
65 secret_scanning_enabled: false,
66 secret_push_protection_enabled: false,
67 vulnerability_scanning_enabled: false,
68 code_scanning_enabled: false,
69 security_policy_present: present,
70 security_policy_has_disclosure: disclosure,
71 default_branch_protected: false,
72 }
73 }
74
75 fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
76 EvidenceBundle {
77 repository_posture: state,
78 ..Default::default()
79 }
80 }
81
82 #[test]
83 fn not_applicable_when_posture_not_applicable() {
84 let findings = SecurityPolicyControl.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 = SecurityPolicyControl.evaluate(&bundle(EvidenceState::missing(vec![
91 EvidenceGap::CollectionFailed {
92 source: "github".to_string(),
93 subject: "posture".to_string(),
94 detail: "API error".to_string(),
95 },
96 ])));
97 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
98 }
99
100 #[test]
101 fn violated_when_no_policy() {
102 let findings =
103 SecurityPolicyControl.evaluate(&bundle(EvidenceState::complete(posture(false, false))));
104 assert_eq!(findings[0].status, ControlStatus::Violated);
105 assert!(findings[0].rationale.contains("No SECURITY.md"));
106 }
107
108 #[test]
109 fn violated_when_policy_without_disclosure() {
110 let findings =
111 SecurityPolicyControl.evaluate(&bundle(EvidenceState::complete(posture(true, false))));
112 assert_eq!(findings[0].status, ControlStatus::Violated);
113 assert!(
114 findings[0]
115 .rationale
116 .contains("lacks a responsible disclosure")
117 );
118 }
119
120 #[test]
121 fn satisfied_when_policy_with_disclosure() {
122 let findings =
123 SecurityPolicyControl.evaluate(&bundle(EvidenceState::complete(posture(true, true))));
124 assert_eq!(findings[0].status, ControlStatus::Satisfied);
125 }
126}