Les journaux USN sont par nature sensibles. Un $J de 100 Mo issu d'un poste d'entreprise contient, ligne à ligne, l'historique fichier récent complet de la machine : chaque document Word ouvert, chaque exécutable lancé, chaque éviction de cache navigateur. Le téléverser vers un SaaS pour « se le faire analyser » est quelque chose qu'un professionnel forensique ne devrait jamais accepter, et quelque chose que nous ne voulons jamais proposer.
Nous avons donc bâti usnparser.com à l'envers : le parseur tourne dans votre navigateur. Le fichier est lu depuis le disque vers JavaScript, transmis à un module WebAssembly, et les enregistrements reviennent sans qu'un seul octet ne quitte votre machine.
Cet article est une plongée technique dans comment ça fonctionne.
La crate Rust
Nous n'avons pas réinventé la roue : la logique de parsing vient de usnrs, l'implémentation propre d'Airbus CERT de USN_RECORD_V2. Elle expose une interface Read + Seek, exactement ce dont nous avions besoin — c'est précisément le trait qu'implémente std::io::Cursor<Vec<u8>>, donc nous pouvons lui passer les octets bruts d'un Uint8Array JS.
La crate wrapper fait une soixantaine de lignes de 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)
}
Elle accepte les octets du journal et, en option, les octets du $MFT pour la résolution des chemins complets. Le tout compile à environ 105 Ko de .wasm après wasm-opt.
Trois gotchas Cargo
Faire compiler usnrs et ses dépendances transitives proprement vers wasm32-unknown-unknown a demandé trois petits contournements :
getrandoma besoin de la featurejssur wasm32. La craterand(tirée parmft) en dépend transitivement, et sans le backend JS la build wasm échoue avec « no available getrandom backend ». On la force dans notreCargo.toml:[target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] }chronoa besoin dewasmbindquand la featureclockest activée, sinon il essaie d'appelertime(2). On l'ajoute via notre déclaration de dépendance directe.- Désactiver les features par défaut de mft n'est en fait pas nécessaire ici — la feature
mft_dumpne tire que des dépendances CLI optionnelles qui se cross-compilent quand même.
La glue côté navigateur
On compile avec wasm-pack build --target web --out-dir public/wasm, ce qui produit un petit shim JS module ES plus le binaire .wasm. Les deux vivent sous /public/wasm/ et sont servis comme assets statiques à des URLs stables.
Le parseur tourne dans un Web Worker pour que le thread principal reste réactif pendant que le parseur mâche un million d'enregistrements :
// 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 });
};
C'est le seul endroit où wasm et worker se rencontrent. Ni le worker ni le wasm ne passent par le bundler de Next.js, ce qui veut dire aucune config webpack/Turbopack.
Chiffres
Un $J représentatif de 60 Mo issu d'un poste Windows 11 :
- Temps de parsing : ~1,4 s sur un Macbook récent
- Mémoire : transitoire, libérée à la fin du worker
- Enregistrements produits : ~720 000
- Octets sortant du poste : 0
L'UI virtualise ensuite la table avec TanStack Virtual, donc même un résultat d'un million de lignes reste instantané.
Et les journaux gigantesques ?
Pour des journaux au-delà de 500 Mo, on passerait à une API en streaming qui livre des lots d'enregistrements plutôt que de tout accumuler dans un Vec. Le changement est petit — Usn est déjà un Iterator, il suffit d'exposer next_batch(n) côté wasm. Nous n'avons pas livré ça parce que personne ne nous a encore signalé en avoir besoin. Si c'est votre cas, ouvrez une issue.
Pourquoi ça compte
L'outillage forensique a historiquement été coupé en deux : suites desktop massives (X-Ways, EnCase) et scripts Python qu'il faut installer et auxquels il faut confier des données sensibles. Il y a une troisième voie : ouverte, inspectable, entièrement côté client. WebAssembly est aujourd'hui assez rapide pour qu'il n'y ait plus d'excuse de performance à laisser cette voie inexploitée.