Los journals USN son sensibles por naturaleza. Un $J de 100 MB de una estación de trabajo corporativa contiene, línea a línea, la historia reciente completa de ficheros de esa máquina: cada documento Word que el usuario tocó, cada ejecutable que corrió, cada evicción de caché del navegador. Subirlo a un SaaS para que «te lo analice» es algo que un profesional forense nunca debería aceptar, y algo que nosotros nunca queremos ofrecer.
Así que construimos usnparser.com al revés: el parser corre en tu navegador. El fichero se lee de disco a JavaScript, se entrega a un módulo WebAssembly, y los registros vuelven sin que un solo byte abandone tu máquina.
Este artículo es un repaso técnico de cómo funciona.
La crate Rust
No reinventamos la rueda: la lógica de parseo viene de usnrs, la implementación limpia de USN_RECORD_V2 de Airbus CERT. Ya exponía una interfaz Read + Seek, que es justo lo que necesitábamos — exactamente el trait que implementa std::io::Cursor<Vec<u8>>, así que podemos pasarle bytes crudos desde un Uint8Array de JS.
La crate envoltorio tiene unas 60 líneas 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)
}
Acepta los bytes del journal y, opcionalmente, los bytes del $MFT para resolución de rutas completas. Todo compila a ~105 KB de .wasm tras wasm-opt.
Tres gotchas de Cargo
Conseguir que usnrs y sus dependencias transitivas compilen limpio para wasm32-unknown-unknown llevó tres pequeños workarounds:
getrandomnecesita la featurejsen wasm32. La craterand(arrastrada pormft) depende de ella transitivamente, y sin el backend JS la build wasm falla con «no available getrandom backend». La forzamos en nuestroCargo.toml:[target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] }chrononecesitawasmbindcuando la featureclockestá activa, si no intenta llamar atime(2). La añadimos vía nuestra declaración directa de dependencia.- Desactivar las features por defecto de mft en realidad no es necesario aquí — la feature
mft_dumpsolo trae dependencias opcionales de CLI que aun así cross-compilan.
El pegamento del navegador
Compilamos con wasm-pack build --target web --out-dir public/wasm, que produce un pequeño shim JS módulo ES más el binario .wasm. Ambos viven bajo /public/wasm/ y se sirven como assets estáticos a URLs estables.
El parser vive en un Web Worker para que el hilo principal siga reactivo mientras el parser mastica un millón de registros:
// 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 });
};
Es el único sitio donde wasm y worker se encuentran. Ni el worker ni el wasm pasan por el bundler de Next.js, lo que significa cero config webpack/Turbopack.
Números
Un $J representativo de 60 MB sacado de un puesto Windows 11:
- Tiempo de parseo: ~1,4 s en un Macbook reciente
- Memoria: transitoria, liberada al terminar el worker
- Registros producidos: ~720 000
- Bytes que abandonan tu máquina: 0
La UI virtualiza luego la tabla con TanStack Virtual, así que incluso un resultado de un millón de filas se siente instantáneo.
¿Y los journals enormes?
Para journals por encima de 500 MB pasaríamos a una API en streaming que entregue lotes de registros en lugar de acumular todo en un Vec. El cambio es pequeño — Usn ya es un Iterator, solo expondríamos next_batch(n) del lado wasm. No lo hemos enviado porque aún no tenemos feedback de que alguien lo necesite. Si es tu caso, abre un issue.
Por qué importa
El tooling forense ha estado históricamente partido entre suites desktop pesadas (X-Ways, EnCase) y scripts Python que hay que instalar y a los que hay que confiar datos sensibles. Hay una tercera vía: abierta, inspeccionable, completamente en cliente. WebAssembly es hoy lo bastante rápido para que ya no haya excusa de rendimiento para dejar esa vía sin explotar.