Skip to main content

libverify_core/controls/
codeowners_coverage.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::EvidenceBundle;
3
4/// Minimum number of targeted entries (without catch-all) considered
5/// intentional ownership coverage.
6const TARGETED_COVERAGE_THRESHOLD: usize = 3;
7
8/// Validates that a CODEOWNERS file exists and provides meaningful coverage.
9///
10/// Maps to SOC2 CC6.1: logical access controls ensure that code changes to
11/// sensitive areas are routed to designated owners for review.
12/// Also an ASPM signal — code ownership coverage reduces unreviewed blast radius.
13///
14/// Evaluation tiers:
15/// - **Satisfied**: catch-all pattern exists, OR 3+ targeted entries (intentional ownership)
16/// - **Violated**: no entries, or fewer than 3 entries without catch-all
17pub 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            // Intentional targeted ownership without catch-all is valid —
57            // many projects deliberately omit * so uncovered paths don't block
58            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}