Security Tooling
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.
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.
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.
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:
trivysc system account./opt/trivysc/lib/.admin account with a one-hour temporary password.trivysc systemd service.From there it is an ordinary systemd unit:
sudo systemctl status trivysc
sudo systemctl restart trivysc
sudo journalctl -u trivysc -f 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.
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.
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))" 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 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.
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 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.
Any role or password change immediately ends the affected sessions and forces a re-login; a graceful shutdown ends all sessions.
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 .env, and a gen_cert CLI command regenerates it..env.trivysc service account, not root.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.