Vulnerability data (Trivy DB)
Starting with v0.10.0, TRUSCA correlates SBOMs against CVEs using a single embedded engine: Trivy from Aqua Security. The worker image ships with the trivy binary; the Trivy DB (a compiled bundle of NVD + OSV + GHSA + EPSS + KEV) is downloaded on first boot and refreshed weekly. This page is how you run and audit that lifecycle.
super_admin operating a deployment. Familiarity with docker-compose and basic shell. If you are reading from a install, see the v0.10.0 release notes for the DT-removal migration.
v0.10.0 removed Dependency-Track (DT) and the /admin/dt connector page. The Trivy DB does the same job — correlate SBOMs against CVEs — with a ~500 MB footprint instead of a 4 GB JVM and an H2 database. See ADR-0001 for the decision.
How the lifecycle works
Worker boot ──► trivy --download-db-only ──► /var/lib/trivy/db
│
↓
Celery beat (weekly) ─► trivy --download-db-only (refresh)
│
↓
trivy sbom <sbom.json> ──► vulnerability_findings
Three moving parts:
- Boot-time download. The worker registers a Celery
worker_readysignal handler that callstrivy --download-db-onlyon a background thread once the worker starts consuming the queue. The first download is ~500 MB and takes 1–3 minutes on a typical network. The download runs in the background so Celery starts accepting tasks immediately — scans that race the bootstrap simply block on Trivy's own file lock (≈seconds, not minutes) and then read the fresh manifest. On a worker restart with a populated cache volume, the download is a no-op (Trivy stats the on-disk manifest and exits 0). - Weekly refresh. A Celery Beat task (
trustedoss.trivy_db_refresh, schedule Sunday 03:00 UTC) runs the sametrivy --download-db-onlycommand. Trivy's manifest contract ensures partial downloads are atomically swapped in — a failed refresh leaves the prior DB intact. The vulnerability-rematch beat (trustedoss.vulnerability_rematch_enqueue, every 6 hours) picks up the new advisories on its next tick and surfaces new criticals as Slack / Teams notifications. - Per-scan use. Each source-scan pipeline calls
trivy sbom <cyclonedx.json>to match components against the local DB. No network round-trip per scan.
A refresh failure (timeout, network unreachable, mirror auth error) fires a TRIVY-DB-REFRESH-FAILED / TRIVY-DB-REFRESH-TIMEOUT notification through the Slack and Teams channels configured under /notifications — operators see the staleness as a webhook event, not a silent miss.
The DB rebuild cadence upstream is ~6 hours; refreshing weekly trades freshness for fewer egress bytes. For a tighter cadence, lower TRIVY_DB_REFRESH_HOURS (advisory — surfaces in the admin panel and as next_refresh_at) and edit the Beat schedule in apps/backend/tasks/celery_app.py to a daily cron.
Environment variables
| Key | Default | Description |
|---|---|---|
TRIVY_DB_REPOSITORY | ghcr.io/aquasecurity/trivy-db | OCI repository the DB is pulled from. Override for an air-gapped mirror. |
TRIVY_DB_REFRESH_HOURS | 168 (weekly) | Advisory cadence surfaced on the admin/health Trivy DB panel as next_refresh_at = last_update + this. The actual beat schedule is crontab(minute=0, hour=3, day_of_week='sun') — change the schedule in apps/backend/tasks/celery_app.py for a tighter cadence. |
TRIVY_CACHE_DIR | /var/lib/trivy | Directory the DB is unpacked into. Both compose files mount the shared trivy-cache named volume here — worker (rw, download + refresh) and backend (ro, admin health / disk panels). |
TRIVY_DB_BOOTSTRAP_ON_START | true | Whether the worker's worker_ready hook fires trivy --download-db-only on boot. Set to false on air-gapped clusters where the DB is mirrored to the cache volume by a separate process — the worker then never attempts a network pull. |
TRIVY_DB_BOOTSTRAP_TIMEOUT_SECONDS | 900 | Wall-clock cap on the boot-time download subprocess. Raise on slow corporate proxies. Bounded [30, 3600]. |
TRIVY_DB_REFRESH_TIMEOUT_SECONDS | 900 | Wall-clock cap on the weekly beat refresh subprocess. Bounded [30, 3600]. |
TRIVY_TIMEOUT_SECONDS | 300 | Per-scan timeout for trivy sbom. Raise for very large monoliths. |
All keys are read at runtime by apps/backend/core/config.py — restart the worker / beat containers to pick up changes; no rebuild needed.
Verify it worked
After install (or after editing TRIVY_* keys):
# 1. Confirm the DB is present in the worker
docker-compose -f docker-compose.yml exec worker \
ls -lh /var/lib/trivy/db/
# 2. Show the DB's metadata (last update timestamp, sources)
docker-compose -f docker-compose.yml exec worker \
trivy --quiet image --download-db-only --skip-update && \
trivy --version
# 3. Spot-check a small scan
docker-compose -f docker-compose.yml exec worker \
trivy sbom --format json /tmp/sample-cyclonedx.json | jq '.Results | length'
A healthy install reports a non-zero file count under db/, a recent (≤ 7 days old) Created timestamp, and at least one Results array on the spot-check.
A Trivy DB card under /admin/health surfaces the last-refresh timestamp, the vulnerability count, freshness (fresh / stale / very_stale / unknown), the configured cadence, the cache directory, and the mirror repository. The panel reads the on-disk metadata.json directly (no live network probe) so it stays useful on air-gapped clusters.

