Safeguard
Container Security

How Snyk Container's static filesystem analysis avoids th...

How Snyk Container inspects image layers, package databases, and lockfiles without ever running the container — and where static filesystem analysis hits its limits.

Karan Patel
Cloud Security Engineer
8 min read

Scanning a container image usually conjures an image of docker run spinning up a sandbox, exec-ing a shell, and poking around. Snyk Container does something different: it never boots the image at all. When you run snyk container test nginx:1.25, the CLI pulls the image manifest and layer blobs directly from the registry (or reads them from a local tarball), unpacks the layer filesystem into a virtual representation, and pattern-matches known package databases and lockfiles against its vulnerability feed — all without a container process, an entrypoint execution, or even a running Docker daemon in the common case. That distinction matters more than it sounds. A scanner that has to launch a container inherits that container's boot time, its resource footprint, and its risk surface — including the possibility of executing an attacker-controlled entrypoint. This post walks through how Snyk's static filesystem approach is documented to work, why it enables scanning distroless and minimal images that have no shell to exec into, and where the technique's limits are.

How does Snyk Container get the filesystem without running the image?

It pulls the image layers as compressed archives directly from the registry API, the same way docker pull does, without invoking docker run. Container images built to the OCI Image Format spec are distributed as a manifest (a JSON document listing layer digests) plus a set of gzip-compressed tar archives, one per layer. Snyk's CLI speaks the Docker Registry HTTP API V2 — the same GET /v2/<name>/manifests/<reference> and GET /v2/<name>/blobs/<digest> calls a registry client uses — to fetch the manifest and each layer blob by its sha256: digest. Because this only requires HTTP requests to the registry (Docker Hub, ECR, GCR, ACR, Artifactory, and so on), Snyk documents support for testing images that were never pulled locally at all, and for testing images inside CI runners that don't have a Docker socket or privileged access, which is a common constraint in hardened GitHub Actions or GitLab runners. Snyk also supports feeding it a pre-exported archive with --file=image.tar for docker save-style workflows, or pointing it at a Dockerfile alongside the image to correlate base-image layers with the instructions that produced them.

What happens to those layers once they're downloaded?

Each layer's tar archive is extracted and stacked in the order the manifest specifies, reconstructing the same union filesystem view the container runtime would present at boot — but as static files on disk or in memory, not as a mounted overlay backing a running process. Because OCI layers are additive (and sometimes subtractive, via "whiteout" files that mark deletions), Snyk has to apply the layers in sequence to know the final state of every file, including which files a later layer deleted or overwrote from an earlier one. This reconstructed tree is what the rest of the analysis operates on. Crucially, nothing in this tree is executed — no ENTRYPOINT, no CMD, no RUN scripts, no init process. The scanner only ever reads bytes from disk, which is the property that lets it treat an untrusted or unverified image as inert data rather than as code that needs a sandboxed VM to run safely.

How does it figure out what packages are actually installed?

It reads the same package-manager metadata files the OS itself would consult, rather than asking the running system to enumerate them. For Debian and Ubuntu-based images, that means parsing /var/lib/dpkg/status, the flat-file database dpkg maintains of every installed package, version, and dependency. For RHEL, CentOS, and Fedora-based images, it reads the RPM database, historically a Berkeley DB file at /var/lib/rpm/Packages and, on newer distributions, an SQLite database at /var/lib/rpm/rpmdb.sqlite. For Alpine images it reads /lib/apk/db/installed, the plain-text ledger apk keeps of installed package names and versions. Layered on top of OS package detection, Snyk's language-level scanning looks for manifest and lockfiles it recognizes anywhere in the filesystem — package-lock.json or yarn.lock for Node.js, Gemfile.lock for Ruby, requirements.txt or Pipfile.lock for Python, go.sum for Go, and Maven/Gradle artifacts for Java — to build a picture of application dependencies baked into the image, not just the base OS. Each identified package-and-version pair is then checked against Snyk's vulnerability database to produce the findings list.

