Security Tooling

Trivy Security Center

Jun 29, 2026~12 min readDjango . Trivy . CycloneDX
DjangoUvicorn / ASGISQLiteTrivyCycloneDX SBOMRPM / systemdRBAC

Trivy Security Center (TrivySC) is a vulnerability management platform I built to centralize how container and SBOM scans are collected, reviewed, and signed off. Trivy is excellent at finding vulnerabilities, but on its own it leaves you with a pile of JSON files and nowhere shared to triage them. TrivySC turns that raw output into a multi-user dashboard with role-based access, audit logging, risk-acceptance waivers, and one-command deployment.

Why I built it

Most teams run Trivy in CI and pipe the results somewhere — a log, an artifact, a chat message — and then lose track of them. There was no single place to ask simple questions: which systems are affected by a given CVE, what risk have we already accepted, and who signed off. I wanted a self-hosted, auditable system of record for vulnerability data that a small team could actually operate.

The design goals were straightforward: ingest what Trivy already produces, enforce who can do what, keep a tamper-evident trail of every action, and make deployment a single command so it can live on a hardened RHEL box without a complicated runbook.

INGESTION PIPELINE 01 Scan trivy image / sbom 02 Submit POST /api/submit 03 Detect + parse CycloneDX or JSON 04 Store SQLite schema 05 Analyze dashboard . reports CROSS-CUTTING CONTROLS RBAC 4 roles, least privilege sessions killed on change Audit Log 24 event types actor + source IP Waivers central registry + expiry CSV import / export Backups tar.gz: db + env + certs guarded restore Django + Uvicorn (ASGI) . SQLite . packaged as an RPM . systemd service over HTTPS

What it does

  • Dual-format ingestion — accepts both CycloneDX and native Trivy JSON; the format is auto-detected and normalised into one schema.
  • Live scan execution — start a Trivy scan from the browser and watch output stream in real time over server-sent events.
  • RBAC — four roles from administrator down to user, with session invalidation on any role or password change.
  • Audit logging — 24 event types recorded with actor and source IP.
  • Waiver management — a central risk-acceptance registry with expiry dates and CSV import / export.
  • Exports and reports — CSV and HTML reports per scan, plus a full REST API.
  • Operable — packaged as an RPM, runs as a systemd service over HTTPS, with built-in backup and restore.

Architecture and stack

TrivySC is a Django application served by Uvicorn as an ASGI app, which is what makes the live scan streaming clean — the server-sent events endpoint pushes Trivy output to the browser as it is produced. Data lives in SQLite, which keeps the deployment dependency-free and trivial to back up as a single file.

The whole thing ships as a versioned RPM built per Python version (3.10 through 3.13). The package post-install scriptlet creates the service account, installs dependencies, generates a self-signed TLS certificate, initialises the schema, creates the first admin with a one-hour temporary password, and enables the systemd unit. The service is reachable over HTTPS the moment the install finishes.

Installation

Install on RHEL, Rocky, or AlmaLinux 8, 9, or 10. Pick the RPM that matches your Python version:

sudo rpm -i trivysc-<version>-0.py312.x86_64.rpm

The %post scriptlet runs automatically and:

  • Creates the trivysc system account.
  • Installs Python dependencies into /opt/trivysc/lib/.
  • Generates a self-signed TLS certificate.
  • Initialises the database schema.
  • Creates the initial admin account with a one-hour temporary password.
  • Enables and starts the trivysc systemd service.

From there it is an ordinary systemd unit:

sudo systemctl status  trivysc
sudo systemctl restart trivysc
sudo journalctl -u trivysc -f

First login

The temporary admin password is printed to the terminal during install and is never stored anywhere retrievable afterward. Log in at /login, then set a permanent password when prompted. The temporary password expires after one hour.

Heads up

The install output scrolls fast because the service starts immediately. Scroll up and save the temporary password before navigating away -- if it is lost, recovery means direct database access or a reinstall. That is by design: nothing keeps a recoverable copy of it.

Configuration

All configuration is read from /opt/trivysc/.env. Edit the file and restart to apply. The most important value is SECRET_KEY — generate a long random one and keep it private:

