# Trivy offline database: guide

This directory holds everything Trivy 0.74 needs to scan with **no network access**.

```
trivy-db/
├── cache/                  # use this as Trivy's --cache-dir
│   ├── db/                 #   vulnerability DB   (mirror.gcr.io/aquasec/trivy-db:2)       ~1.3 GB
│   ├── java-db/            #   Java index DB      (mirror.gcr.io/aquasec/trivy-java-db:1)  ~1.5 GB
│   └── policy/             #   misconfig checks   (mirror.gcr.io/aquasec/trivy-checks:2)   ~3 MB
├── trivy-offline.env       # source it and every trivy command runs offline
├── test-offline.sh         # self-test with the network cut off
├── download-db.sh          # refresh + package (on a host with Internet)
├── samples/                # known-vulnerable targets used by the test
├── DB-VERSION.txt          # DB timestamps / checks digest
└── SHA256SUMS              # checksums of cache/ files
```

The DB version and date are in `DB-VERSION.txt`. Trivy's DB format must match the binary. `trivy-db:2` works with Trivy 0.74.

---

## 1. Quick test

```bash
cd /work/acb/trivy-db
./test-offline.sh
```

When run as root, the script puts itself in an empty network namespace (`unshare -n`), so a download attempt would fail. Expected output:

```
== network: blocked
PASS  fs: Python lockfile (vuln DB)
PASS  rootfs: log4j JAR (Java DB)
PASS  image: UBI 9 tarball (OS pkgs)
PASS  config: Dockerfile (checks)
PASS  gate: --exit-code 1 on CRITICAL
== 5 passed, 0 failed
```

## 2. Use the local DB

### Option A: environment variables (recommended)

```bash
source /work/acb/trivy-db/trivy-offline.env
trivy fs  /path/to/project
```

This sets `TRIVY_CACHE_DIR` and the skip-update, offline and no-telemetry switches. To make it permanent, add the `source` line to `~/.bashrc` or to the CI job.

### Option B: CLI flags

```bash
OFF="--cache-dir /work/acb/trivy-db/cache --skip-db-update --skip-java-db-update \
     --skip-check-update --offline-scan --skip-version-check --disable-telemetry"

trivy fs     $OFF /path/to/project
trivy rootfs $OFF /path/to/dir-with-jars
trivy image  $OFF --input image.tar
```

> **Gotcha:** `trivy config` rejects the DB flags with `FATAL unknown flag: --skip-db-update`.
> For it, use only `--cache-dir /work/acb/trivy-db/cache --skip-check-update`, or use Option A.

What each switch prevents:

| Setting | Without it, offline… |
|---|---|
| `--cache-dir` / `TRIVY_CACHE_DIR` | Trivy looks in `~/.cache/trivy`, finds no DB, and tries to download |
| `--skip-db-update` | tries to pull `trivy-db` once the DB's NextUpdate time passes (every ~24 h) |
| `--skip-java-db-update` | tries to pull `trivy-java-db` when a JAR is found |
| `--skip-check-update` | tries to pull `trivy-checks` for misconfig scans |
| `--offline-scan` | queries Maven Central for JARs that have no embedded metadata |
| `--skip-version-check`, `--disable-telemetry` | calls home (non-fatal, but causes timeouts or log noise) |

## 3. Test examples

All examples assume `source trivy-offline.env` and `cd /work/acb/trivy-db`.

```bash
# Language lockfiles / source tree: requirements.txt, package-lock.json, go.sum, pom.xml …
trivy fs --scanners vuln samples/python-app

# Vulnerabilities + secrets + misconfigurations in one pass
trivy fs --scanners vuln,secret,misconfig samples/python-app

# JAR/WAR/EAR files (uses the Java DB): should report CVE-2021-44228 (Log4Shell)
trivy rootfs --scanners vuln samples/java

# Container image from a tarball (podman save / docker save)
trivy image --input samples/images/ubi9-minimal-9.8.tar

# Image stored in local podman: export it first, or scan through the podman socket
podman save -o /tmp/myimg.tar localhost/myapp:latest && trivy image --input /tmp/myimg.tar
systemctl --user start podman.socket   # or: systemctl start podman.socket (root)
trivy image --image-src podman localhost/myapp:latest

# Dockerfile / Kubernetes / Terraform misconfigurations (uses the checks bundle)
trivy config samples/python-app

# CI gate: exit code 1 if HIGH/CRITICAL vulnerabilities that have a fix exist
trivy fs --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 /path/to/project

# Reports
trivy fs -f json  -o report.json  samples/python-app
trivy fs -f sarif -o report.sarif samples/python-app      # SonarQube / Gitea code scanning
trivy fs -f cyclonedx -o sbom.cdx.json samples/python-app # SBOM
trivy sbom sbom.cdx.json                                  # rescan an SBOM later
```

