Skip to main content

audit_scenarios/
audit_scenarios.rs

1//! SOC2 Type II audit edge-case scenarios.
2//!
3//! Tests five boundary conditions that a security auditor probes during
4//! examination, then runs each finding through both the OSS and SOC2 policy
5//! presets to verify differentiated gate decisions.
6//!
7//! Run: cargo run -p libverify-policy --example audit_scenarios
8
9use libverify_core::control::{Control, ControlStatus};
10use libverify_core::controls::codeowners_coverage::CodeownersCoverageControl;
11use libverify_core::controls::secret_scanning::SecretScanningControl;
12use libverify_core::controls::security_policy::SecurityPolicyControl;
13use libverify_core::controls::vulnerability_scanning::VulnerabilityScanningControl;
14use libverify_core::evidence::{CodeownersEntry, EvidenceBundle, EvidenceState, RepositoryPosture};
15use libverify_core::profile::{ControlProfile, GateDecision};
16use libverify_policy::OpaProfile;
17
18// ---------------------------------------------------------------------------
19// Helpers
20// ---------------------------------------------------------------------------
21
22fn default_posture() -> RepositoryPosture {
23    RepositoryPosture {
24        codeowners_entries: vec![],
25        secret_scanning_enabled: false,
26        secret_push_protection_enabled: false,
27        vulnerability_scanning_enabled: false,
28        code_scanning_enabled: false,
29        security_policy_present: false,
30        security_policy_has_disclosure: false,
31        default_branch_protected: false,
32    }
33}
34
35fn bundle_from_posture(posture: RepositoryPosture) -> EvidenceBundle {
36    EvidenceBundle {
37        repository_posture: EvidenceState::complete(posture),
38        ..Default::default()
39    }
40}
41
42fn entry(pattern: &str, owners: &[&str]) -> CodeownersEntry {
43    CodeownersEntry {
44        pattern: pattern.to_string(),
45        owners: owners.iter().map(|s| s.to_string()).collect(),
46    }
47}
48
49// ---------------------------------------------------------------------------
50// Scenario runner
51// ---------------------------------------------------------------------------
52
53struct ScenarioResult {
54    name: &'static str,
55    expected_status: ControlStatus,
56    actual_status: ControlStatus,
57    rationale: String,
58    subjects: Vec<String>,
59    oss_decision: GateDecision,
60    soc2_decision: GateDecision,
61}
62
63fn run_scenario(
64    name: &'static str,
65    control: &dyn Control,
66    bundle: &EvidenceBundle,
67    expected_status: ControlStatus,
68    oss: &dyn ControlProfile,
69    soc2: &dyn ControlProfile,
70) -> ScenarioResult {
71    let findings = control.evaluate(bundle);
72    let finding = &findings[0];
73    let oss_outcome = oss.map(finding);
74    let soc2_outcome = soc2.map(finding);
75
76    ScenarioResult {
77        name,
78        expected_status,
79        actual_status: finding.status,
80        rationale: finding.rationale.clone(),
81        subjects: finding.subjects.clone(),
82        oss_decision: oss_outcome.decision,
83        soc2_decision: soc2_outcome.decision,
84    }
85}
86
87// ---------------------------------------------------------------------------
88// Main
89// ---------------------------------------------------------------------------
90
91fn main() {
92    let oss = OpaProfile::oss_preset().expect("oss preset");
93    let soc2 = OpaProfile::soc2_preset().expect("soc2 preset");
94
95    let secret_ctrl = SecretScanningControl;
96    let codeowners_ctrl = CodeownersCoverageControl;
97    let vuln_ctrl = VulnerabilityScanningControl;
98    let secpol_ctrl = SecurityPolicyControl;
99
100    // --- Scenario A: secret scanning ON, push protection OFF ---
101    // Expected: Satisfied with "detection" tier (not "prevention")
102    let mut posture_a = default_posture();
103    posture_a.secret_scanning_enabled = true;
104    posture_a.secret_push_protection_enabled = false;
105    let bundle_a = bundle_from_posture(posture_a);
106
107    // --- Scenario B: CODEOWNERS with exactly 2 entries (below threshold of 3) ---
108    // Expected: Violated (insufficient targeted coverage)
109    let mut posture_b = default_posture();
110    posture_b.codeowners_entries = vec![
111        entry("/src/auth/", &["@org/security"]),
112        entry("/infra/", &["@org/platform"]),
113    ];
114    let bundle_b = bundle_from_posture(posture_b);
115
116    // --- Scenario C: CODEOWNERS with exactly 3 targeted entries (at threshold) ---
117    // Expected: Satisfied (intentional targeted coverage)
118    let mut posture_c = default_posture();
119    posture_c.codeowners_entries = vec![
120        entry("/src/auth/", &["@org/security"]),
121        entry("/infra/", &["@org/platform"]),
122        entry("/.github/", &["@org/devops"]),
123    ];
124    let bundle_c = bundle_from_posture(posture_c);
125
126    // --- Scenario D: vulnerability scanning ON, code scanning OFF ---
127    // Expected: Satisfied with "sca-only" tier (not "sca+sast")
128    let mut posture_d = default_posture();
129    posture_d.vulnerability_scanning_enabled = true;
130    posture_d.code_scanning_enabled = false;
131    let bundle_d = bundle_from_posture(posture_d);
132
133    // --- Scenario E: SECURITY.md present but no disclosure process ---
134    // Expected: Violated (policy exists but incomplete)
135    let mut posture_e = default_posture();
136    posture_e.security_policy_present = true;
137    posture_e.security_policy_has_disclosure = false;
138    let bundle_e = bundle_from_posture(posture_e);
139
140    let results = vec![
141        run_scenario(
142            "A: Secret scanning ON, push protection OFF",
143            &secret_ctrl,
144            &bundle_a,
145            ControlStatus::Satisfied,
146            &oss,
147            &soc2,
148        ),
149        run_scenario(
150            "B: CODEOWNERS 2 entries (below threshold)",
151            &codeowners_ctrl,
152            &bundle_b,
153            ControlStatus::Violated,
154            &oss,
155            &soc2,
156        ),
157        run_scenario(
158            "C: CODEOWNERS 3 entries (at threshold)",
159            &codeowners_ctrl,
160            &bundle_c,
161            ControlStatus::Satisfied,
162            &oss,
163            &soc2,
164        ),
165        run_scenario(
166            "D: Vuln scanning ON, code scanning OFF",
167            &vuln_ctrl,
168            &bundle_d,
169            ControlStatus::Satisfied,
170            &oss,
171            &soc2,
172        ),
173        run_scenario(
174            "E: SECURITY.md present, no disclosure",
175            &secpol_ctrl,
176            &bundle_e,
177            ControlStatus::Violated,
178            &oss,
179            &soc2,
180        ),
181    ];
182
183    // --- Report ---
184    println!("=== SOC2 Type II Audit Edge-Case Scenarios ===\n");
185
186    let mut all_pass = true;
187    for r in &results {
188        let status_ok = r.actual_status == r.expected_status;
189        let marker = if status_ok { "PASS" } else { "FAIL" };
190        if !status_ok {
191            all_pass = false;
192        }
193
194        println!("--- {} ---", r.name);
195        println!(
196            "  Status:    {} (expected: {}) [{}]",
197            r.actual_status, r.expected_status, marker
198        );
199        println!("  Rationale: {}", r.rationale);
200        println!("  Subjects:  {:?}", r.subjects);
201        println!(
202            "  OSS decision:  {:?}   SOC2 decision: {:?}",
203            r.oss_decision, r.soc2_decision
204        );
205        println!();
206    }
207
208    // --- Policy differentiation summary ---
209    println!("=== Policy Differentiation Summary ===\n");
210    for r in &results {
211        let diff = if r.oss_decision != r.soc2_decision {
212            format!(
213                "DIFFER (OSS={:?}, SOC2={:?})",
214                r.oss_decision, r.soc2_decision
215            )
216        } else {
217            format!("SAME ({:?})", r.oss_decision)
218        };
219        println!("  {}: {}", r.name, diff);
220    }
221
222    // --- Tiered subject verification ---
223    println!("\n=== Tiered Subject Verification ===\n");
224    let tier_checks: &[(&str, &str, usize)] = &[
225        ("A: detection tier", "detection", 0),
226        ("D: sca-only tier", "sca-only", 3),
227    ];
228    for (label, needle, idx) in tier_checks {
229        let found = results[*idx].subjects.iter().any(|s| s.contains(needle));
230        let mark = if found { "PRESENT" } else { "MISSING" };
231        println!("  {}: '{}' -> [{}]", label, needle, mark);
232    }
233
234    println!();
235    if all_pass {
236        println!("All scenarios produced the expected control status.");
237    } else {
238        println!("WARNING: Some scenarios did NOT match expected status.");
239        std::process::exit(1);
240    }
241}