USN ジャーナルは本質的に機微です。企業ワークステーションから取った 100MB の $J には、ユーザが触れた Word 文書、起動された実行ファイル、立ち退きされたブラウザ キャッシュなど、そのマシンの直近のファイル履歴が一行ずつ収まっています。これを SaaS に「解析してもらう」ためにアップロードするのは、フォレンジックの専門家が決して同意してはならないことで、私たちが決して提供したくないことでもあります。
そこで usnparser.com は逆方向に作りました: パーサはあなたのブラウザで動きます。ファイルはディスクから JavaScript に読み込まれ、WebAssembly モジュールに渡され、1 バイトたりともあなたの端末を出ることなくレコードが返ってきます。
本記事はその仕組みの技術的なウォークスルーです。
Rust の crate
車輪を再発明はしていません。実際のパース ロジックは Airbus CERT による USN_RECORD_V2 の clean な実装 usnrs から借りています。すでに Read + Seek インターフェイスを公開しており、これがまさに必要なものでした — std::io::Cursor<Vec<u8>> が実装する trait そのものなので、JS の Uint8Array からの生バイトを流し込めます。
ラッパー crate は Rust で 60 行程度です:
#[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)
}
ジャーナル バイト列を受け取り、任意で $MFT バイト列も受け取ってフル パス解決を行います。全体で wasm-opt 後に約 105KB の .wasm になります。
Cargo の三つの落とし穴
usnrs とその推移的依存を wasm32-unknown-unknown 向けに綺麗にコンパイルさせるには、三つの小さな回避策が必要でした:
getrandomにjsフィーチャが必要。randcrate (mftから推移的に入る) が依存しており、JS バックエンドなしだと「no available getrandom backend」で wasm ビルドが失敗します。我々のCargo.tomlで強制します:[target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] }chronoのclockフィーチャ有効化時にはwasmbindが必要。 これがないとtime(2)を呼ぼうとして失敗します。我々の直接宣言で追加します。mftのデフォルト フィーチャを無効化する必要は実はない。mft_dumpフィーチャは CLI 用のオプション依存を引きずるだけで、いずれもクロスコンパイルできます。
ブラウザ側のグルー
ビルドは wasm-pack build --target web --out-dir public/wasm で行い、小さな ES モジュール JS シムと .wasm バイナリを生成します。両方とも /public/wasm/ の下に置かれ、安定した URL の静的アセットとして配信されます。
パーサは Web Worker に住んでいるので、パーサが百万件のレコードをかみ砕いている間もメイン スレッドは応答性を維持します:
// 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 });
};
ここが wasm と worker が出会う唯一の場所です。worker も wasm も Next.js のバンドラを通らないため、webpack/Turbopack の設定はゼロです。
数値
Windows 11 ワークステーション由来の代表的な 60MB の $J:
- 解析時間: 最近の Macbook で約 1.4 秒
- メモリ: 一時的、worker の終了時に解放
- 生成レコード: 約 720,000 件
- 端末を出るバイト数: 0
UI 側は TanStack Virtual でテーブルを仮想化するため、100 万行の結果でも体感は即座です。
巨大なジャーナルは?
500MB を超えるジャーナルでは、Vec に全て積み上げるのではなく ストリーミング API に切り替えて、バッチ単位でレコードを返す形にします。変更は小さく — Usn はすでに Iterator なので、wasm 側で next_batch(n) を公開するだけです。誰かが必要としているという声がまだ無いため出荷していません。ご希望なら issue を開いてください。
なぜこれが重要か
フォレンジック ツーリングは歴史的に、重厚なデスクトップ スイート (X-Ways、EnCase) と、インストールして機微なデータを預けねばならない Python スクリプトに二分されてきました。第三の道があります: オープン、検証可能、完全にクライアント側で動く。WebAssembly は今や十分に高速で、その道を未踏のままにしておくパフォーマンス上の言い訳はありません。