Container Image Scanning: Tools and Integration
Container image scanning checks a built image's OS packages, language dependencies, and configuration against known-vulnerability databases before that image ever reaches production. It's one of the highest-value, lowest-effort controls available in a container pipeline — but only if it's wired into CI in a way developers can't route around, and only if the team understands what a scan does and does not catch.
What image scanners actually check
Modern scanners parse the image's filesystem layers to build a software bill of materials (SBOM) — a list of OS packages (from the Debian/Alpine/RHEL package database) and language-level dependencies (from lockfiles like package-lock.json, requirements.txt, go.sum, or embedded metadata in the built artifact). Each component version is then matched against vulnerability feeds such as the National Vulnerability Database (NVD), distro security advisories, and the scanner vendor's own curated feed.
Most scanners also check for hardcoded secrets left in image layers, misconfigurations in the Dockerfile or associated Kubernetes manifests, and — for some tools — known-malicious packages in addition to known-vulnerable ones.
Scan results are typically reported with a severity rating (Critical/High/Medium/Low, often mapped from the CVSS score of the underlying CVE) and, where the scanner can determine it, whether a fixed package version is available. That fix-availability flag matters operationally: a Critical finding with no available fix yet needs a different response — compensating controls, deferral, or accepting the risk with a documented review date — than one where upgrading a single dependency resolves it immediately.
What scanning does not catch
It's worth being explicit about the limits, since teams sometimes treat a clean scan as a broader guarantee than it is. Image scanning is a point-in-time check against known vulnerabilities — it will not catch zero-days, logic flaws in your own application code, business-logic vulnerabilities, or misuse of legitimate functionality. A vulnerability database entry also takes time to appear after a CVE is disclosed, so a scan run today can miss a vulnerability disclosed yesterday.
Scanning also depends on accurate package identification. Statically compiled binaries, vendored dependencies without standard manifest files, and custom-built base images can all reduce what a scanner is able to identify, producing false confidence from an apparently clean result.
Comparing common scanners
There's no single "best" scanner — teams often run more than one, or pick based on what integrates most cleanly with their existing CI and registry. At a high level:
| Tool | Type | Vulnerability Sources | Notable Strengths |
|---|---|---|---|
| Trivy | Open source (Aqua Security) | NVD, distro advisories, GitHub Security Advisories, language ecosystem feeds | Broad coverage in a single binary — images, filesystems, IaC, git repos, SBOM generation; simple to run locally and in CI |
| Grype | Open source (Anchore) | NVD, distro advisories, language ecosystem feeds | Fast, focused scanner that pairs well with Syft for SBOM generation; straightforward CLI output |
| Snyk Container | Commercial (free tier available) | Snyk's curated vulnerability database plus public feeds | Developer-focused remediation guidance (suggested base image or version fixes), IDE and PR integrations |
Scanning in the Dockerfile build stage
A multi-stage Dockerfile that minimises the final image surface makes scanning more effective, since fewer packages in the final image means fewer things that can carry a vulnerability. Build tooling, compilers, and dev dependencies should live only in an earlier build stage and never reach the final runtime image.
Base image choice has an outsized effect on scan results before a single line of application code is added. A full general-purpose distro base image typically carries hundreds of OS packages, most of which the application never uses but which still count toward the vulnerability surface. Slim or distroless base images strip this down to close to what's actually required at runtime, which is why teams that are serious about scan noise tend to standardise on a small, centrally maintained set of approved base images rather than letting each service pick its own.
Multi-stage Dockerfile reducing final image surface
# Build stage — includes compilers and build deps
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app
# Final stage — minimal runtime image
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]Wiring scanning into CI/CD
Scanning is most effective run at multiple points: locally or in a pre-commit hook for fast feedback, in CI on every build to gate merges and image pushes, and continuously against images already deployed, since new CVEs are published against packages that were clean when originally scanned.
A common pattern is to fail the pipeline on Critical and High severity findings that have a known fix available, while allowing findings without an available fix (or below a severity threshold) to be logged without blocking — an unconditional fail-on-any-finding policy tends to get bypassed or disabled by frustrated teams within a few months.
Example CI step: fail on Critical/High with a known fix
trivy image \
--severity CRITICAL,HIGH \
--ignore-unfixed \
--exit-code 1 \
--format table \
registry.internal/app:${GIT_SHA}Registry-side and runtime scanning
CI-time scanning catches issues before deployment, but images sitting in a registry accumulate risk as new CVEs are disclosed against their existing packages. Most major registries (ECR, GCR/Artifact Registry, ACR, Docker Hub, Harbor) offer continuous or on-push scanning that re-evaluates stored images against updated vulnerability feeds, and can alert or block promotion of images that were clean at build time but are no longer.
Runtime scanning and admission control add a final layer — an admission controller (see the Kubernetes hardening article for OPA/Gatekeeper and Kyverno) can block deployment of images with known Critical vulnerabilities or without an attached SBOM/attestation, catching images that bypassed or predate CI scanning.
- •Generate and store an SBOM (Syft, or a scanner's built-in SBOM output) alongside each build for later vulnerability lookups without re-scanning
- •Sign images (Cosign/Sigstore) so admission control can verify provenance, not just scan results
- •Track mean time to remediate for Critical/High findings as a pipeline health metric, not just point-in-time scan pass rate
References
Primary sources for the material above. Standards are cited by identifier so they stay findable as publishers reorganise their sites.
- Aqua Security — Trivy Documentation
- Anchore — Grype (open-source container vulnerability scanner)
- Snyk — Snyk Container Documentation
- NIST National Vulnerability Database (NVD)