cve-tracker
README.md
# CVE Tracker

**Every public CVE in one place, free and open source.** See how many vulnerabilities are published over time, browse the newest ones first, filter them down to what matters to you, and open any CVE for its scores, exploitation signals, affected products and links to the primary sources.
**Live at [trackcve.app](https://trackcve.app)**, with its public API at [api.trackcve.app](https://api.trackcve.app/v1/summary).
[](https://trackcve.app)
[](https://github.com/janszotkowski/cve-tracker/actions/workflows/ci.yml)
[](LICENSE)
CVE Tracker keeps its own copy of the public sources (the CVE List, NVD, CISA KEV, FIRST EPSS and the GitHub Advisory Database) and refreshes it every few minutes. It needs no paid API and no account, and runs on a single small server.
[Website](https://trackcve.app) · [API reference](https://trackcve.app/api) · [MCP server](https://trackcve.app/mcp) · [Architecture](docs/ARCHITECTURE.md) · [Contributing](CONTRIBUTING.md) · [License](LICENSE)
For terminal-first CVE tracking, try [CVE Tracker CLI](https://github.com/OSSPhilippines/cve-tracker-cli).
## ✨ Features
- **The whole CVE List**: every published record (400,000+), refreshed every 10 minutes from the official CVE Program repository.
- **Timeline**: CVEs per hour, day, week, month or year, depending on the period you look at, in three views:
- **New**: counts stacked by severity.
- **Severity mix**: each severity's share of a period.
- **Cumulative**: the running total.
- **One filter for the chart and the table**: full-text search (title, description, vendor, product) or a CVE id, a publication period (today, last 24 hours, this week … all time, or a custom range), severity, "known exploited only", a vendor and product (with suggestions, matching every affected product of a record) a package ecosystem (npm, PyPI, Maven, Go, NuGet, crates.io…, from reviewed GitHub advisories), a weakness (CWE) and the CNA that assigned it.
- **Take it with you**: any filtered list downloads as CSV or JSON (up to 10,000 rows), and has its own Atom feed for a feed reader or a chat integration.
- **Recently added to KEV**: what CISA added to its Known Exploited Vulnerabilities catalog, by day, whatever the CVE's age, with its own feed.
- **Statistics**: severity, the most common weaknesses, vendors and CNAs of the last 7 days to 12 months.
- **Watchlist**: watch any filter and see what each found in the last week, with all their feeds as one OPML file; kept in the browser, no account.
- **Badges**: `/badge/CVE-….svg` shows a CVE's severity, score and KEV status in a README or a ticket.
- **Server-side sorting and paging**: by publication date, CVE id, CVSS score, EPSS probability or the day it was added to KEV.
- **Shareable state**: the filter, order, page and chart view all live in the URL. Copy it, bookmark it, send it; the browser's back button walks through your changes, and the way back from a CVE restores the list exactly as you left it.
- **CVE detail page**:
- description and dates
- every CVSS score (assigner, CISA Vulnrichment, NVD) with vectors
- EPSS with its history over the last month, and CISA SSVC
- the CISA KEV entry, including known ransomware use
- CWE, affected products and version ranges, affected packages from GitHub advisories
- solutions, workarounds and references
- links to CVE.org, NVD, OSV.dev, GitHub Advisories and CIRCL
- **Made to be found and cited**: every CVE has its own server-rendered page with structured data and a share image; sitemaps for all of them, `llms.txt`, and a Markdown version of every CVE for AI agents (`/cve/CVE-….md` or `Accept: text/markdown`).
- **An MCP server for AI assistants**: connect Claude, Cursor or any other client to `https://api.trackcve.app/v1/mcp` and let it search CVEs itself, with no key.
- **How it's built**: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), also on the site at [trackcve.app/architecture](https://trackcve.app/architecture).
- **Accessible**: WCAG 2.2 AA, checked with axe-core; full keyboard use, screen reader announcements, light and dark themes, reduced motion, and a table view of every chart's numbers.
### How it works
```
CVE List (GitHub) ─┐
NVD API 2.0 ─┤ in-process sync ┌───────────────┐ /v1/* ┌──────────────────────┐
CISA KEV (JSON) ─┼─────────────────▶ │ backend │ ───────▶ │ frontend │
FIRST EPSS (CSV) ─┤ │ Hono + SQLite │ │ TanStack Start (SSR) │
GitHub advisories ─┘ │ │ │ │
└───────────────┘ └──────────────────────┘
```
| Source | What it adds | How it is read | Default interval |
| ------------------------------------------------------------------------ | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------- |
| [CVE List (cvelistV5)](https://github.com/CVEProject/cvelistV5) | Every record, CNA scores, CISA Vulnrichment (ADP) | The whole repository as a tarball on the first run (~200 MB, about a minute), then `deltaLog.json` | 10 min |
| [NVD API 2.0](https://nvd.nist.gov/developers/vulnerabilities) | CVSS and CWE where the record has none of its own | Every CVE on the first run (~1 h without a key, minutes with a free one), then `lastMod` windows | 60 min |
| [CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) | Known exploitation, ransomware use, remediation date | One JSON file from cisa.gov, or the same file from CISA's GitHub copy (cisagov/kev-data) when refused | 6 h |
| [FIRST EPSS](https://www.first.org/epss/) | Probability of exploitation in the next 30 days | One gzipped CSV with every score; a CVE's history from FIRST's API when its page asks (cached) | 6 h |
| [GitHub Advisory Database](https://github.com/github/advisory-database) | Affected packages and their ecosystems | The reviewed advisories (OSV format) from the repository's tarball (~150 MB, about 15 s) | 12 h |
The severity a CVE is listed with comes from the assigner's (CNA) CVSS score, else CISA's, else NVD's; a CVE nobody has scored yet is "Not scored".
#### Security and performance
- **Queries never block the server.** Reads run in worker threads with read-only SQLite connections; a query still running after 3 s is stopped and its worker replaced, and past 200 waiting calls the API sheds load with `503` instead of piling up.
- **Fair use.** Every caller gets 120 requests a minute; the frontend's own rendering is exempt through a shared token. Paging is capped at 10,000 results, and one- or two-letter prefixes are not expanded.
- **Answers are reused.** The API keeps each answer for 30 seconds and drops them all when the sync changes the data; pages and answers carry `s-maxage` for a CDN.
- **Hardened pages.** A strict Content-Security-Policy that allows inline scripts only by hash, `frame-ancestors 'none'`, HSTS, `nosniff`, a strict referrer policy; external links open without passing the referrer.
- **The sync never loses changes.** It only advances over runs the delta log really contains, retries records that could not be read or were still the cached previous version, and never re-downloads everything because of one bad answer.
- **Supply chain.** Exact dependency versions, each at least 7 days old before pnpm installs it; only fixed, public sources are ever fetched.
#### Repository layout
```
apps/backend HTTP API (Hono on Node 24), SQLite store with query worker threads, and the source sync
apps/frontend Web app (TanStack Start, Router, Query, Table, Charts; Tailwind; Nitro)
packages/core Shared API contract (zod/mini), typed API client, severity and time-bucket model
skills A skill that tells an AI agent when and how to use the MCP server
```
| Technology | Used for |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------ |
| TypeScript 7, Node 24, pnpm workspaces, Turborepo | One language end to end, exact dependency pins, 7-day release-age policy |
| Hono, `node:sqlite` (with FTS5), pino, zod | API, storage and full-text search with no native dependency, logging |
| React 19, TanStack Start / Router / Query / Table / Charts, Nitro | Server-rendered UI, URL-driven state, server-side table, charts |
| Tailwind CSS 4, Base UI, Lucide | Styling and accessible primitives |
| oxlint, oxfmt, Vitest | Lint, format and tests |
## 🚀 Getting Started
You need **Node 24.22.1** (see `.node-version`) and **pnpm 12** (the exact version is pinned in `package.json` and fetched automatically).
```bash
git clone https://github.com/janszotkowski/cve-tracker.git
cd cve-tracker
pnpm install
cp apps/backend/.env.example apps/backend/.env
cp apps/frontend/.env.example apps/frontend/.env
pnpm dev
```
| What | Address |
| ------- | ------------------------------- |
| Web app | http://localhost:3000 |
| API | http://localhost:4000/v1/health |
On the first start the backend downloads the CVE List into `apps/backend/data/cve.db`, which takes about a minute; KEV and EPSS follow within seconds. NVD then fills in scores in the background, so older CVEs show as "Not scored" for a while. A free [NVD API key](https://nvd.nist.gov/developers/request-an-api-key) in `NVD_API_KEY` makes that take minutes instead of about an hour.
## 📖 Usage
The public API is read-only and works directly from a browser, script, or other HTTP client. For example:
```bash
curl https://api.trackcve.app/v1/summary
curl "https://api.trackcve.app/v1/cves?severity=CRITICAL"
```
**Full reference with examples: [trackcve.app/api](https://trackcve.app/api)** (source: [`docs/API.md`](docs/API.md)). OpenAPI 3.1: [`/v1/openapi.json`](https://api.trackcve.app/v1/openapi.json).
The frontend uses a public, read-only JSON API that anyone may call, within a fair-use limit (120 requests a minute per address by default; `RateLimit-*` headers tell you where you stand, and a `429` carries `Retry-After`). Answers carry `Cache-Control` headers suitable for a CDN; please cache on your side too. Paging reaches the first 10,000 results of a filter; narrow it or change the order for the rest.
| Endpoint | Returns |
| ----------------------- | ------------------------------------------------------------------------------------------------- |
| `GET /v1/health` | `{ status, version }` |
| `GET /v1/summary` | Headline counts and when each source was last read |
| `GET /v1/cves` | One page of CVEs for a filter |
| `GET /v1/timeline` | Matching CVEs per time bucket and severity, for the same filter (bucket width follows the period) |
| `GET /v1/cves/:id` | Everything known about one CVE, with links to its sources |
| `GET /v1/cves/:id/epss` | The CVE's EPSS day by day over the last month (from FIRST, cached) |
| `GET /v1/export` | Every CVE of a filter (up to 10,000), in the list's order: `format=csv` (default) or `json` |
| `GET /v1/vendors` | Vendors whose name contains `q` (the start of it for one or two letters), with CVE counts |
| `GET /v1/products` | The same for products, of one `vendor` when given |
| `GET /v1/ecosystems` | Package ecosystems with how many CVEs have a reviewed GitHub advisory in each |
Filter parameters (shared by `/v1/cves`, `/v1/timeline` and `/v1/export`; the site's own address and its Atom feed, `/feed.xml`, take the same ones):
| Parameter | Values |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `q` | A CVE id or its start (`CVE-2024-3`), or words to search for |
| `severity` | Comma-separated: `CRITICAL,HIGH,MEDIUM,LOW,NONE,UNKNOWN` |
| `kev` | `true` for known exploited CVEs only |
| `vendor`, `product` | A vendor and/or product among the record's affected products, any case (`vendor=Microsoft`) |
| `ecosystem` | `npm`, `PyPI`, `Maven`, `NuGet`, `Go`, `RubyGems`, `crates.io`, `Packagist`, `Pub`, `Hex`, `SwiftURL`, `GitHub Actions` |
| `range` | `all` (default), `today`, `24h`, `week`, `7d`, `month`, `30d`, `90d`, `year`, `6m`, `12m`, `custom` |
| `from`, `to` | First and last day (`YYYY-MM-DD`, inclusive, UTC) of a `custom` range |
| `sort`, `dir` | `published` (default), `updated`, `id`, `score`, `epss`; `desc` (default) or `asc` |
| `page`, `pageSize` | Page number from 1; up to 100 per page (default 50) |
The response shapes are defined once, as zod schemas, in [`packages/core/src/contract`](packages/core/src/contract), and [`packages/core/src/client`](packages/core/src/client/client.ts) is a typed client for them.
### MCP server
An AI assistant can use the same data over the [Model Context Protocol](https://modelcontextprotocol.io). The server is free and read-only and needs no key. It has limits of its own and never uses more than one query worker, so agents cannot slow the site down.
```bash
claude mcp add --transport http cve-tracker https://api.trackcve.app/v1/mcp
```
| Tool | Answers |
| -------------------- | ------------------------------------------------------------------------------- |
| `search_cves` | One page of CVEs for a filter, with the total |
| `get_cve` | One CVE in full |
| `get_epss_history` | A CVE's EPSS day by day over the last month |
| `get_timeline` | Matching CVEs per time bucket and severity |
| `get_summary` | Headline counts and when each source was last read |
| `get_vendor` | One vendor's or product's CVEs by severity and year, with its top weaknesses |
| `get_weakness` | One weakness's CVEs by severity and year, with the vendors it comes up for most |
| `get_weekly_digest` | One ISO week: the most severe, additions to KEV, top vendors and weaknesses |
| `get_stats` | Severity, top weaknesses, vendors and CNAs of a recent window |
| `get_epss_movers` | The CVEs whose EPSS rose the most |
| `find_filter_values` | How a vendor, product or CNA is spelled, and which filter values exist |
**Other clients and details: [trackcve.app/mcp](https://trackcve.app/mcp)** (source: [`docs/MCP.md`](docs/MCP.md)).
## ⚙️ Configuration
The backend reads its settings from the environment once at start-up and refuses to start with a readable message when one is invalid. The full list, with defaults, is in [`apps/backend/src/config/env.ts`](apps/backend/src/config/env.ts).
| Variable | Default | Meaning |
| ----------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `PORT`, `HOST` | `4000`, `0.0.0.0` | Where the API listens |
| `DATABASE_PATH` | `./data/cve.db` | The SQLite file (created when missing) |
| `CORS_ORIGINS` | any localhost port (development) | Comma-separated origins of the frontend; required in production |
| `NVD_API_KEY` | none | Free NVD key: 50 requests per 30 s instead of 5 |
| `SYNC_ENABLED` | `true` | Run the source sync in this process |
| `SYNC_NVD_ENABLED` | `true` | Enrich with NVD at all |
| `SYNC_*_MINUTES` | 10 / 60 / 360 / 360 / 720 | How often the CVE List, NVD, KEV, EPSS and GitHub advisories (`GHSA`) are read |
| `LOG_LEVEL` | `info` | pino log level |
| `RATE_LIMIT_PER_MINUTE` | `120` | Requests per caller per minute to `/v1/*` (health excepted) |
| `CLIENT_IP_HEADER` | connection address | Single-value header the proxy overwrites with the visitor's address (`cf-connecting-ip`, `x-real-ip`); `x-forwarded-for` is refused |
| `INTERNAL_API_TOKEN` | none | Shared with the frontend's server, so its rendering is limited per visitor (32+ chars) |
| `RATE_LIMIT_RENDER_PER_MINUTE` | `600` | Requests the frontend's rendering may make per minute for one visitor it names |
| `RATE_LIMIT_EXPORT_PER_MINUTE` | `10` | Exports (`/v1/export`) per caller per minute, on top of the general limit |
| `MCP_ENABLED` | `true` | Offer the MCP server at `/v1/mcp` |
| `RATE_LIMIT_MCP_PER_MINUTE` | `60` | Requests to `/v1/mcp` per caller per minute, on top of the general limit |
| `RATE_LIMIT_MCP_TOTAL_PER_MINUTE` | `1200` | Requests to `/v1/mcp` per minute for all callers together |
| `QUERY_WORKERS`, `QUERY_TIMEOUT_MS` | `2`, `3000` | Query worker threads, and how long one query may run before it is stopped |
| `RESPONSE_CACHE_SECONDS` | `30` | How long an answer is reused; dropped whenever the sync changes the data |
The frontend's settings are build-time (`VITE_*`, baked into the bundle) plus one runtime variable:
| Variable | Meaning |
| --------------------- | ------------------------------------------------------------------------------ |
| `VITE_API_URL` | The API the browser calls (required for a build) |
| `VITE_SITE_URL` | The public origin of the site (canonical links) |
| `VITE_ALLOW_INDEXING` | `true` only for the production build; everything else answers `noindex` |
| `API_INTERNAL_URL` | Runtime, server only: an internal address of the API for server-side rendering |
| `INTERNAL_API_TOKEN` | Runtime, server only: the same token as the backend's |
| `CLIENT_IP_HEADER` | Runtime, server only: the header with the visitor's address behind a proxy |
## 🚢 Deployment
The whole service is two small Node processes and one SQLite file of about 1.5 GB. Any server with 1–2 GB of RAM and a persistent disk is enough; a Hetzner CX22 (about €4 a month) runs it comfortably.
```bash
SITE_URL=https://cve.example.org \
API_URL=https://api.cve.example.org \
ALLOW_INDEXING=true \
NVD_API_KEY=your-key \
docker compose up -d --build
```
**On your own server:** [DEPLOY.md](DEPLOY.md) is a step-by-step guide for a Hetzner server behind a Cloudflare Tunnel (about 7 € a month), with a script that sets everything up.
[`docker-compose.yml`](docker-compose.yml) builds both images from the [`Dockerfile`](Dockerfile) and keeps the database on a volume, so a new release reads only what changed since the last one. Put a CDN or reverse proxy (Cloudflare's free plan, Caddy) in front for TLS. Pages and API answers carry `s-maxage`, so a CDN that caches them (on Cloudflare, add a cache rule for HTML) answers most traffic; the API reuses its own answers for 30 seconds as well. Set `INTERNAL_API_TOKEN` (required by the Compose file) and, behind Cloudflare, `CLIENT_IP_HEADER=cf-connecting-ip` (both services read it).
Railway or Fly.io with a volume mounted at `/data` work too. Serverless platforms without a persistent disk do not fit: the database and the sync loop need one long-lived process.
## 📚 Documentation
- [API reference](docs/API.md) explains every public endpoint and filter with examples.
- [MCP server](docs/MCP.md) shows how to connect an AI assistant, and lists its tools.
- [Architecture](docs/ARCHITECTURE.md) describes the source sync, storage, backend, and frontend.
- [Deployment guide](DEPLOY.md) covers a production deployment behind a Cloudflare Tunnel.
- [Security policy](SECURITY.md) explains how to report a vulnerability in CVE Tracker.
## 🛠️ Development
Use the commands below while developing or validating a change:
| Command | What it does |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pnpm dev` | Backend and frontend with reload |
| `pnpm build` | Production build of every package |
| `pnpm test` | Unit and integration tests (Vitest) |
| `pnpm e2e` | Browser tests (Playwright) of production builds on a seeded database, desktop and phone, with an axe-core audit; run `pnpm --filter @cve-tracker/e2e exec playwright install chromium` once first |
| `pnpm typecheck` | TypeScript across the monorepo |
| `pnpm lint` | oxlint, warnings fail |
| `pnpm format` | oxfmt (use `pnpm format:check` to only check) |
| `pnpm verify` | Everything CI runs: lint, format check, typecheck, tests, build |
## 🤝 Contributing
Contributions are welcome: bug reports, data issues, new chart views, new sources. Please read [CONTRIBUTING.md](CONTRIBUTING.md) first; everyone taking part follows the [Code of Conduct](CODE_OF_CONDUCT.md). To report a security problem in CVE Tracker itself, see [SECURITY.md](SECURITY.md).
## 🐛 Issues & Support
Open an issue for a [bug](https://github.com/janszotkowski/cve-tracker/issues/new?template=bug_report.yml), [data issue](https://github.com/janszotkowski/cve-tracker/issues/new?template=data_issue.yml), or [feature request](https://github.com/janszotkowski/cve-tracker/issues/new?template=feature_request.yml). For a security issue in CVE Tracker itself, follow the private reporting process in [SECURITY.md](SECURITY.md).
## 📋 Code of Conduct
Everyone taking part in the project is expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md).
## 📄 License
CVE Tracker shows public data as published by its sources, without warranty of any kind. Always check the primary source before acting on it.
- CVE® is a registered trademark of The MITRE Corporation. CVE records come from the [CVE Program](https://www.cve.org/).
- This product uses the NVD API but is not endorsed or certified by the NVD.
- Known exploited vulnerabilities come from the [CISA KEV catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog). cisa.gov refuses many data-centre addresses, so the sync falls back to CISA's own copy of the same file on GitHub ([cisagov/kev-data](https://github.com/cisagov/kev-data)).
- EPSS scores come from [FIRST](https://www.first.org/epss/): Jacobs, Romanosky, et al., _Exploit Prediction Scoring System_.
- Affected packages come from the [GitHub Advisory Database](https://github.com/github/advisory-database), licensed under [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/).
[MIT](LICENSE) © Jan Szotkowski. The data shown by the service stays under the terms of its respective sources.
## 👥 Authors & Contributors
Created and maintained by [Jan Szotkowski](https://github.com/janszotkowski). See the [contributor graph](https://github.com/janszotkowski/cve-tracker/graphs/contributors) for everyone who has helped improve CVE Tracker.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues