Skip to content

UlisesGascon/ghsa-dashboard

ghsa-dashboard

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.

Screenshots

Screenshot of Grafana rendering the demo dataset Preview using Demo dataset.

Features

  • One-table schema, plain SQL. Flat columns for common fields plus a raw JSONB column with the unmodified /repos/{owner}/{repo}/security-advisories response. 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.

Prerequisites

  • Docker + Docker Compose
  • Node.js 22+
  • pnpm 11+
  • gh CLI authenticated (gh auth login) — only if you use the bundled scraper

Quick start

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:demo

Open 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.

Data sources

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.

1. Bundled gh CLI scraper

pnpm scrape

Iterates 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.

2. Your own GitHub data dump

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.

3. Demo dataset

pnpm seed:generate    # writes seed/advisories.json (~250 fake advisories)
pnpm seed:demo        # loads it into Postgres

Uses @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).

Pruning the database

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 -v is 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');"

Scheduling re-syncs

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>&1

Or a systemd timer, GitHub Actions on a schedule, or just watch -n 3600 pnpm scrape in a terminal.

Env vars

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.

What's NOT in scope

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.

Architecture

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"]
Loading

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.

License

MIT

About

Self-hosted Grafana dashboard for GitHub Security Advisories. Postgres-backed, mirrors the GitHub API 1:1.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors