Skip to main content

libverify_core/controls/
security_policy.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Validates that a security policy (SECURITY.md) exists with a responsible
5/// disclosure process.
6///
7/// Maps to SOC2 CC7.3 / CC7.4: incident response communication.
8/// ASPM signal — a published security policy enables external reporters to
9/// disclose vulnerabilities responsibly, reducing exposure window.
10///
11/// Note: In enterprise settings (SOC2 preset), this control's violations are
12/// treated as "review" rather than "fail" because enterprises typically
13/// maintain disclosure processes in internal portals, not repo-level files.
14/// In OSS (OSS preset), this is strict — SECURITY.md is the primary channel.
15pub 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}