Skip to main content

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.

Audience

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.

Replaces the Dependency-Track connector

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:

  1. Boot-time download. The worker registers a Celery worker_ready signal handler that calls trivy --download-db-only on 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).
  2. Weekly refresh. A Celery Beat task (trustedoss.trivy_db_refresh, schedule Sunday 03:00 UTC) runs the same trivy --download-db-only command. 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.
  3. 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

KeyDefaultDescription
TRIVY_DB_REPOSITORYghcr.io/aquasecurity/trivy-dbOCI repository the DB is pulled from. Override for an air-gapped mirror.
TRIVY_DB_REFRESH_HOURS168 (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/trivyDirectory 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_STARTtrueWhether 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_SECONDS900Wall-clock cap on the boot-time download subprocess. Raise on slow corporate proxies. Bounded [30, 3600].
TRIVY_DB_REFRESH_TIMEOUT_SECONDS900Wall-clock cap on the weekly beat refresh subprocess. Bounded [30, 3600].
TRIVY_TIMEOUT_SECONDS300Per-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.

Admin UI panel

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.

/admin/health — Trivy vulnerability DB card with cache directory and mirror repository footer (empty-state shown for a freshly booted worker)

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-db in an internal OCI registry your network can reach. The worker still runs trivy --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.

Mirror tag drift

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_update field 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.
Volume name

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.

SourceCoversRefresh
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 versionsContinuous
EPSS (FIRST)30-day exploit probabilityDaily
KEV (CISA)Known Exploited Vulnerabilities catalogueAs published

Troubleshooting

Logs to check first
  • 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/ and cat /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:
    docker-compose -f docker-compose.yml exec worker \
    curl -fsS https://ghcr.io/v2/ -o /dev/null -w "%{http_code}\n"
    Expect 200. If your worker is air-gapped, switch to a mirror — see Air-gapped operation.
  • Corporate proxy not honoured. Trivy reads HTTP_PROXY / HTTPS_PROXY from the worker environment. Set them in .env and 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 /notifications log 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:

kindTrigger
scan_completedScan finished successfully
scan_failedScan ended in failed state
cve_detectedNew CVE detected on an existing project (driven by the Celery beat re-match — see Re-detection)
license_violationForbidden / conditional license observed on a scan
approval_pendingComponent pending approval awaiting decision
policy_gate_failedBuild 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