1pub mod attacks;
2pub mod corpus;
3pub mod differential;
4pub mod mutators;
5pub mod report;
6pub mod subprocess;
7pub mod suite;
8
9pub use report::{AttackResult, AttackStatus, SimReport};
10pub use suite::{run_suite, tier_default_limits, SuiteConfig, SuiteTier, TimeBudget};
11
12#[cfg(test)]
13mod tests {
14 use super::*;
15 use std::path::PathBuf;
16
17 const QUICK_REFUSALS: &[QuickRefusal] = &[
23 QuickRefusal::class("integrity.bitflip", "Integrity"),
24 QuickRefusal::code_and_message(
25 "integrity.bitflip_crc",
26 "Integrity",
27 "IntegrityGzip",
28 "Gzip trailer",
29 ),
30 QuickRefusal::code("integrity.truncate", "Integrity", "IntegrityIo"),
31 QuickRefusal::code(
32 "integrity.inject_file",
33 "Contract",
34 "ContractUnexpectedFile",
35 ),
36 QuickRefusal::code("security.zip_bomb", "Limits", "LimitDecodeBytes"),
37 QuickRefusal::code(
38 "integrity.tar_duplicate",
39 "Contract",
40 "ContractDuplicateFile",
41 ),
42 QuickRefusal::code_and_message(
43 "integrity.ndjson_bom",
44 "Contract",
45 "ContractInvalidJson",
46 "BOM not allowed",
47 ),
48 QuickRefusal::passed("integrity.ndjson_crlf"),
49 QuickRefusal::code("integrity.limit_bundle_bytes", "Limits", "LimitBundleBytes"),
50 QuickRefusal::passed("differential.invariants"),
51 ];
52
53 struct QuickRefusal {
54 name: &'static str,
55 status: AttackStatus,
56 class: Option<&'static str>,
57 code: Option<&'static str>,
58 message_contains: Option<&'static str>,
59 }
60
61 impl QuickRefusal {
62 const fn class(name: &'static str, class: &'static str) -> Self {
63 Self {
64 name,
65 status: AttackStatus::Blocked,
66 class: Some(class),
67 code: None,
68 message_contains: None,
69 }
70 }
71
72 const fn code(name: &'static str, class: &'static str, code: &'static str) -> Self {
73 Self {
74 name,
75 status: AttackStatus::Blocked,
76 class: Some(class),
77 code: Some(code),
78 message_contains: None,
79 }
80 }
81
82 const fn code_and_message(
83 name: &'static str,
84 class: &'static str,
85 code: &'static str,
86 message_contains: &'static str,
87 ) -> Self {
88 Self {
89 name,
90 status: AttackStatus::Blocked,
91 class: Some(class),
92 code: Some(code),
93 message_contains: Some(message_contains),
94 }
95 }
96
97 const fn passed(name: &'static str) -> Self {
98 Self {
99 name,
100 status: AttackStatus::Passed,
101 class: None,
102 code: None,
103 message_contains: None,
104 }
105 }
106 }
107
108 #[test]
109 fn test_quick_suite() {
110 let cfg = SuiteConfig {
111 tier: SuiteTier::Quick,
112 target_bundle: PathBuf::from("placeholder"),
113 seed: 42,
114 verify_limits: None,
115 time_budget_secs: 60,
116 };
117
118 let report = run_suite(cfg).expect("Suite failed to run");
119
120 if report.summary.bypassed > 0 {
122 println!("{}", serde_json::to_string_pretty(&report).unwrap());
123 }
124
125 assert_eq!(
128 report.summary.bypassed, 0,
129 "SECURITY: {} attacks bypassed verification",
130 report.summary.bypassed
131 );
132 assert!(
134 report.summary.blocked >= 1,
135 "SANITY: no attacks were blocked — suite may not have run"
136 );
137 let differential_ran = report
139 .results
140 .iter()
141 .any(|r| r.name == "differential.invariants");
142 assert!(
143 report.summary.passed >= 1 || differential_ran,
144 "SANITY: no checks passed and differential did not run — suite may not have run"
145 );
146 for r in &report.results {
151 let is_chaos_io = r.name.starts_with("chaos.io_fault.");
152 match r.status {
153 AttackStatus::Blocked | AttackStatus::Passed => {} AttackStatus::Error if is_chaos_io => {} _ => panic!(
156 "Unexpected status {:?} for '{}': {:?}",
157 r.status, r.name, r.message
158 ),
159 }
160 }
161
162 assert_named_quick_refusals(&report);
163 }
164
165 fn assert_named_quick_refusals(report: &SimReport) {
166 for expected in QUICK_REFUSALS {
167 let r = report
168 .results
169 .iter()
170 .find(|r| r.name == expected.name)
171 .unwrap_or_else(|| panic!("Quick suite is missing '{}'", expected.name));
172 assert_eq!(
173 r.status, expected.status,
174 "{}: status {:?} (message {:?})",
175 expected.name, r.status, r.message
176 );
177 if let Some(class) = expected.class {
178 assert_eq!(
179 r.error_class.as_deref(),
180 Some(class),
181 "{}: refusing class",
182 expected.name
183 );
184 }
185 if let Some(code) = expected.code {
186 assert_eq!(
187 r.error_code.as_deref(),
188 Some(code),
189 "{}: refusing code (message {:?})",
190 expected.name,
191 r.message
192 );
193 }
194 if let Some(part) = expected.message_contains {
195 let message = r.message.as_deref().unwrap_or("");
196 assert!(
197 message.contains(part),
198 "{}: message {message:?} should name {part:?}",
199 expected.name
200 );
201 }
202 }
203
204 for r in &report.results {
205 assert!(
206 QUICK_REFUSALS
207 .iter()
208 .any(|expected| expected.name == r.name),
209 "Quick suite result '{}' has no pinned refusal",
210 r.name
211 );
212 }
213 }
214}