1use super::test_bundle::create_single_event_bundle;
2use crate::mutators::inject::InjectFile;
3use crate::mutators::Mutator;
4use crate::report::SimReport;
5use crate::suite::TimeBudget;
6use anyhow::Result as AnyhowResult;
7use assay_evidence::types::EvidenceEvent;
8use assay_evidence::{verify_bundle_with_limits, VerifyError, VerifyLimits};
9use chrono::{TimeZone, Utc};
10use flate2::read::GzEncoder;
11use flate2::Compression;
12use rand::Rng;
13use rand::SeedableRng;
14use std::io::{self, Cursor, Read};
15
16pub fn check_integrity_attacks(
17 report: &mut SimReport,
18 seed: u64,
19 limits: VerifyLimits,
20 budget: &TimeBudget,
21) -> Result<(), IntegrityError> {
22 let valid_bundle = create_single_event_bundle().map_err(IntegrityError::from)?;
23
24 run_attack(report, "integrity.bitflip", limits, budget, || {
26 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
27 let mut corrupted = valid_bundle.clone();
28 for _ in 0..10 {
29 let idx = rng.gen_range(0..corrupted.len());
30 corrupted[idx] ^= 1 << rng.gen_range(0..8);
31 }
32 Ok(corrupted)
33 })?;
34
35 run_attack(report, "integrity.truncate", limits, budget, || {
37 Ok(valid_bundle[..valid_bundle.len() / 2].to_vec())
38 })?;
39
40 run_attack(report, "integrity.inject_file", limits, budget, || {
42 let injector = InjectFile {
43 name: "malicious.sh".into(),
44 content: b"echo 'bad'".to_vec(),
45 };
46 injector.mutate(&valid_bundle)
47 })?;
48
49 run_attack(report, "security.zip_bomb", limits, budget, || {
51 create_zip_bomb(1100 * 1024 * 1024)
52 })?;
53
54 run_attack(report, "integrity.tar_duplicate", limits, budget, || {
56 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
57 {
58 let mut builder = tar::Builder::new(&mut encoder);
59 let manifest = serde_json::json!({
60 "schema_version": 1, "run_id": "test", "event_count": 1, "run_root": "sha256:...",
61 "files": { "events.ndjson": { "sha256": "..." } }
62 });
63 let manifest_bytes = serde_json::to_vec(&manifest)?;
64 let mut header = tar::Header::new_gnu();
65 header.set_path("manifest.json")?;
66 header.set_size(manifest_bytes.len() as u64);
67 header.set_cksum();
68 builder.append(&header, manifest_bytes.as_slice())?;
69
70 let event = create_event(0);
71 let event_bytes = serde_json::to_vec(&event)?;
72 for _ in 0..2 {
73 let mut header = tar::Header::new_gnu();
74 header.set_path("events.ndjson")?;
75 header.set_size(event_bytes.len() as u64);
76 header.set_cksum();
77 builder.append(&header, event_bytes.as_slice())?;
78 }
79 builder.finish()?;
80 }
81 Ok(encoder.finish()?)
82 })?;
83
84 run_attack(report, "integrity.ndjson_bom", limits, budget, || {
86 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
87 {
88 let mut builder = tar::Builder::new(&mut encoder);
89 let manifest = serde_json::json!({
90 "schema_version": 1, "run_id": "test", "event_count": 1, "run_root": "sha256:...",
91 "files": { "events.ndjson": { "sha256": "..." } }
92 });
93 let manifest_bytes = serde_json::to_vec(&manifest)?;
94 let mut header = tar::Header::new_gnu();
95 header.set_path("manifest.json")?;
96 header.set_size(manifest_bytes.len() as u64);
97 header.set_cksum();
98 builder.append(&header, manifest_bytes.as_slice())?;
99
100 let mut content = vec![0xEF, 0xBB, 0xBF];
101 content.extend_from_slice(&serde_json::to_vec(&create_event(0))?);
102 let mut header = tar::Header::new_gnu();
103 header.set_path("events.ndjson")?;
104 header.set_size(content.len() as u64);
105 header.set_cksum();
106 builder.append(&header, content.as_slice())?;
107 builder.finish()?;
108 }
109 Ok(encoder.finish()?)
110 })?;
111
112 run_attack(report, "integrity.ndjson_crlf", limits, budget, || {
114 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
115 {
116 let mut builder = tar::Builder::new(&mut encoder);
117 let mut content = serde_json::to_vec(&create_event(0))?;
119 content.extend_from_slice(b"\r\n");
120
121 let mut header = tar::Header::new_gnu();
122 header.set_path("events.ndjson")?;
123 header.set_size(content.len() as u64);
124 header.set_cksum();
125 builder.append(&header, content.as_slice())?;
126 builder.finish()?;
127 }
128 Ok(encoder.finish()?)
129 })?;
130
131 run_attack_reader(
133 report,
134 "integrity.limit_bundle_bytes",
135 limits,
136 budget,
137 || {
138 let n = limits.max_bundle_bytes.saturating_add(1);
139 let src = io::repeat(0u8).take(n);
140 Ok(GzEncoder::new(src, Compression::none()))
141 },
142 )?;
143
144 Ok(())
145}
146
147fn create_event(seq: u64) -> EvidenceEvent {
148 let mut event = EvidenceEvent::new("assay.test", "urn:test", "run", seq, serde_json::json!({}));
149 event.time = Utc.timestamp_opt(1700000000, 0).unwrap();
150 event
151}
152
153#[derive(Debug)]
154pub enum IntegrityError {
155 BudgetExceeded,
156 Other(anyhow::Error),
157}
158impl From<anyhow::Error> for IntegrityError {
159 fn from(e: anyhow::Error) -> Self {
160 Self::Other(e)
161 }
162}
163
164fn run_attack_reader<F, R>(
165 report: &mut SimReport,
166 name: &str,
167 limits: VerifyLimits,
168 budget: &TimeBudget,
169 make_reader: F,
170) -> Result<(), IntegrityError>
171where
172 F: FnOnce() -> AnyhowResult<R>,
173 R: Read,
174{
175 if budget.exceeded() {
176 return Err(IntegrityError::BudgetExceeded);
177 }
178 let reader = make_reader()?;
179 let start = std::time::Instant::now();
180 let res = verify_bundle_with_limits(reader, limits);
181 let duration = start.elapsed().as_millis() as u64;
182
183 match res {
184 Ok(_) => report.add_attack(name, Err(anyhow::anyhow!("Attack Bypassed")), duration),
185 Err(e) => {
186 if let Some(ve) = e.downcast_ref::<VerifyError>() {
187 report.add_attack(name, Ok((ve.class(), ve.code)), duration);
188 } else {
189 report.add_attack(
190 name,
191 Err(anyhow::anyhow!("Unexpected error: {}", e)),
192 duration,
193 );
194 }
195 }
196 }
197
198 if budget.exceeded() {
199 return Err(IntegrityError::BudgetExceeded);
200 }
201 Ok(())
202}
203
204fn run_attack<F>(
205 report: &mut SimReport,
206 name: &str,
207 limits: VerifyLimits,
208 budget: &TimeBudget,
209 mutator: F,
210) -> Result<(), IntegrityError>
211where
212 F: FnOnce() -> AnyhowResult<Vec<u8>>,
213{
214 run_attack_reader(report, name, limits, budget, || {
215 let data = mutator()?;
216 Ok(Cursor::new(data))
217 })
218}
219
220fn create_zip_bomb(target_uncompressed: u64) -> AnyhowResult<Vec<u8>> {
221 use flate2::write::GzEncoder;
222 use flate2::Compression;
223 use std::io::Write;
224
225 let mut buf = Vec::new();
226 let mut encoder = GzEncoder::new(&mut buf, Compression::best());
227 let chunk = vec![0u8; 1024 * 1024];
228 let mut remaining = target_uncompressed;
229 while remaining > 0 {
230 let to_write = remaining.min(chunk.len() as u64);
231 encoder.write_all(&chunk[..to_write as usize])?;
232 remaining -= to_write;
233 }
234 encoder.finish()?;
235 Ok(buf)
236}
237
238#[cfg(test)]
240fn run_limit_bundle_bytes_only(
241 report: &mut SimReport,
242 limits: VerifyLimits,
243 budget: &TimeBudget,
244) -> Result<(), IntegrityError> {
245 run_attack_reader(
246 report,
247 "integrity.limit_bundle_bytes",
248 limits,
249 budget,
250 || {
251 let n = limits.max_bundle_bytes.saturating_add(1);
252 let src = io::repeat(0u8).take(n);
253 Ok(GzEncoder::new(src, Compression::none()))
254 },
255 )
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::report::AttackStatus;
262 use crate::suite::TimeBudget;
263 use assay_evidence::VerifyLimits;
264
265 #[test]
266 fn test_limit_bundle_bytes_blocked_with_limit_bundle_bytes() {
267 let limits = VerifyLimits {
270 max_bundle_bytes: 100,
271 ..Default::default()
272 };
273
274 let mut report = SimReport::new("test", 0);
275 let budget = TimeBudget::new(std::time::Duration::from_secs(60));
276
277 run_limit_bundle_bytes_only(&mut report, limits, &budget).unwrap();
278
279 let r = report
280 .results
281 .iter()
282 .find(|r| r.name == "integrity.limit_bundle_bytes")
283 .expect("limit_bundle_bytes result");
284 assert_eq!(r.status, AttackStatus::Blocked);
285 assert_eq!(
286 r.error_code.as_deref(),
287 Some("LimitBundleBytes"),
288 "expected LimitBundleBytes, got {:?}",
289 r.error_code
290 );
291 }
292}