The Trivy vulnerability database is a critical component for security scanning in CI/CD pipelines, but it’s notoriously prone to falling out of date, leading to missed vulnerabilities.
Here’s a typical scenario:
# .gitlab-ci.yml example
scan-app:
stage: scan
image: aquasec/trivy:latest
script:
- trivy fs --security-checks vuln --exit-code 1 .
When this job runs, trivy fetches its vulnerability database. If the database hasn’t been updated recently, it won’t know about the latest CVEs. This means your scan might pass, but your application is actually vulnerable.
The core problem is that trivy fs (or trivy image, trivy rootfs, etc.) by default downloads the latest vulnerability database at runtime. This is convenient for interactive use but problematic for CI where you want deterministic, repeatable scans and often run jobs with limited network access or on ephemeral runners. The latest tag on the aquasec/trivy image doesn’t guarantee the database inside is fresh, only that the Trivy binary is the latest version.
Here’s how to ensure your Trivy scans in CI are always using the freshest vulnerability data:
1. Explicitly Update the Database at the Start of Your CI Job
The simplest and most robust method is to force Trivy to download the latest database before it performs the scan.
-
Diagnosis: Run your CI job. If it passes, manually check a known recent vulnerability (e.g., a CVE published yesterday) against your application using
trivy <your-target>on your local machine. If your local scan finds it and the CI scan didn’t, the CI database was stale. -
Fix: Add an explicit update command before your scan.
# .gitlab-ci.yml example scan-app: stage: scan image: aquasec/trivy:0.49.1 # Pin to a specific version of Trivy binary script: - trivy image --download-db-only # Download the latest DB - trivy image --security-checks vuln --exit-code 1 your-docker-image:latest # Then scan- Why it works: The
trivy image --download-db-onlycommand ensures that the vulnerability database is downloaded and updated to the absolute latest version available from Aqua Security’s servers before the actual scanning command is executed. Pinning the Trivy image version (aquasec/trivy:0.49.1) is also good practice for CI to prevent unexpected changes from image updates.
- Why it works: The
2. Cache the Vulnerability Database
CI runners are often ephemeral. Downloading the database on every single run can be slow and consume bandwidth. Caching the database can significantly speed up subsequent scans.
-
Diagnosis: Observe the CI job logs. Notice the time spent downloading the vulnerability database, especially on subsequent pipeline runs for the same branch or tag.
-
Fix: Configure your CI/CD system to cache the Trivy database directory. The database is stored in
~/.cache/trivyby default.GitLab CI Example:
# .gitlab-ci.yml variables: TRI_CACHE_DIR: "$CI_PROJECT_DIR/.cache/trivy" # Define a project-specific cache path cache: key: ${CI_COMMIT_REF_SLUG} # Cache per branch paths: - .cache/trivy/ scan-app: stage: scan image: aquasec/trivy:0.49.1 script: # Trivy will automatically use the cache if it exists and is valid # If the cache is missing or invalid, it will download to the cache dir - trivy image --security-checks vuln --exit-code 1 your-docker-image:latestGitHub Actions Example:
# .github/workflows/ci.yml name: CI Scan jobs: scan: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Trivy cache uses: actions/cache@v3 id: trivy-cache with: path: | ~/.cache/trivy key: ${{ runner.os }}-${{ hashFiles('**/go.sum') }} # Example key, adjust as needed restore-keys: | ${{ runner.os }}- - name: Run Trivy uses: aquasecurity/trivy-action@master # Using trivy-action simplifies this with: image-ref: 'your-docker-image:latest' format: 'table' ignore-unfixed: true vuln-type: 'os,library' # The trivy-action automatically handles DB updates and caching # by default, if the cache is configured correctly. # If you were running trivy directly: # - name: Install Trivy # run: | # apt-get update && apt-get install -y wget apt-transport-https gnupg lsb-release # wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | apt-key add - # echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | tee /etc/apt/sources.list.d/trivy.list # apt-get update && apt-get install -y trivy # - name: Trivy Scan # run: trivy image --download-db-only && trivy image --security-checks vuln --exit-code 1 your-docker-image:latest- Why it works: By caching the
~/.cache/trivydirectory, subsequent CI runs will first attempt to restore this directory from the cache. If a valid cache exists (based on the definedkey), Trivy will find its database files already present and skip the download. If the cache is missed or invalidated, Trivy will download the database into the cache directory, which will then be uploaded for future runs. Thetrivy-actionin GitHub Actions abstracts much of this, but understanding the underlying cache directory is key.
- Why it works: By caching the
3. Use a Custom Trivy Docker Image with Pre-loaded Database
For maximum control and offline scenarios, you can build your own Docker image that includes a recent vulnerability database.
-
Diagnosis: You need to run scans in an environment with no or very limited internet access, or you want absolute certainty about the database version used without relying on external downloads during the pipeline.
-
Fix: Create a
Dockerfilethat starts from a Trivy base image and then downloads the database. Commit this image to your own registry.# Dockerfile FROM aquasec/trivy:0.49.1 # Download the vulnerability database during the image build RUN trivy image --download-db-onlyBuild and push this image:
docker build -t my-registry/my-trivy-scanner:latest . docker push my-registry/my-trivy-scanner:latestThen, use this custom image in your CI:
# .gitlab-ci.yml scan-app: stage: scan image: my-registry/my-trivy-scanner:latest # Use your custom image script: - trivy image --security-checks vuln --exit-code 1 your-docker-image:latest- Why it works: The
RUN trivy image --download-db-onlycommand executes during thedocker buildprocess. This means the vulnerability database is downloaded once when the image is built and baked into the image layers. When this custom image is used in CI, the database is already present and doesn’t need to be downloaded at runtime, ensuring a consistent and up-to-date database for every scan.
- Why it works: The
4. Scheduled Database Updates (Less Common for CI)
While not directly for CI runtime, some organizations might set up a separate CI job or a cron job to periodically update a Trivy database file and store it as an artifact or in cloud storage. The CI job would then download this pre-updated artifact instead of fetching from the live source. This is more complex and usually overkill if caching or explicit updates are sufficient.
The most common and recommended approach for CI is Option 1 (Explicit Update) combined with Option 2 (Caching). This balances freshness, speed, and simplicity. If you have strict offline requirements, Option 3 (Custom Image) is the way to go.
The next common issue you’ll encounter after ensuring your Trivy database is fresh is dealing with false positives or vulnerabilities in third-party dependencies that are outside your direct control.