postinstall-radar
Live leaderboard of the top npm packages that execute scripts during npm install — ranked by weekly-download blast radius, with a change feed that fires when a popular package's install-time scripts mutate.
Built after the Mini Shai-Hulud worm (May 11–12, 2026) compromised 172 npm + PyPI packages by smuggling credential-stealing payloads through postinstall hooks. The recurring question on every engineer's Slack — "how many of my deps actually run code during install?" — finally has a public dashboard.
What this is
- A read-only public web app at
/postinstall-radar/(port 4803). - A leaderboard of packages whose
package.jsondeclares any of:preinstall,install,postinstall,prepare,prepublish. - A real-time change feed: every time a tracked package republishes with different install-script contents, a row appears.
- A paste-your-
package.jsonauditor that returns the install-time risk of each dependency. - A JSON Feed at
/api/feed.jsonyou can drop into your reader.
Sibling products in the Holy AI fleet (complementary, no overlap):
shai-watch— known-malicious advisory feed.supply-pulse— cross-ecosystem malware radar.slopsquat-radar— LLM-hallucinated package squat detection.
postinstall-radar's lane: the attack surface itself — which popular packages could run arbitrary code on your laptop the next time you npm i, regardless of whether they've been compromised yet.
Stack
| | |
|---|---|
| Runtime | Node.js ≥ 20 (uses global fetch) |
| Web | Express 4 |
| DB | better-sqlite3 (WAL mode) |
| Scheduler| node-cron |
| Security | helmet, compression |
| Logger | morgan |
| Auth | none — every endpoint public, including refresh |
| UI | vanilla JS SPA, dark theme, Chart.js via CDN |
Quick start
cp .env.example .env
npm install
npm start
# open http://localhost:4803/postinstall-radar/
Health check: curl localhost:4803/postinstall-radar/health — should return 200 JSON with db: "ok".
On first boot the database is empty. The server kicks a bootstrap pass through ~30 keyword seeds (1s pacing between seeds), then immediately starts populating install-script metadata for the first ~100 packages. The leaderboard fills in over the next 5–10 minutes.
Data sources (all real, all public, no mocks)
| Source | URL | Refresh |
|---|---|---|
| npm registry search | https://registry.npmjs.org/-/v1/search?text=<seed>&size=250&popularity=1.0 | 1 seed every 12 h (rotating) |
| npm registry package detail | https://registry.npmjs.org/<pkg>/latest | 150 oldest packages every 1 h |
| npm downloads (point) | https://api.npmjs.org/downloads/point/last-week/<pkg> | 200 oldest packages every 6 h |
If a fetch fails: we log it and leave the row alone. A package whose download count is unknown renders as — in the UI, not a fabricated number. There is no seed array, no Math.random() jitter, no preset fallback.
Endpoints
| | |
|---|---|
| GET /postinstall-radar/ | SPA |
| GET /postinstall-radar/health | smoke check (200) |
| GET /postinstall-radar/api/leaderboard | limit, sort=risk\|downloads\|recent, min_downloads |
| GET /postinstall-radar/api/stats | totals + per-hook breakdown |
| GET /postinstall-radar/api/package/:name | full row + last 20 change events |
| GET /postinstall-radar/api/changes | recent install-script change feed |
| POST /postinstall-radar/api/check | body: {names:[...]} or {packageJson:"..."} |
| GET /postinstall-radar/api/feed.json | JSON Feed 1.1 of recent changes |
| GET /postinstall-radar/api/refresh-log | cron pipeline visibility |
| POST /postinstall-radar/api/refresh-now | kick a single batch (rate-limited only by inflight guard) |
/api/check is rate-limited to ~30 req/min/IP via an in-memory token bucket. No auth though — Arda wants to open the page and just use it.
How risk_score is computed
risk_score = log10(max(weekly_downloads, 1)) × hook_count
Deterministic. A package with 10 M weekly downloads and a single postinstall (e.g. esbuild) gets 7 × 1 = 7.00. A package with 50 k weekly downloads and three hooks gets 4.7 × 3 ≈ 14.1 — making the dashboard surface "small-but-noisy" packages that quietly run more than one script. No randomness, no machine learning, no opinion.
What this does NOT do
- We do not parse the script contents for malicious patterns. We surface the attack surface, not the verdict — sibling product
shai-watchdoes the advisory work. - We do not walk your
node_modulestree or upload your lockfile to the server. The paste-your-package.jsontool only sends the top-level dependency names to our API; the script values you see come from our cached registry data. - We do not track every npm package — only the ~3,000–5,000 surfaced through the popularity search seeds. Specialty packages outside the long tail won't appear.
- We do not require auth. Every endpoint, including
/api/refresh-now, is public per design. There is noADMIN_PASS, no Basic Auth middleware, nothing to log into.
Deployment
DEPLOY_MANIFEST.json follows the RNDLAB orchestrator schema. The Mac watcher under ~/development/RNDLAB/rndlab-core/ picks up queue.jsonl entries every 30 s, rsyncs the directory to /var/www/projects/postinstall-radar, registers a systemd unit, wires up the nginx route, smoke-checks /health, and posts to the showcase.
No vault secrets to inject — the npm registry is fully unauthenticated.
License
MIT.