Vulnerability Scanning in CI: Scout, Trivy & SARIF
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
| What | Tool | When |
|---|---|---|
| Dockerfile and Compose files (misconfigurations) | trivy config or docker build --check | Every pull request |
| The built image (known vulnerabilities) | trivy image or docker scout cves | Before every push |
| Images already running in production | Same tools on the deployed tag | On 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:
trivy config .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:
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:
# 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-0002Each 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:
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: trueputs the built image into the runner's Docker so Trivy can find it asshop-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-sarifsends the file to the repository's Security > Code scanning page, where findings are tracked, assigned and closed like issues. It needssecurity-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:
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:
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 1is a practical gate;trivy configfinds Dockerfile misconfigurations..trivyignoreentries need an owner's comment and anexp:date.- SARIF output uploaded with
upload-sarifputs findings in GitHub's code scanning page; it needssecurity-events: write. - Docker Scout offers the same gate with
--exit-code, pluscompareand 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
- 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 }}# Aarav, Nov 2026: only the PDF export uses this library, and it is disabled in production.
CVE-2026-40001 exp:2027-01-31The 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?
The gate stops the job before the push steps run.
2. What does exp:2026-12-31 after a CVE ID in .trivyignore do?
Expiry dates force a regular review of accepted risks.
3. Which permission does the SARIF upload step need?
It writes findings to the repository's code scanning page.
4. Why pin aquasecurity/trivy-action to a full commit SHA?
Pinning by SHA protects your CI secrets from a hijacked tag.
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
