provenance-watch
Live dashboard for npm package SLSA provenance — refreshed every 4 hours from the live npm registry. Built after the TanStack / Mini Shai-Hulud worm of May 2026 broke the "provenance = safe" assumption.
provenance-watch is a single-binary Node service that tracks ~150 popular npm packages across four cohorts and surfaces who publishes verifiable build provenance, who doesn't, who recently gained or lost the attestation — and which packages shipped with valid provenance and also got compromised.
No auth. No tracking. Public endpoints. Real data only.
Why this exists (May 2026)
On 2026-05-11, the Mini Shai-Hulud worm published 84 malicious versions across 42 @tanstack/* npm packages in a six-minute window. At least some of those versions carried valid SLSA provenance — the first known case of a malicious npm package shipping with verifiable build provenance, because the attacker compromised the maintainer's CI rather than the artifacts directly.
SLSA provenance binds an artifact to a build pipeline. It does not vouch for the pipeline. provenance-watch makes that distinction visible.
Live data sources
| Source | URL pattern | Refresh |
|---|---|---|
| npm package metadata | GET https://registry.npmjs.org/<pkg> | every 4 h |
| npm attestations | GET https://registry.npmjs.org/-/npm/v1/attestations/<pkg>@<version> | every 4 h |
| npm weekly downloads | GET https://api.npmjs.org/downloads/point/last-week/<pkg> | every 24 h |
All sources are unauthenticated. NPM_REGISTRY_TOKEN is an optional bearer-token bypass for the 60 req/h soft quota; the product works fully without it.
If a fetch fails (network, 5xx, rate limit), the failure is recorded into fetch_errors and skipped. We never synthesize values.
Refresh cadence
| Job | Cron | What it does |
|---|---|---|
| refresh-packages | 0 /4 | For every tracked package, pull the registry record + attestations for the 3 most recent versions, then run drift detection |
| refresh-downloads | 30 /4 | Refresh weekly download counts for any package whose downloads.fetched_at is older than 24 h |
| daily-snapshot | 15 1 * | Aggregate cohort-level provenance counters into snapshots for the 30-day trend chart |
A one-shot refresh runs at boot if the DB was last refreshed more than 30 minutes ago.
Endpoints (all under /provenance-watch, all public)
| Method | Path | Returns |
|---|---|---|
| GET | / | Dashboard SPA |
| GET | /health | { status, version, uptime, db_size, last_refresh_at, … } |
| GET | /api/overview | Totals + per-cohort summary |
| GET | /api/packages?cohort=&q=&sort=&limit=&offset= | Paginated package list |
| GET | /api/package/<name> | Per-package detail + 25-version history + drift events |
| GET | /api/drift?kind=&limit= | Recent drift events |
| GET | /api/compromised | Packages in the May 2026 compromised cohort |
| GET | /api/snapshots?cohort=&days= | Daily aggregates for the trend chart |
| GET | /api/errors?limit= | Recent fetch failures |
| POST | /api/scan | Body: { packageJson: <object|string> }; returns per-dep provenance breakdown |
| POST | /refresh | Kicks a refresh-packages run; returns 202 |
How to read a provenance signal correctly
A ✓ on this dashboard means "this version was published with a SLSA v1 provenance attestation, signed by Sigstore-backed npm tooling, recording the source repository, ref, SHA and builder identity." That is real. It is also bounded.
It does not mean:
- The published code is safe. (TanStack proves this.)
- The maintainer is unchanged. (Account takeovers happen.)
- The CI configuration is benign. (Workflow injection is a separate class of bug.)
- The dependencies of this package are safe. (Provenance is per-artifact.)
provenance-watch is one of several signals. Use it alongside download history (/api/packages?sort=weekly), maintainer activity, dist-tag stability, and the compromised-despite-provenance drift events that highlight the cases where the signal was honoured by the registry but exploited in practice.
Run locally
cp .env.example .env
npm install
node server.js
# → http://localhost:4804/provenance-watch/
The first refresh starts automatically after a few seconds and takes 2–5 minutes against the live npm registry. Use the Refresh button to trigger another run on demand.
Stack
Node.js 18+, Express 4, better-sqlite3 (WAL), node-cron, helmet, compression, morgan, dotenv. Vanilla JS SPA, Chart.js (CDN) for the trend line. SQLite database stored at data/provenance-watch.db.
License
MIT — see source headers. Built by Cowork × Claude Opus 4.7.