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// --- Container image attestation predicates ---
145
146/// Core predicate for container signature verification severity (PI1.4).
147/// All signed and count > 0 -> Pass, count == 0 -> NotApplicable (Warning), otherwise -> Error.
148///
149/// Verified by Creusot in `libverify-verif` crate.
150pub fn container_signature_severity(all_signed: bool, count: u32) -> Severity {
151    if count == 0 {
152        Severity::Warning
153    } else if all_signed {
154        Severity::Pass
155    } else {
156        Severity::Error
157    }
158}
159
160/// Core predicate for container provenance severity (PI1.4).
161/// All have provenance and count > 0 -> Pass, count == 0 -> NotApplicable (Warning), otherwise -> Error.
162///
163/// Verified by Creusot in `libverify-verif` crate.
164pub fn container_provenance_severity(all_have_provenance: bool, count: u32) -> Severity {
165    if count == 0 {
166        Severity::Warning
167    } else if all_have_provenance {
168        Severity::Pass
169    } else {
170        Severity::Error
171    }
172}
173
174// --- Compliance control predicates ---
175
176/// Core predicate for stale review severity (CC7.2).
177/// Zero stale approvals -> Pass, any stale -> Error.
178///
179/// Verified by Creusot in `gh-verify-verif` crate.
180pub fn stale_review_severity(stale_count: usize) -> Severity {
181    if stale_count == 0 {
182        Severity::Pass
183    } else {
184        Severity::Error
185    }
186}
187
188/// Core predicate for description quality severity (CC8.1).
189/// Body length >= minimum -> Pass, otherwise -> Error.
190///
191/// Verified by Creusot in `gh-verify-verif` crate.
192pub fn description_quality_severity(body_length: usize, min_length: usize) -> Severity {
193    if body_length >= min_length {
194        Severity::Pass
195    } else {
196        Severity::Error
197    }
198}
199
200/// Core predicate for merge commit policy severity (CC8.1).
201/// Zero merge commits -> Pass, any merge commits -> Error.
202///
203/// Verified by Creusot in `gh-verify-verif` crate.
204pub fn merge_commit_policy_severity(merge_count: usize) -> Severity {
205    if merge_count == 0 {
206        Severity::Pass
207    } else {
208        Severity::Error
209    }
210}
211
212/// Core predicate for conventional title severity (CC8.1).
213/// Title is conventional -> Pass, otherwise -> Error.
214///
215/// Verified by Creusot in `gh-verify-verif` crate.
216pub fn conventional_title_severity(is_conventional: bool) -> Severity {
217    if is_conventional {
218        Severity::Pass
219    } else {
220        Severity::Error
221    }
222}
223
224/// Core predicate for security file change severity (CC7.2).
225/// Zero sensitive files changed -> Pass, any sensitive -> Error.
226///
227/// Verified by Creusot in `gh-verify-verif` crate.
228pub fn security_file_change_severity(sensitive_count: usize) -> Severity {
229    if sensitive_count == 0 {
230        Severity::Pass
231    } else {
232        Severity::Error
233    }
234}
235
236/// Core predicate for release traceability severity (CC7.1).
237/// At least one linked change request -> Pass, none -> Error.
238///
239/// Verified by Creusot in `gh-verify-verif` crate.
240pub fn release_traceability_severity(linked_cr_count: usize) -> Severity {
241    if linked_cr_count > 0 {
242        Severity::Pass
243    } else {
244        Severity::Error
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    // --- verif↔core equivalence tests (exhaustive for boolean predicates) ---
253
254    #[test]
255    fn is_approver_independent_exhaustive_equivalence() {
256        // Exhaustive truth table: both inputs are bool, so 4 combinations
257        for &ca in &[false, true] {
258            for &pa in &[false, true] {
259                let result = is_approver_independent(ca, pa);
260                let spec = !ca && !pa;
261                assert_eq!(
262                    result, spec,
263                    "is_approver_independent({ca}, {pa}): got {result}, spec {spec}"
264                );
265            }
266        }
267    }
268
269    #[test]
270    fn signature_severity_equivalence() {
271        assert_eq!(signature_severity(0), Severity::Pass);
272        for count in 1..=100 {
273            assert_eq!(
274                signature_severity(count),
275                Severity::Error,
276                "signature_severity({count}) should be Error"
277            );
278        }
279    }
280
281    #[test]
282    fn build_provenance_severity_equivalence() {
283        assert_eq!(build_provenance_severity(0), Severity::Pass);
284        for count in 1..=100 {
285            assert_eq!(
286                build_provenance_severity(count),
287                Severity::Error,
288                "build_provenance_severity({count}) should be Error"
289            );
290        }
291    }
292
293    /// Exhaustive equivalence for classify_scope against Creusot spec.
294    #[test]
295    fn classify_scope_exhaustive_equivalence() {
296        use crate::scope::classify_scope;
297        for files in 0..=20usize {
298            for comps in 0..=20usize {
299                let result = classify_scope(files, comps);
300                let spec = if files <= 1 {
301                    Severity::Pass
302                } else if comps <= 1 {
303                    Severity::Pass
304                } else if comps == 2 {
305                    Severity::Warning
306                } else {
307                    Severity::Error
308                };
309                assert_eq!(
310                    result, spec,
311                    "classify_scope({files}, {comps}): got {result:?}, spec {spec:?}"
312                );
313            }
314        }
315    }
316
317    #[test]
318    fn required_status_checks_severity_equivalence() {
319        assert_eq!(required_status_checks_severity(0), Severity::Pass);
320        for count in 1..=10 {
321            assert_eq!(
322                required_status_checks_severity(count),
323                Severity::Error,
324                "required_status_checks_severity({count}) should be Error"
325            );
326        }
327    }
328
329    #[test]
330    fn branch_history_severity_equivalence() {
331        assert_eq!(branch_history_severity(0), Severity::Pass);
332        for count in 1..=10 {
333            assert_eq!(
334                branch_history_severity(count),
335                Severity::Error,
336                "branch_history_severity({count}) should be Error"
337            );
338        }
339    }
340
341    #[test]
342    fn branch_protection_enforcement_severity_equivalence() {
343        assert_eq!(branch_protection_enforcement_severity(0), Severity::Pass);
344        for count in 1..=10 {
345            assert_eq!(
346                branch_protection_enforcement_severity(count),
347                Severity::Error,
348                "branch_protection_enforcement_severity({count}) should be Error"
349            );
350        }
351    }
352
353    #[test]
354    fn two_party_review_severity_equivalence() {
355        assert_eq!(two_party_review_severity(0), Severity::Error);
356        assert_eq!(two_party_review_severity(1), Severity::Error);
357        assert_eq!(two_party_review_severity(2), Severity::Pass);
358        for count in 3..=10 {
359            assert_eq!(
360                two_party_review_severity(count),
361                Severity::Pass,
362                "two_party_review_severity({count}) should be Pass"
363            );
364        }
365    }
366
367    #[test]
368    fn hosted_build_severity_equivalence() {
369        assert_eq!(hosted_build_severity(0), Severity::Pass);
370        for count in 1..=10 {
371            assert_eq!(
372                hosted_build_severity(count),
373                Severity::Error,
374                "hosted_build_severity({count}) should be Error"
375            );
376        }
377    }
378
379    #[test]
380    fn provenance_authenticity_severity_equivalence() {
381        assert_eq!(provenance_authenticity_severity(0), Severity::Pass);
382        for count in 1..=10 {
383            assert_eq!(
384                provenance_authenticity_severity(count),
385                Severity::Error,
386                "provenance_authenticity_severity({count}) should be Error"
387            );
388        }
389    }
390
391    #[test]
392    fn build_isolation_severity_equivalence() {
393        assert_eq!(build_isolation_severity(0), Severity::Pass);
394        for count in 1..=10 {
395            assert_eq!(
396                build_isolation_severity(count),
397                Severity::Error,
398                "build_isolation_severity({count}) should be Error"
399            );
400        }
401    }
402
403    // --- Compliance predicate equivalence tests ---
404
405    #[test]
406    fn stale_review_severity_equivalence() {
407        assert_eq!(stale_review_severity(0), Severity::Pass);
408        for count in 1..=10 {
409            assert_eq!(
410                stale_review_severity(count),
411                Severity::Error,
412                "stale_review_severity({count}) should be Error"
413            );
414        }
415    }
416
417    #[test]
418    fn description_quality_severity_equivalence() {
419        let min = 10;
420        for len in 0..min {
421            assert_eq!(
422                description_quality_severity(len, min),
423                Severity::Error,
424                "description_quality_severity({len}, {min}) should be Error"
425            );
426        }
427        for len in min..=50 {
428            assert_eq!(
429                description_quality_severity(len, min),
430                Severity::Pass,
431                "description_quality_severity({len}, {min}) should be Pass"
432            );
433        }
434    }
435
436    #[test]
437    fn merge_commit_policy_severity_equivalence() {
438        assert_eq!(merge_commit_policy_severity(0), Severity::Pass);
439        for count in 1..=10 {
440            assert_eq!(
441                merge_commit_policy_severity(count),
442                Severity::Error,
443                "merge_commit_policy_severity({count}) should be Error"
444            );
445        }
446    }
447
448    #[test]
449    fn conventional_title_severity_equivalence() {
450        assert_eq!(conventional_title_severity(true), Severity::Pass);
451        assert_eq!(conventional_title_severity(false), Severity::Error);
452    }
453
454    #[test]
455    fn security_file_change_severity_equivalence() {
456        assert_eq!(security_file_change_severity(0), Severity::Pass);
457        for count in 1..=10 {
458            assert_eq!(
459                security_file_change_severity(count),
460                Severity::Error,
461                "security_file_change_severity({count}) should be Error"
462            );
463        }
464    }
465
466    #[test]
467    fn release_traceability_severity_equivalence() {
468        assert_eq!(release_traceability_severity(0), Severity::Error);
469        for count in 1..=10 {
470            assert_eq!(
471                release_traceability_severity(count),
472                Severity::Pass,
473                "release_traceability_severity({count}) should be Pass"
474            );
475        }
476    }
477
478    #[test]
479    fn dependency_signature_severity_equivalence() {
480        assert_eq!(dependency_signature_severity(0), Severity::Pass);
481        for count in 1..=100 {
482            assert_eq!(
483                dependency_signature_severity(count),
484                Severity::Error,
485                "dependency_signature_severity({count}) should be Error"
486            );
487        }
488    }
489
490    // --- Container image attestation predicate tests ---
491
492    #[test]
493    fn container_signature_severity_equivalence() {
494        // No images -> Warning (NotApplicable at control level)
495        assert_eq!(container_signature_severity(true, 0), Severity::Warning);
496        assert_eq!(container_signature_severity(false, 0), Severity::Warning);
497        // All signed -> Pass
498        assert_eq!(container_signature_severity(true, 1), Severity::Pass);
499        assert_eq!(container_signature_severity(true, 5), Severity::Pass);
500        // Not all signed -> Error
501        assert_eq!(container_signature_severity(false, 1), Severity::Error);
502        assert_eq!(container_signature_severity(false, 10), Severity::Error);
503    }
504
505    #[test]
506    fn container_provenance_severity_equivalence() {
507        // No images -> Warning (NotApplicable at control level)
508        assert_eq!(container_provenance_severity(true, 0), Severity::Warning);
509        assert_eq!(container_provenance_severity(false, 0), Severity::Warning);
510        // All have provenance -> Pass
511        assert_eq!(container_provenance_severity(true, 1), Severity::Pass);
512        assert_eq!(container_provenance_severity(true, 5), Severity::Pass);
513        // Missing provenance -> Error
514        assert_eq!(container_provenance_severity(false, 1), Severity::Error);
515        assert_eq!(container_provenance_severity(false, 10), Severity::Error);
516    }
517}