Skip to content

Browser WASM target#6

Open
gronke wants to merge 14 commits into
crabnebula-dev:mainfrom
gronke:browser-wasm-target
Open

Browser WASM target#6
gronke wants to merge 14 commits into
crabnebula-dev:mainfrom
gronke:browser-wasm-target

Conversation

@gronke

@gronke gronke commented Jun 29, 2026

Copy link
Copy Markdown

Achilles as native web application via Web Assembly.

  • Scan folders (including .app "directories).
  • Scan files (.zip, .asar).
  • Served with GitHub pages as /browser/ path.
  • Install as Chrome App from browser bar.
  • Drag and drop folders (Edge/Chrome) or files (all); works for /Applications/*.app on macOS with Chrome.
  • Poll EUVD database for offline use and refresh in the background (new settings).
  • Mention EUVD license in the settings page.

Known Limitation

  • Codesign checks depend on macOS and are not supported by the browser target.
  • Chrome on macOS refuses to upload from system directories, users need to zip an app and upload the asset instead of using "Scan folder" on globally installed /Applications/.
  • Supported formats can be expanded; currently limited to .zip and .asar.
  • No memory, could use IndexDB or Local Storage to store results, work across tabs.
  • "Scan folder" uses File System Access API, which is only supported on Chrome and Edge (see https://caniuse.com/native-filesystem-api).
  • Safari in Lockdown Mode supports no Web Assembly (macOS + iOS).
  • EUVD refuses API requests with a CORS Origin header. Offline download is mandatory. (~ 3.2 MB)

Preview

Try it on https://gronke.github.io/achilles/browser/ (see deploy status).

image

Worth mentioning, it adds a button to "Try Online":

image

Can be installed as a Chrome app:

image

gronke added 8 commits June 29, 2026 10:54
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.
@gronke gronke force-pushed the browser-wasm-target branch from c7701db to c3d816d Compare June 29, 2026 11:44
gronke added 6 commits June 29, 2026 11:47
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant