shai-watch
Live malware advisory radar for npm, PyPI, RubyGems, Maven, Composer, Go (and more). Paste your manifest, see what's pwned.
What it does
shai-watch is a tiny Node + SQLite service that mirrors the public
malware-typed entries from the GitHub Security Advisory
Database and cross-checks them against
OSV.dev. Every advisory is enriched with the
affected package's weekly download count (so you can see which
compromise actually touches a lot of code) and tagged with a known
campaign label when one matches (shai-hulud, mini-shai-hulud,axios-compromise, teampcp, …).
A single page lets you:
- Browse the live malware feed, filtered by ecosystem.
- Open any advisory to see all affected packages + references.
- Paste a
package.json,requirements.txt,Gemfile.lock, -
go.sum,composer.jsonorpackage-lock.jsonand get an instant - audit of which dependencies are listed in the malware database.
Real data sources (no mocks, no seeds)
| Source | URL | Refresh |
| ------ | --- | ------- |
| GitHub Security Advisories | https://api.github.com/advisories?type=malware&ecosystem=… | every 15 min, all 8 ecosystems |
| OSV.dev /v1/query | https://api.osv.dev/v1/query | every 60 min, top 500 affected packages |
| npm registry downloads | https://api.npmjs.org/downloads/point/last-week/{name} | every 6 h |
| PyPI Stats | https://pypistats.org/api/packages/{name}/recent | every 6 h |
If a fetch fails the row simply records weekly_downloads = NULL — we
never invent numbers. Health endpoint exposes the timestamps and row
counts of the most recent successful fetch for each source.
Endpoints (all public, no auth)
| Method | Path | Purpose |
| --- | --- | --- |
| GET | /shai-watch/ | dashboard SPA |
| GET | /shai-watch/health | { ok, version, last_fetch, counts } |
| GET | /shai-watch/api/feed?ecosystem=npm&limit=100 | recent malware advisories |
| GET | /shai-watch/api/advisory/:id | full advisory + affected packages |
| GET | /shai-watch/api/package/:eco/:name | every advisory matching an ecosystem+name |
| GET | /shai-watch/api/stats | aggregate counters + top campaign |
| GET | /shai-watch/api/campaigns | grouped advisories by detected campaign |
| GET | /shai-watch/api/leaderboard?days=30&limit=25 | top blast-radius advisories |
| POST | /shai-watch/api/scan | body {type, manifest} → matches |
| GET | /shai-watch/api/badge.svg?ecosystem=npm&days=7 | shields.io-style badge |
Stack
- Node.js 20 LTS + Express 4
- better-sqlite3 (WAL mode)
- node-cron
- helmet + compression
- Vanilla JS SPA, dark theme, no build step
Running locally
cp .env.example .env
npm install
node server.js
# -> [shai-watch] v1.0.0 listening on :4743 base=/shai-watch
Hit http://localhost:4743/shai-watch/ and the bootstrap fetch
populates the database within ~30 s.
A GITHUB_TOKEN is optional — without one the GitHub API runs in
the 60 req/h unauthenticated bucket which is plenty for a single
15-min cron over 8 ecosystems. Set the token to bump the limit to
5000 req/h.
Project layout
shai-watch/
├── server.js # Express bootstrap + cron schedule
├── db.js # better-sqlite3 + WAL + prepared statements
├── fetchers/
│ ├── ghsa.js # GitHub Security Advisories paginated upsert
│ ├── osv.js # OSV.dev backfill for non-GitHub entries
│ └── downloads.js # npm + PyPI weekly download stat refresh
├── lib/
│ ├── campaigns.js # named-campaign pattern detection
│ ├── parsers.js # package.json / requirements.txt / etc parsers
│ └── score.js # blast radius helpers
├── routes/
│ ├── api.js # /api/* routes
│ └── badge.js # /api/badge.svg
└── public/ # vanilla JS SPA
License
MIT.