Skip to main content

max / makenotwork

server: parallel path-based entries on yara/zip/sniff layers
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-27 15:53 UTC
Commit: 44cef50da5a86425a1761d1239472dab4dc85871
Parent: 4512c3d
3 files changed, +156 insertions, -2 deletions
@@ -40,8 +40,57 @@ pub fn check_archive_safety(data: &[u8], file_type: FileType) -> LayerResult {
40 40 };
41 41 }
42 42
43 - let cursor = Cursor::new(data);
44 - let mut archive = match zip::ZipArchive::new(cursor) {
43 + inspect_zip(Cursor::new(data))
44 + }
45 +
46 + /// Path-based entry. Opens the spooled file directly so we never have to
47 + /// buffer the whole archive; `ZipArchive::new` only needs `Read + Seek`.
48 + /// File-type gating happens at the call site (same shape as the buffered
49 + /// variant — caller already checked `file_type`).
50 + pub fn check_archive_safety_path(path: &std::path::Path, file_type: FileType) -> LayerResult {
51 + if file_type == FileType::Cover {
52 + return LayerResult {
53 + layer: "archive",
54 + verdict: LayerVerdict::Skip,
55 + detail: Some("Archive check skipped for cover images".to_string()),
56 + };
57 + }
58 +
59 + let mut file = match std::fs::File::open(path) {
60 + Ok(f) => f,
61 + Err(e) => {
62 + return LayerResult {
63 + layer: "archive",
64 + verdict: LayerVerdict::Error,
65 + detail: Some(format!("open spool {}: {e}", path.display())),
66 + };
67 + }
68 + };
69 +
70 + let mut magic = [0u8; 4];
71 + use std::io::{Read, Seek, SeekFrom};
72 + let read = file.read(&mut magic).unwrap_or(0);
73 + let is_zip = read == 4 && magic == [0x50, 0x4B, 0x03, 0x04];
74 + if !is_zip {
75 + return LayerResult {
76 + layer: "archive",
77 + verdict: LayerVerdict::Skip,
78 + detail: Some("Not a ZIP archive".to_string()),
79 + };
80 + }
81 + if file.seek(SeekFrom::Start(0)).is_err() {
82 + return LayerResult {
83 + layer: "archive",
84 + verdict: LayerVerdict::Error,
85 + detail: Some(format!("seek spool {}", path.display())),
86 + };
87 + }
88 +
89 + inspect_zip(file)
90 + }
91 +
92 + fn inspect_zip<R: std::io::Read + std::io::Seek>(reader: R) -> LayerResult {
93 + let mut archive = match zip::ZipArchive::new(reader) {
45 94 Ok(a) => a,
46 95 Err(e) => {
47 96 return LayerResult {
@@ -534,4 +583,27 @@ mod tests {
534 583 assert_eq!(result.verdict, LayerVerdict::Error);
535 584 assert!(result.detail.unwrap().contains("Failed to parse ZIP"));
536 585 }
586 +
587 + #[test]
588 + fn path_entry_matches_buffered_for_non_zip() {
589 + let data = b"not a zip at all";
590 + let buffered = check_archive_safety(data, FileType::Download);
591 + let tmp = tempfile::NamedTempFile::new().unwrap();
592 + std::fs::write(tmp.path(), data).unwrap();
593 + let path_based = check_archive_safety_path(tmp.path(), FileType::Download);
594 + assert_eq!(buffered.verdict, path_based.verdict);
595 + assert_eq!(buffered.verdict, LayerVerdict::Skip);
596 + }
597 +
598 + #[test]
599 + fn path_entry_matches_buffered_for_cover_skip() {
600 + let mut data = vec![0x50, 0x4B, 0x03, 0x04];
601 + data.extend_from_slice(&[0xFF; 100]);
602 + let buffered = check_archive_safety(&data, FileType::Cover);
603 + let tmp = tempfile::NamedTempFile::new().unwrap();
604 + std::fs::write(tmp.path(), &data).unwrap();
605 + let path_based = check_archive_safety_path(tmp.path(), FileType::Cover);
606 + assert_eq!(buffered.verdict, path_based.verdict);
607 + assert_eq!(buffered.verdict, LayerVerdict::Skip);
608 + }
537 609 }
@@ -147,6 +147,37 @@ pub fn verify_content_type(data: &[u8], claimed_type: FileType) -> LayerResult {
147 147 }
148 148 }
149 149
150 + /// Path-based entry. Reads only the head bytes (`infer` inspects ~262
151 + /// bytes); no need to mmap or buffer the whole spooled file.
152 + pub fn verify_content_type_path(path: &std::path::Path, claimed_type: FileType) -> LayerResult {
153 + use std::io::Read;
154 +
155 + let mut file = match std::fs::File::open(path) {
156 + Ok(f) => f,
157 + Err(e) => {
158 + return LayerResult {
159 + layer: "content_type",
160 + verdict: LayerVerdict::Error,
161 + detail: Some(format!("open spool {}: {e}", path.display())),
162 + };
163 + }
164 + };
165 +
166 + let mut head = [0u8; 4096];
167 + let n = match file.read(&mut head) {
168 + Ok(n) => n,
169 + Err(e) => {
170 + return LayerResult {
171 + layer: "content_type",
172 + verdict: LayerVerdict::Error,
173 + detail: Some(format!("read spool head: {e}")),
174 + };
175 + }
176 + };
177 +
178 + verify_content_type(&head[..n], claimed_type)
179 + }
180 +
150 181 #[cfg(test)]
151 182 mod tests {
152 183 use super::*;
@@ -324,4 +355,14 @@ mod tests {
324 355 let result = verify_content_type(b"just random bytes", FileType::Video);
325 356 assert_eq!(result.verdict, LayerVerdict::Fail);
326 357 }
358 +
359 + #[test]
360 + fn path_entry_matches_buffered() {
361 + let data = b"just random bytes";
362 + let buffered = verify_content_type(data, FileType::Audio);
363 + let tmp = tempfile::NamedTempFile::new().unwrap();
364 + std::fs::write(tmp.path(), data).unwrap();
365 + let path_based = verify_content_type_path(tmp.path(), FileType::Audio);
366 + assert_eq!(buffered.verdict, path_based.verdict);
367 + }
327 368 }
@@ -128,6 +128,21 @@ pub fn scan_with_yara(rules: &yara_x::Rules, data: &[u8]) -> LayerResult {
128 128 }
129 129 }
130 130
131 + /// Scan a spooled file against YARA rules. Reads the file into memory
132 + /// (yara-x's `Scanner::scan` takes a byte slice). Path-based entry exists
133 + /// so the streaming code path has a clean call site even though it does
134 + /// not yet save on memory; the win comes when yara-x exposes mmap input.
135 + pub fn scan_with_yara_path(rules: &yara_x::Rules, path: &std::path::Path) -> LayerResult {
136 + match std::fs::read(path) {
137 + Ok(data) => scan_with_yara(rules, &data),
138 + Err(e) => LayerResult {
139 + layer: "yara",
140 + verdict: LayerVerdict::Error,
141 + detail: Some(format!("read spool {}: {e}", path.display())),
142 + },
143 + }
144 + }
145 +
131 146 #[cfg(test)]
132 147 mod tests {
133 148 use super::*;
@@ -183,6 +198,32 @@ mod tests {
183 198 assert!(result.detail.unwrap().contains("test_malware"));
184 199 }
185 200
201 + #[test]
202 + fn path_entry_matches_buffered() {
203 + let mut compiler = yara_x::Compiler::new();
204 + compiler
205 + .add_source(
206 + r#"
207 + rule test_malware {
208 + strings:
209 + $sig = "MALWARE_SIGNATURE"
210 + condition:
211 + $sig
212 + }
213 + "#,
214 + )
215 + .unwrap();
216 + let rules = compiler.build();
217 +
218 + for sample in [&b"clean bytes"[..], &b"hit MALWARE_SIGNATURE here"[..]] {
219 + let buffered = scan_with_yara(&rules, sample);
220 + let tmp = tempfile::NamedTempFile::new().unwrap();
221 + std::fs::write(tmp.path(), sample).unwrap();
222 + let path_based = scan_with_yara_path(&rules, tmp.path());
223 + assert_eq!(buffered.verdict, path_based.verdict);
224 + }
225 + }
226 +
186 227 // ── Adversarial tests (test-fuzz) ──
187 228
188 229 #[test]