I journal USN sono per natura sensibili. Un $J da 100 MB di una workstation aziendale contiene, riga per riga, la storia recente completa dei file di quella macchina: ogni documento Word toccato dall'utente, ogni eseguibile lanciato, ogni evizione di cache del browser. Caricarlo su un SaaS perché «te lo analizzi» è qualcosa che un professionista forense non dovrebbe mai accettare, e qualcosa che noi non vogliamo mai offrire.
Quindi abbiamo costruito usnparser.com al contrario: il parser gira nel tuo browser. Il file viene letto dal disco in JavaScript, consegnato a un modulo WebAssembly, e i record tornano senza che un singolo byte abbandoni la tua macchina.
Questo articolo è una passeggiata tecnica su come funziona.
La crate Rust
Non abbiamo reinventato la ruota: la logica di parsing viene da usnrs, l'implementazione pulita di USN_RECORD_V2 di Airbus CERT. Esponeva già un'interfaccia Read + Seek — esattamente ciò che ci serviva — proprio il trait implementato da std::io::Cursor<Vec<u8>>, quindi possiamo passarle byte grezzi da un Uint8Array JS.
La crate wrapper è di circa 60 righe di 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)
}
Accetta i byte del journal e, opzionalmente, i byte del $MFT per la risoluzione dei percorsi completi. Il tutto compila a circa 105 KB di .wasm dopo wasm-opt.
Tre intoppi di Cargo
Far compilare usnrs e le sue dipendenze transitive in modo pulito verso wasm32-unknown-unknown ha richiesto tre piccoli workaround:
getrandomha bisogno della featurejssu wasm32. La craterand(tirata damft) ne dipende transitivamente, e senza il backend JS la build wasm fallisce con «no available getrandom backend». La forziamo nel nostroCargo.toml:[target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] }chronoha bisogno diwasmbindquando la featureclockè attiva, altrimenti tenta di chiamaretime(2). La aggiungiamo via la nostra dichiarazione diretta.- Disabilitare le feature di default di mft in realtà non è necessario qui — la feature
mft_dumptira solo dipendenze CLI opzionali che comunque cross-compilano.
La colla lato browser
Compiliamo con wasm-pack build --target web --out-dir public/wasm, che produce un piccolo shim JS modulo ES più il binario .wasm. Entrambi vivono sotto /public/wasm/ e sono serviti come asset statici a URL stabili.
Il parser vive in un Web Worker così il thread principale resta reattivo mentre il parser mastica un milione di record:
// 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 });
};
È l'unico posto dove wasm e worker si incontrano. Né il worker né il wasm passano dal bundler di Next.js, il che significa zero config webpack/Turbopack.
Numeri
Un $J rappresentativo da 60 MB di una workstation Windows 11:
- Tempo di parsing: ~1,4 s su un Macbook recente
- Memoria: transitoria, liberata alla terminazione del worker
- Record prodotti: ~720 000
- Byte che lasciano la tua macchina: 0
L'UI virtualizza poi la tabella con TanStack Virtual, così anche un risultato da un milione di righe si sente istantaneo.
E i journal enormi?
Per journal oltre 500 MB passeremmo a una API in streaming che restituisce batch di record invece di accumularli in un Vec. Il cambiamento è piccolo — Usn è già un Iterator, esporremmo semplicemente next_batch(n) lato wasm. Non l'abbiamo spedito perché non abbiamo ancora feedback che serva a qualcuno. Se è il tuo caso, apri un issue.
Perché conta
Il tooling forense è stato storicamente diviso tra suite desktop pesanti (X-Ways, EnCase) e script Python da installare e a cui affidare dati sensibili. C'è una terza via: aperta, ispezionabile, gira interamente lato client. WebAssembly è oggi abbastanza veloce da non lasciare più scuse di performance per non sfruttarla.