A vulnerability management system for tracking and triaging security vulnerabilities across your Ubuntu Linux infrastructure using agent-based scanning, OVAL definitions, and Ubuntu VEX data.
VulTrack was developed entirely with AI. There might be dragons. 🐉
The VulTrack Agent can be found in this repository.
- Dashboard: Overview of your vulnerability landscape with key metrics and charts
- Server Management: View all monitored servers and their vulnerability status
- Findings Browser: Search and filter vulnerability findings across all servers
- Triage Queue: Efficiently assess and categorize high-severity vulnerabilities
- Statistics: Trend analysis and detailed reports
- Agent Management: Manage agents and enrollment keys
- OVAL Integration: Automatic vulnerability detection using OVAL definitions (Ubuntu and others)
- NVD Enrichment: CVE details enriched with CVSS scores, CWE IDs, and descriptions
- ExploitDB Integration: Identify vulnerabilities with known exploits
- Ubuntu VEX Integration: Automatic enrichment of findings with Canonical's VEX (Vulnerability Exploitability eXchange) data — surfaces
Not Affected,Will Not Fix, andUnder Investigationassessments from Ubuntu Security directly in your findings and triage queue - Scheduled Reports: Automated PDF reports delivered by email (weekly / monthly)
- Server Groups: Organize servers into groups for targeted reporting and filtering
- Jira Integration: Automatically create Jira tickets for relevant findings
- AI Assessment: Optional advisory LLM assessment of CVEs — explains the attack vector and prerequisites and recommends a triage status, using a configurable model and an admin-defined infrastructure-context prompt. Advisory only; it never changes the human assessment. Requires an Anthropic API key
- OIDC Authentication: Optional single sign-on via any OpenID Connect identity provider
┌────────────────┐
│ PostgreSQL │
│ Port: 5432 │
└───────▲────────┘
│
┌─────────────────┐ │
┌───────▶│ Frontend │ │
│ │ (React) │ │
│ │ Port: 8080 │ │
┌───────────┐ ┌─────────┴─────┐ └─────────────────┘ │
│ │ │ │ │
│ Browser │─────▶│ Reverse │ ┌─────────────────┐ │
│ │ │ Proxy │─▶│ Backend │──────┘
│ │ │ │ │ (Go/Fiber) │
└───────────┘ │ Port: 443 │ │ Port: 8080 │
│ │ └─────────────────┘
┌───────────┐ │ /* → FE │ ▲
│ │ │ /api/* → BE │ │
│ Agents │─────▶│ /metrics → BE │───────────┘
│ │ │ │
└───────────┘ └───────────────┘
Components:
- Frontend: React SPA served by Nginx (static files only)
- Backend: Go/Fiber REST API
- PostgreSQL: Database for all application data
- Reverse Proxy: Routes requests to Frontend or Backend (not included, bring your own)
- Agents: Lightweight agents on monitored servers that report package information
If you don't enable OIDC (see below) VulTrack can be used without any authentication.
- Docker and Docker Compose
- A reverse proxy (e.g., Caddy, Traefik, Nginx)
- PostgreSQL database
# Build backend image
docker build -t vultrack-backend ./backend
# Build frontend image
docker build -t vultrack-frontend ./frontendBelow is an example using Caddy as a reverse proxy. Adjust according to your infrastructure.
docker-compose.yml:
services:
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
depends_on:
- frontend
- backend
restart: unless-stopped
frontend:
image: vultrack-frontend:latest
restart: unless-stopped
backend:
image: vultrack-backend:latest
environment:
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_USER=vultrack
- POSTGRES_PASSWORD=changeme
- POSTGRES_DB=vultrack
- LOG_LEVEL=info
depends_on:
- postgres
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=vultrack
- POSTGRES_PASSWORD=changeme
- POSTGRES_DB=vultrack
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
caddy_data:
postgres_data:Caddyfile:
vultrack.example.com {
# Frontend (static files)
handle {
reverse_proxy frontend:8080
}
# Backend API
handle /api/* {
reverse_proxy backend:8080
}
# Backend metrics (Prometheus scrape endpoint)
handle /metrics {
reverse_proxy backend:8080
}
}
- Start the services:
docker compose up -d - Access the web interface at
https://vultrack.example.com - Create an enrollment key in Admin → Enrollment Keys
- Deploy agents on your servers using the enrollment key
All configuration is done via environment variables.
| Variable | Description | Default |
|---|---|---|
POSTGRES_HOST |
PostgreSQL hostname | localhost |
POSTGRES_PORT |
PostgreSQL port | 5432 |
POSTGRES_USER |
PostgreSQL username | vultrack |
POSTGRES_PASSWORD |
PostgreSQL password | vultrack |
POSTGRES_DB |
PostgreSQL database name | vultrack |
BACKEND_PORT |
Port the backend listens on | 8080 |
LOG_LEVEL |
Log level (debug, info, warn, error) | info |
TRIAGE_CVSS_THRESHOLD |
Minimum CVSS score for triage queue | 7.0 |
CORS_ORIGINS |
Comma-separated origins for CORS (required when using OIDC) | (empty) |
VulTrack automatically downloads and syncs Canonical's VEX data (vex-all.tar.xz) to enrich findings with Ubuntu's own exploitability assessments. The sync runs on a configurable schedule.
| Variable | Description | Default |
|---|---|---|
VEX_SYNC_INTERVAL_HOURS |
How often the VEX data is refreshed (hours) | 24 |
| Variable | Description | Default |
|---|---|---|
SCAN_WORKERS |
Number of concurrent scan workers | 5 |
SCAN_TIMEOUT |
Timeout per scan in seconds | 600 |
SCAN_MAX_RETRIES |
Maximum retry attempts for failed scans | 3 |
SCAN_QUEUE_SIZE |
Maximum scan queue depth | 500 |
When SMTP_ENABLED=true, VulTrack sends email reports via SMTP.
| Variable | Description | Default |
|---|---|---|
SMTP_ENABLED |
Enable email sending | false |
SMTP_HOST |
SMTP server hostname | (empty) |
SMTP_PORT |
SMTP server port | 587 |
SMTP_USER |
SMTP username (leave empty for unauthenticated relay) | (empty) |
SMTP_PASSWORD |
SMTP password | (empty) |
SMTP_FROM |
Sender address (e.g. VulTrack <vultrack@example.com>) |
(empty) |
SMTP_TLS |
TLS mode: starttls (port 587), tls (port 465), none (port 25) |
starttls |
SMTP_HELO_HOST |
EHLO hostname sent to the relay. Many relays reject localhost; set to your server's FQDN if needed |
(OS hostname) |
When JIRA_ENABLED=true, a Jira ticket is automatically created whenever a finding is assessed as Relevant.
| Variable | Description | Default |
|---|---|---|
JIRA_ENABLED |
Enable Jira integration | false |
JIRA_BASE_URL |
Jira instance URL (e.g. https://your-org.atlassian.net) |
(empty) |
JIRA_USER_EMAIL |
Jira user email for API authentication | (empty) |
JIRA_API_TOKEN |
Jira API token | (empty) |
JIRA_PROJECT_KEY |
Jira project key (e.g. SEC) |
(empty) |
JIRA_ISSUE_TYPE |
Issue type for created tickets | Task |
When ANTHROPIC_API_KEY is set, VulTrack can produce an advisory LLM assessment for a CVE: a plain-language explanation of the attack vector and prerequisites plus a recommended triage status and confidence. The result is advisory only — it never changes the human assessment.
The API key is environment-only and is never exposed through the API or UI. If the key is identity-linked (bound to a user identity rather than to a single workspace), also set ANTHROPIC_WORKSPACE_ID — such keys are not tied to one workspace, so the API rejects requests that do not name one (400 … anthropic-workspace-id is required). A workspace-scoped key needs no such setting. The rest of the configuration lives in Admin → AI Assessment: a master switch (Enable AI assessment), optional Auto-assess triage findings (assess every CVE entering the triage queue once), the model, and an infrastructure-context system prompt. Both the API key and the master switch must be enabled for assessments to run. CVEs can also be assessed on demand from the finding/triage views. A re-assessment of the same CVE is rate-limited to once every 30 minutes.
| Variable | Description | Default |
|---|---|---|
ANTHROPIC_API_KEY |
Anthropic API key. Enables the feature when set | (empty) |
ANTHROPIC_WORKSPACE_ID |
Workspace to act in (wrkspc_…). Required for identity-linked API keys |
(empty) |
AI_ASSESSMENT_WORKERS |
Number of concurrent assessment workers | 2 |
AI_ASSESSMENT_MAX_RETRIES |
Maximum retry attempts on transient errors | 2 |
AI_ASSESSMENT_TIMEOUT |
Timeout per model request in seconds | 60 |
| Variable | Description | Default |
|---|---|---|
VITE_API_URL |
Backend API base URL used during the frontend build | http://localhost:8080/api/v1 |
When OIDC_ENABLED=true, the web UI requires users to sign in via an OpenID Connect identity provider (e.g. Keycloak, Auth0, Okta). When disabled, no login is required.
| Variable | Description | Default |
|---|---|---|
OIDC_ENABLED |
Enable OIDC login for the web UI | false |
OIDC_ISSUER |
IdP issuer URL (e.g. https://idp.example.com/realms/vultrack) |
(empty) |
OIDC_CLIENT_ID |
OAuth2 client ID | (empty) |
OIDC_CLIENT_SECRET |
OAuth2 client secret | (empty) |
OIDC_REDIRECT_URL |
Backend callback URL (e.g. https://vultrack.example.com/api/v1/auth/callback) |
(empty) |
OIDC_FRONTEND_URL |
Frontend URL for redirect after login (e.g. https://vultrack.example.com) |
/ |
OIDC_SCOPES |
OAuth2 scopes (space or comma separated) | openid profile email |
Setup:
- Create an OAuth2/OIDC client in your IdP with redirect URI =
OIDC_REDIRECT_URL. - Set
CORS_ORIGINSto your frontend origin(s) so session cookies work (e.g.http://localhost:3000orhttps://vultrack.example.com). - The first user to sign in when no admin exists becomes an admin. Further admins can be assigned in Admin → Users.
The agent API (/api/v1/agent/*) is unchanged and uses enrollment keys and agent tokens, not OIDC.
cd backend
go mod download
go run ./cmd/vultrackcd frontend
npm install
npm run devFor development, you can start just the database:
docker compose up -d postgresGET /api/v1/auth/login- Redirect to OIDC identity providerGET /api/v1/auth/callback- OIDC callback (handles token exchange and session creation)POST /api/v1/auth/logout- Invalidate current sessionGET /api/v1/auth/me- Current user info (returnsauthEnabled: falsewhen OIDC is disabled)
GET /api/v1/dashboard- Dashboard statistics
GET /api/v1/servers- List all serversGET /api/v1/servers/:id- Get server detailsGET /api/v1/servers/:id/findings- Get server findingsGET /api/v1/servers/:id/packages- Get server packagesGET /api/v1/servers/:id/groups- Get groups for a serverPUT /api/v1/servers/:id/groups- Set groups for a serverPOST /api/v1/servers/:id/scan- Trigger a vulnerability scanDELETE /api/v1/admin/servers/:id- Delete server
GET /api/v1/findings- List findings with filters (supportsvexStatusquery param:not_affected,will_not_fix,under_investigation)GET /api/v1/findings/triage- Get triage queue (supportshideVexNotAffected=falseto include not-affected findings; default istrue)GET /api/v1/findings/:id- Get finding details
GET /api/v1/cves/:id- Get CVE detailsGET /api/v1/cves/:id/servers- Get servers affected by CVE
GET /api/v1/assessments- List all assessmentsPOST /api/v1/assessments- Create assessmentPUT /api/v1/assessments/:cveId- Update assessmentDELETE /api/v1/assessments/:cveId- Delete assessment
GET /api/v1/ai-assessments- List AI assessments (filters:status,search; paginated)GET /api/v1/ai-assessments/:cveId- Get the AI assessment for a CVE (404 if none yet)POST /api/v1/findings/:cveId/ai-assess- Request an AI assessment (?force=trueto re-assess; only for CVEs with active findings)
GET /api/v1/reason-templates- List reason templatesPOST /api/v1/reason-templates- Create reason templatePUT /api/v1/reason-templates/:id- Update reason templateDELETE /api/v1/reason-templates/:id- Delete reason template
GET /api/v1/stats/severity- Severity breakdownGET /api/v1/stats/trend- Finding trends over timeGET /api/v1/stats/top-servers- Most affected serversGET /api/v1/stats/top-cves- Most widespread CVEsGET /api/v1/stats/assessments-by-severity- Assessments grouped by severity
POST /api/v1/reports/generate- Generate a PDF report on demandGET /api/v1/report-schedules- List report schedulesPOST /api/v1/report-schedules- Create report scheduleGET /api/v1/report-schedules/:id- Get report schedulePUT /api/v1/report-schedules/:id- Update report scheduleDELETE /api/v1/report-schedules/:id- Delete report schedulePOST /api/v1/report-schedules/:id/toggle- Enable or disable a schedulePOST /api/v1/report-schedules/:id/run-now- Run a schedule immediately
GET /api/v1/scans- List scan jobsGET /api/v1/scans/stats- Scan queue statisticsPOST /api/v1/scans/:id/cancel- Cancel a queued scanPOST /api/v1/scans/:id/retry- Retry a failed scan
POST /api/v1/agent/enroll- Register a new agent (requiresX-Enrollment-Keyheader)POST /api/v1/agent/report- Submit agent report (requiresX-Agent-Tokenheader)
GET /api/v1/admin/settings- Get application settingsPUT /api/v1/admin/settings- Update application settings
GET /api/v1/admin/users- List users (requires OIDC)
GET /api/v1/admin/server-groups- List server groupsPOST /api/v1/admin/server-groups- Create server groupGET /api/v1/admin/server-groups/:id- Get server groupPUT /api/v1/admin/server-groups/:id- Update server groupDELETE /api/v1/admin/server-groups/:id- Delete server groupGET /api/v1/admin/server-groups/:id/members- List group membersPOST /api/v1/admin/server-groups/:id/members- Add member to groupDELETE /api/v1/admin/server-groups/:id/members/:serverId- Remove member from group
GET /api/v1/admin/enrollment-keys- List enrollment keysPOST /api/v1/admin/enrollment-keys- Create enrollment keyPUT /api/v1/admin/enrollment-keys/:id- Update enrollment keyDELETE /api/v1/admin/enrollment-keys/:id- Delete enrollment key
GET /api/v1/admin/api-tokens- List API tokens (for the MCP interface)POST /api/v1/admin/api-tokens- Create an API token (the full token is returned once)DELETE /api/v1/admin/api-tokens/:id- Delete (revoke) an API token
GET /api/v1/admin/agents- List registered agentsPUT /api/v1/admin/agents/:id/approve- Approve pending agentPUT /api/v1/admin/agents/:id/revoke- Revoke agent accessDELETE /api/v1/admin/agents/:id- Delete agent
GET /api/v1/admin/oval/distributions- List available distributionsGET /api/v1/admin/oval/sources- List enabled OVAL sourcesPOST /api/v1/admin/oval/sources- Enable OVAL sourcePUT /api/v1/admin/oval/sources/:id/disable- Disable an OVAL sourceDELETE /api/v1/admin/oval/sources/:id- Remove an OVAL sourcePOST /api/v1/admin/oval/sources/:id/sync- Trigger sync for a specific sourcePOST /api/v1/admin/oval/sync-all- Trigger sync for all enabled sources
POST /api/v1/admin/nvd/sync- Trigger NVD CVE syncGET /api/v1/admin/nvd/status- NVD sync statusPOST /api/v1/admin/exploitdb/sync- Trigger ExploitDB syncGET /api/v1/admin/exploitdb/status- ExploitDB sync statusGET /api/v1/admin/sync/status- Combined sync statusPOST /api/v1/admin/vex/sync- Trigger Ubuntu VEX syncGET /api/v1/admin/vex/status- VEX sync status and statement count
POST /api/v1/admin/system/reset- Reset all application data
GET /metrics- Prometheus metrics endpoint (no authentication required)
VulTrack exposes a Model Context Protocol server so that AI agents can query the vulnerability landscape and perform triage actions. It is built into the backend and served at:
POST /api/mcp
The transport is Streamable HTTP in stateless / JSON-response mode (plain request/response, no long-lived SSE stream).
The MCP endpoint always requires an API token, regardless of whether OIDC is enabled — it is a machine interface and is never exposed unauthenticated. Tokens are created by admins under Admin → API Tokens in the UI:
- Each token has a description (so it is clear what it is used for).
- A token can optionally be marked read-only — it is then routed to a server that exposes only the query tools; the mutating tools are not visible to it at all.
- A token can optionally be given an expiry date.
The full token (prefixed vt_) is shown only once at creation time; only its hash is stored. Present it as a bearer token:
Authorization: Bearer vt_<token>
Deleting a token revokes it immediately. All write actions are logged with the acting token's prefix and description for auditing.
Read tools (available to every token):
list_servers, get_server, list_findings, get_finding, get_cve, list_cve_servers, get_ai_assessment, list_triage_queue, get_triage_config, list_assessments, get_dashboard_stats, get_severity_stats, get_trend_stats, get_top_servers, get_top_cves, get_assessments_by_severity, list_server_groups, get_server_group, get_server_group_members
Write tools (only available to non-read-only tokens):
upsert_assessment, delete_assessment, trigger_server_scan, create_server_group, update_server_group, delete_server_group, set_server_group_members
All MCP tools share the same logic as the REST API / UI, including admin-configured settings. In particular, list_triage_queue honours the configured triage filter (vendor severities or CVSS threshold) and returns the same findings as the UI, and upsert_assessment creates a Jira ticket when the status is set to relevant and Jira is enabled — just like the UI.
The list tools (list_findings, list_triage_queue, list_assessments) are paginated: each call returns at most limit items (and the full total) along with a hasMore flag. To retrieve every entry, the agent repeats the call with offset increased by limit while hasMore is true.
To make results self-describing, the responses also report which filter produced them: list_triage_queue returns a filter object with the admin-configured settings (mode, vendor severities or CVSS threshold, include-unrated, hide-VEX-not-affected), and list_findings / list_assessments echo the effective filter (including defaults like includeResolved and sort order) in appliedFilter. The get_triage_config tool exposes the same triage settings directly.
For an MCP client that supports Streamable HTTP servers (e.g. Claude Code / Claude Desktop):
{
"mcpServers": {
"vultrack": {
"type": "http",
"url": "https://vultrack.example.com/api/mcp",
"headers": {
"Authorization": "Bearer vt_your_token_here"
}
}
}
}Agents are lightweight scripts or binaries that run on monitored servers. They:
- Enroll with VulTrack using an enrollment key
- Periodically report system information and installed packages
- VulTrack matches packages against OVAL definitions to detect vulnerabilities
See docs/AGENT_API.md for the complete agent API specification.
For a deeper OIDC setup guide including IdP-specific instructions, see docs/OIDC_SETUP.md.
Next to the OVAL files, Canonical publishes com.ubuntu.<codename>.pkg.json.xz —
the dataset the pro cves command evaluates on a machine. VulTrack imports it
server-side as a third source per Ubuntu release, alongside the USN and CVE OVAL
feeds. Agents need no Ubuntu Pro client and no subscription; the feed is public.
It closes two gaps in the OVAL feeds:
- Binary packages OVAL does not enumerate. OVAL lists the binaries of a
source package in
constant_variableelements, and that list is incomplete. On a 24.04 host with 10,269 packages installed, the feed found 3,329 findings the OVAL feeds miss (bluez-tests,libc6-prof, and similar). - CVSS scores, Canonical's triage notes and mitigations. The feed carries CVSS for 27,046 of the CVEs in the 24.04 data, plus free-text notes for 5,330 and mitigation advice for 105. VulTrack otherwise waits for the rate-limited NVD sync for scores and has no access to the notes at all. Scores and descriptions are backfilled into the CVE catalog for fields NVD has not delivered; wherever NVD has a value it keeps it.
It also records the archive pocket a fix comes from. An esm-infra/esm-apps
pocket means the fix only reaches a machine with an Ubuntu Pro subscription,
which is shown on the finding.
The feed also corrects OVAL's kernel findings. Ubuntu's OVAL identifies the
running kernel by name pattern and cannot express an architecture, while the
riscv64 kernel flavour is also called generic — so on an amd64 host the
criteria for linux-riscv match too, and every CVE that affects only the riscv
kernel is reported. That was 3,670 of 5,779 kernel findings on 24.04 with
6.8.0-124-generic. VulTrack now resolves which kernel source package the
running kernel was built from and drops kernel findings the feed attributes to a
different one. See
docs/KERNEL_FLAVOUR_FILTER_PLAN.md.
Two deliberate limits:
- The kernel stays with OVAL. 53 kernel source packages carry 95% of the
feed's CVE statuses, and the feed sees installed kernel packages, so it
would report every kernel still present in
/boot. OVAL evaluates the kernel against the running kernel instead, which is more precise and far less noisy, so kernel source packages contribute no findings from this source — only the cross-check above. - A weaker source never overwrites a stronger one. A USN advisory is the
most specific statement Ubuntu makes; the per-CVE OVAL adds the vendor's
triage state (
will_not_fix,deferred); the feed only distinguishes vulnerable from fixed. The pocket is the exception: only the feed knows it, so it is filled in regardless of which source owns the rest of the finding.
The feed is optional. A distribution without a feed URL never gets such a source, a release whose feed is missing fails only its own sync, and the source can be disabled individually under Admin → OVAL Sources. With it off, detection behaves exactly as it did with the OVAL feeds alone.
Database migrations run automatically on backend startup.
Already-enabled Ubuntu releases get a package feed source on the next start;
the syncer imports it shortly afterwards. Expect the finding count to rise,
mostly with affected entries for binary packages that were previously
invisible.
One migration needs a note: OVAL tests now store the OVAL IDs of the object and state they reference, instead of the link being guessed from the test's own ID. Stored OVAL data cannot be converted, so the migration clears it and marks every enabled source for re-sync. The syncer re-downloads the feeds shortly after startup; until it finishes, scans find nothing. Re-scan your servers afterwards so their findings are rebuilt from the corrected data.
- Support for more Linux distributions (Debian, RHEL, ...)
- REST API for report export (CSV)
- Multi-tenancy / team workspaces
MIT License