libverify_core/controls/
codeowners_coverage.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4const TARGETED_COVERAGE_THRESHOLD: usize = 3;
7
8pub struct CodeownersCoverageControl;
18
19impl Control for CodeownersCoverageControl {
20 fn id(&self) -> ControlId {
21 builtin::id(builtin::CODEOWNERS_COVERAGE)
22 }
23
24 fn description(&self) -> &'static str {
25 "CODEOWNERS must exist with meaningful ownership coverage for review routing"
26 }
27
28 fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
29 let posture = match ControlFinding::extract_posture(self.id(), evidence) {
30 Ok(p) => p,
31 Err(findings) => return findings,
32 };
33
34 if posture.codeowners_entries.is_empty() {
35 return vec![ControlFinding::violated(
36 self.id(),
37 "No CODEOWNERS file found or file contains no entries",
38 vec!["CODEOWNERS".to_string()],
39 )];
40 }
41
42 let has_catch_all = posture
43 .codeowners_entries
44 .iter()
45 .any(|e| e.pattern == "*" || e.pattern == "/**");
46
47 let entry_count = posture.codeowners_entries.len();
48
49 if has_catch_all {
50 vec![ControlFinding::satisfied(
51 self.id(),
52 format!("CODEOWNERS has {entry_count} entries with catch-all coverage"),
53 vec!["CODEOWNERS".to_string()],
54 )]
55 } else if entry_count >= TARGETED_COVERAGE_THRESHOLD {
56 vec![ControlFinding::satisfied(
59 self.id(),
60 format!(
61 "CODEOWNERS has {entry_count} targeted entries \
62 (no catch-all, but coverage appears intentional)"
63 ),
64 vec!["CODEOWNERS".to_string()],
65 )]
66 } else {
67 vec![ControlFinding::violated(
68 self.id(),
69 format!(
70 "CODEOWNERS has only {entry_count} entries and no catch-all pattern — \
71 coverage appears incomplete"
72 ),
73 vec!["CODEOWNERS".to_string()],
74 )]
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::control::ControlStatus;
83 use crate::evidence::{CodeownersEntry, EvidenceGap, EvidenceState, RepositoryPosture};
84
85 fn posture(entries: Vec<CodeownersEntry>) -> RepositoryPosture {
86 RepositoryPosture {
87 codeowners_entries: entries,
88 secret_scanning_enabled: false,
89 secret_push_protection_enabled: false,
90 vulnerability_scanning_enabled: false,
91 code_scanning_enabled: false,
92 security_policy_present: false,
93 security_policy_has_disclosure: false,
94 default_branch_protected: false,
95 }
96 }
97
98 fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
99 EvidenceBundle {
100 repository_posture: state,
101 ..Default::default()
102 }
103 }
104
105 fn entry(pattern: &str, owners: &[&str]) -> CodeownersEntry {
106 CodeownersEntry {
107 pattern: pattern.to_string(),
108 owners: owners.iter().map(|s| s.to_string()).collect(),
109 }
110 }
111
112 #[test]
113 fn not_applicable_when_posture_not_applicable() {
114 let findings = CodeownersCoverageControl.evaluate(&bundle(EvidenceState::not_applicable()));
115 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
116 }
117
118 #[test]
119 fn indeterminate_when_posture_missing() {
120 let findings = CodeownersCoverageControl.evaluate(&bundle(EvidenceState::missing(vec![
121 EvidenceGap::CollectionFailed {
122 source: "github".to_string(),
123 subject: "posture".to_string(),
124 detail: "API error".to_string(),
125 },
126 ])));
127 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
128 }
129
130 #[test]
131 fn violated_when_no_entries() {
132 let findings =
133 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![]))));
134 assert_eq!(findings[0].status, ControlStatus::Violated);
135 }
136
137 #[test]
138 fn violated_when_too_few_entries_without_catch_all() {
139 let findings =
140 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
141 entry("/src/", &["@org/core-team"]),
142 ]))));
143 assert_eq!(findings[0].status, ControlStatus::Violated);
144 assert!(findings[0].rationale.contains("only 1 entries"));
145 }
146
147 #[test]
148 fn satisfied_with_catch_all() {
149 let findings =
150 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
151 entry("/src/auth/", &["@org/security-team"]),
152 entry("*", &["@org/default-reviewers"]),
153 ]))));
154 assert_eq!(findings[0].status, ControlStatus::Satisfied);
155 }
156
157 #[test]
158 fn satisfied_with_glob_catch_all() {
159 let findings =
160 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
161 entry("/**", &["@org/default-reviewers"]),
162 ]))));
163 assert_eq!(findings[0].status, ControlStatus::Satisfied);
164 }
165
166 #[test]
167 fn satisfied_with_targeted_entries_no_catch_all() {
168 let findings =
169 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
170 entry("/src/auth/", &["@org/security-team"]),
171 entry("/infra/", &["@org/platform-team"]),
172 entry("/.github/", &["@org/devops"]),
173 ]))));
174 assert_eq!(findings[0].status, ControlStatus::Satisfied);
175 assert!(findings[0].rationale.contains("targeted entries"));
176 }
177
178 #[test]
179 fn violated_with_two_entries_no_catch_all() {
180 let findings =
181 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
182 entry("/src/auth/", &["@org/security-team"]),
183 entry("/infra/", &["@org/platform-team"]),
184 ]))));
185 assert_eq!(findings[0].status, ControlStatus::Violated);
186 }
187}