Previous docs dated Feb 1-2 — pre multi-provider auth, pre Nessus, pre compliance/URS. Full rewrite covering dev-branch state at migration 022: - ARCHITECTURE.md: current stack table, scheduler job list, auth strategy diagram, OWASP mitigation matrix, deployment + perf notes. - DATABASE_SCHEMA.md: all 20+ tables with columns, indexes, enums, FK CASCADE semantics, full migration 001-022 history. - PROJECT_OVERVIEW.md: feature list (5 threat-intel sources, multi- scanner, multi-auth, compliance/URS), workflow examples, debugging commands, current limitations. README.DEV.md still ahead of these — keeps the long-form feature deep-dives.
14 KiB
VulnCheck — Project Overview
Vulnerability Management Dashboard for a Swiss-school IT-Security setup. 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
VulnCheck 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=vulncheck-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 VulnCheck 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 + tester verification.
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
Internal IT-Security team. See README.md for project owner contact.