EthicsComputerCourses
Join training +91 90532 10052

Vulnerability Scanning in CI: Scout, Trivy & SARIF

Docker & DevOps · Lesson 29 of 39 Advanced 8 min read Updated

In Image Security you ran Docker Scout and Trivy by hand. Scans that depend on someone remembering to run them do not happen on busy weeks. In a real team, scanning is part of the pipeline: every pull request checks the Dockerfile, every image is scanned before it is pushed, a serious fixable vulnerability stops the release, and published images are rescanned every week because new vulnerabilities are found in old packages all the time.

This lesson builds that pipeline with GitHub Actions and Trivy, shows the Docker Scout alternative, and covers the two things that make scanning bearable in practice: ignoring findings in a controlled way, and sending results to GitHub's security tab instead of burying them in logs.

What to scan, and when

WhatToolWhen
Dockerfile and Compose files (misconfigurations)trivy config or docker build --checkEvery pull request
The built image (known vulnerabilities)trivy image or docker scout cvesBefore every push
Images already running in productionSame tools on the deployed tagOn a schedule, for example weekly

A misconfiguration is a risky setting rather than a buggy package, for example a Dockerfile whose last USER is root. A vulnerability is a known bug in a package version, identified by a CVE number.

Trivy on your laptop first

Before automating, see what the tools report. trivy config checks configuration files in a folder:

Terminal
trivy config .
Output
Dockerfile (dockerfile)
=======================
Tests: 17 (SUCCESSES: 16, FAILURES: 1)
Failures: 1 (HIGH: 1, CRITICAL: 0)

HIGH: Last USER command in Dockerfile should not be 'root'
...

This output is traced from Trivy's documentation and trimmed; the number of tests depends on your Trivy version. Then scan the image, showing only high and critical findings that have a fix:

Terminal
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 shop-api:2.1.0
echo "exit code: $?"

When nothing matches, Trivy prints a report with zero findings and the exit code is 0. When something matches, it prints a table of each vulnerable package with its installed and fixed versions, and the exit code is 1. That exit code is what makes a CI step fail.

Ignoring findings, carefully

Some findings do not apply to you: the vulnerable function is never used, or the package is only present in a test tool. Hiding them forever is dangerous; hiding them for a reviewed period is normal practice. Trivy reads a .trivyignore file from the current folder:

Text
# Kabir, 2 Nov 2026: affects the XML parser only; our API never parses XML.
# Review again when the base image is updated.
CVE-2026-12345 exp:2026-12-31

# Accepted: tools/backup.Dockerfile must run as root; it is never deployed.
AVD-DS-0002

Each line is a finding ID. exp:YYYY-MM-DD makes the exception expire, so after that date the finding fails the build again and someone must look at it. Always write who accepted the risk and why in a comment. The IDs above are examples; copy real IDs from your scan output.

The pipeline

Add these steps to the workflow from CI/CD: Build & Push with GitHub Actions. The image is first built and loaded into the runner's local Docker, scanned, and only pushed when the scan passes:

YAML
permissions:
  contents: read
  packages: write
  security-events: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: docker/setup-buildx-action@v4

      - name: Build image for scanning
        uses: docker/build-push-action@v7
        with:
          context: .
          load: true
          tags: shop-api:ci
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Trivy report (SARIF)
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          image-ref: shop-api:ci
          format: sarif
          output: trivy-results.sarif
          exit-code: "0"

      - name: Upload to GitHub code scanning
        if: always()
        uses: github/codeql-action/upload-sarif@v4
        with:
          sarif_file: trivy-results.sarif

      - name: Trivy gate
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          image-ref: shop-api:ci
          severity: HIGH,CRITICAL
          ignore-unfixed: true
          exit-code: "1"

