Scan Docker Images for CVEs with Trivy and Grype
Scan Docker images for vulnerabilities before deployment using Trivy and Grype. Covers CI integration, severity filtering, SBOM, and remediation.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Container images often contain packages with known vulnerabilities (CVEs). Scanning images before deployment catches these issues early. Trivy and Grype are two popular open-source scanners that detect vulnerabilities in OS packages and application dependencies. This recipe covers scanning images, filtering by severity, integrating with CI, generating SBOMs, and remediating vulnerabilities.
When to Use
- You want to scan images before pushing to a registry
- You need to block deployments with critical vulnerabilities
- You generate an SBOM (Software Bill of Materials) for compliance
- You want to audit images in a registry for known CVEs
Solution
Install Trivy
# macOS
brew install trivy
# Linux (Debian/Ubuntu)
sudo apt-get install trivy
# Docker
docker run --rm aquasec/trivy:latest --version
Install Grype
# macOS
brew tap anchore/grype && brew install grype
# Linux
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh
# Docker
docker run --rm anchore/grype:latest --version
Basic image scan with Trivy
# Scan a local image
trivy image myapp:latest
# Scan a remote image
trivy image nginx:1.25
# Output as JSON
trivy image --format json --output trivy-report.json myapp:latest
# Output as table (default)
trivy image --format table myapp:latest
Basic image scan with Grype
# Scan a local image
grype myapp:latest
# Scan a remote image
grype nginx:1.25
# Output as JSON
grype myapp:latest -o json > grype-report.json
# Output as table
grype myapp:latest -o table
Filter by severity
# Trivy — only show HIGH and CRITICAL
trivy image --severity HIGH,CRITICAL myapp:latest
# Trivy — ignore LOW and UNKNOWN
trivy image --severity HIGH,CRITICAL,MEDIUM myapp:latest
# Grype — only show critical
grype myapp:latest --only-cve-matches
# Grype — fail only on critical
grype myapp:latest --fail-on critical
Ignore specific vulnerabilities
# Trivy — create a .trivyignore file
echo "CVE-2023-1234" >> .trivyignore
echo "CVE-2023-5678" >> .trivyignore
# Trivy — use ignore file
trivy image --ignorefile .trivyignore myapp:latest
# Grype — use ignore file
grype myapp:latest --ignore-policy .grype-ignore.yaml
Scan in CI with GitHub Actions
# .github/workflows/scan.yml
name: Container Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: table
severity: HIGH,CRITICAL
exit-code: 1 # Fail the job if vulnerabilities found
- name: Generate SBOM
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: json
output: sbom.json
scan-type: fs
scan-ref: .
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
grype-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Grype scan
uses: anchore/scan-action@v3
with:
image: myapp:${{ github.sha }}
fail-build: true
severity-cutoff: critical
Generate SBOM (Software Bill of Materials)
# Trivy — generate SBOM in CycloneDX format
trivy image --format cyclonedx --output sbom.json myapp:latest
# Trivy — generate SBOM in SPDX format
trivy image --format spdx-json --output sbom.spdx.json myapp:latest
# Syft — dedicated SBOM tool (companion to Grype)
syft myapp:latest -o cyclonedx-json > sbom.json
syft myapp:latest -o spdx-json > sbom.spdx.json
Scan Dockerfile for misconfigurations
# Trivy — scan Dockerfile for best practices
trivy config Dockerfile
# Trivy — scan IaC files (Terraform, K8s manifests)
trivy config ./k8s/
trivy config ./terraform/
Scan a running container
# Trivy — scan from inside a running container
docker exec myapp trivy fs --severity HIGH,CRITICAL /
# Grype — scan a running container's filesystem
docker exec myapp grype /
Remediation strategies
# 1. Update the base image to a newer version
# Before: FROM node:18
# After: FROM node:18.20.4-slim
# 2. Use a minimal base image to reduce attack surface
# Before: FROM ubuntu:22.04
# After: FROM alpine:3.20
# 3. Update packages in the Dockerfile
# Add after FROM line:
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
# 4. Use distroless images (no shell, no package manager)
FROM gcr.io/distroless/nodejs18-debian12
# 5. Pin specific package versions
RUN apt-get install -y --no-install-recommends \
curl=7.88.1-10+deb12u6 \
&& rm -rf /var/lib/apt/lists/*
Multi-stage build to reduce vulnerabilities
# Build stage — has build tools (larger attack surface)
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage — minimal image
FROM node:18-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
Scheduled registry scan
# .github/workflows/registry-scan.yml
name: Weekly Registry Scan
on:
schedule:
- cron: "0 2 * * 1" # Every Monday at 2 AM
jobs:
scan-registry:
runs-on: ubuntu-latest
strategy:
matrix:
image:
- myapp:latest
- myapp:stable
steps:
- name: Run Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ matrix.image }}
format: sarif
output: trivy-results.sarif
- name: Upload to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
Explanation
Trivy and Grype scan container images by comparing installed packages against vulnerability databases:
- Trivy: Scans OS packages (apt, apk, yum), language dependencies (npm, pip, gem), and config files. Uses the NVD and OS-specific advisories. Fast, single binary.
- Grype: Scans OS packages and language dependencies. Uses the Anchore vulnerability database. Pairs with Syft for SBOM generation.
- SBOM: Software Bill of Materials. A list of all components and dependencies in an image. CycloneDX and SPDX are standard formats. Required for compliance (EO 14028).
- Severity: LOW, MEDIUM, HIGH, CRITICAL. Filter to focus on actionable vulnerabilities. Block only CRITICAL in CI to avoid noise.
- CVE: Common Vulnerabilities and Exposures. A unique identifier for a known vulnerability. Each CVE has a severity score (CVSS).
- CVSS: Common Vulnerability Scoring System. A score from 0 to 10 indicating severity. 7-9 is HIGH, 9-10 is CRITICAL.
Variants
| Tool | Scope | SBOM | CI Integration | Use When |
|---|---|---|---|---|
| Trivy | OS + lang deps + config | Yes | GitHub Actions | All-in-one scanner |
| Grype | OS + lang deps | Via Syft | GitHub Actions | Paired with Syft SBOM |
| Snyk | OS + lang deps | Yes | Snyk CI | Commercial, deeper DB |
| Clair | OS packages | No | Quay registry | Registry-level scanning |
| Docker Scout | OS + lang deps | Yes | Docker Hub | Docker-native solution |
Guidelines
- Scan images in CI before pushing to a registry. Block on CRITICAL vulnerabilities.
- Use
--severity HIGH,CRITICALto focus on actionable issues. LOW and MEDIUM create noise. - Update base images regularly. New CVEs are discovered constantly.
- Use minimal base images (alpine, distroless, slim) to reduce attack surface.
- Use multi-stage builds. Build tools in the build stage, minimal runtime image.
- Generate SBOMs for compliance. Store as build artifacts or in a registry.
- Don’t ignore vulnerabilities without documentation. Track ignored CVEs with expiry dates.
- Scan registries on a schedule. New CVEs affect already-pushed images.
- Pin base image versions. Avoid
latesttags — they can introduce unexpected changes. - Use SARIF output for GitHub Security tab integration. Visualizes vulnerabilities in PRs.
Common Mistakes
- Scanning only once at build time. New CVEs are discovered daily. Schedule regular registry scans.
- Ignoring all vulnerabilities to make CI pass. This defeats the purpose of scanning.
- Using
latestbase image tags. A new version can introduce breaking changes or new vulnerabilities. - Not updating base images. Old base images accumulate CVEs over time.
- Scanning only OS packages. Language dependencies (npm, pip) also have vulnerabilities.
- Not generating SBOMs. SBOMs are required for compliance and incident response.
- Running as root in the container. Even without vulnerabilities, root increases blast radius.
- Not failing CI on critical vulnerabilities. A scan that does not block is just a report.
Frequently Asked Questions
Should I use Trivy or Grype?
Both are excellent. Trivy is an all-in-one tool that also scans config files and IaC. Grype pairs well with Syft for SBOM generation. Try both and pick the one that fits your workflow.
How do I fix a vulnerability in a base image?
Update to a newer version of the base image. If the vulnerability is in a package, update that package with apt-get upgrade or apk upgrade. If no fix is available, consider switching to a different base image (alpine, distroless).
What is an SBOM and why do I need it?
An SBOM (Software Bill of Materials) is a formal list of all components in a software artifact. It is required for compliance (US Executive Order 14028) and helps with incident response — when a new CVE is announced, you check your SBOM to see if you are affected.
How do I scan images in a private registry?
Provide credentials to the scanner:
trivy image --username "$REGISTRY_USER" --password "$REGISTRY_PASS" registry.example.com/myapp:latest