Journals USN são sensíveis por natureza. Um $J de 100 MB de uma estação de trabalho corporativa contém, linha por linha, todo o histórico recente de arquivos daquela máquina: cada documento Word que o usuário tocou, cada executável que rodou, cada despejo de cache do navegador. Enviar isso para um SaaS para que «seja analisado para você» é algo que um profissional forense nunca deveria aceitar — e algo que nunca queremos oferecer.
Por isso construímos usnparser.com ao contrário: o parser roda no seu navegador. O arquivo é lido do disco para o JavaScript, entregue a um módulo WebAssembly, e os registros voltam sem que um único byte deixe sua máquina.
Este artigo é um passeio técnico por como isso funciona.
A crate Rust
Não reinventamos a roda: a lógica de parsing vem de usnrs, a implementação limpa de USN_RECORD_V2 da Airbus CERT. Já expunha uma interface Read + Seek — exatamente do que precisávamos. É precisamente o trait que std::io::Cursor<Vec<u8>> implementa, então podemos passar bytes crus de um Uint8Array de JS.
A crate wrapper tem cerca de 60 linhas 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)
}
Aceita os bytes do journal e, opcionalmente, os bytes do $MFT para resolução de caminhos completos. O todo compila para cerca de 105 KB de .wasm após wasm-opt.
Três pegadinhas do Cargo
Fazer usnrs e suas dependências transitivas compilarem limpinho para wasm32-unknown-unknown exigiu três pequenos workarounds:
getrandomprecisa da featurejsno wasm32. A craterand(puxada pormft) depende dela transitivamente, e sem o backend JS a build wasm falha com «no available getrandom backend». Forçamos no nossoCargo.toml:[target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] }chronoprecisa dewasmbindquando a featureclockestá ativa, senão ele tenta chamartime(2). Habilitamos via nossa declaração direta.- Desabilitar as default features do mft não é, na verdade, necessário aqui — a feature
mft_dumpsó puxa deps opcionais de CLI que cross-compilam mesmo assim.
A cola do lado do navegador
Compilamos com wasm-pack build --target web --out-dir public/wasm, que produz um pequeno shim JS em módulo ES mais o binário .wasm. Ambos vivem sob /public/wasm/ e são servidos como assets estáticos em URLs estáveis.
O parser vive em um Web Worker para que a thread principal continue responsiva enquanto o parser tritura um milhão 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 });
};
É o único lugar onde wasm e worker se encontram. Nem o worker nem o wasm passam pelo bundler do Next.js, o que significa zero config webpack/Turbopack.
Números
Um $J representativo de 60 MB de uma estação Windows 11:
- Tempo de parsing: ~1,4 s em um Macbook recente
- Memória: transitória, liberada quando o worker termina
- Registros produzidos: ~720 000
- Bytes que saem da sua máquina: 0
A UI então virtualiza a tabela com TanStack Virtual, então mesmo um resultado de um milhão de linhas se sente instantâneo.
E os journals enormes?
Para journals acima de 500 MB, mudaríamos para uma API em streaming que entrega lotes de registros em vez de acumular tudo num Vec. A mudança é pequena — Usn já é um Iterator, basta expor next_batch(n) do lado wasm. Não enviamos isso porque ainda não temos feedback de que alguém precise. Se for o seu caso, abra uma issue.
Por que isso importa
O tooling forense foi historicamente dividido entre suítes desktop pesadas (X-Ways, EnCase) e scripts Python que você precisa instalar e nos quais precisa confiar dados sensíveis. Existe uma terceira via: aberta, inspecionável, roda inteiramente no cliente. WebAssembly hoje é rápido o suficiente para que não haja mais desculpa de desempenho para deixar essa via inexplorada.