What a container vulnerability scanner actually looks at

A Docker image vulnerability scanner extracts a software inventory (similar to an SBOM) from every layer of the image, then cross-references each package against vulnerability databases (NVD, OSV, Alpine secdb, Debian Security Tracker, etc.).

The scanner typically identifies vulnerabilities in:

  • OS packages: apt/dpkg packages on Debian/Ubuntu, apk on Alpine, rpm on RHEL/CentOS/Amazon Linux.
  • Language runtime packages: npm, PyPI, Gem, Maven, Go modules β€” detected from lockfiles, node_modules/, or site-packages directories inside the image.
  • Binary files: Statically compiled binaries with detectable version information.

Alpine vs Debian base images: Alpine-based images typically have far fewer OS packages (and therefore fewer CVEs) than Debian/Ubuntu images. Switching from python:3.12 to python:3.12-alpine often reduces OS-level CVE counts by 80%.

Trivy β€” the recommended starting point

Trivy (by Aqua Security) is the most widely adopted open-source container scanner. It requires no daemon, no registry credentials for local images, and produces output in multiple formats (table, JSON, SARIF). Installation is a single binary.

Trivy β€” basic image scanshell
# Install (macOS/Linux)
$ brew install trivy
# or: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Scan a local image
$ trivy image myapp:latest

# Scan and show only fixable HIGH/CRITICAL CVEs
$ trivy image \
    --ignore-unfixed \
    --severity HIGH,CRITICAL \
    myapp:latest

# Scan a remote image from Docker Hub
$ trivy image nginx:1.25

# Output as JSON for automation
$ trivy image --format json --output results.json myapp:latest

# Scan OS packages AND language packages together
$ trivy image --scanners vuln,secret,misconfig myapp:latest

Sample output (truncated):

Trivy scan outputtext
myapp:latest (debian 12.4)
==========================
Total: 23 (HIGH: 8, CRITICAL: 2)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Library    β”‚ Vulnerability  β”‚ Severity β”‚ Installed Versionβ”‚  Fixed Version β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ openssl      β”‚ CVE-2023-5678  β”‚ CRITICAL β”‚ 3.0.11-1         β”‚ 3.0.13-1       β”‚
β”‚ libcurl4     β”‚ CVE-2023-38545 β”‚ HIGH     β”‚ 7.88.1-10        β”‚ 7.88.1-10+deb12β”‚
β”‚ ...          β”‚ ...            β”‚ ...      β”‚ ...              β”‚ ...            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Grype + Syft β€” SBOM-first scanning

Grype (by Anchore) pairs with Syft (SBOM generator) to give you a two-step workflow: generate a Software Bill of Materials once, then scan it repeatedly against updated vulnerability databases without re-inspecting the image.

Grype β€” scan image and via SBOMshell
# Install
$ curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
$ curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# Direct image scan
$ grype myapp:latest

# Two-step: generate SBOM, then scan (store SBOM for re-use)
$ syft myapp:latest -o cyclonedx-json > sbom.cdx.json
$ grype sbom:./sbom.cdx.json --fail-on high

# Fail CI if any HIGH/CRITICAL with a fix available
$ grype myapp:latest --only-fixed --fail-on high

Docker Scout β€” built into Docker Desktop

Docker Scout is Docker's native vulnerability scanner, integrated directly into Docker Desktop and the Docker CLI since Docker 24. If your team already uses Docker Desktop, it requires zero additional setup.

Docker Scout commandsshell
# Quick vulnerability overview
$ docker scout quickview myapp:latest

# Detailed CVE list
$ docker scout cves myapp:latest

# Show only fixable critical/high CVEs
$ docker scout cves --only-fixed --only-severity critical,high myapp:latest

# Compare two image versions (useful after base image update)
$ docker scout compare myapp:v1.0 myapp:v1.1

Tool comparison

Tool OS packages Language packages Secrets scan IaC scan SBOM output Price
Trivy βœ“ βœ“ βœ“ βœ“ βœ“ Free / OSS
Grype βœ“ βœ“ βœ— βœ— Via Syft Free / OSS
Docker Scout βœ“ βœ“ βœ— βœ— βœ“ Free tier available
Snyk Container βœ“ βœ“ βœ“ βœ“ βœ“ Paid (free tier)
AquilaX βœ“ βœ“ βœ“ βœ“ βœ“ Free trial available

CI/CD gate: block vulnerable images before push

.github/workflows/container-scan.ymlyaml
name: Container Security Scan
on: [push, pull_request]

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

      - name: Build image
        run: docker build -t myapp:ci .

      - name: Trivy vulnerability scan
        run: |
          curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
          trivy image \
            --exit-code 1 \
            --ignore-unfixed \
            --severity CRITICAL,HIGH \
            myapp:ci

      # Image only pushed if scan passes
      - name: Push to registry
        if: github.ref == 'refs/heads/main'
        run: docker push myapp:latest

Reducing your vulnerability count at the source

The best way to deal with container vulnerabilities is to have fewer packages to scan:

  • Use minimal base images: alpine, distroless, or scratch. A distroless Python image has no shell, no package manager, and no system utilities β€” the attack surface shrinks dramatically.
  • Multi-stage builds: Build in a full image, copy only the compiled artefact into a minimal final image. Build tools (gcc, make, pip) don't appear in the final image.
  • Pin base image versions with digests: FROM python:3.12-slim@sha256:abc123... prevents base image drift introducing new vulnerabilities silently.
  • Keep base images updated: Most OS CVEs are fixed in updated base images. Rebuild images weekly even if your application code hasn't changed.

Scan container images automatically in your pipeline

AquilaX scans Docker images for CVEs, secrets, misconfigurations, and malware β€” with a single integration step and prioritised, actionable results.

See container scanning β†’