← back to gallery

shai-watch

Live malware advisory radar — paste your manifest and see what's pwned.

dev-toolssecuritysupply-chainnpmpypimalwareadvisory
Open product ↗

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:

  1. Browse the live malware feed, filtered by ecosystem.
  2. Open any advisory to see all affected packages + references.
  3. Paste a package.json, requirements.txt, Gemfile.lock,
  4. go.sum, composer.json or package-lock.json and get an instant
  5. 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

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.