Skip to main content

max / makenotwork

server: parallel path-based entries on structural + signing layers
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-27 16:32 UTC
Commit: dff45ff5cd3d69a18b06f02047e31367945f2b2c
Parent: f93487b
7 files changed, +117 insertions, -0 deletions
@@ -4178,6 +4178,7 @@ dependencies = [
4178 4178 "infer",
4179 4179 "jsonwebtoken",
4180 4180 "log",
4181 + "memmap2",
4181 4182 "metrics",
4182 4183 "metrics-exporter-prometheus",
4183 4184 "object 0.37.3",
@@ -81,6 +81,7 @@ goblin = "0.10"
81 81 zip = "8.2"
82 82 yara-x = "1.16"
83 83 fs2 = "0.4"
84 + memmap2 = "0.9"
84 85
85 86 # CSV parsing (import system)
86 87 csv = "1.3"
@@ -26,6 +26,17 @@ pub const ERROR_POLICY: ErrorPolicy = ErrorPolicy::FailOpen;
26 26 /// the modern format and what every current toolchain emits.
27 27 const APPIMAGE_MARKER: [u8; 3] = *b"AI\x02";
28 28
29 + /// Path-based entry. Mmaps the spooled file and delegates.
30 + pub fn verify_appimage_signature_path(path: &std::path::Path, file_type: FileType) -> LayerResult {
31 + if !matches!(file_type, FileType::Download) {
32 + return skip("Not a download file type");
33 + }
34 + match crate::scanning::spool::mmap_read(path) {
35 + Ok(map) => verify_appimage_signature(&map, file_type),
36 + Err(e) => error(e),
37 + }
38 + }
39 +
29 40 /// Entry point: detect AppImage, walk ELF sections for the signature pair.
30 41 pub fn verify_appimage_signature(data: &[u8], file_type: FileType) -> LayerResult {
31 42 if !matches!(file_type, FileType::Download) {
@@ -151,4 +162,14 @@ mod tests {
151 162 let r = classify(false, false);
152 163 assert!(r.detail.unwrap().contains("no signature"));
153 164 }
165 +
166 + #[test]
167 + fn path_entry_matches_buffered_on_plain_text() {
168 + let data = b"definitely not an AppImage";
169 + let buffered = verify_appimage_signature(data, FileType::Download);
170 + let tmp = tempfile::NamedTempFile::new().unwrap();
171 + std::fs::write(tmp.path(), data).unwrap();
172 + let path_based = verify_appimage_signature_path(tmp.path(), FileType::Download);
173 + assert_eq!(buffered.verdict, path_based.verdict);
174 + }
154 175 }
@@ -43,6 +43,19 @@ use super::{ErrorPolicy, LayerResult, LayerVerdict};
43 43 /// dashboard surfaces parser errors via the health panel.
44 44 pub const ERROR_POLICY: ErrorPolicy = ErrorPolicy::FailOpen;
45 45
46 + /// Path-based entry. Mmaps the spooled file (DMG signature lookup walks
47 + /// the trailer; MachFile parses headers + load commands — both touch
48 + /// specific offsets, demand-paged through the mmap) and delegates.
49 + pub fn verify_apple_signature_path(path: &std::path::Path, file_type: FileType) -> LayerResult {
50 + if !matches!(file_type, FileType::Download | FileType::Insertion) {
51 + return skip("Not a download file type");
52 + }
53 + match crate::scanning::spool::mmap_read(path) {
54 + Ok(map) => verify_apple_signature(&map, file_type),
55 + Err(e) => error(e),
56 + }
57 + }
58 +
46 59 /// Top-level entry: verify any Apple code-signing evidence in the bytes.
47 60 pub fn verify_apple_signature(data: &[u8], file_type: FileType) -> LayerResult {
48 61 // Only download-class uploads are plausible Apple binaries. Audio /
@@ -381,4 +394,14 @@ mod tests {
381 394 assert_eq!(r.verdict, LayerVerdict::Pass);
382 395 assert!(r.detail.unwrap().contains("no signature"));
383 396 }
397 +
398 + #[test]
399 + fn path_entry_matches_buffered_on_plain_text() {
400 + let data = b"definitely not a mach-o";
401 + let buffered = verify_apple_signature(data, FileType::Download);
402 + let tmp = tempfile::NamedTempFile::new().unwrap();
403 + std::fs::write(tmp.path(), data).unwrap();
404 + let path_based = verify_apple_signature_path(tmp.path(), FileType::Download);
405 + assert_eq!(buffered.verdict, path_based.verdict);
406 + }
384 407 }
@@ -33,6 +33,19 @@ use super::{ErrorPolicy, LayerResult, LayerVerdict};
33 33
34 34 pub const ERROR_POLICY: ErrorPolicy = ErrorPolicy::FailOpen;
35 35
36 + /// Path-based entry. Mmaps the spooled file and delegates. PE parsing
37 + /// walks the NT headers + attribute-cert directory at fixed offsets, so
38 + /// demand-paging covers the whole inspection without buffering.
39 + pub fn verify_authenticode_path(path: &std::path::Path, file_type: FileType) -> LayerResult {
40 + if !matches!(file_type, FileType::Download) {
41 + return skip("Not a download file type");
42 + }
43 + match crate::scanning::spool::mmap_read(path) {
44 + Ok(map) => verify_authenticode(&map, file_type),
45 + Err(e) => error(e),
46 + }
47 + }
48 +
36 49 /// Top-level entry. Detect PE format, walk the attribute-cert table, and
37 50 /// surface a Pass detail describing the signing state.
38 51 pub fn verify_authenticode(data: &[u8], file_type: FileType) -> LayerResult {
@@ -248,4 +261,14 @@ mod tests {
248 261 let r = verify_authenticode(&data, FileType::Download);
249 262 assert!(matches!(r.verdict, LayerVerdict::Error | LayerVerdict::Pass));
250 263 }
264 +
265 + #[test]
266 + fn path_entry_matches_buffered_on_plain_text() {
267 + let data = b"definitely not a PE";
268 + let buffered = verify_authenticode(data, FileType::Download);
269 + let tmp = tempfile::NamedTempFile::new().unwrap();
270 + std::fs::write(tmp.path(), data).unwrap();
271 + let path_based = verify_authenticode_path(tmp.path(), FileType::Download);
272 + assert_eq!(buffered.verdict, path_based.verdict);
273 + }
251 274 }
@@ -15,6 +15,22 @@ use std::path::{Path, PathBuf};
15 15 use tokio::fs::{File, OpenOptions};
16 16 use tokio::io::AsyncWriteExt;
17 17
18 + /// Memory-map a spooled file for the byte-slice-only layers (structural,
19 + /// signing_*). Mmap gives us `&[u8]` backed by the OS page cache: the
20 + /// pages get demand-paged in as the parser walks them, so a 500 MB file
21 + /// doesn't allocate 500 MB of RSS.
22 + ///
23 + /// Safety: the caller must guarantee no other process is writing to the
24 + /// file while the mapping is alive. Scan-spool tempfiles are written
25 + /// exclusively by the scanner before the layer runs and unlinked on
26 + /// drop, so no other writer exists.
27 + pub fn mmap_read(path: &Path) -> Result<memmap2::Mmap, String> {
28 + let file = std::fs::File::open(path)
29 + .map_err(|e| format!("open spool {}: {e}", path.display()))?;
30 + unsafe { memmap2::Mmap::map(&file) }
31 + .map_err(|e| format!("mmap spool {}: {e}", path.display()))
32 + }
33 +
18 34 /// Owned handle to a spooled tempfile. The file is unlinked when the
19 35 /// handle drops, even if a layer panics mid-scan.
20 36 pub struct SpoolHandle {
@@ -41,6 +41,28 @@ const SUSPICIOUS_MACHO_SYMBOLS: &[&str] = &[
41 41 "thread_create_running",
42 42 ];
43 43
44 + /// Path-based entry. Mmaps the spooled file so goblin's parsers walk OS
45 + /// page cache rather than a heap buffer; suitable for files larger than
46 + /// `SCAN_MAX_MEMORY_BYTES`. Delegates to `analyze_binary` for the actual
47 + /// analysis.
48 + pub fn analyze_binary_path(path: &std::path::Path, file_type: FileType) -> LayerResult {
49 + if file_type != FileType::Download {
50 + return LayerResult {
51 + layer: "structural",
52 + verdict: LayerVerdict::Skip,
53 + detail: Some("Not a download file".to_string()),
54 + };
55 + }
56 + match crate::scanning::spool::mmap_read(path) {
57 + Ok(map) => analyze_binary(&map, file_type),
58 + Err(e) => LayerResult {
59 + layer: "structural",
60 + verdict: LayerVerdict::Error,
61 + detail: Some(e),
62 + },
63 + }
64 + }
65 +
44 66 /// Analyze a binary for suspicious structural patterns.
45 67 /// Only runs for Download file types; returns Skip for Audio/Cover.
46 68 pub fn analyze_binary(data: &[u8], file_type: FileType) -> LayerResult {
@@ -488,4 +510,14 @@ mod tests {
488 510 let warnings = check_mach_warnings(symbols);
489 511 assert!(warnings.is_empty());
490 512 }
513 +
514 + #[test]
515 + fn path_entry_matches_buffered_on_plain_text() {
516 + let data = b"this is not a binary";
517 + let buffered = analyze_binary(data, FileType::Download);
518 + let tmp = tempfile::NamedTempFile::new().unwrap();
519 + std::fs::write(tmp.path(), data).unwrap();
520 + let path_based = analyze_binary_path(tmp.path(), FileType::Download);
521 + assert_eq!(buffered.verdict, path_based.verdict);
522 + }
491 523 }