# 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

```bash
# 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 `DISABLED` status suppresses mails for that policy's assets
- Master kill-switch via `sla_breach_enabled` setting
- 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:

```bash
# 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_groups`
- `assets`, `asset_groups`
- `vulnerabilities`
- `scans`, `scan_schedules`
- `policies`
- `notification_logs`, `audit_logs`
- `compliance_results`, `compliance_checks`, `compliance_impacts`, `asset_risk_snapshots`
- `settings` (KV store), `ai_analyses`, `ai_reports`

---

## 🎨 Workflow Examples

### Daily admin morning routine

1. Dashboard widget shows URS trend + Critical / Newly Published CVEs
2. SLA-breach mail already arrived (00:00–08:00 stack)
3. Click "Correct CVSS" once a week to refresh Vulnrichment-corrected scores
4. Compliance widget shows worst 5 assets — drill in via per-asset modal
5. Investigate "Recent Critical" rows — KEV / EUVD badges flag externally-confirmed exploit risk

### Adding a Nessus scan

1. Settings → Integrations → Tenable Nessus → Configure
2. Paste Base URL + API access/secret keys → Test connection
3. Pick default scan IDs → Save
4. Sync now (or wait for next scheduled run)
5. Vulns list now shows Wazuh + Nessus + cross-confirmed rows

### Updating a Vulnrichment-corrected score for a single CVE

```bash
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

```bash
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:
```sql
UPDATE settings SET value='true' WHERE key='sla_breach_enabled';
```

---

## 🚧 Known Limitations

- **`sla_breach_enabled` toggle 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

```bash
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

1. Edit `app/models/vulnerability.py` — add the `Column(...)`
2. Edit `app/schemas/vulnerability.py` (or wherever the Pydantic schemas live)
3. Generate migration:
   ```bash
   docker compose exec backend alembic revision --autogenerate -m "add foo column"
   ```
4. Review the generated file (autogenerate misses index intent sometimes)
5. `alembic upgrade head`
6. Update `_build_vuln_response()` in `app/routers/vulnerabilities.py` if it should be in the API response

### Add a new threat-intel source

1. New service module in `app/services/` (e.g. `mysource_service.py`)
2. Cache feed JSON in `settings` table for 24 h
3. Wire into `enrichment_service.refresh_threat_intel_enrichment()`
4. Add columns to `vulnerabilities` table via alembic
5. 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_sources` JSON columns avoid join-heavy queries

---

## 📚 Further Documentation

- `ARCHITECTURE.md` — system topology, security model, scheduler jobs
- `DATABASE_SCHEMA.md` — every column, every enum, every migration
- `CODEBASE_KNOWLEDGE_BASE.md` — module-by-module walk-through for new contributors
- `README.DEV.md` — dev-branch feature deep-dives (auth, Nessus, compliance, URS)
- `docs/TROUBLESHOOTING.md` — common production issues and fixes

---

## 🐛 Debugging

```bash
# 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.
