How to Scan Container Image Layers and the Docker Malware Feed
Catch vulnerabilities hidden beneath a clean manifest with per-layer Trivy scanning, and close the OpenSSF index gap for containers with the Docker-specific feed.
Overview
Container images are two classes of supply-chain blind spot wrapped in one artifact:
- Manifest-only scans miss layers. An attacker can publish an image whose top-level manifest references clean base layers and sneak a compromised binary into a lower layer that the manifest-level scan never reaches.
- OpenSSF malware index is empty for Docker. The community OSV malicious-packages feed publishes almost no Docker entries, so image-based malware has been invisible to the standard lookup.
Two features close both gaps:
- Per-layer Trivy walk — every layer of every pulled image is
unpacked and scanned for known CVEs, configured via
CHAINSAW_DOCKER_LAYER_SCAN=on(default). - Docker-specific malware feed — Chainsaw ships an embedded seed of known-malicious image digests plus an optional remote feed URL.
Prerequisites
- Chainsaw admin access to environment configuration
- Docker / OCI repositories proxied through Chainsaw
- Sufficient disk for layer unpacking (sized by the layer cap, see Step 2)
Step 1: Confirm Layer Scanning Is On
Layer scanning is on by default. Verify via the /api/settings endpoint
or the environment:
echo $CHAINSAW_DOCKER_LAYER_SCAN
# expected: on (or empty, which defaults to on)
Layer scan timings are emitted at DEBUG log level, and the counter
chainsaw_docker_layers_scanned_total tracks volume:
sum(rate(chainsaw_docker_layers_scanned_total[5m]))
Step 2: Tune the Layer Size Cap
Very large layers (1 GiB+ base images, ML model layers) can blow the scan budget. Tune via:
export CHAINSAW_DOCKER_LAYER_SIZE_CAP_BYTES=1073741824 # default 1 GiB
Layers larger than the cap are skipped with reason
layer_over_size_cap; the manifest-level scan still runs. Raise the cap
if your builds pull legitimately-large layers, lower it if you need a
faster ingestion budget.
Step 3: Create a Vulnerability Policy That Covers Layers
The existing isVulnerable condition covers both the manifest-level and
the per-layer scan — no separate condition is required. A Docker-scoped
rule that blocks critical CVEs now catches vulnerabilities hidden in
lower layers:
- Name:
Block — critical CVE in Docker layer or manifest - Condition:
isVulnerable = true && cvssMin >= 9.0 - Scope: Docker / OCI repositories
- Action: Block
Step 4: Enable the Docker Malware Feed
The feed is on by default (cfg.EnableDockerMalware = true). The
embedded seed covers publicly-disclosed known-malicious digests at
build time. To pull a live remote feed on top of the seed, set:
export CHAINSAW_DOCKER_MALWARE_FEED_URL=https://feeds.example.com/docker-malware.json
The feed format matches the embedded seed: a JSON array of entries with
digest (in sha256:... form) and optional name + tag fields. At
load time, Chainsaw populates two lookup indices — dockerDigests
(exact sha256 match) and dockerNames (name+tag fallback for tagged
pulls that haven’t yet resolved to a digest).
Step 5: Verify Digest-First Lookup
The malware lookup takes a fast-path on digest pulls:
docker pull registry.example.com/foo/bar@sha256:deadbeef...
If that digest is in either feed, the request is blocked with a malware
violation. Name+tag pulls (foo/bar:1.2.3) fall back to the name+tag
table — it’s a secondary signal because tags are mutable upstream, so
the digest index should remain the primary defence.
Step 6: Alert on Malicious Image Detections
# Prometheus alerting rule
- alert: ChainsawMaliciousDockerImage
expr: sum(rate(chainsaw_malware_detections_total{ecosystem="docker"}[5m])) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "Chainsaw detected a known-malicious Docker image"
description: "Check the traffic view for the image digest and identify the requesting client."
Ecosystem Notes
| Signal | Support |
|---|---|
| Per-layer Trivy vulnerability scan | ✅ Docker / OCI |
| Docker malware feed (digest + name+tag) | ✅ Docker / OCI |
| Hidden-Unicode scan on layer text files | ❌ (deferred — separate surface) |
hasInstallScript on a container image | ❌ (no lifecycle-script concept) |
versionAnomaly on image tags | ❌ (tags are not SemVer) |
For container images, the layered defence-in-depth stack is:
isSuspectedTyposquat + isKnownMalicious (via the Docker feed) +
isVulnerable (manifest + per-layer) + hasProvenance (Sigstore where
the image is signed).