sudo nano /opt/trivysc/.env
sudo systemctl restart trivysc

# Generate a strong SECRET_KEY
python -c "import secrets; print(secrets.token_urlsafe(50))"

Submitting scans

Scans are submitted to /api/submit, which accepts CycloneDX or native Trivy JSON and auto-detects the format. From CI or a workstation, post the file with an API key:

curl -sk -X POST https://<host>:<port>/api/submit -H "X-API-Key: <your-api-key>" -F "system_name=registry.example.com/org/app:latest" -F "[email protected]"

In the lab I push results straight from Ansible after a Trivy run, using the same endpoint:

- name: Submit CycloneDX results to TrivySC
  ansible.builtin.uri:
    url: "{{ trivysc_url }}/api/submit"
    method: POST
    headers:
      X-API-Key: "{{ trivysc_api_key }}"
    body_format: form-multipart
    body:
      system_name: "{{ inventory_hostname }}"
      file:
        filename: results.json
        content: "{{ lookup('file', trivy_output_path) }}"
        mime_type: application/json

Running a live scan

For ad-hoc work, the /start-scan page runs Trivy on the server and streams live output to the browser. Image and SBOM targets are supported: pull and scan a registry image, upload a local image tar, or scan an existing CycloneDX SBOM. CycloneDX results are ingested automatically on completion; JSON results can be downloaded and re-submitted. Trivy just needs to be installed and on the server PATH.

Waivers and risk acceptance

Real environments carry vulnerabilities that are accepted, deferred, or confirmed false positives. TrivySC manages these through a dedicated central waiver registry at /waivers (auditor and above). Entries are matched by CVE and package name across every scan, support optional expiry dates, and can be added, edited, deleted, imported, or exported as CSV. Matching waivers appear in System Detail right next to the findings they cover:

Package_Name,CVE,Waiver_ID,Justification,Expires_At
openssl,CVE-2023-1234,WVR-0001,Not exploitable in our configuration,
curl,CVE-2024-5678,WVR-0002,Fix scheduled for next release,2026-12-31
libxml2,CVE-2025-1111,WVR-0003,Compensating controls in place,2026-09-30
Expiry

Waivers can carry an optional expiry date. Expired waivers stop being applied to new scans, and the registry flags entries that are already expired or due within 30 days so nothing silently lingers.

Roles and RBAC

  • administrator — every action, including role changes, password resets, and the administration panel.
  • maintainer — create users, reset passwords, enable or disable accounts, view the audit log.
  • auditor — view everything, submit and start scans, manage waivers; no user management.
  • user — submit scans, manage their own profile and API key.

Any role or password change immediately ends the affected sessions and forces a re-login; a graceful shutdown ends all sessions.

Exports and API

Every view that lists data can export it — per-scan CSV and HTML reports, full-database CSV, SBOM CSV, and the original CycloneDX JSON, all available from the UI or the REST API. A few examples:

# Full database export
curl -sk https://<host>:<port>/api/export/database.csv -o all_scans.csv

# Single scan as an HTML report
curl -sk https://<host>:<port>/api/scans/5/export.html -o scan5_report.html

# Original CycloneDX JSON (as submitted)
curl -sk https://<host>:<port>/api/scans/5/export.cdx.json -o scan5.cdx.json

Security design notes

  • HTTPS by default — a self-signed certificate is generated on install; swap in your own via .env, and a gen_cert CLI command regenerates it.
  • Session hygiene — idle timeout, session invalidation on privilege changes, and full invalidation on shutdown.
  • Safe restore — backup archives are validated on restore; members with absolute paths or directory-traversal sequences are rejected.
  • Secret handling — the temporary admin password is shown once and never persisted in a recoverable form; the Django secret key lives only in .env.
  • Least privilege — runs under a dedicated trivysc service account, not root.

What I learned

The most satisfying part of this build was making the boring parts boring: a single RPM that stands the whole service up, a database that is one file to back up, and an audit log that answers who did what. Trivy finds the problems; TrivySC is the place a team agrees on what to do about them.