Skip to content
SP StackPractices
intermediate By StackPractices

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

ToolScopeSBOMCI IntegrationUse When
TrivyOS + lang deps + configYesGitHub ActionsAll-in-one scanner
GrypeOS + lang depsVia SyftGitHub ActionsPaired with Syft SBOM
SnykOS + lang depsYesSnyk CICommercial, deeper DB
ClairOS packagesNoQuay registryRegistry-level scanning
Docker ScoutOS + lang depsYesDocker HubDocker-native solution

Guidelines

  • Scan images in CI before pushing to a registry. Block on CRITICAL vulnerabilities.
  • Use --severity HIGH,CRITICAL to 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 latest tags — 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 latest base 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