Self-hosted dashboard for GitHub Security Advisories. Point it at a Postgres table that mirrors the GitHub API shape, get a Grafana view of what's in triage, what's drafted, and what's published.
Built for CNAs, OSS maintainers, and security teams coordinating disclosure across many repos. Read-only viewer for your GitHub security advisories.
Preview using Demo dataset.
- One-table schema, plain SQL. Flat columns for common fields plus a
rawJSONB column with the unmodified/repos/{owner}/{repo}/security-advisoriesresponse. Flat columns are mechanical projections — no transformation layer to maintain. - Pre-provisioned overview dashboard (18 panels): state distribution, severity breakdown, CVSS, CWE ranking, top reporters, top repos, throughput over time, time-to-publish, aging in triage / draft, recent activity.
- Multi-org filter. Pick which orgs you're looking at from a Grafana dropdown.
- Three loading paths. Bundled
gh-CLI scraper, your own GitHub data dump, or the included demo dataset. - Two containers (Postgres + Grafana), one Node script, ~100 lines of code total.
- MIT licensed.
- Docker + Docker Compose
- Node.js 22+
- pnpm 11+
ghCLI authenticated (gh auth login) — only if you use the bundled scraper
git clone https://github.com/UlisesGascon/ghsa-dashboard.git
cd ghsa-dashboard
# Default settings are fine for sample usage:
cp .env.example .env
$EDITOR .env
docker compose up -d
pnpm install
# Use Sample data:
pnpm seed:generate
pnpm seed:demoOpen http://localhost:3000 (default login: admin / admin). The Postgres datasource named ghsa and the overview dashboard are pre-provisioned.
To replace the demo data with real advisories, see Data sources below.
ghsa-dashboard is a viewer over a Postgres table. The table's raw column matches the GitHub API response shape 1:1, so anything that produces data in that shape can feed it.
pnpm scrapeIterates the orgs in ORGS, lists their repos, fetches /repos/{org}/{repo}/security-advisories?per_page=100 for each, and UPSERTs. Inherits the OAuth scope of your gh CLI session, so you see whatever you would see in the browser — including triage and draft advisories you collaborate on.
The bundled scraper is intentionally narrow. It is the path the maintainer uses to feed their own dashboard. It is not a general-purpose exporter and has no rate-limit backoff, parallelism, or filtering knobs. Fork it if you need something more elaborate, or use option 2.
If you already pull advisories through your own pipeline (CI, a different language, an enterprise mirror), you can feed the dashboard directly. The contract: one row per advisory keyed by ghsa_id, with the unmodified API response item in raw and flat columns as mechanical projections from it. See schema.sql for the column list and scraper.js for the extraction logic; seed/index.js is a minimal Node UPSERT example you can adapt.
Populating advisories is all you need — the advisory_cwes and advisory_credits views derive from raw->'cwes' and raw->'credits' automatically.
pnpm seed:generate # writes seed/advisories.json (~250 fake advisories)
pnpm seed:demo # loads it into PostgresUses @faker-js/faker to produce ~250 advisories across 2 orgs × 20 repos, spread over 2 years, with a state mix matching a real CNA queue (~2% triage, ~2% draft, ~5% published, ~1% withdrawn, ~90% closed).
No GitHub auth, no real data, nothing to disclose — good for evaluation, dashboard/query development, and reproducing UI bugs in public issues.
The seeder only deletes rows for the orgs in the JSON it's loading, so real advisories are safe. Org names are randomized each generate run; prune old demo orgs as needed (see below).
Three options, depending on how clean you want it:
Full wipe. Drops the Postgres volume, then schema.sql re-runs on next boot.
docker compose down -v
docker compose up -d
down -vis irreversible. It deletes the named Docker volume and everything in it.
Truncate the table. Keeps the container and volume.
docker compose exec postgres psql -U postgres -d ghsa -c 'TRUNCATE advisories;'Delete only specific orgs. Useful when mixing demo data with real advisories.
docker compose exec postgres psql -U postgres -d ghsa \
-c "DELETE FROM advisories WHERE org IN ('demo-org-a','demo-org-b');"pnpm scrape runs once and exits. Wire it into whatever scheduler you prefer:
# /etc/cron.d/ghsa-dashboard - re-sync hourly
0 * * * * cd /path/to/ghsa-dashboard && /usr/local/bin/pnpm scrape >> sync.log 2>&1Or a systemd timer, GitHub Actions on a schedule, or just watch -n 3600 pnpm scrape in a terminal.
| Var | Required | Description |
|---|---|---|
ORGS |
scraper only | Comma-separated GitHub org names, e.g. expressjs,fastify |
DATABASE_URL |
yes | Postgres connection string (default in .env.example works for local docker-compose) |
PG_PASSWORD |
no | Postgres password (default: ghsa). Must match DATABASE_URL |
PG_PORT |
no | Host port for Postgres (default: 5432) |
GRAFANA_PASSWORD |
no | Grafana admin password (default: admin) |
GRAFANA_PORT |
no | Host port for Grafana (default: 3000) |
The defaults in .env.example are for local evaluation only. Change PG_PASSWORD and GRAFANA_PASSWORD before running anywhere reachable from the network.
This tool deliberately stays narrow:
- No workflow / triage state / phase tracking. Use your own tooling.
- No write operations. Read-only viewer.
- No multi-tenancy. Self-host per team.
- No scoring or recommendations.
- No comments / timeline events. GitHub doesn't expose them via API.
flowchart LR
gh["GitHub API<br/>(your OAuth)"] --> scraper["scraper<br/>(Node + gh CLI)"]
scraper -->|UPSERT| pg[("Postgres<br/>(docker)")]
pg -->|SELECT| grafana["Grafana<br/>(docker)"]
grafana --> browser["browser"]
Two containers (Postgres, Grafana), one Node script (the scraper, runs on host), single advisories table, plain SQL queries. No app server, no ORM, no custom UI.
advisories is the only table. Two read-only views, advisory_cwes and advisory_credits, flatten the JSONB arrays inside raw so the dashboard queries that aggregate by CWE or by reporter can be plain SELECT ... FROM advisory_cwes WHERE org IN (...) instead of jsonb_array_elements(...) calls. The views are query rewrites, not materialized — no extra storage, no synchronization to keep up.
MIT