Browser WASM target#6
Open
gronke wants to merge 14 commits into
Open
Conversation
Add a `vfs` crate the analysis crates call instead of `std::fs`. On native it forwards to `std::fs` (inlined, behaviour-identical); on wasm32 it reads an ambient thread-local in-memory tree, so the same `detect`, `static-scan` and `app-audit` walk a real disk or an uploaded bundle behind plain `&Path` signatures. A `macos-bundle` feature (a `macos_layout` build cfg) compiles the macOS `.app` bundle-layout detectors off-macOS, and wasm forces it on so an uploaded `.app` uses the Info.plist + Frameworks probes. - detect: fs/plist via `vfs`; owned-buffer string scanners on wasm; `goblin` and `memmap2` gated off wasm. - static-scan: `vfs` for the corpus walk and asar reader; sequential rayon fallback on wasm. - app-audit: `vfs` for ASAR integrity and Info.plist; the `codesign` process shellouts move behind a `codesign` feature, on for native and off on wasm.
Split the HTTP, runtime and persistence pieces by target. - reqwest: native keeps rustls-tls; wasm uses the browser `fetch` backend, and concurrent lookups are boxed with `LocalBoxFuture` because its `Response` isn't `Send`. - time: a `now_unix()` shim — `SystemTime` on native, `js_sys::Date` on wasm. - backoff: `tokio::time::sleep` on native, `gloo-timers` on wasm. - persistence: the on-disk cache and settings file are native-only; wasm uses an in-memory cache and defaults. - NVD is off by default on the web (CORS plus client-side key exposure); OSV and EUVD stay on, both keyless.
`analyze_app(bytes, filename)` unpacks an uploaded zip or a bare `app.asar` into a `vfs::MemTree`, installs it as the ambient filesystem, and runs `detect` + `app-audit` + `static-scan`, returning one JSON object the UI shim re-splits per command. The `Analyzer` builder serves the Chromium `showDirectoryPicker` path, streaming each file in with `add_file` / `add_symlink` before analysing one `.app`. `cve_lookup` and `dependency_scan` cover the vulnerability lookups against OSV and EUVD. The crate is wasm32-only: its dependencies are target-gated, so on native it is an empty lib and `cargo build --workspace` is unaffected.
`ui/tauri-shim.js` installs a `window.__TAURI__` in the browser and no-ops under desktop Tauri, so one `index.html` serves both targets. It is installed synchronously, before main.js evaluates, and `invoke` awaits a `ready` promise the background wasm load resolves. It routes the commands main.js issues into achilles-wasm: - `scan` opens the File System Access folder picker on Chromium and streams a `scan_event` per `.app`; an injected Upload control handles a zipped `.app` or bare `app.asar` on any browser. - a bare `app.asar` has no bundle, so the shim synthesises a minimal detection keyed by the filename, giving its static-scan and dependency results a row. - `audit` / `static_scan` come from each app's cached analysis; `cve_lookup` / `dependency_scan` call the async wasm exports; export becomes a Blob download and settings live in localStorage.
`scripts/build-web.sh` builds the wasm package into ui/pkg (debug, or `--release`). Serving ui/ needs no cross-origin-isolation headers, because the build uses no threads or SharedArrayBuffer. `.github/workflows/ci.yml` runs two jobs: native `cargo test --workspace --exclude achilles`, so the gate needs no webkit2gtk, and a wasm build plus a type-check of achilles-wasm. It uses only first-party `actions/*` (checkout v7, cache v6) with the runner's built-in Rust and a hand-rolled cache.
`scripts/build-demo.sh` builds the release wasm and copies the bundler-free UI into `docs/browser/` (gitignored, regenerated not committed). `.github/workflows/pages.yml` runs that build and publishes the whole `docs/` site — the existing landing page at the root, the demo at `/browser/` — so each push redeploys it. It uses only first-party `actions/*` (configure-pages v6, upload-pages-artifact v5, deploy-pages v5) with the runner's built-in Rust. The repo's Pages source must be "GitHub Actions", and the `github-pages` environment must allow this branch; the app uses relative URLs so it runs unchanged under `…/achilles/browser/`.
Add a "Try Online" button beside the download options that opens `./browser/`, so visitors can run the analyzer in their browser without installing.
Dragging a file or folder over the page shows a dashed-border overlay inviting the drop, and dropping a `.app` folder, a folder of `.app`s, a zipped `.app`, or a bare `app.asar` scans it. Anything else is refused with a warning in the overlay. The DataTransfer is read synchronously, since it is cleared once the handler returns, and dropped directories are walked through the FileSystemEntry API into the same streaming `Analyzer` the folder picker uses.
c7701db to
c3d816d
Compare
Add a web app manifest, icons, and a service worker so the demo offers the browser's install hint and works offline. The worker is registered from `tauri-shim.js` on the web only — it no-ops under desktop Tauri — and `build-demo.sh` ships the manifest, worker, and icons into `docs/browser/`.
Safari Lockdown Mode removes the `WebAssembly` global, so the wasm load threw a raw ReferenceError; detect it up front, show a clear message, and guard the scan paths so they explain instead of crashing. Safari also invalidates dropped directory entries once the drop settles, which surfaced as an unhandled `NotFoundError`; walk the dropped tree into File objects before awaiting the wasm, and fall back to a "zip the .app and select it" message if the read still fails.
EUVD blocks browser-origin requests (CORS), so the browser build cannot query the API directly. Split `euvd::lookup` on wasm to read a pre-loaded per-runtime snapshot and run the same range filter, leaving the native HTTP path untouched. Add `euvd_set_shard` / `euvd_commit` / `euvd_snapshot_version` bindings so the JS side can load shard bytes in and swap the active set; committing a snapshot clears the session memo so a mid-session update can't serve stale advisories.
EUVD blocks browser-origin requests, so the web build reads a pre-fetched snapshot from the same origin instead of the live API. `scripts/fetch-euvd.sh` paginates the nine runtimes Achilles tracks server-side (no browser Origin, no CORS block), trims each advisory to the fields the analysis reads, and writes content-addressed shards alongside an `index.json` HEAD whose version is a content hash — so identical data redeploys without signalling a change, and an immutable shard is safe to cache indefinitely. `build-demo.sh` runs it into `docs/browser/euvd/`; the hourly `euvd-refresh` workflow probes per-runtime totals against the deployed snapshot and only re-dispatches the deploy when they differ.
EUVD blocks browser-origin requests, so the web build reads the pre-fetched snapshot from the same origin instead of the live API. `euvd-updater.js` revalidates the `index.json` HEAD, fetches only the content-addressed shards that changed, validates them, and commits to a dedicated Cache Storage bucket shards-first so a reader never sees a torn set; each fetch is bounded by a timeout so a stall can't wedge a check. The shim runs it on the main thread: it loads any cached snapshot into wasm at startup (offline-first) and refreshes in the background on load, reconnect, refocus, and a slow interval. Each tab fetches independently — the snapshot is small and content-addressed, so redundant fetches stay cheap. `sw.js` leaves `/euvd/` to that cache and won't evict it on a shell bump.
Add the snapshot controls to the settings dialog (web only), nested under the EUVD source entry and collapsed when it's unchecked: offline mode shown checked and locked — the browser can't reach EUVD directly — an "auto-update vulnerability databases" toggle, an "Update now" button showing when the snapshot was last checked, and the ENISA / CC BY 4.0 attribution. While no snapshot is loaded, mark EUVD unavailable so an empty result never reads as a clean bill of health; it clears itself once the snapshot lands. When a background update brings the open app a new advisory, re-render the CVE pane in place — expanded entries stay open, the scroll holds, and the new rows flash.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Achilles as native web application via Web Assembly.
.app"directories)./browser/path./Applications/*.appon macOS with Chrome.Known Limitation
/Applications/..zipand.asar.Preview
Try it on https://gronke.github.io/achilles/browser/ (see deploy status).
Worth mentioning, it adds a button to "Try Online":
Can be installed as a Chrome app: