Skip to main content

startup_graduation/
startup_graduation.rs

1//! OSS -> SOC2 graduation path demo for a 15-person startup.
2//!
3//! Simulates a realistic startup posture:
4//!   - No CODEOWNERS file
5//!   - Dependabot enabled, no secret scanning
6//!   - Basic SECURITY.md (present, no disclosure process)
7//!   - Self-reviewed PRs (author == reviewer)
8//!   - No branch protection, no signed commits
9//!   - CI running but no build provenance/attestations
10//!
11//! Run: cargo run -p libverify-policy --example startup_graduation
12
13use libverify_core::control::evaluate_all;
14use libverify_core::controls::all_controls;
15use libverify_core::evidence::*;
16use libverify_core::profile::{GateDecision, apply_profile};
17use libverify_policy::OpaProfile;
18
19/// Build an EvidenceBundle representing a typical early-stage startup.
20fn startup_evidence() -> EvidenceBundle {
21    // A self-reviewed PR: alice authored, alice approved
22    let pr = GovernedChange {
23        id: ChangeRequestId::new("github", "42"),
24        title: "feat: add user auth".to_string(),
25        summary: Some("Adds JWT-based authentication flow for the API.".to_string()),
26        submitted_by: Some("alice".to_string()),
27        changed_assets: EvidenceState::complete(vec![
28            ChangedAsset {
29                path: "src/auth/mod.rs".to_string(),
30                diff_available: true,
31                additions: 120,
32                deletions: 5,
33                status: "added".to_string(),
34                diff: None,
35            },
36            ChangedAsset {
37                path: "src/auth/jwt.rs".to_string(),
38                diff_available: true,
39                additions: 85,
40                deletions: 0,
41                status: "added".to_string(),
42                diff: None,
43            },
44            ChangedAsset {
45                path: "tests/auth_test.rs".to_string(),
46                diff_available: true,
47                additions: 40,
48                deletions: 0,
49                status: "added".to_string(),
50                diff: None,
51            },
52        ]),
53        approval_decisions: EvidenceState::complete(vec![ApprovalDecision {
54            actor: "alice".to_string(), // same as submitter = self-review
55            disposition: ApprovalDisposition::Approved,
56            submitted_at: Some("2026-03-25T10:00:00Z".to_string()),
57        }]),
58        source_revisions: EvidenceState::complete(vec![SourceRevision {
59            id: "abc1234".to_string(),
60            authored_by: Some("alice".to_string()),
61            committed_at: Some("2026-03-25T09:30:00Z".to_string()),
62            merge: false,
63            authenticity: EvidenceState::complete(AuthenticityEvidence {
64                verified: false, // unsigned commits
65                mechanism: None,
66            }),
67        }]),
68        work_item_refs: EvidenceState::complete(vec![]), // no linked issues
69    };
70
71    // Repository posture: typical early startup
72    let posture = RepositoryPosture {
73        codeowners_entries: vec![],            // no CODEOWNERS
74        secret_scanning_enabled: false,        // not enabled
75        secret_push_protection_enabled: false, // not enabled
76        vulnerability_scanning_enabled: true,  // Dependabot on
77        code_scanning_enabled: false,          // no CodeQL
78        security_policy_present: true,         // basic SECURITY.md
79        security_policy_has_disclosure: false, // no disclosure process
80        default_branch_protected: false,       // no branch protection
81    };
82
83    EvidenceBundle {
84        change_requests: vec![pr],
85        promotion_batches: vec![],
86        artifact_attestations: EvidenceState::NotApplicable,
87        check_runs: EvidenceState::complete(vec![
88            CheckRunEvidence {
89                name: "ci / test".to_string(),
90                conclusion: CheckConclusion::Success,
91                app_slug: Some("github-actions".to_string()),
92            },
93            CheckRunEvidence {
94                name: "ci / lint".to_string(),
95                conclusion: CheckConclusion::Success,
96                app_slug: Some("github-actions".to_string()),
97            },
98        ]),
99        build_platform: EvidenceState::NotApplicable, // no SLSA build evidence
100        dependency_signatures: EvidenceState::NotApplicable, // no dep signing
101        repository_posture: EvidenceState::complete(posture),
102    }
103}
104
105fn main() {
106    println!("==========================================================");
107    println!("  libverify: OSS -> SOC2 Graduation Path Demo");
108    println!("  Startup: 15-person team preparing for SOC2 Type II");
109    println!("==========================================================\n");
110
111    // --- Describe the startup posture ---
112    println!("STARTUP POSTURE:");
113    println!("  - No CODEOWNERS file");
114    println!("  - Dependabot enabled, no secret scanning");
115    println!("  - Basic SECURITY.md (no disclosure process)");
116    println!("  - Self-reviewed PRs (author == sole reviewer)");
117    println!("  - Unsigned commits, no branch protection");
118    println!("  - CI running (tests + lint pass), no build provenance");
119    println!("  - No dependency signature verification");
120    println!();
121
122    // --- Run all 28 controls ---
123    let evidence = startup_evidence();
124    let controls = all_controls();
125    println!("Running {} controls against evidence...\n", controls.len());
126    let findings = evaluate_all(&controls, &evidence);
127
128    // --- Evaluate through OSS preset ---
129    let oss_profile = OpaProfile::oss_preset().expect("OSS preset should load");
130    let oss_outcomes = apply_profile(&oss_profile, &findings);
131
132    // --- Evaluate through SOC2 preset ---
133    let soc2_profile = OpaProfile::soc2_preset().expect("SOC2 preset should load");
134    let soc2_outcomes = apply_profile(&soc2_profile, &findings);
135
136    // --- Print side-by-side comparison ---
137    println!(
138        "{:<40} {:<12} {:<12}  {}",
139        "CONTROL", "OSS", "SOC2", "DELTA"
140    );
141    println!("{}", "-".repeat(90));
142
143    let mut oss_pass = 0u32;
144    let mut oss_review = 0u32;
145    let mut oss_fail = 0u32;
146    let mut soc2_pass = 0u32;
147    let mut soc2_review = 0u32;
148    let mut soc2_fail = 0u32;
149    let mut graduation_blockers: Vec<(String, String)> = Vec::new();
150
151    for (oss_out, soc2_out) in oss_outcomes.iter().zip(soc2_outcomes.iter()) {
152        let oss_dec = oss_out.decision;
153        let soc2_dec = soc2_out.decision;
154
155        match oss_dec {
156            GateDecision::Pass => oss_pass += 1,
157            GateDecision::Review => oss_review += 1,
158            GateDecision::Fail => oss_fail += 1,
159        }
160        match soc2_dec {
161            GateDecision::Pass => soc2_pass += 1,
162            GateDecision::Review => soc2_review += 1,
163            GateDecision::Fail => soc2_fail += 1,
164        }
165
166        let delta = if oss_dec == soc2_dec {
167            " ".to_string()
168        } else {
169            let arrow = format!("{} -> {}", oss_dec, soc2_dec);
170            if soc2_dec == GateDecision::Fail {
171                graduation_blockers.push((
172                    oss_out.control_id.as_str().to_string(),
173                    oss_out.rationale.clone(),
174                ));
175                format!("!! {arrow}")
176            } else {
177                format!("   {arrow}")
178            }
179        };
180
181        println!(
182            "{:<40} {:<12} {:<12}  {}",
183            oss_out.control_id.as_str(),
184            format!("{}", oss_dec),
185            format!("{}", soc2_dec),
186            delta,
187        );
188    }
189
190    // --- Summary ---
191    println!("\n{}", "=".repeat(90));
192    println!("SUMMARY\n");
193    println!(
194        "  OSS  preset:  {} pass / {} review / {} fail",
195        oss_pass, oss_review, oss_fail
196    );
197    println!(
198        "  SOC2 preset:  {} pass / {} review / {} fail",
199        soc2_pass, soc2_review, soc2_fail
200    );
201
202    println!("\n{}", "-".repeat(90));
203    println!(
204        "GRADUATION BLOCKERS (review in OSS, fail in SOC2): {}\n",
205        graduation_blockers.len()
206    );
207    if graduation_blockers.is_empty() {
208        println!("  None -- you are already SOC2-ready (unlikely for a startup!)");
209    } else {
210        for (id, rationale) in &graduation_blockers {
211            println!("  [BLOCK] {id}");
212            println!("          {rationale}\n");
213        }
214    }
215
216    // Also show controls that fail in both (already broken)
217    let both_fail: Vec<_> = oss_outcomes
218        .iter()
219        .zip(soc2_outcomes.iter())
220        .filter(|(o, s)| o.decision == GateDecision::Fail && s.decision == GateDecision::Fail)
221        .map(|(o, _)| o.control_id.as_str().to_string())
222        .collect();
223
224    if !both_fail.is_empty() {
225        println!("{}", "-".repeat(90));
226        println!(
227            "FAIL IN BOTH presets (fix these regardless): {}\n",
228            both_fail.len()
229        );
230        for id in &both_fail {
231            println!("  [FAIL] {id}");
232        }
233    }
234
235    // Controls that are review in SOC2 (needs attention but not blocking)
236    let soc2_review_list: Vec<_> = soc2_outcomes
237        .iter()
238        .filter(|o| o.decision == GateDecision::Review)
239        .map(|o| o.control_id.as_str().to_string())
240        .collect();
241
242    if !soc2_review_list.is_empty() {
243        println!("\n{}", "-".repeat(90));
244        println!(
245            "SOC2 REVIEW items (auditor will ask questions): {}\n",
246            soc2_review_list.len()
247        );
248        for id in &soc2_review_list {
249            println!("  [REVIEW] {id}");
250        }
251    }
252
253    println!("\n{}", "=".repeat(90));
254    println!("GRADUATION ROADMAP:");
255    println!("  1. Enable branch protection + require reviews  (unblocks review-independence,");
256    println!("     two-party-review, branch-protection-enforcement, branch-history-integrity)");
257    println!("  2. Enable secret scanning + push protection    (unblocks secret-scanning)");
258    println!("  3. Add CODEOWNERS file                         (unblocks codeowners-coverage)");
259    println!("  4. Sign commits (GPG/SSH)                      (unblocks source-authenticity)");
260    println!("  5. Link issues to PRs                          (unblocks issue-linkage)");
261    println!("  6. Add disclosure process to SECURITY.md       (unblocks security-policy)");
262    println!("  7. Set up build provenance (SLSA)              (unblocks build-track controls)");
263    println!("{}", "=".repeat(90));
264}