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            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}