The login and push steps follow, unchanged. What each new part does:

  • load: true puts the built image into the runner's Docker so Trivy can find it as shop-api:ci. Load works for a single platform; scan multi-platform images after pushing, by digest.
  • The SARIF step writes every finding to a file in SARIF (Static Analysis Results Interchange Format), a standard JSON format that GitHub understands. It never fails the build (exit-code: "0").
  • upload-sarif sends the file to the repository's Security > Code scanning page, where findings are tracked, assigned and closed like issues. It needs security-events: write. Code scanning is available for public repositories, and for private ones with GitHub's paid code security features. if: always() uploads the report even when an earlier step fails.
  • The gate fails the job on fixable high and critical vulnerabilities, so the push steps after it never run for a vulnerable image.

Docker Scout as an alternative

Docker Scout gives similar results and adds base-image recommendations. It needs you to be logged in with a Docker account. In CI the same gate looks like this:

Terminal
docker scout cves --only-severity critical,high --only-fixed --exit-code local://shop-api:ci
docker scout cves --format sarif --output scout.sarif local://shop-api:ci
docker scout compare --to ghcr.io/priya-sharma/shop-api:2.0.3 local://shop-api:ci

--exit-code makes the command exit with code 2 when vulnerabilities are found. The local:// prefix tells Scout to use the image in the local image store instead of looking in a registry. compare shows which vulnerabilities a new build adds or removes compared with the version in production, which is a useful comment on a pull request. Docker also publishes docker/scout-action for GitHub Actions.

Weekly rescans of what is deployed

An image that passed last month can fail today. Add a scheduled workflow that scans the tag running in production and opens an alert when it no longer passes:

YAML
on:
  schedule:
    - cron: "0 4 * * 1"

Every Monday at 04:00 UTC (09:30 in India), the job scans ghcr.io/priya-sharma/shop-api:2.1.0 with the same gate. A failure usually means "rebuild on the patched base image and redeploy", which your pipeline already knows how to do.

Common mistakes

  • Scanning after pushing. A vulnerable image is already available to servers. Load, scan, then push.
  • Failing on every CVE, including unfixed ones. Teams then turn the scanner off. Gate on fixable high and critical issues and track the rest.
  • Ignore files without reasons or expiry dates. Exceptions become permanent. Use exp: and comments.
  • Actions pinned by tag only. Tags can be moved; pin third-party actions by commit SHA.
  • No rescans. New CVEs appear in old images every week.

Key points

  • Scan configuration on every pull request, images before every push, and deployed images on a schedule.
  • trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 is a practical gate; trivy config finds Dockerfile misconfigurations.
  • .trivyignore entries need an owner's comment and an exp: date.
  • SARIF output uploaded with upload-sarif puts findings in GitHub's code scanning page; it needs security-events: write.
  • Docker Scout offers the same gate with --exit-code, plus compare and base-image recommendations; pin all third-party actions by commit SHA.

Exercise

Aarav's pipeline pushes ghcr.io/aarav-shah/hostel-api and then scans it. Reorder and rewrite the steps so the image is built and loaded as hostel-api:ci, scanned with a Trivy gate that fails only on fixable critical vulnerabilities, and pushed only if the gate passes. Also write a .trivyignore entry that accepts CVE-2026-40001 until 31 January 2027 with a reason.

Show solution
YAML
- name: Build and load
  uses: docker/build-push-action@v7
  with:
    context: .
    load: true
    tags: hostel-api:ci

- name: Trivy gate
  uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
  with:
    image-ref: hostel-api:ci
    severity: CRITICAL
    ignore-unfixed: true
    exit-code: "1"

- name: Build and push
  uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    tags: ghcr.io/aarav-shah/hostel-api:${{ github.ref_name }}
Text
# Aarav, Nov 2026: only the PDF export uses this library, and it is disabled in production.
CVE-2026-40001 exp:2027-01-31

The push step runs only after the gate succeeds, because a failed step stops the job. The second build reuses the layers from the first, so it is fast.

Quick Quiz

1. Why scan the image before pushing it?

2. What does exp:2026-12-31 after a CVE ID in .trivyignore do?

3. Which permission does the SARIF upload step need?

4. Why pin aquasecurity/trivy-action to a full commit SHA?

Learn Docker & DevOps with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call