Comments across the codebase credited one individual by role and, in places, described that person's own machines: which SQL Server versions a host ran, which devices were enrolled, what a particular dashboard showed, how many findings sat open on which server. In a public repository that reads as a profile of someone's unpatched estate. The observations are why the code looks the way it does, so they stay. Every CVE id, version, build number, count and date is preserved, as are the verbatim quotes that motivated specific sort and filter rules — only the attribution changes, to "field report", "observed", "a host". A local variable in tests/test_autodesk_year.py was renamed for the same reason; its value and every assertion around it are byte-identical. PROJECT_OVERVIEW.md additionally loses a subtitle naming the kind of organisation this was built for, and a support section pointing at an internal team, both replaced with neutral wording. Comments, docstrings and markdown prose only: 74 files, 200 lines, one-for-one swaps. detect_changes reports 104 touched symbols and zero affected execution flows, and all 55 test scripts pass. Nothing here needs re-testing.
14 KiB
TrueVuln — Project Overview
Self-hosted vulnerability management for small and mid-sized IT teams. Wazuh agents + Nessus scanner + CISA/ENISA/EPSS threat intel + CIS-Benchmark compliance + Unified Risk Score, all behind a single FastAPI + Next.js stack.
Stand: Mai 2026, dev-Branch, alembic head 022.
🎯 Summary
TrueVuln collects vulnerability findings from multiple scanners (Wazuh, Nessus), enriches them with several free public threat-intel sources, scores the result on a unified 0-100 risk scale, and surfaces them in a role-based web UI with SLA-tracking, mail notifications, compliance evidence (Wazuh SCA
- CIS-Benchmark impact weights), and an LLM-backed remediation analysis.
Designed for internal small-to-mid IT teams (5-100 hosts). Single-server deployable via Docker Compose. Postgres-only persistence.
📁 Project Structure
vulnerability-dashboard/
├── app/ # FastAPI backend
│ ├── routers/ # HTTP API by resource
│ │ ├── auth.py, auth_admin.py
│ │ ├── auth_oidc.py, auth_saml.py
│ │ ├── vulnerabilities.py # vulns, sync, enrichment, overrides
│ │ ├── nessus.py # Tenable Nessus
│ │ ├── assets.py, scans.py, policies.py, groups.py
│ │ ├── compliance.py # SCA + URS endpoints
│ │ ├── notifications.py, reports.py, settings.py, audit.py
│ ├── services/ # business logic, scheduler-reusable
│ │ ├── enrichment_service.py
│ │ ├── vuln_override_service.py # 3-stage CVSS cascade
│ │ ├── override_jobs.py # async job tracker
│ │ ├── nessus_sync.py
│ │ ├── compliance_service.py, compliance_impact_import.py
│ │ ├── urs_service.py
│ │ └── email_service.py
│ ├── integrations/ # thin HTTP clients
│ │ ├── wazuh_client.py
│ │ ├── nessus_client.py
│ │ ├── ai_client.py, infomaniak_ai_client.py
│ ├── auth/ # strategy-pattern auth
│ │ ├── orchestrator.py
│ │ ├── strategies/ # local, ldap, oidc, saml
│ │ ├── jit_provisioner.py, role_mapper.py
│ ├── models/ # SQLAlchemy
│ │ └── vulnerability.py, asset.py, user.py, group.py, ...
│ ├── schemas/ # Pydantic v2 request/response
│ ├── scheduler.py # APScheduler
│ ├── main.py # FastAPI app + middleware
│ └── deps.py # auth dependency, DB session
├── alembic/
│ └── versions/ # 001 .. 022 migrations
├── alembic.ini
├── frontend/
│ ├── app/ # Next.js 14 App Router
│ │ ├── page.tsx # Dashboard (2-widget Recent Critical + Newly Published)
│ │ ├── vulnerabilities/page.tsx # main list
│ │ ├── vulnerabilities/[id]/ # detail
│ │ ├── assets/, compliance/, scans/, notifications/, settings/
│ ├── components/
│ ├── lib/api.ts # axios + auth header
│ ├── types/index.ts
│ └── public/logo.png
├── docs/
│ └── TROUBLESHOOTING.md
├── docker-compose.yml
├── ARCHITECTURE.md
├── DATABASE_SCHEMA.md
├── README.md # stable main feature set
└── README.DEV.md # dev-branch additions (this branch)
🚀 Quick Start
# 1. Clone
git clone <repo> && cd vulnerability-dashboard
# 2. Configure .env (see ARCHITECTURE.md §6 for full list)
cp .env.example .env
$EDITOR .env # set DATABASE_URL, JWT_SECRET, AUTH_PROVIDER_CRYPTO_KEY,
# WAZUH_API_URL/USER/PASS, WAZUH_INDEXER_URL
# 3. Start
docker compose up -d
# 4. First-start bootstrap creates the admin via /auth/setup-admin (run once)
curl -X POST http://localhost:8000/api/v1/auth/setup-admin \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"<strong-pass>","email":"you@example.com"}'
# 5. Open
# UI: http://localhost:3000
# API: http://localhost:8000/docs
docker compose up -d runs alembic upgrade head automatically on backend
boot — no separate migration step needed for fresh deploys.
📊 Implemented Features
Threat-intel enrichment (5 sources)
- EPSS — FIRST.org probability of exploitation, refreshed every 24 h
- CISA KEV — known-exploited catalog, ~1500 CVEs, ransomware-use flag
- ENISA EUVD — EU exploited + ENISA-flagged critical, ~1600 CVEs
- CISA Vulnrichment — authoritative per-CVE CVSSv3.1 corrections + SSVC decision points (Exploitation / Technical Impact / Automatable). Parses CNA + ADP containers.
- NVD REST API — 2nd-stage fallback for CVEs Vulnrichment hasn't analysed yet (rate-limit bounded ≤ 100 per run)
- cvelistV5 — 3rd-stage backstop, exhaustive ~250 k CVEs, 12 h on-disk ZIP cache
All sources free, no API key required.
Scoring
- Priority Score (0-100) — CVSS + exploit-bonus (KEV/EUVD/EPSS/SSVC/Wazuh/Nessus, capped via max) + SSVC technical-impact/automatable add-on + asset-criticality multiplier + age bonus
- CPR Score (0-100) — JacquesKruger percentile-weighted blend:
CVSS×10 × 0.6 + EPSS_percentile × 100 × 0.4 - Tenable VPR (0-10) — surfaced as parallel reference, not folded into Priority/CPR
- URS (0-100) — Unified Risk Score combining AVS (CPR-derived) + ASS (impact-weighted SCA) × criticality multiplier, daily snapshots feed trend arrow
Multi-scanner ingest
- Wazuh — agent-based vulnerability detection + SCA compliance
- Nessus — REST-API import alongside Wazuh on the same
(cve_id, asset_id)row; pseudo-CVE pseudo-IDs for non-CVE plugins; per-host scan rows - Manual overrides — 3-stage CVSS/SSVC/fixed_version cascade triggered by "Correct CVSS" button or 03:00 UTC nightly job
Multi-provider authentication
- Local — username + password (bcrypt cost-12) + optional TOTP MFA
- LDAPS — search-then-bind, encrypted bind password
- OIDC — PKCE + JWKS, Entra ID / Okta / Keycloak / Google
- SAML 2.0 — python3-saml, strict signature + XSW protection
- JIT provisioning + group-to-role mapping editable in UI
Compliance + URS
- Wazuh SCA pulled per agent and stored as
compliance_results+compliance_checks - CIS-Benchmark impact CSV upload weighted into per-policy
weighted_score - Unified Risk Score per asset with daily snapshots and 7-day trend arrow
SLA tracking + notifications
- Per-severity SLA days configurable globally + per policy
- Hourly SLA breach checker → digest or single mode mails
- Policy
DISABLEDstatus suppresses mails for that policy's assets - Master kill-switch via
sla_breach_enabledsetting - Per-vuln 24 h throttle prevents repeat-pinging
- New-vuln digest after every Wazuh/Nessus sync
UI features
- 2-widget dashboard ("Recent Critical" + "Newly Published")
- Vulns list with VPR badge, EPSS / KEV / EUVD / SSVC pills, cross-scanner confirmation, badge legend popover
- Per-vuln detail with threat-intel sections + Priority/CPR breakdown
- Compliance page with per-asset modal and URS table
- Settings UI for SMTP, Nessus, LDAP/OIDC/SAML, role mappings, mail templates
🔧 Configuration
.env keys at a glance:
# Database
DATABASE_URL=postgresql://vulnmanager:<pw>@postgres/vulnmanager
# JWT
JWT_SECRET=<random-256-bit>
JWT_ALGORITHM=HS256
JWT_EXPIRY_MINUTES=60
# Auth crypto (Fernet, encrypts TOTP + LDAP pw + SMTP pw at rest)
AUTH_PROVIDER_CRYPTO_KEY=<fernet-key>
# Wazuh
WAZUH_API_URL=https://wazuh-manager:55000
WAZUH_INDEXER_URL=https://wazuh-indexer:9200
WAZUH_API_USER=truevuln-readonly
WAZUH_API_PASS=<pw>
# Optional
DASHBOARD_URL=http://localhost:3000 # used in mail links
INFOMANIAK_API_KEY=<key> # AI analysis
LDAP_BIND_PASSWORD_BOOTSTRAP=<pw> # one-shot for first LDAP setup
HTTPS_PROXY=http://proxy:8080 # for outbound enrichment
All UI-relevant config (SMTP, Nessus, LDAP, OIDC, SAML, role mappings,
mail templates) lives in the settings table — no env-var redeploy needed.
📡 API Overview
See ARCHITECTURE.md §5 for the endpoint reference and /docs (Swagger UI)
on the running backend for the full schema. Curl-friendly examples in
README.DEV.md.
🔒 RBAC Permissions
| Role | Capabilities |
|---|---|
| admin | Everything — user management, provider config, settings, role mappings, audit log |
| editor | Sync, override, enrich, assign, defer, mark FP, schedule scans, compliance refresh, impact import |
| viewer | Read-only — list / search vulns, assets, scans, reports, exports |
Enforced by require_role() dependency on every router. JWT carries the role
claim; group→role mapping re-evaluates on each SSO/LDAP login.
🗄️ Database Schema (PostgreSQL)
See DATABASE_SCHEMA.md for column-level detail. 20+ tables, 22 alembic
migrations. Key tables:
users,groups,user_groupsassets,asset_groupsvulnerabilitiesscans,scan_schedulespoliciesnotification_logs,audit_logscompliance_results,compliance_checks,compliance_impacts,asset_risk_snapshotssettings(KV store),ai_analyses,ai_reports
🎨 Workflow Examples
Daily admin morning routine
- Dashboard widget shows URS trend + Critical / Newly Published CVEs
- SLA-breach mail already arrived (00:00–08:00 stack)
- Click "Correct CVSS" once a week to refresh Vulnrichment-corrected scores
- Compliance widget shows worst 5 assets — drill in via per-asset modal
- Investigate "Recent Critical" rows — KEV / EUVD badges flag externally-confirmed exploit risk
Adding a Nessus scan
- Settings → Integrations → Tenable Nessus → Configure
- Paste Base URL + API access/secret keys → Test connection
- Pick default scan IDs → Save
- Sync now (or wait for next scheduled run)
- Vulns list now shows Wazuh + Nessus + cross-confirmed rows
Updating a Vulnrichment-corrected score for a single CVE
TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"…"}' | jq -r .access_token)
curl -X POST "http://localhost:8000/api/v1/vulnerabilities/override/vulnrichment?dry_run=false&cve_ids=CVE-2026-8390" \
-H "Authorization: Bearer $TOKEN"
Response stats include not_found (CVEs CISA hasn't analysed yet — silently skipped).
Disabling SLA mails system-wide
docker compose exec postgres psql -U vulnmanager -d vulnmanager -c "
INSERT INTO settings (key, value) VALUES ('sla_breach_enabled','false')
ON CONFLICT (key) DO UPDATE SET value='false';"
Re-enable:
UPDATE settings SET value='true' WHERE key='sla_breach_enabled';
🚧 Known Limitations
sla_breach_enabledtoggle has no UI yet — DB-only (Settings page improvement on backlog)- CIRCL aggregator not used — we hit CISA + ENISA directly
- No real-time KEV push — we poll the CISA feed (24 h cache)
- No two-way Nessus FP sync — marking false-positive in TrueVuln doesn't update Nessus
- Wazuh-side fix-version field doesn't exist in the indexer schema — relying entirely on Vulnrichment/NVD/cvelistV5 cascade for PATCH AVAILABLE
- Per-framework URS breakdown schema-ready but UI hidden
- No WebAuthn / FIDO2 — TOTP is the only second factor
🛠️ Development
Run tests
docker compose exec backend pytest tests/
Smoke test covers auth, RBAC, vuln CRUD, sync mocking. Coverage is partial — integration paths (Wazuh, Nessus, enrichment HTTP) rely on manual verification against a live environment.
Add a new vuln column
- Edit
app/models/vulnerability.py— add theColumn(...) - Edit
app/schemas/vulnerability.py(or wherever the Pydantic schemas live) - Generate migration:
docker compose exec backend alembic revision --autogenerate -m "add foo column" - Review the generated file (autogenerate misses index intent sometimes)
alembic upgrade head- Update
_build_vuln_response()inapp/routers/vulnerabilities.pyif it should be in the API response
Add a new threat-intel source
- New service module in
app/services/(e.g.mysource_service.py) - Cache feed JSON in
settingstable for 24 h - Wire into
enrichment_service.refresh_threat_intel_enrichment() - Add columns to
vulnerabilitiestable via alembic - Surface in
_build_vuln_response()+ frontend list/detail
📈 Performance Optimisations
- Wazuh indexer pagination beyond OpenSearch 10 000-hit cap (search-after)
- Vulnrichment cascade auto-routes to ZIP when > 25 CVEs requested
- cvelistV5 ZIP cached on disk 12 h
- NVD stage capped at 100 CVEs per run (rate-limit safe)
- Async "Correct CVSS" — long-running cascade returns a job_id and a progress card
- Indexed columns on every filter / sort path
sources/enrichment_sourcesJSON columns avoid join-heavy queries
📚 Further Documentation
ARCHITECTURE.md— system topology, security model, scheduler jobsDATABASE_SCHEMA.md— every column, every enum, every migrationCODEBASE_KNOWLEDGE_BASE.md— module-by-module walk-through for new contributorsREADME.DEV.md— dev-branch feature deep-dives (auth, Nessus, compliance, URS)docs/TROUBLESHOOTING.md— common production issues and fixes
🐛 Debugging
# Backend logs (tail and follow)
docker compose logs -f backend
# All services
docker compose logs -f
# Postgres logs
docker compose logs -f postgres
# Inside backend container
docker compose exec backend bash
docker compose exec backend python3 -c "from app.services.enrichment_service import refresh_threat_intel_enrichment; refresh_threat_intel_enrichment()"
# DB shell
docker compose exec postgres psql -U vulnmanager -d vulnmanager
docs/TROUBLESHOOTING.md lists symptom → cause → fix patterns for the
recurring production issues (alembic head conflicts, MFA setup 500s,
LDAP bind decryption failures, missing SLA mails, etc.).
📧 Support
Open an issue on GitHub. For security vulnerabilities in TrueVuln itself, see SECURITY.md.