USN-Journals sind von Natur aus sensibel. Eine 100-MB-$J-Datei von einer Unternehmens-Workstation enthält Zeile für Zeile die komplette jüngere Dateihistorie der Maschine: jedes Word-Dokument, das der Nutzer angefasst hat, jede ausgeführte Executable, jede Browser-Cache-Verdrängung. Das in ein SaaS hochzuladen, damit es „für dich analysiert wird", ist etwas, dem ein forensischer Profi nie zustimmen sollte — und etwas, das wir nie anbieten wollen.
Also haben wir usnparser.com andersherum gebaut: der Parser läuft in deinem Browser. Die Datei wird von der Platte in JavaScript gelesen, an ein WebAssembly-Modul übergeben, und die Einträge kommen zurück, ohne dass ein einziges Byte deine Maschine verlässt.
Dieser Artikel ist ein technischer Durchgang, wie das funktioniert.
Die Rust-Crate
Wir haben das Rad nicht neu erfunden: Die eigentliche Parsing-Logik kommt aus usnrs, Airbus CERTs sauberer Implementierung von USN_RECORD_V2. Sie exponiert bereits ein Read + Seek-Interface — genau das, was wir brauchten. Es ist exakt der Trait, den std::io::Cursor<Vec<u8>> implementiert, also können wir rohe Bytes aus einem JS-Uint8Array reinreichen.
Die Wrapper-Crate sind rund 60 Zeilen Rust:
#[wasm_bindgen(js_name = parseUsn)]
pub fn parse_usn(
usn_bytes: &[u8],
mft_bytes: Option<Box<[u8]>>,
) -> Result<JsValue, JsValue> {
let usn = Cursor::new(usn_bytes.to_vec());
let mft = mft_bytes
.map(|b| MftParser::from_buffer(b.into_vec()))
.transpose()?;
let iter = Usn::new(mft, usn, None)?;
let records: Vec<UsnRecord> = iter.map(into_record).collect();
serde_wasm_bindgen::to_value(&records)
}
Sie akzeptiert die Journal-Bytes und optional die $MFT-Bytes für die Pfad-Auflösung. Das Ganze kompiliert nach wasm-opt zu ca. 105 KB .wasm.
Drei Cargo-Stolpersteine
Damit usnrs und seine transitiven Abhängigkeiten sauber gegen wasm32-unknown-unknown bauen, waren drei kleine Workarounds nötig:
getrandombraucht diejs-Feature auf wasm32. Dierand-Crate (durchmftmitgezogen) hängt transitiv daran, und ohne JS-Backend scheitert der Wasm-Build mit „no available getrandom backend". Wir erzwingen es in unsererCargo.toml:[target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] }chronobrauchtwasmbind, wenn dieclock-Feature aktiv ist, sonst versucht estime(2)aufzurufen. Wir aktivieren es über unsere direkte Deklaration.- Default-Features von mft abschalten ist hier eigentlich nicht nötig — die
mft_dump-Feature zieht nur optionale CLI-Deps, die trotzdem cross-kompilieren.
Der Browser-Glue
Wir bauen mit wasm-pack build --target web --out-dir public/wasm, was einen kleinen ES-Modul-JS-Shim plus die .wasm-Datei erzeugt. Beide leben unter /public/wasm/ und werden als statische Assets unter bekannten URLs ausgeliefert.
Der Parser lebt in einem Web Worker, damit der Hauptthread reaktiv bleibt, während der Parser sich durch eine Million Einträge frisst:
// public/workers/parse.js
import init, { parseUsn } from "/wasm/usn_wasm.js";
await init();
self.onmessage = (event) => {
const { usnBytes, mftBytes } = event.data;
const records = parseUsn(
new Uint8Array(usnBytes),
mftBytes ? new Uint8Array(mftBytes) : null,
);
self.postMessage({ type: "result", records });
};
Das ist die einzige Stelle, wo Wasm und Worker sich treffen. Weder Worker noch Wasm laufen durch den Next.js-Bundler — was bedeutet: keine Webpack/Turbopack-Konfiguration.
Zahlen
Eine repräsentative 60-MB-$J von einer Windows-11-Workstation:
- Parse-Zeit: ~1,4 s auf einem aktuellen Macbook
- Speicher: transient, freigegeben, wenn der Worker beendet wird
- Erzeugte Einträge: ~720 000
- Bytes, die deine Maschine verlassen: 0
Die UI virtualisiert dann die Tabelle mit TanStack Virtual — so fühlt sich selbst ein Millionen-Zeilen-Ergebnis sofort an.
Was ist mit riesigen Journals?
Für Journals jenseits von 500 MB würden wir auf eine Streaming-API umschalten, die Batches von Einträgen liefert, statt alles in einem Vec zu sammeln. Die Änderung ist klein — Usn ist bereits ein Iterator, wir würden auf der Wasm-Seite einfach next_batch(n) exponieren. Wir haben es noch nicht ausgeliefert, weil wir noch kein Feedback haben, dass es jemand braucht. Falls du es brauchst: öffne ein Issue.
Warum das zählt
Forensisches Tooling war historisch zweigeteilt — zwischen schweren Desktop-Suiten (X-Ways, EnCase) und Python-Skripten, die man installieren und denen man sensible Daten anvertrauen muss. Es gibt eine dritte Spur: offen, inspizierbar, läuft komplett im Client. WebAssembly ist heute schnell genug, dass es keinen Performance-Vorwand mehr gibt, diese Spur ungenutzt zu lassen.