Results from the verification run on 2026-09-15:

| Target | Result |
|---|---|
| `samples/python-app` (Django 3.2.0, requests 2.19.1, PyYAML 5.3) | 43 vulns (8 CRITICAL, 16 HIGH) |
| `samples/java/log4j-core-2.14.1.jar` | 7 vulns incl. CVE-2021-44228 CRITICAL |
| `ubi9-minimal-9.8.tar` | RHEL 9.8 detected, 119 vulns (6 HIGH) |
| `samples/python-app/Dockerfile` | 2 failures: DS-0002 (no USER), DS-0026 (no HEALTHCHECK) |

## 4. Prove it is really offline

Make sure the "offline" result doesn't depend on a network that happens to be reachable:

```bash
# root: run in a namespace with no network interfaces
unshare -n bash -c 'source /work/acb/trivy-db/trivy-offline.env; trivy fs /path/to/project'

# any user: container with no network, trivy binary and DB mounted read-only
podman run --rm --network none \
  -v /usr/local/bin/trivy:/usr/local/bin/trivy:ro \
  -v /work/acb/trivy-db/cache:/cache:ro \
  -v "$PWD":/src:ro \
  registry.access.redhat.com/ubi9/ubi-minimal:9.8 \
  trivy fs --cache-dir /cache --skip-db-update --skip-java-db-update --offline-scan \
           --skip-version-check --disable-telemetry --scanners vuln /src
```

Typical errors and what they mean:

| Message | Cause |
|---|---|
| `Downloading vulnerability DB...` then `dial tcp … no such host` | wrong `--cache-dir`, or `--skip-db-update` missing |
| `--skip-db-update cannot be specified on the first run` | `cache/db/trivy.db` not found at the given `--cache-dir` |
| `The first run cannot skip downloading Java DB` | `cache/java-db/trivy-java.db` not found |
| `unknown flag: --skip-db-update` | passed to `trivy config`; see the gotcha in section 2 |
| `failed to query Maven Central` | `--offline-scan` missing |
| `database schema mismatch` / `DB version` error | the DB is newer or older than the Trivy binary supports; download a matching one |
| `timeout` waiting for the cache lock | two scans are using the same `--cache-dir` at once; run them one after another, or add `--cache-backend memory` |

Mounting `cache/` read-only works: the `podman --network none` example above was verified with `:ro`. If you ever see a write error on `cache/fanal/` (the scan cache), add `--cache-backend memory`.

## 5. Refresh the DB (weekly)

The DB is a snapshot. Each day old means CVEs published since then are missed, so refresh it regularly:

```bash
# On a host with Internet access (needs trivy 0.74 and jq)
cd /work/acb/trivy-db
./download-db.sh          # → ../trivy-db-offline-YYYYMMDD.tar.gz + .sha256

# Move the tarball into the air-gapped network, then on the target host:
sha256sum -c trivy-db-offline-YYYYMMDD.tar.gz.sha256
tar -xzf trivy-db-offline-YYYYMMDD.tar.gz -C /work/acb     # replaces trivy-db/
cd /work/acb/trivy-db && sha256sum -c --quiet SHA256SUMS && ./test-offline.sh
```

Behind an internal registry mirror (Harbor, Nexus, Quay), point the script at it:
`TRIVY_DB_REPOSITORY=registry.local/aquasec/trivy-db:2 ./download-db.sh`.
Scans can also pull from that registry directly, via `--db-repository` / `--java-db-repository` / `--checks-bundle-repository` without the skip flags.

Check the DB age before trusting a clean result:

```bash
jq -r .UpdatedAt /work/acb/trivy-db/cache/db/metadata.json
```
