Skip to main content

assay_sim/
suite.rs

1use crate::attacks;
2use crate::differential;
3use crate::report::{AttackResult, AttackStatus, SimReport};
4use anyhow::Result;
5use assay_evidence::VerifyLimits;
6use std::path::PathBuf;
7use std::time::{Duration, Instant};
8
9#[derive(Debug, Clone)]
10pub enum SuiteTier {
11    Quick,
12    Nightly,
13    Stress,
14    Chaos,
15}
16
17#[derive(Debug, Clone)]
18pub struct SuiteConfig {
19    pub tier: SuiteTier,
20    pub target_bundle: PathBuf, // Placeholder for future file-based targets
21    pub seed: u64,
22    pub verify_limits: Option<VerifyLimits>,
23}
24
25/// Time budget for an entire suite run.
26///
27/// If the elapsed time exceeds the budget, remaining phases are skipped and
28/// the runner reports `AttackStatus::Error` with "time budget exceeded".
29#[derive(Debug, Clone)]
30pub struct TimeBudget {
31    start: Instant,
32    limit: Duration,
33}
34
35impl TimeBudget {
36    pub fn new(limit: Duration) -> Self {
37        Self {
38            start: Instant::now(),
39            limit,
40        }
41    }
42
43    /// Default suite budget: 60 seconds.
44    /// Note: Raised from 30s because zip bomb attack (1.1GB decompression)
45    /// can take 30+ seconds on slower CI runners (macOS).
46    pub fn default_suite() -> Self {
47        Self::new(Duration::from_secs(60))
48    }
49
50    pub fn exceeded(&self) -> bool {
51        self.start.elapsed() > self.limit
52    }
53
54    pub fn remaining(&self) -> Duration {
55        self.limit.saturating_sub(self.start.elapsed())
56    }
57}
58
59pub fn run_suite(cfg: SuiteConfig) -> Result<SimReport> {
60    let mut report = SimReport::new(&format!("{:?}", cfg.tier), cfg.seed);
61    let budget = TimeBudget::default_suite();
62
63    // 1. Integrity Attacks (all tiers)
64    //
65    // Note: The workspace uses panic="abort" in dev/release profiles, so catch_unwind
66    // is not effective. Integrity attacks run in-process (they don't trigger panics —
67    // they test verification outcomes). Chaos/differential attacks use subprocess
68    // isolation instead.
69    {
70        let seed = cfg.seed;
71        let start = Instant::now();
72        let mut inner_report = SimReport::new("integrity", seed);
73        match attacks::integrity::check_integrity_attacks(&mut inner_report, seed) {
74            Ok(()) => {
75                for r in inner_report.results {
76                    report.add_result(r);
77                }
78            }
79            Err(e) => {
80                for r in inner_report.results {
81                    report.add_result(r);
82                }
83                report.add_result(AttackResult {
84                    name: "integrity_attacks".into(),
85                    status: AttackStatus::Error,
86                    error_class: None,
87                    error_code: None,
88                    message: Some(e.to_string()),
89                    duration_ms: start.elapsed().as_millis() as u64,
90                });
91            }
92        }
93    }
94
95    if budget.exceeded() {
96        report.add_result(AttackResult {
97            name: "time_budget".into(),
98            status: AttackStatus::Error,
99            error_class: None,
100            error_code: None,
101            message: Some("time budget exceeded after integrity attacks".into()),
102            duration_ms: budget.start.elapsed().as_millis() as u64,
103        });
104        return Ok(report);
105    }
106
107    // 2. Differential Testing
108    let iterations = match cfg.tier {
109        SuiteTier::Quick => 5,
110        SuiteTier::Nightly => 100,
111        SuiteTier::Stress => 1000,
112        SuiteTier::Chaos => 50,
113    };
114
115    {
116        let start = Instant::now();
117        let inner = differential::check_invariants(iterations, Some(cfg.seed));
118        let duration = start.elapsed().as_millis() as u64;
119        report.add_check("differential.invariants", inner, duration);
120    }
121
122    if budget.exceeded() {
123        report.add_result(AttackResult {
124            name: "time_budget".into(),
125            status: AttackStatus::Error,
126            error_class: None,
127            error_code: None,
128            message: Some("time budget exceeded after differential tests".into()),
129            duration_ms: budget.start.elapsed().as_millis() as u64,
130        });
131        return Ok(report);
132    }
133
134    // 3. Chaos-tier extras (use subprocess isolation for panic=abort safety)
135    if matches!(cfg.tier, SuiteTier::Chaos) {
136        run_chaos_phase(&mut report, cfg.seed, &budget);
137    }
138
139    Ok(report)
140}
141
142fn run_chaos_phase(report: &mut SimReport, seed: u64, budget: &TimeBudget) {
143    // IO chaos attacks (in-process — these inject IO errors, not panics)
144    match attacks::chaos::check_chaos_attacks(seed) {
145        Ok(results) => {
146            for r in results {
147                report.add_result(r);
148            }
149        }
150        Err(e) => {
151            report.add_result(AttackResult {
152                name: "chaos.io_faults".into(),
153                status: AttackStatus::Error,
154                error_class: None,
155                error_code: None,
156                message: Some(format!("chaos attacks failed: {}", e)),
157                duration_ms: 0,
158            });
159        }
160    }
161
162    if budget.exceeded() {
163        report.add_result(AttackResult {
164            name: "time_budget".into(),
165            status: AttackStatus::Error,
166            error_class: None,
167            error_code: None,
168            message: Some("time budget exceeded during chaos phase".into()),
169            duration_ms: 0,
170        });
171        return;
172    }
173
174    // Differential parity checks (uses subprocess isolation for production verifier)
175    match attacks::differential::check_differential_parity(seed) {
176        Ok(results) => {
177            for r in results {
178                report.add_result(r);
179            }
180        }
181        Err(e) => {
182            report.add_result(AttackResult {
183                name: "differential.parity".into(),
184                status: AttackStatus::Error,
185                error_class: None,
186                error_code: None,
187                message: Some(format!("differential parity failed: {}", e)),
188                duration_ms: 0,
189            });
190        }
191    }
192}