Docker audit fix

Container Image Vulnerability Scanning Best Practices

Container image vulnerability scanning matches the OS and language packages baked into an image against published CVE feeds. Run trivy image against every image you deploy and fail the build on CRITICAL findings that have a fix available.

Hiren KalariyaLast reviewed: Oct 12, 2026Check image-vulnerability-scan
High
severity
Yes
needs root or sudo
6
results it can return
1 of 8
checks in this audit

Every threshold on this page is transcribed from the docker-security-image-vulnerabilities audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 8 Image Vulnerabilities checks, or how the audit runs.

High severityNeeds sudo

What this check reads

The check takes the first 5 tagged local images from docker images --format '{{.Repository}}:{{.Tag}}' with <none> entries excluded, and counts that truncated list, never more than 5, as the total it reports against. If trivy is installed it runs trivy image --quiet --severity CRITICAL,HIGH --format json --timeout 120s <img> on each of those 5 and counts occurrences of "Severity":"CRITICAL" and "Severity":"HIGH" in the JSON. One or more criticals is the FAIL, highs with no criticals is the WARN, and zero of both across the images that returned output is the PASS, while every scan coming back empty is a SKIP. If only grype or docker scout is available, or no scanner at all, the check WARNs and prints the command to run by hand instead of parsing anything.

That is the intended logic, and it does not work today. Trivy pretty-prints its JSON with a space after the colon ("Severity": "CRITICAL"), so both patterns come back empty and both counts stay at zero. With trivy installed the check can currently only return the PASS or the no-output SKIP, whatever the images contain.

When it applies

Needs the docker binary and daemon access (directly, or via root/sudo); otherwise SKIP. It scans a sample, not the whole host: the first 5 tagged images from docker images, with <none> excluded. Only trivy is parsed automatically - grype and docker scout are detected and reported as "run it yourself" WARNs. Each scan has a 120-second timeout, and images that time out or error are silently dropped from the count, which is why the message distinguishes "scanned" from "total". It reports CRITICAL and HIGH only; MEDIUM and LOW findings never appear. Images used only by stopped containers, or pulled fresh at deploy time and not present locally, are outside its view.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPDocker missing, daemon unreachable, no imagesNothing was scanned. No image list was ever produced, so the check makes no claim about the CVE state of this host.
SKIPTrivy produced no output for any imageTrivy ran but returned nothing for every image, usually a database download failure or a timeout. Nothing was verified.
FAIL≥1 CRITICAL across scanned imagesAt least one critical CVE was counted in the sampled images. This branch cannot be reached today, because the count never matches the JSON trivy actually prints.
WARNHIGH onlyHigh-severity CVEs were counted but no criticals. Also unreachable today, for the same reason.
PASSNo CRITICAL/HIGH in the scanned sampleWith trivy installed this is the only verdict the check can reach today, so it means trivy produced output, not that the images are clean. Run trivy yourself before trusting it.
WARNgrype/scout present (manual run) or no scanner installedNo scan happened. Only trivy output is parsed, so another scanner or none at all leaves the CVE state unknown.

Why it matters

Images bundle an OS layer and language packages; each ages into known CVEs. Scanning images (and rebuilding on a schedule) is CIS Docker 4.4 "Ensure images are scanned and rebuilt to include security patches", OWASP Docker Cheat Sheet Rule #9 "Integrate container scanning tools into your CI/CD pipeline", NIST SP 800-190 4.1.1 (image vulnerabilities), OWASP Top 10 A06:2021 Vulnerable and Outdated Components.

Why it fails, and when it is wrong

  • The counts never match (known issue). The script greps for "Severity":"CRITICAL" and "Severity":"HIGH" with no space, and trivy writes "Severity": "CRITICAL". Every count is zero, so an image full of critical CVEs returns PASS. Read trivy's own output, not this verdict.
  • First run downloads the vulnerability DB (tens of MB) and can exceed the 120 s timeout → empty output → SKIP "produced no output". Run trivy image --download-db-only once, or pre-warm the cache, or run the audit twice.
  • Air-gapped hosts need the DB mirrored (TRIVY_DB_REPOSITORY).
  • Even with the pattern corrected, the count is a grep over JSON and counts every matching package entry, including duplicates across layers and including vulnerabilities with no fix available. Use --ignore-unfixed for a more actionable view.
  • Only 5 images are sampled; a host with 30 images may have unscanned criticals.
  • Distroless/scratch images report few CVEs because there is no OS package manager to inventory; this is expected.
  • Scout requires a Docker Hub login for some features.

How to fix it

Scan every image, not the first five

for i in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>'); do
  echo "== $i"; trivy image --quiet --severity CRITICAL,HIGH --ignore-unfixed "$i"
done

--ignore-unfixed hides findings that have no patch yet, which leaves the list you can act on. Go by this output, not by the check's verdict.

Rebuild on a current base image

Most CVEs sit in the base layer, not in your code. Move the FROM line to a current tag such as node:22-bookworm-slim, and rebuild so the new base and its distro patches are actually fetched. Minimal bases (distroless, Alpine, Chainguard, Docker Hardened Images) carry fewer packages and so fewer CVEs.

docker build --pull --no-cache -t app:1.4.3 .

Gate CI on critical findings

trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed <image>

A non-zero exit fails the pipeline before an image with a fixable critical CVE is deployed.

Record what has no fix

For a finding with no fix available, add the CVE ID to .trivyignore with a comment giving the date and the reason. Review the file on a schedule, so an accepted risk does not turn into a forgotten one.

Verify the fix

# Reproduce the check's sample:
docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>' | head -5

# Scan properly - every image, all severities, and fail the shell on findings:
for i in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>'); do
  echo "== $i"; trivy image --quiet --severity CRITICAL,HIGH --exit-code 1 "$i" || true
done
# expected: no CRITICAL or HIGH rows

# Confirm a rebuilt image actually picked up the fixes:
docker pull <image> && trivy image --quiet --severity CRITICAL "$_"

Debugging

The audit account could not reach the daemon. Check systemctl is-active docker, then docker ps as that user and sudo docker ps, and run the audit as root, with sudo, or as a docker group member. The Docker Inventory check covers this case in more detail.

Every image is untagged (<none>) or the host pulls images at deploy time and keeps none. Check with docker images -a. It is also what you get when the audit has root but the daemon is down, because an empty answer and an empty image list look the same to the script.

Trivy ran but returned nothing for every image. Usually the vulnerability database could not be downloaded (no outbound network, or a rate-limited registry) or every scan hit the 120-second timeout. Run trivy image <name> by hand to see the real error, and trivy image --download-db-only to test database access.

Do not trust it at all at the moment: the known issue above means the count is always zero, so this PASS only says trivy returned output. Even with that fixed, the "of N total" in the message is capped at 5, so it is the sample size and not the number of images on the host, and a scanned count below N means some scans timed out. Use the loop above for real coverage.

Nothing was actually assessed. This is a "cannot tell" result, not a mild finding. Install trivy.

Many CVEs are in the base image, not your code. Rebuild on a current base (FROM node:22-alpine rather than a year-old tag), rebuild with --no-cache to pick up distro patches, and use trivy image --ignore-unfixed to see how many have a fix available at all.

Sources

How the script reads this

Next

Re-run the Image Vulnerabilities audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 8 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 8 Image Vulnerabilities fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals