What this check reads
It reads the image reference of every running container with docker ps --format '{{.Image}}' and counts the ones matching :latest$ or having no tag and no digest at all (^[^:@]+$, a bare nginx). Separately it hunts for a scanner, checking PATH for trivy, grype, docker-scout, clair and snyk and then trying docker scout version. The two signals are combined into one result: a PASS needs an unpinned count of zero and at least one scanner, and every other combination, pinned images with no scanner included, is a WARN.
When it applies
Needs a reachable daemon; otherwise SKIP. Note it does not SKIP when nothing is running - with no containers the image count is zero and the result then turns purely on whether a scanner is installed. It combines two unrelated signals: the images of running containers matching :latest or carrying no tag or digest at all (^[^:@]+$, i.e. bare nginx), and whether any of trivy, grype, docker-scout, clair, snyk is on PATH or docker scout version works. PASS requires both - pinned images and a scanner present. Other floating tags (stable, main, edge) are not matched, and the scanner test only proves a binary exists, never that anything is scanned.
What each result means
| Result | When | What it means |
|---|---|---|
| SKIP | Daemon unreachable | Neither half ran, so nothing is known about your image references or your scanning tools. The scanner probe sits inside the same branch and is skipped with everything else. |
| WARN | Unpinned containers and no scanner | Both halves failed: containers are running from references that move, and no scanner binary is installed here to tell you what is inside them. |
| WARN | Unpinned containers (scanner present) | A scanner was found, but at least one running container came from a bare name or :latest, so you cannot say which build is actually in production. The message names the scanner it found. |
| WARN | Pinned but no scanner | Image references are pinned, but no scanner binary exists on this host. Scanning done in CI is invisible here and is a perfectly good answer to this half. |
| PASS | Pinned and a scanner is available | No running image matched the unpinned pattern and a scanner binary is present. Other floating tags such as stable or main are not matched, and an installed scanner is not evidence that anything gets scanned. |
Why it matters
Combines two supply-chain hygiene signals: reproducible, pinned deployments (CIS Docker 4.2, OWASP Docker Cheat Sheet Rule #9) and the presence of scanning tooling (CIS Docker 4.4). A container running app or app:latest cannot be correlated with a tested build.
Why it fails, and when it is wrong
docker psshows the image name as given atdocker run. Compose withimage: appand a localbuild:showsapp(no tag) → unpinned. Tag local builds with a version (image: app:${GIT_SHA}).- Digest-pinned images (
nginx@sha256:...) contain@and correctly count as pinned. - The bare-name pattern treats any
:as a tag, so an untagged image from a registry with a port,localhost:5000/apporregistry.example.com:5000/team/app, counts as pinned even though it resolves tolatest. - Scanner presence on the host is a proxy; CI-side scanning is invisible and perfectly acceptable. A PASS requires a local scanner binary, which many teams do not install on production hosts. Treat the scanner half as advisory.
How to fix it
Find the unpinned containers
docker ps --format '{{.Image}} {{.Names}}' | grep -E '^([^:@ ]+|[^ ]*:latest) 'Also check by eye for untagged references to a registry with a port, such as localhost:5000/app, which the check counts as pinned.
Pin a version or digest
docker image inspect <image> --format '{{index .RepoDigests 0}}'image: nginx:1.27.1 # or nginx@sha256:<digest>For a service built locally, give the build a version: image: app:${GIT_SHA}.
Recreate the containers
A container keeps the reference it was created from, so the finding only clears once it is recreated.
docker compose up -d --force-recreateScan images, here or in CI
Install trivy on the host to clear the other half of the check, or rely on the scanning stage in CI, where a failed scan can block a release. The CI route is the stronger one even though this check cannot see it.
Verify the fix
# Running images, and which of them are unpinned:
docker ps --format '{{.Names}}\t{{.Image}}'
docker ps --format '{{.Image}}' | grep -E '(:latest$|^[^:@]+$)'
# expected: no output
# The strongest pin is a digest:
docker inspect -f '{{.Name}} {{.Config.Image}}' $(docker ps -q) # @sha256:... in the reference means digest-pinned
docker image inspect <image> --format '{{index .RepoDigests 0}}'
# A scanner being installed is not the same as scanning - prove it runs:
trivy image --quiet --severity CRITICAL,HIGH <image>Debugging
The audit account could not talk to the daemon. To find out why, check systemctl is-active docker, then docker ps as the audit user and sudo docker ps, and run the audit as root or with sudo if only the second works. A host where the daemon answers but nothing is running does not SKIP here; it gets a result decided by the scanner half alone. The Docker Inventory check walks through the unreachable case in more detail.
Half the check. Install trivy (or grype/scout); more importantly, scan in CI where you can block a release, since a scanner on the host proves nothing about your pipeline.
Repin in the Compose file or run command and recreate the containers. A container keeps the image it was created from, so the finding will not clear until you docker compose up -d --force-recreate.
stable, main, edge and truncated version tags are not matched by the pattern. Review your image: keys directly.
That is the trade-off. Pair digest pins with a scheduled rebuild or a bot (Renovate/Dependabot) that raises the pin deliberately, so upgrades are a reviewed change rather than an accident.
Image tagging is also covered by Floating Image Tags (local images, not running ones) and scanning by Image Vulnerability Scan (which actually runs the scanner). Treat this one as the summary and those as the detail.
Sources
- Docker: docker ps format
- Docker: Pull by digest
- Trivy install
- Docker Scout quickstart
- OWASP Docker Security Cheat Sheet (Rule #9)
How the script reads this
Next
Re-run the Network & Supply Chain audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 5 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.
All 5 Network & Supply Chain fixes