QA Engineer Skills 2026QA-2026Vulnerability Scanning for Container Images

Vulnerability Scanning for Container Images

Why Container Scanning Is Non-Negotiable

Container images accumulate vulnerabilities through three vectors: base images ship with OS-level CVEs, installed packages introduce application-level vulnerabilities, and application dependencies pull in transitive risks. A single unscanned image can contain dozens of known exploits.

The critical insight is that scanning must happen at build time, not after deployment. A vulnerability discovered in production means your attack surface was already exposed. A vulnerability caught in CI means it never reached a cluster.


The Three Scanning Vectors

Vector Example Detection Method
OS packages OpenSSL CVE in base image Image layer scanning (Trivy, Grype)
Language dependencies Vulnerable npm/pip/gem package SBOM analysis + advisory database
Misconfigurations Running as root, exposed ports Dockerfile linting (Trivy config, Hadolint)

A comprehensive scanning strategy covers all three. Most teams start with image scanning and add configuration scanning as they mature.


Trivy: The Swiss Army Knife

Trivy (by Aqua Security) is the most versatile open-source scanner. It handles images, filesystems, Git repositories, and Kubernetes clusters with a single binary.

Basic Scanning

# Scan a local image for vulnerabilities
trivy image --severity HIGH,CRITICAL myapp:latest

# Scan and fail CI if critical vulnerabilities are found
trivy image --exit-code 1 --severity CRITICAL myapp:latest

# Scan a Dockerfile for misconfigurations
trivy config --severity HIGH,CRITICAL ./Dockerfile

# Scan a filesystem (catches vulnerabilities in source code dependencies)
trivy filesystem --severity HIGH,CRITICAL .

# Generate an SBOM (Software Bill of Materials)
trivy image --format spdx-json -o sbom.json myapp:latest

Trivy in CI/CD

# .github/workflows/container-scan.yml
name: Container Security Scan
on:
  push:
    paths:
      - 'Dockerfile*'
      - 'package*.json'
      - 'requirements*.txt'

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Scan for vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'HIGH,CRITICAL'
          exit-code: '1'

      - name: Upload scan results to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

      - name: Scan Dockerfile for misconfigurations
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: '.'
          severity: 'HIGH,CRITICAL'

Ignoring False Positives

Not every CVE is exploitable in your context. Trivy supports a .trivyignore file:

# .trivyignore
# CVE-2024-1234: Not exploitable because we don't use the affected function
CVE-2024-1234

# Temporary ignore until upstream fix is released (expires 2026-03-01)
# CVE-2025-5678: Waiting for Node.js 22.3 patch
CVE-2025-5678

Always document why you are ignoring a CVE. Undocumented ignores accumulate into hidden risk.


Grype: Anchore's Vulnerability Scanner

Grype is another popular scanner from the Anchore ecosystem. It pairs well with Syft (Anchore's SBOM generator):

# Scan a local image
grype myapp:latest --fail-on high

# Scan from an SBOM (faster for repeated scans)
syft myapp:latest -o json > sbom.json
grype sbom:sbom.json --fail-on critical

# Output in table format for human review
grype myapp:latest --output table

# Output in SARIF for CI integration
grype myapp:latest --output sarif > grype-results.sarif

Grype vs Trivy Comparison

Feature Trivy Grype
Vulnerability databases NVD, OS-specific, language-specific NVD, OS-specific, language-specific
Dockerfile scanning Yes (config mode) No (image only)
K8s cluster scanning Yes No (use Anchore Enterprise)
SBOM generation Built-in Separate tool (Syft)
Speed Fast Fast
False positive rate Low Low
Ignore mechanism .trivyignore file .grype.yaml config

Many teams use both and compare results. Different scanners have different advisory databases and may catch different CVEs.


Snyk Container: Commercial with Fix Recommendations

Snyk differentiates itself by providing fix recommendations -- it tells you which base image upgrade or package version would resolve the vulnerability:

# Scan with fix recommendations
snyk container test myapp:latest --severity-threshold=high

# Monitor continuously (alerts on new CVEs for deployed images)
snyk container monitor myapp:latest

# Get base image upgrade recommendations
snyk container test myapp:latest --file=Dockerfile
# Output includes:
# Tested 1 image, found 12 vulnerabilities
# Base Image      Vulnerabilities  Severity
# node:20         42               8 critical, 15 high
#
# Recommendations:
# Minor upgrade: node:20.11 (removes 8 vulnerabilities)
# Alternative:   node:20-slim (removes 30 vulnerabilities)
# Best:          node:20-alpine (removes 38 vulnerabilities)

Registry Scanning: Continuous Protection

Build-time scanning is necessary but not sufficient. New CVEs are disclosed daily, and an image that was clean yesterday may be vulnerable today. Registry scanning catches this drift.

Scanning Strategies

Strategy When Tool
Build-time scan Every CI build Trivy, Grype, Snyk
Registry scan Daily / on push Harbor, ECR scanning, Trivy operator
Runtime scan Continuous Falco, Sysdig, Aqua
SBOM audit On new CVE disclosure Grype against stored SBOMs

AWS ECR Scanning

# Enable enhanced scanning on ECR repository
aws ecr put-image-scanning-configuration \
  --repository-name myapp \
  --image-scanning-configuration scanOnPush=true

# Check scan results
aws ecr describe-image-scan-findings \
  --repository-name myapp \
  --image-id imageDigest=sha256:abc123

Harbor Registry Scanning

# Harbor automatically scans on push and can block vulnerable images
# Configuration in Harbor UI or API:
# - Scan on push: enabled
# - Prevent vulnerable images from running:
#   severity threshold: High
# - Auto-scan schedule: daily at 02:00 UTC

Building a Scanning Pipeline

The recommended approach layers scans at multiple points:

Developer machine          CI Pipeline              Registry           Runtime
    |                         |                        |                  |
    |-- Hadolint             |-- Build image          |-- Scan on push  |-- Falco
    |   (Dockerfile lint)    |-- Trivy scan           |-- Daily rescan  |   (anomaly
    |                        |-- Grype scan           |-- Block pulls   |    detection)
    |-- docker scan          |-- Fail on CRITICAL     |   of vulnerable |
    |   (quick local check)  |-- Generate SBOM        |   images        |
    |                        |-- Upload to registry   |                  |

Sample CI Stage

#!/bin/bash
# scripts/scan-container.sh
set -euo pipefail

IMAGE="$1"
SEVERITY_THRESHOLD="${2:-HIGH}"

echo "=== Scanning $IMAGE ==="

# Stage 1: Dockerfile lint
echo "--- Hadolint ---"
hadolint Dockerfile --failure-threshold warning

# Stage 2: Image vulnerability scan
echo "--- Trivy ---"
trivy image --exit-code 1 --severity "$SEVERITY_THRESHOLD" "$IMAGE"

# Stage 3: SBOM generation
echo "--- SBOM ---"
trivy image --format spdx-json -o "sbom-$(date +%Y%m%d).json" "$IMAGE"

# Stage 4: Cross-reference with Grype
echo "--- Grype ---"
grype "$IMAGE" --fail-on high

echo "=== All scans passed ==="

This layered approach ensures that even if one scanner misses a CVE, another catches it. The SBOM provides an audit trail for future vulnerability disclosures.