Air-gapped operation
If the worker host cannot reach ghcr.io, you have two supported paths. Pick the one that fits your operations model:
- Path A — internal OCI mirror. Maintain a copy of
trivy-dbin an internal OCI registry your network can reach. The worker still runstrivy --download-db-only(bootstrap + weekly refresh) — it just pulls from the mirror. Recommended for most enterprises because Trivy's manifest contract still does the atomic swap and integrity check on each pull. - Path B — fully offline cache volume. Disable the in-worker download path entirely (
TRIVY_DB_BOOTSTRAP_ON_START=false) and pre-populate the cache volume from a tarball sneaker-netted in from a connected host. Use this when even your internal registry is on a separate side of the air gap.
Path A — internal OCI mirror
1. Mirror the DB
Run this on a host that can reach ghcr.io:
# Install oras (one-time)
curl -fsSL https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_amd64.tar.gz \
| tar -xz oras
sudo mv oras /usr/local/bin/
# Pull the upstream DB
oras pull ghcr.io/aquasecurity/trivy-db:2
# Re-push to your internal registry
oras push registry.internal.acme.com/trivy/trivy-db:2 \
db.tar.gz:application/vnd.aquasec.trivy.db.layer.v1.tar+gzip
The Trivy DB is a multi-arch OCI artefact tagged :1 and :2. The portal currently uses :2 (Trivy's v2 schema); pin the same tag your portal version expects.
2. Point the worker at the mirror
In .env:
TRIVY_DB_REPOSITORY=registry.internal.acme.com/trivy/trivy-db
If the mirror needs authentication, log in once on the worker host:
docker-compose -f docker-compose.yml exec worker \
trivy registry login --username svc-trivy --password-stdin \
registry.internal.acme.com < /run/secrets/trivy_registry_pw
Trivy stores the credential under ~/.docker/config.json inside the container. For persistence across container restarts, mount that path from a host volume.
3. Restart and re-download
docker-compose -f docker-compose.yml restart worker beat
docker-compose -f docker-compose.yml exec worker \
trivy --download-db-only
The first download against your mirror should match the size and SHA of the upstream blob — Trivy verifies the manifest before swapping in.
4. Schedule the mirror refresh
Run the mirror-pull command on a cron schedule (typically daily) on the bastion that has internet egress, so the internal mirror keeps pace with upstream:
# /etc/cron.d/trivy-db-mirror
0 4 * * * trivyops oras pull ghcr.io/aquasecurity/trivy-db:2 \
&& oras push registry.internal.acme.com/trivy/trivy-db:2 db.tar.gz
The portal's weekly refresh then pulls from your mirror — no host on the portal network needs internet egress.
Trivy DB tags follow the schema version, not a calendar date. If Aqua publishes a :3 (schema bump), the portal worker pinned to :2 will keep working until the portal upgrades. Coordinate mirror updates with portal upgrades to avoid an empty Results set after an upgrade.
Path B — fully offline cache volume
If even your internal registry is air-gapped from the worker (e.g. the worker runs on a side of the network that cannot speak OCI to anything), disable the worker's network-pull path and pre-populate the cache volume from a connected host.
1. Disable the bootstrap + beat on the worker side
In .env:
# Turn the worker_ready boot-time download off.
TRIVY_DB_BOOTSTRAP_ON_START=false
Comment out the trivy-db-refresh-weekly entry in apps/backend/tasks/celery_app.py::_build_beat_schedule and rebuild the beat image, OR — simpler — leave the beat schedule alone. With no network egress the beat tick fires, the subprocess fails, and the failure notification reminds you to refresh the cache. Both shapes are valid; the first is quieter on Slack, the second gives you an active reminder.
2. Pre-populate the cache from a connected host
Run on a host that can reach ghcr.io (a build server, a developer laptop, a connected bastion):
# Spin up a throwaway container that has trivy + your TRIVY_CACHE_DIR layout.
docker run --rm \
-v trivy-cache-export:/var/lib/trivy \
-e TRIVY_CACHE_DIR=/var/lib/trivy \
ghcr.io/trustedoss/trusca-backend-worker:0.11.0 \
trivy --quiet image --download-db-only
# Pack the populated volume into a tarball.
docker run --rm \
-v trivy-cache-export:/cache:ro \
-v "$PWD":/out \
alpine \
tar -C /cache -czf /out/trivy-db.tar.gz .
3. Transfer + load on the air-gapped worker host
Copy trivy-db.tar.gz across the air gap (USB, internal artifact store, approved file-transfer channel) and load it onto the worker's cache volume:
# On the air-gapped host. Stop the worker so trivy is not mid-read.
docker-compose -f docker-compose.yml stop worker
# Load the tarball into the named cache volume mount.
docker run --rm \
-v trustedoss_trivy-cache:/cache \
-v "$PWD":/in:ro \
alpine \
sh -c "cd /cache && tar -xzf /in/trivy-db.tar.gz"
# Restart the worker. The bootstrap hook is disabled, so it just uses the
# cache as-is.
docker-compose -f docker-compose.yml start worker
4. Refresh cadence
The admin/health Trivy DB panel surfaces last_update and freshness. Schedule the export-transfer-load pipeline above on a cadence that matches your security policy (weekly is typical). Two operator habits to keep:
- Note the
last_updatefield on the panel before the swap so you have a known-good rollback. - Do the swap during a low-traffic window. The worker is briefly stopped; in-flight scans (held by the scan task's own workspace finalisation) survive but enqueued scans wait until the restart completes.
The named volume on the worker side is trustedoss_trivy-cache under default docker-compose naming (project prefix trustedoss + the trivy-cache: declaration in docker-compose.yml). If your project uses a different COMPOSE_PROJECT_NAME, adjust the -v <prefix>_trivy-cache:/cache argument accordingly. docker volume ls confirms the resolved name on your host.
Data sources
The Trivy DB consolidates five public feeds. See the data sources reference for the per-source coverage matrix, refresh cadence, and ecosystem mapping.
| Source | Covers | Refresh |
|---|---|---|
| NVD (NIST) | All CVE IDs, CVSS v3 | ~6 hours upstream |
| OSV (Google) | Per-ecosystem vulnerabilities (npm, PyPI, Maven, Go, …) | Continuous |
| GHSA (GitHub) | Advisory metadata, fix versions | Continuous |
| EPSS (FIRST) | 30-day exploit probability | Daily |
| KEV (CISA) | Known Exploited Vulnerabilities catalogue | As published |
Troubleshooting
docker-compose logs --tail=200 worker | grep trivy— boot-time download + per-scan invocation.docker-compose logs --tail=200 beat | grep trivy_db_refresh— weekly refresh schedule.- Worker container:
ls -lh /var/lib/trivy/db/andcat /var/lib/trivy/db/metadata.json.
DB download fails on first boot
Symptom: worker container restarts in a crash loop; logs show trivy: failed to download vulnerability DB.
Common causes:
- No outbound HTTPS to ghcr.io. Confirm:
Expectdocker-compose -f docker-compose.yml exec worker \curl -fsS https://ghcr.io/v2/ -o /dev/null -w "%{http_code}\n"
200. If your worker is air-gapped, switch to a mirror — see Air-gapped operation. - Corporate proxy not honoured. Trivy reads
HTTP_PROXY/HTTPS_PROXYfrom the worker environment. Set them in.envand restart. - Disk full at
/var/lib/trivy. The DB is ~500 MB; allow at least 2 GB of headroom.docker-compose -f docker-compose.yml exec worker df -h /var/lib/trivy.
DB corruption — unexpected EOF or invalid manifest
A partial download from a previous run can leave a truncated archive. Wipe and re-download:
docker-compose -f docker-compose.yml exec worker \
rm -rf /var/lib/trivy/db
docker-compose -f docker-compose.yml restart worker
Trivy verifies the manifest checksum on download; if the swap completes the new DB is good. If the error repeats, the upstream artefact is the suspect — try oras pull from a different network as a sanity check.
Authentication failure to a private mirror
Symptom: trivy: failed to pull image: unauthorized.
Confirm the credential is present inside the worker container:
docker-compose -f docker-compose.yml exec worker \
cat /root/.docker/config.json
If the file is missing or has no entry for your mirror host, re-run trivy registry login (see step 2 above). Mount /root/.docker from a host volume if you need credentials to persist across docker-compose down.
Weekly refresh skipped
Symptom: /var/lib/trivy/db/metadata.json shows a Created timestamp older than TRIVY_DB_REFRESH_HOURS, or the /admin/health Trivy DB panel shows freshness: stale / very_stale.
- Check beat is scheduling the task:
docker-compose -f docker-compose.yml logs --tail=200 beat | grep trivy_db_refresh
- Confirm the worker is consuming the queue (no other long-running task starving the slot).
- Force a one-shot refresh:
docker-compose -f docker-compose.yml exec worker \celery -A tasks.celery_app call trustedoss.trivy_db_refresh
- Failed / timeout refreshes fire a Slack / Teams notification with
cve_id: TRIVY-DB-REFRESH-FAILED(or-TIMEOUT). Search the/notificationslog if the panel shows staleness but you have not received an alert — a misconfigured webhook URL hides the failure.
Boot-time download stuck or stalled
Symptom: a freshly-deployed worker is up but scans take 3+ minutes longer than usual on the first run.
The boot-time download runs on a background thread (see tasks/trivy_db_bootstrap.py) — the worker is healthy and consuming the queue while the download progresses. Per-scan trivy sbom calls block on Trivy's own file lock until the download completes, typically within 1–3 minutes. If your network is slow, raise TRIVY_DB_BOOTSTRAP_TIMEOUT_SECONDS (default 900s, bounded [30, 3600]) in .env and restart the worker.
If the bootstrap fails, the worker logs trivy_db_bootstrap_degraded with the status (failed / timeout) and the error tail. Scans continue using whatever DB is already on the cache volume (if any) — the worker does not crash. Investigate the network egress (curl https://ghcr.io/v2/) and then either restart the worker (re-fires the bootstrap) or run the manual refresh command above.
trivy sbom times out on a large SBOM
Raise the per-scan limit:
# .env
TRIVY_TIMEOUT_SECONDS=900
Restart the worker. The default 300 s covers SBOMs with up to ~10 000 components; very large monorepos may need 600–900 s.
Notifications
The notification triggers for vulnerability events are configured at /notifications. The kind enum has six values:
kind | Trigger |
|---|---|
scan_completed | Scan finished successfully |
scan_failed | Scan ended in failed state |
cve_detected | New CVE detected on an existing project (driven by the Celery beat re-match — see Re-detection) |
license_violation | Forbidden / conditional license observed on a scan |
approval_pending | Component pending approval awaiting decision |
policy_gate_failed | Build gate (POST /v1/scans/{id}/policy-gate) returned block |
Channels: email (SMTP), Slack webhook, MS Teams webhook. Configure in .env (SMTP_*, SLACK_WEBHOOK_URL, TEAMS_WEBHOOK_URL).
See also
- Data sources reference — per-source coverage matrix.
- Environment variables — every
TRIVY_*key. - System health dashboard — where the upcoming Trivy DB card lives.
- ADR-0001 — Dependency-Track removal — why we switched.