Why does this matter for distroless and minimal images?

Because there's no shell, package manager, or even a /bin/sh to exec into, and static analysis doesn't need one. Distroless images — a pattern popularized by Google's gcr.io/distroless base images and widely adopted for minimizing attack surface — deliberately strip out shells, package managers, and most userland utilities, leaving only the application binary and its runtime dependencies. A scanning approach that depends on shelling into the container to run dpkg -l or rpm -qa simply cannot work here, because the tools needed to enumerate packages don't exist inside the image. Snyk's approach sidesteps this entirely: it doesn't care whether a shell exists, because it's reading the dpkg or rpm database files directly off the reconstructed filesystem rather than asking a running process to report on itself. The same logic applies to scratch-based images and statically compiled Go binaries shipped in otherwise-empty containers — as long as recognizable dependency metadata exists somewhere in the layers, static extraction can find it.

What are the practical advantages of never starting the container?

The two biggest are speed and safety, and both come from skipping the container runtime lifecycle entirely. Booting a container — even a minimal one — involves pulling additional layers if not cached, initializing namespaces and cgroups, and running an entrypoint that may itself take seconds to initialize a language runtime like the JVM. A static scan skips all of that: once the layers are fetched, the analysis is a filesystem walk plus a database lookup, which is why Snyk Container scans typically complete for a single image in the range of seconds rather than the tens of seconds a full container boot-and-inspect cycle would take. The safety argument is arguably more important for a security tool specifically: because nothing in the image is executed, a malicious or compromised image can't use its entrypoint, a build hook, or a crafted binary to attack the scanning host. This is also why the daemonless registry-pull mode matters in CI: pipelines that can't grant a build job access to a Docker socket (a common hardening step, since Docker socket access is broadly equivalent to root on the host) can still get a full dependency and vulnerability inventory of an image without ever needing that privilege.

What can static filesystem analysis miss?

It can't see anything that only comes into existence at runtime — packages installed by an entrypoint script on first boot, dependencies pulled down dynamically, or vulnerabilities that only manifest through a specific runtime configuration or environment variable. Static analysis is a point-in-time inventory of what's baked into the image layers; it has no visibility into behavior that only occurs after the container starts, such as a startup script that runs apt-get install or pip install before launching the main process — a pattern that's discouraged for reproducibility reasons but does still show up in older or less disciplined Dockerfiles. It also depends on recognizing the package manager's on-disk format, so custom or nonstandard package managers, statically linked binaries with no accompanying lockfile, or dependencies vendored by copying source files directly (rather than through a manifest a scanner recognizes) can go undetected. None of this is a flaw specific to Snyk — it's an inherent tradeoff of any tool in this category, including Syft, Trivy, and Grype, which use broadly comparable layer-extraction-and-package-database-matching techniques. The tradeoff is a deliberate one: trade a small amount of runtime blind spot for the ability to scan any image, from any registry, safely and quickly, without ever having to trust it enough to run it.

How Safeguard Helps

Static filesystem scanning is a strong way to answer "what's in this image and is any of it vulnerable," but a vulnerability list at a point in time is only one input into a real container security program. Safeguard focuses on the layer above the individual scan: tracking image provenance from build to registry to deployment, verifying that the artifact a scanner evaluated is the same one that actually ships, and correlating dependency and SBOM data across your entire fleet of images rather than one snyk container test at a time. Because static analysis inherently can't see runtime behavior, Safeguard's approach pairs build-time and registry-time findings — whether generated by Snyk, Trivy, Grype, or another scanner in your pipeline — with policy gates that block unattested or unverified images from being promoted, and with continuous re-evaluation as new CVEs are disclosed against packages you've already shipped. If your organization already relies on static container scanning as part of CI, Safeguard's role is to make sure those results are trustworthy, tied to a verifiable build provenance chain, and enforced consistently across every registry and cluster in your environment — not just checked once at merge time and forgotten.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.