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 ..Default::default()
89 }
90 }
91
92 fn bundle(state: EvidenceState<RepositoryPosture>) -> EvidenceBundle {
93 EvidenceBundle {
94 repository_posture: state,
95 ..Default::default()
96 }
97 }
98
99 fn entry(pattern: &str, owners: &[&str]) -> CodeownersEntry {
100 CodeownersEntry {
101 pattern: pattern.to_string(),
102 owners: owners.iter().map(|s| s.to_string()).collect(),
103 }
104 }
105
106 #[test]
107 fn not_applicable_when_posture_not_applicable() {
108 let findings = CodeownersCoverageControl.evaluate(&bundle(EvidenceState::not_applicable()));
109 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
110 }
111
112 #[test]
113 fn indeterminate_when_posture_missing() {
114 let findings = CodeownersCoverageControl.evaluate(&bundle(EvidenceState::missing(vec![
115 EvidenceGap::CollectionFailed {
116 source: "github".to_string(),
117 subject: "posture".to_string(),
118 detail: "API error".to_string(),
119 },
120 ])));
121 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
122 }
123
124 #[test]
125 fn violated_when_no_entries() {
126 let findings =
127 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![]))));
128 assert_eq!(findings[0].status, ControlStatus::Violated);
129 }
130
131 #[test]
132 fn violated_when_too_few_entries_without_catch_all() {
133 let findings =
134 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
135 entry("/src/", &["@org/core-team"]),
136 ]))));
137 assert_eq!(findings[0].status, ControlStatus::Violated);
138 assert!(findings[0].rationale.contains("only 1 entries"));
139 }
140
141 #[test]
142 fn satisfied_with_catch_all() {
143 let findings =
144 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
145 entry("/src/auth/", &["@org/security-team"]),
146 entry("*", &["@org/default-reviewers"]),
147 ]))));
148 assert_eq!(findings[0].status, ControlStatus::Satisfied);
149 }
150
151 #[test]
152 fn satisfied_with_glob_catch_all() {
153 let findings =
154 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
155 entry("/**", &["@org/default-reviewers"]),
156 ]))));
157 assert_eq!(findings[0].status, ControlStatus::Satisfied);
158 }
159
160 #[test]
161 fn satisfied_with_targeted_entries_no_catch_all() {
162 let findings =
163 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
164 entry("/src/auth/", &["@org/security-team"]),
165 entry("/infra/", &["@org/platform-team"]),
166 entry("/.github/", &["@org/devops"]),
167 ]))));
168 assert_eq!(findings[0].status, ControlStatus::Satisfied);
169 assert!(findings[0].rationale.contains("targeted entries"));
170 }
171
172 #[test]
173 fn violated_with_two_entries_no_catch_all() {
174 let findings =
175 CodeownersCoverageControl.evaluate(&bundle(EvidenceState::complete(posture(vec![
176 entry("/src/auth/", &["@org/security-team"]),
177 entry("/infra/", &["@org/platform-team"]),
178 ]))));
179 assert_eq!(findings[0].status, ControlStatus::Violated);
180 }
181}