Skip to main content

libverify_core/
integrity.rs

1//! Release integrity verification predicates.
2//!
3//! Pure functions amenable to formal verification with Creusot.
4//! The critical decision logic is proven correct in the `gh-verify-verif` crate.
5
6use crate::verdict::Severity;
7
8/// Truncate a SHA to 7 characters for display.
9pub fn short_sha(sha: &str) -> &str {
10    if sha.len() >= 7 { &sha[..7] } else { sha }
11}
12
13// --- Creusot-verifiable core predicates ---
14//
15// These pure functions operate on primitive types only, making them
16// directly verifiable by Creusot's SMT backend. Controls in the
17// `controls/` module delegate to these predicates, ensuring the
18// critical logic is proven correct.
19
20/// Core predicate for the four-eyes principle.
21/// An approver is independent iff they are neither a commit author nor the change request author.
22///
23/// Verified by Creusot in `gh-verify-verif` crate.
24pub fn is_approver_independent(is_commit_author: bool, is_pr_author: bool) -> bool {
25    !is_commit_author && !is_pr_author
26}
27
28/// Core predicate for signature verification result severity.
29/// Verified by Creusot in `gh-verify-verif` crate.
30pub fn signature_severity(unsigned_count: usize) -> Severity {
31    if unsigned_count == 0 {
32        Severity::Pass
33    } else {
34        Severity::Error
35    }
36}
37
38/// Core predicate for build provenance verification severity.
39/// Zero unverified attestations → Pass, any unverified → Error.
40pub fn build_provenance_severity(unverified_count: usize) -> Severity {
41    if unverified_count == 0 {
42        Severity::Pass
43    } else {
44        Severity::Error
45    }
46}
47
48/// Core predicate for required status checks severity.
49/// Pass iff zero check runs have a failing conclusion.
50///
51/// Verified by Creusot in `gh-verify-verif` crate.
52pub fn required_status_checks_severity(fail_count: usize) -> Severity {
53    if fail_count == 0 {
54        Severity::Pass
55    } else {
56        Severity::Error
57    }
58}
59
60/// Core predicate for branch history integrity severity (Source L2).
61/// Zero merge commits (linear history) -> Pass, any merge commits -> Error.
62///
63/// Verified by Creusot in `gh-verify-verif` crate.
64pub fn branch_history_severity(unprotected_count: usize) -> Severity {
65    if unprotected_count == 0 {
66        Severity::Pass
67    } else {
68        Severity::Error
69    }
70}
71
72/// Core predicate for technical enforcement severity (Source L3).
73/// Zero violations (CI passed + independent review) -> Pass, any violations -> Error.
74///
75/// Verified by Creusot in `gh-verify-verif` crate.
76pub fn branch_protection_enforcement_severity(non_enforced_count: usize) -> Severity {
77    if non_enforced_count == 0 {
78        Severity::Pass
79    } else {
80        Severity::Error
81    }
82}
83
84/// Core predicate for two-party review severity (Source L4).
85/// At least 2 independent approvers -> Pass, fewer -> Error.
86///
87/// Verified by Creusot in `gh-verify-verif` crate.
88pub fn two_party_review_severity(independent_count: usize) -> Severity {
89    if independent_count >= 2 {
90        Severity::Pass
91    } else {
92        Severity::Error
93    }
94}
95
96/// Core predicate for hosted build platform severity (Build L2).
97/// Zero non-hosted builds -> Pass, any non-hosted -> Error.
98///
99/// Verified by Creusot in `gh-verify-verif` crate.
100pub fn hosted_build_severity(non_hosted_count: usize) -> Severity {
101    if non_hosted_count == 0 {
102        Severity::Pass
103    } else {
104        Severity::Error
105    }
106}
107
108/// Core predicate for provenance authenticity severity (Build L2).
109/// Zero unauthenticated attestations -> Pass, any unauthenticated -> Error.
110///
111/// Verified by Creusot in `gh-verify-verif` crate.
112pub fn provenance_authenticity_severity(unauthenticated_count: usize) -> Severity {
113    if unauthenticated_count == 0 {
114        Severity::Pass
115    } else {
116        Severity::Error
117    }
118}
119
120/// Core predicate for build isolation severity (Build L3).
121/// Zero non-isolated builds -> Pass, any non-isolated -> Error.
122///
123/// Verified by Creusot in `gh-verify-verif` crate.
124pub fn build_isolation_severity(non_isolated_count: usize) -> Severity {
125    if non_isolated_count == 0 {
126        Severity::Pass
127    } else {
128        Severity::Error
129    }
130}
131
132/// Core predicate for dependency signature verification severity.
133/// Zero unverified dependencies -> Pass, any unverified -> Error.
134///
135/// Verified by Creusot in `libverify-verif` crate.
136pub fn dependency_signature_severity(unsigned_count: usize) -> Severity {
137    if unsigned_count == 0 {
138        Severity::Pass
139    } else {
140        Severity::Error
141    }
142}
143
144// --- Compliance control predicates ---
145
146/// Core predicate for stale review severity (CC7.2).
147/// Zero stale approvals -> Pass, any stale -> Error.
148///
149/// Verified by Creusot in `gh-verify-verif` crate.
150pub fn stale_review_severity(stale_count: usize) -> Severity {
151    if stale_count == 0 {
152        Severity::Pass
153    } else {
154        Severity::Error
155    }
156}
157
158/// Core predicate for description quality severity (CC8.1).
159/// Body length >= minimum -> Pass, otherwise -> Error.
160///
161/// Verified by Creusot in `gh-verify-verif` crate.
162pub fn description_quality_severity(body_length: usize, min_length: usize) -> Severity {
163    if body_length >= min_length {
164        Severity::Pass
165    } else {
166        Severity::Error
167    }
168}
169
170/// Core predicate for merge commit policy severity (CC8.1).
171/// Zero merge commits -> Pass, any merge commits -> Error.
172///
173/// Verified by Creusot in `gh-verify-verif` crate.
174pub fn merge_commit_policy_severity(merge_count: usize) -> Severity {
175    if merge_count == 0 {
176        Severity::Pass
177    } else {
178        Severity::Error
179    }
180}
181
182/// Core predicate for conventional title severity (CC8.1).
183/// Title is conventional -> Pass, otherwise -> Error.
184///
185/// Verified by Creusot in `gh-verify-verif` crate.
186pub fn conventional_title_severity(is_conventional: bool) -> Severity {
187    if is_conventional {
188        Severity::Pass
189    } else {
190        Severity::Error
191    }
192}
193
194/// Core predicate for security file change severity (CC7.2).
195/// Zero sensitive files changed -> Pass, any sensitive -> Error.
196///
197/// Verified by Creusot in `gh-verify-verif` crate.
198pub fn security_file_change_severity(sensitive_count: usize) -> Severity {
199    if sensitive_count == 0 {
200        Severity::Pass
201    } else {
202        Severity::Error
203    }
204}
205
206/// Core predicate for release traceability severity (CC7.1).
207/// At least one linked change request -> Pass, none -> Error.
208///
209/// Verified by Creusot in `gh-verify-verif` crate.
210pub fn release_traceability_severity(linked_cr_count: usize) -> Severity {
211    if linked_cr_count > 0 {
212        Severity::Pass
213    } else {
214        Severity::Error
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    // --- verif↔core equivalence tests (exhaustive for boolean predicates) ---
223
224    #[test]
225    fn is_approver_independent_exhaustive_equivalence() {
226        // Exhaustive truth table: both inputs are bool, so 4 combinations
227        for &ca in &[false, true] {
228            for &pa in &[false, true] {
229                let result = is_approver_independent(ca, pa);
230                let spec = !ca && !pa;
231                assert_eq!(
232                    result, spec,
233                    "is_approver_independent({ca}, {pa}): got {result}, spec {spec}"
234                );
235            }
236        }
237    }
238
239    #[test]
240    fn signature_severity_equivalence() {
241        assert_eq!(signature_severity(0), Severity::Pass);
242        for count in 1..=100 {
243            assert_eq!(
244                signature_severity(count),
245                Severity::Error,
246                "signature_severity({count}) should be Error"
247            );
248        }
249    }
250
251    #[test]
252    fn build_provenance_severity_equivalence() {
253        assert_eq!(build_provenance_severity(0), Severity::Pass);
254        for count in 1..=100 {
255            assert_eq!(
256                build_provenance_severity(count),
257                Severity::Error,
258                "build_provenance_severity({count}) should be Error"
259            );
260        }
261    }
262
263    /// Exhaustive equivalence for classify_scope against Creusot spec.
264    #[test]
265    fn classify_scope_exhaustive_equivalence() {
266        use crate::scope::classify_scope;
267        for files in 0..=20usize {
268            for comps in 0..=20usize {
269                let result = classify_scope(files, comps);
270                let spec = if files <= 1 {
271                    Severity::Pass
272                } else if comps <= 1 {
273                    Severity::Pass
274                } else if comps == 2 {
275                    Severity::Warning
276                } else {
277                    Severity::Error
278                };
279                assert_eq!(
280                    result, spec,
281                    "classify_scope({files}, {comps}): got {result:?}, spec {spec:?}"
282                );
283            }
284        }
285    }
286
287    #[test]
288    fn required_status_checks_severity_equivalence() {
289        assert_eq!(required_status_checks_severity(0), Severity::Pass);
290        for count in 1..=10 {
291            assert_eq!(
292                required_status_checks_severity(count),
293                Severity::Error,
294                "required_status_checks_severity({count}) should be Error"
295            );
296        }
297    }
298
299    #[test]
300    fn branch_history_severity_equivalence() {
301        assert_eq!(branch_history_severity(0), Severity::Pass);
302        for count in 1..=10 {
303            assert_eq!(
304                branch_history_severity(count),
305                Severity::Error,
306                "branch_history_severity({count}) should be Error"
307            );
308        }
309    }
310
311    #[test]
312    fn branch_protection_enforcement_severity_equivalence() {
313        assert_eq!(branch_protection_enforcement_severity(0), Severity::Pass);
314        for count in 1..=10 {
315            assert_eq!(
316                branch_protection_enforcement_severity(count),
317                Severity::Error,
318                "branch_protection_enforcement_severity({count}) should be Error"
319            );
320        }
321    }
322
323    #[test]
324    fn two_party_review_severity_equivalence() {
325        assert_eq!(two_party_review_severity(0), Severity::Error);
326        assert_eq!(two_party_review_severity(1), Severity::Error);
327        assert_eq!(two_party_review_severity(2), Severity::Pass);
328        for count in 3..=10 {
329            assert_eq!(
330                two_party_review_severity(count),
331                Severity::Pass,
332                "two_party_review_severity({count}) should be Pass"
333            );
334        }
335    }
336
337    #[test]
338    fn hosted_build_severity_equivalence() {
339        assert_eq!(hosted_build_severity(0), Severity::Pass);
340        for count in 1..=10 {
341            assert_eq!(
342                hosted_build_severity(count),
343                Severity::Error,
344                "hosted_build_severity({count}) should be Error"
345            );
346        }
347    }
348
349    #[test]
350    fn provenance_authenticity_severity_equivalence() {
351        assert_eq!(provenance_authenticity_severity(0), Severity::Pass);
352        for count in 1..=10 {
353            assert_eq!(
354                provenance_authenticity_severity(count),
355                Severity::Error,
356                "provenance_authenticity_severity({count}) should be Error"
357            );
358        }
359    }
360
361    #[test]
362    fn build_isolation_severity_equivalence() {
363        assert_eq!(build_isolation_severity(0), Severity::Pass);
364        for count in 1..=10 {
365            assert_eq!(
366                build_isolation_severity(count),
367                Severity::Error,
368                "build_isolation_severity({count}) should be Error"
369            );
370        }
371    }
372
373    // --- Compliance predicate equivalence tests ---
374
375    #[test]
376    fn stale_review_severity_equivalence() {
377        assert_eq!(stale_review_severity(0), Severity::Pass);
378        for count in 1..=10 {
379            assert_eq!(
380                stale_review_severity(count),
381                Severity::Error,
382                "stale_review_severity({count}) should be Error"
383            );
384        }
385    }
386
387    #[test]
388    fn description_quality_severity_equivalence() {
389        let min = 10;
390        for len in 0..min {
391            assert_eq!(
392                description_quality_severity(len, min),
393                Severity::Error,
394                "description_quality_severity({len}, {min}) should be Error"
395            );
396        }
397        for len in min..=50 {
398            assert_eq!(
399                description_quality_severity(len, min),
400                Severity::Pass,
401                "description_quality_severity({len}, {min}) should be Pass"
402            );
403        }
404    }
405
406    #[test]
407    fn merge_commit_policy_severity_equivalence() {
408        assert_eq!(merge_commit_policy_severity(0), Severity::Pass);
409        for count in 1..=10 {
410            assert_eq!(
411                merge_commit_policy_severity(count),
412                Severity::Error,
413                "merge_commit_policy_severity({count}) should be Error"
414            );
415        }
416    }
417
418    #[test]
419    fn conventional_title_severity_equivalence() {
420        assert_eq!(conventional_title_severity(true), Severity::Pass);
421        assert_eq!(conventional_title_severity(false), Severity::Error);
422    }
423
424    #[test]
425    fn security_file_change_severity_equivalence() {
426        assert_eq!(security_file_change_severity(0), Severity::Pass);
427        for count in 1..=10 {
428            assert_eq!(
429                security_file_change_severity(count),
430                Severity::Error,
431                "security_file_change_severity({count}) should be Error"
432            );
433        }
434    }
435
436    #[test]
437    fn release_traceability_severity_equivalence() {
438        assert_eq!(release_traceability_severity(0), Severity::Error);
439        for count in 1..=10 {
440            assert_eq!(
441                release_traceability_severity(count),
442                Severity::Pass,
443                "release_traceability_severity({count}) should be Pass"
444            );
445        }
446    }
447
448    #[test]
449    fn dependency_signature_severity_equivalence() {
450        assert_eq!(dependency_signature_severity(0), Severity::Pass);
451        for count in 1..=100 {
452            assert_eq!(
453                dependency_signature_severity(count),
454                Severity::Error,
455                "dependency_signature_severity({count}) should be Error"
456            );
457        }
458    }
459}