Self-hosted vulnerability intelligence dashboard for security teams. A CVE monitoring tool that tracks vendor/product assets, syncs vulnerability data through ProjectDiscovery vulnx, and surfaces CVSS x EPSS analytics, CISA KEV tracking, BOD 26-04 remediation tiers, PoC/Nuclei coverage, and multi-channel alerting, all from a single Go binary with a React UI.
Keywords: vulnerability management, CVE tracker, CVSS, EPSS, CISA KEV, vulnerability intelligence, security dashboard, self-hosted, vulnx, Nuclei, patch prioritization, attack surface, security operations, RBAC, remediation tracking.
Part of the odysafe family (dark glassmorphism design language). All source code, comments, and UI strings are in English.
| Dashboard | Excel export |
|---|---|
![]() |
![]() |
On GitHub, open the images directly: Dashboard.png · excel.png
- Scan now button to trigger a vulnx sync from the dashboard header.
- Views: Assets (risk cards per equipment), Vulnerabilities (triage-first CVE list), Overview (heatmap, KPIs, charts).
- Vulnerability tabs: Action required (default), Critical, KEV, New, Show all.
- Fast search (
kev,remote,status:,tag:) and multi-asset filter. - Bulk triage (ignore, snooze, accept risk) and one-click quick actions in the CVE modal.
- CVE detail modal (3 levels): summary, scores/exploit signals, technical references;
affected_equipmentfrom API. - Load more pagination and scroll-to-results on the CVE list.
- CVSS x EPSS heatmap with severity-aligned colors (
#FF453A/#FF9F0A/#FFD60A/#30D158). Click a cell to filter the CVE table. - KPI pills: KEV count, new CVEs (24h / 7d / 30d) with trend percentage versus previous period, PoC/Nuclei ratios, EPSS mean, remote/OSS counts, exposed assets, critical CVEs on exposed assets (KEV or CVSS >= 9).
- Average MTTR panel for CVE x equipment pairs marked
remediated. - Composite risk score:
(CVSS/10) x EPSS x (2 if KEV else 1), capped between 0 and 10. - BOD 26-04 remediation tier per CVE x equipment (simplified 2-variable model; see below).
- Top vendors and products, EPSS 30-day trend, equipment exposure bars with Exposed / Internal and business criticality badges.
- Global filters: equipment group and business criticality scope KPIs, heatmap, CVE list, and exposure charts.
- CVE detail modal with live vulnx lookup, PoC links, Nuclei template ID, references, per-asset remediation status, and CVE notes.
- Sticky data freshness banner on every page (sync status, vulnx health, rate-limit state).
This app implements a 2-variable remediation window (is_exposed on equipment + is_kev on CVE), not the full 16-cell CISA SSVC matrix.
| Exposed | KEV | Tier |
|---|---|---|
| Yes | Yes | 14d |
| Yes | No | 60d |
| No | Yes | 60d |
| No | No | Next upgrade |
- Deadline = CVE
published_at(fallback:synced_at) + tier days. - Overdue filter = past deadline and status is not
remediated. - Default CVE table sort: tier urgency first, then CVSS.
- Cache filters: KEV, Exposed + KEV, Overdue, Remote, OSS, CVSS/EPSS/date ranges, has notes, sync run changes.
- vulnx query syntax in the UI (for example
cve_updated_at:>=2024-01-01,is_oss:true). - Dynamic filter metadata from
GET /api/vulnx/filters. - Scoped filters:
?group_id=,?business_criticality=,?sync_run_id=,?has_notes=true.
- Dedicated Executive page for leadership: exposed assets, critical overdue SLA, MTTR with trend, accepted risks, open CVEs by remediation tier (bar chart).
- Equipment: name, vendor/product (vulnx CPE fields), Internet exposure, exposure justification, business criticality, group assignment, CSV bulk import (preview + confirm).
- Groups: named equipment groups with color badges (many-to-many).
- Users (admin): RBAC roles, temporary password on create, forced password change on first login.
- Audit log (admin): read-only history with filters and CSV export.
- Sync: cron schedule, per-source toggles (NVD, KEV, EPSS, CPE, Nuclei), manual sync, last sync change summary, rate-limit status.
- API key: ProjectDiscovery PDCP key, encrypted at rest.
- Alerts: Discord, Slack, SMTP with thresholds (CVSS, EPSS, KEV, exposed + KEV); digest notifications per sync cycle.
- Export: formatted Excel workbook (
.xlsx) with executive KPI sections, per-equipment metrics, and full CVE rows (see Export formats). - Authentication: RBAC (admin / analyst / viewer), password change, optional TOTP (API).
| Role | Access |
|---|---|
| admin | Full access: settings, API key, alerts, user management, equipment delete |
| analyst | Remediation status, notes, equipment CRUD, manual sync, CSV import; no API key, alerts, or user management |
| viewer | Read-only dashboard, CVE list, Excel export, Executive Summary |
- One-command install and update (
install.sh). - Optional systemd service with paths generated for your user and install directory.
- Offline and degraded mode: cached SQLite data when sync or vulnx is unavailable.
- Schema migrations applied automatically on server start (
001through009). - Sync change tracking: last 30 sync runs retained with per-CVE diffs (
new_cve,became_kev,score_updated,poc_added).
+--------------------------------------------------------------------+
| Single Go binary (chi HTTP server) |
| +-- /api/* REST JSON API (session auth + RBAC) |
| +-- /* React SPA (frontend-dist/) |
| +-- SQLite cache data/cve.db (WAL, modernc, no CGO) |
| +-- Encrypted config config/config.enc (AES-256-GCM) |
| +-- robfig/cron scheduled sync per equipment |
| +-- vulnx v2 client github.com/projectdiscovery/vulnx/v2 |
+--------------------------------------------------------------------+
| Layer | Technology |
|---|---|
| Backend | Go 1.22+, chi, modernc.org/sqlite |
| Frontend | React 18, Vite, TypeScript, TailwindCSS, recharts (web/ sources, built to frontend-dist/) |
| Vulnerability data | vulnx v2, ProjectDiscovery cloud API |
| Excel export | excelize (in-process .xlsx, no external deps) |
| Auth | bcrypt, HttpOnly cookies, RBAC, optional TOTP |
| Secrets | AES-256-GCM (config.enc), log redaction |
| Table | Purpose |
|---|---|
users |
Accounts with role, must_change_password |
equipment |
Monitored assets; is_exposed, business_criticality, justification |
equipment_groups / equipment_group_members |
Named groups (many-to-many) |
cves |
Cached vulnx records per equipment |
equipment_cve_status |
Per CVE x equipment remediation_status, remediated_at (MTTR) |
cve_equipment_notes |
Per CVE x equipment comment history |
audit_log |
Immutable actions with old_value / new_value JSON |
sync_runs / sync_run_changes |
Sync history and per-run diffs |
dashboard_kpi_cache |
KPI snapshot refreshed after sync |
alert_sent |
Alert deduplication per sync cycle |
| Dependency | Version | Purpose |
|---|---|---|
| Go | >= 1.22 | Backend build |
| Node.js | >= 20 | Frontend build |
| npm | >= 9 | Frontend dependencies |
| openssl | any | Encryption key generation |
Excel export requires no browser. On Debian/Ubuntu, Fedora, or Arch, install.sh can install missing packages via apt, dnf, or pacman.
git clone <your-repo-url> odysafe-cve
cd odysafe-cve
# Optional: clone vulnx as sibling only when hacking the upstream client locally
# git clone https://github.com/projectdiscovery/vulnx ../vulnx
# echo 'replace github.com/projectdiscovery/vulnx/v2 => ../../vulnx' >> backend/go.mod
chmod +x install.sh start.sh stop.sh uninstall.sh
./install.sh
./start.shDefault install listens on port 5000 with HTTPS (self-signed certificate) unless you choose HTTP during install.
- Open https://localhost:5000 (accept the browser certificate warning if using self-signed TLS).
- If authentication is disabled, go to Settings > Authentication and create an admin account, or log in with existing credentials.
- Go to Settings > Equipment and add monitored vendor/product pairs (or import via CSV).
- Click Scan now on the dashboard or run sync from Settings > Sync (default cron: every 6 hours).
- Review the CVE list (Tier, Exposure, Deadline columns); set remediation status in the detail modal.
- Optionally add a ProjectDiscovery API key for higher vulnx rate limits.
| Script | Description |
|---|---|
install.sh |
Install dependencies, build backend and web UI, init DB/config |
start.sh |
Start via systemd (if installed) or background process with PID file |
stop.sh |
Stop running instance |
uninstall.sh |
Stop services, remove runtime data (interactive inventory) |
scripts/build.sh |
Rebuild binaries and web UI without changing config/DB |
scripts/smoke_test.sh |
Syntax checks, backend tests, config tool smoke test |
scripts/fix-permissions.sh |
Restore ownership after a root-owned install |
scripts/audit-equipment-tokens.sh |
List/fix invalid vendor/product CPE tokens in presets and DB |
scripts/rewrite-git-history.sh |
Single-commit history rewrite after GitHub secret-scan push rejection |
scripts/cleanup-legacy.sh |
Remove legacy frontend/ and deploy/ folders |
Environment overrides: ODYSAFE_ROOT, DATA_DIR, PORT.
odysafe-cve/
+-- install.sh / start.sh / stop.sh / uninstall.sh
+-- scripts/
| +-- build.sh rebuild backend + web UI
| +-- smoke_test.sh integration smoke tests
| +-- generate-ssl-cert.sh TLS certificate helper
| +-- fix-permissions.sh ownership repair after root install
| +-- cleanup-legacy.sh remove old frontend/ and deploy/
+-- packaging/systemd/
| +-- odysafe-cve.service systemd unit template
+-- backend/
| +-- cmd/server/ main HTTP server
| +-- cmd/init/ DB bootstrap
| +-- cmd/config/ encrypted config writer (used by install.sh)
| +-- internal/api/ REST handlers, middleware, RBAC
| +-- internal/vulnx/ vulnx client wrapper
| +-- internal/store/ SQLite layer
| +-- internal/scheduler/ cron sync, change tracking
| +-- internal/alerts/ Discord / Slack / SMTP digest
| +-- internal/export/ Excel export
| +-- migrations/ versioned schema (001-009)
+-- web/ React SPA sources, built to frontend-dist/
| +-- src/ Dashboard, Settings, Executive, Guide, ...
+-- config/ .secret_key, config.enc, tls/ (gitignored)
+-- data/ cve.db (gitignored)
+-- logs/ server.log (gitignored)
+-- bin/ odysafe-cve, odysafe-init, odysafe-config (gitignored)
+-- docs/
+-- README.md extended technical reference
+-- Dashboard.png dashboard screenshot
+-- excel.png Excel export screenshot
+-- config.example.yaml configuration schema reference
Legacy folders frontend/ and deploy/ from older installs can be removed with sudo ./scripts/cleanup-legacy.sh.
Runtime settings live in config/config.enc (encrypted). The schema is documented in docs/config.example.yaml.
| Setting | Default | Description |
|---|---|---|
port |
5000 (install) / 8080 (dev) |
HTTP(S) listen port |
sync_cron |
0 */6 * * * |
Automatic sync schedule |
sync_sources |
all true |
NVD, KEV, EPSS, CPE, Nuclei toggles |
age_alert_threshold |
90 |
Highlight average CVE age above this (days) |
secure_cookies |
true when HTTPS install |
Set for HTTPS deployments |
pdcp_api_key |
none | ProjectDiscovery API key (recommended) |
During install, source toggles and port are persisted via bin/odysafe-config. Most other settings are editable in the Settings UI.
When exposing the app beyond localhost, use HTTPS (install option or reverse proxy) and enable Secure cookies in Settings. The UI warns when HTTP is used without secure_cookies.
odysafe-cve uses the vulnx v2 Go client to search and fetch CVE records.
| Capability | Endpoint / location |
|---|---|
| Equipment sync | Unquoted affected_products.vendor / product queries with fallbacks |
| Live search | GET /api/cves?q=... |
| CVE by ID | GET /api/cves/{id}, cache first, then live vulnx |
| Filter metadata | GET /api/vulnx/filters |
| Date queries | cve_updated_at:>=YYYY-MM-DD, operators >=, >, <, <= |
Rate limits (without API key): approximately 10 requests per minute. The client adds inter-page delays, retries on HTTP 429 with backoff, and surfaces a sync temporarily disabled banner in the UI. Equipment syncs run sequentially; exposed assets are prioritized.
Example queries (Dashboard search bar or API ?q=):
is_oss:true
is_remote:true
cve_updated_at:>=2024-06-01
affected_products.product:mongodb
All routes except /api/healthcheck, /api/auth/login, and /api/auth/status require a session cookie when authentication is enabled. Write endpoints enforce RBAC (403 on denial).
| Method | Path | Description |
|---|---|---|
| GET | /api/healthcheck |
System, sync, and vulnx health |
| GET | /api/auth/status |
Whether login is required |
| POST | /api/auth/login |
Create session |
| POST | /api/auth/logout |
Destroy session |
| GET | /api/auth/me |
Current user (email, role, must_change_password) |
| POST | /api/auth/change-password |
Change password |
| POST | /api/auth/enable |
Bootstrap admin and enable login |
| POST | /api/auth/totp/setup |
TOTP setup |
| POST | /api/auth/totp/verify |
TOTP verify |
| GET/POST/PUT/DELETE | /api/users |
User management (admin) |
| Method | Path | Description |
|---|---|---|
| GET | /api/dashboard/kpis |
KPIs (?compare=previous, ?group_id=, ?business_criticality=) |
| GET | /api/dashboard/heatmap |
CVSS x EPSS grid (?group_id=, ?business_criticality=) |
| GET | /api/cves |
CVE list; filters: kev, overdue, group_id, has_notes, sync_run_id, ?q= |
| GET | /api/cves/{id} |
CVE detail (cache and live) |
| GET | /api/vulnx/filters |
vulnx filter field metadata |
| PUT | /api/equipment/{eqId}/cves/{cveId}/remediation |
Set remediation status |
| Method | Path | Description |
|---|---|---|
| GET/POST | /api/equipment |
List / create equipment |
| PUT/DELETE | /api/equipment/{id} |
Update / delete equipment (delete: admin) |
| POST | /api/equipment/bulk-delete |
Bulk delete (admin) |
| GET | /api/equipment/suggestions |
Vendor/product suggestions |
| GET/POST/PUT/DELETE | /api/equipment-groups |
Equipment groups |
| POST | /api/equipment/import/preview |
CSV import preview |
| POST | /api/equipment/import/confirm |
CSV import confirm (duplicate_mode=skip|update) |
| Method | Path | Description |
|---|---|---|
| GET/POST | /api/notes |
List / create CVE x equipment notes |
| DELETE | /api/notes/{id} |
Delete note (author or admin) |
| GET | /api/audit-log |
Paginated audit log (admin; filters: user, action, dates) |
| GET | /api/audit-log/export.csv |
Audit log CSV export |
| GET | /api/executive/summary |
Executive KPI summary (all roles) |
| Method | Path | Description |
|---|---|---|
| GET/PUT | /api/settings |
Read/update config (masked secrets) |
| POST | /api/settings/test/{vulnx,discord,slack,smtp} |
Integration tests |
| GET | /api/settings/storage |
DB size, source status |
| POST | /api/settings/storage/clear |
Clear cache |
| POST | /api/settings/storage/reload |
Reload sources |
| POST | /api/sync |
Trigger manual sync (?failed_only=true for retries) |
| GET | /api/sync/status |
Sync state, rate limit, last change summary |
| POST | /api/export/xlsx |
Formatted Excel export |
| Control | Implementation |
|---|---|
| Password storage | bcrypt |
| RBAC | admin / analyst / viewer on all write endpoints |
| Session cookies | HttpOnly, SameSite=Strict, optional Secure, sliding TTL |
| Config and secrets | AES-256-GCM in config.enc, key in config/.secret_key (mode 600) |
| Webhook validation | Regex check on save and test (Discord/Slack) |
| Log redaction | Webhooks, API keys, passwords masked in access logs and error bodies |
| Rate limiting | Per-IP token bucket, JSON 429 with retry_after |
| Audit log | Login, settings, remediation, equipment CRUD, risk accepted, group assignments (read-only, no UI delete) |
Never commit config/.secret_key, config/config.enc, or data/.
# Backend tests
cd backend
go test ./...
CGO_ENABLED=0 go build -o ../bin/odysafe-cve ./cmd/server
# Frontend dev server (proxies /api to localhost:8080 via vite.config.ts)
cd web
npm install
npm run dev
# Full smoke test (scripts + unit tests + config tool)
bash scripts/smoke_test.sh# Terminal 1: backend
cd backend && go run ./cmd/server -root .. -port 8080
# Terminal 2: frontend
cd web && npm run devInitialize admin user once:
go run ./cmd/init -root ..UI changes: edit files under web/src/, then run ./scripts/build.sh or ./install.sh.
If config/config.enc already exists, ./install.sh detects an existing install and runs in update mode (rebuild binaries and web UI, preserve data and config).
git pull
./install.sh
./stop.sh && ./start.shDatabase migrations run automatically on server start (backend/migrations/, currently 001 through 009: notes nullable user, equipment product_version, triage fields).
For a quick rebuild:
./scripts/build.sh
./stop.sh && ./start.shIf install was run as root:
sudo ./scripts/fix-permissions.sh
sudo ./scripts/cleanup-legacy.shHard-refresh the browser (Ctrl+F5) after frontend updates.
| Format | Description |
|---|---|
Excel (.xlsx) |
On-demand via POST /api/export/xlsx or Settings > Export. Sheets: Summary (executive, risk, exposure, temporal, exploitable, and coverage KPI sections), Equipment (per-asset CVE/KEV/PoC/risk/sync metrics), CVEs (CVE ID, vendor, product, version, CVSS, severity, EPSS, KEV/PoC/Nuclei/Remote/OSS, dates, description, references, risk score, equipment). Styled headers and conditional formatting via excelize. |
| Audit CSV | GET /api/audit-log/export.csv for compliance exports. |
Legacy CSV, JSON, and PDF export endpoints have been removed.
install.sh can install a user-specific unit under /etc/systemd/system/odysafe-cve.service. The committed template is at packaging/systemd/odysafe-cve.service - do not copy it directly.
sudo systemctl start odysafe-cve
sudo systemctl status odysafe-cve
journalctl -u odysafe-cve -fWhen systemd is enabled, use ./start.sh only (it delegates to systemctl; do not run both systemd and manual nohup).
| Symptom | Action |
|---|---|
| 404 on / | Run ./install.sh or ./scripts/build.sh, then ./stop.sh && ./start.sh |
| Empty dashboard | Add equipment in Settings, then run Scan now or Sync |
| Empty Vendor column (live search) | Broad text queries may omit vendor; use CPE-style queries or cached list after sync |
| Tier shows - | CVE not linked to equipment (live vulnx results only) |
| MTTR is 0 | Mark CVE x equipment pairs as remediated in the detail modal |
| "Rate limit reached" banner | Add PDCP API key or wait for cooldown |
| Degraded / cached data banner | Check logs/server.log, verify vulnx connectivity |
| Excel export fails / 404 | Re-run ./install.sh or ./scripts/build.sh; use POST /api/export/xlsx |
| 401 on API calls | Session expired, log in again |
| 403 on API calls | Role lacks permission for this action |
| 429 on API | Per-IP rate limit, wait retry_after seconds |
| Permission errors after install | sudo ./scripts/fix-permissions.sh |
| Dashboard missing triage tabs / asset cards | Run ./scripts/build.sh then hard-refresh (Ctrl+F5) |
| Excel export outdated layout | Rebuild backend (./scripts/build.sh) and re-export after upgrade |
- docs/README.md: extended technical reference
- docs/config.example.yaml: configuration schema
- docs/Dashboard.png: dashboard screenshot
- docs/excel.png: Excel export sample
Thanks to IT-Connect for bringing this project to my attention and for the existing coverage of ProjectDiscovery vulnx that helped shape the integration.
Vulnerability intelligence is powered by the vulnx API and CVE data ecosystem from ProjectDiscovery.
MIT

