Find "dark" files in container images — files that exist in the image but are not tracked by the package manager database.
Dark files represent an unknown attack surface: they won't show up in vulnerability scanners that rely on package databases, they can hide malware or supply-chain tampering, and they signal image hygiene problems (leftover build artifacts, secret files, injected binaries).
- Auto-detects the distro from
/etc/os-release— no--distroflag needed - Supports Alpine, Wolfi/Chainguard, Debian/Ubuntu package databases
- Reports by file count and bytes — a 1,000-file shell script collection is less alarming than a single 200 MB injected binary
- Cross-references an SBOM (
--sbom) — fetches the image's SPDX SBOM attestation and excludes the files it documents from the dark set, reporting them separately - Detects vendored libraries (
--detect-libs) — scans binaries for statically-linked libraries using string-signature heuristics ported from cve-bin-tool - JSON output for integration with pipelines and dashboards
go install github.com/chainguard-sandbox/darkfiles2@latest
Or build from source:
git clone https://github.com/chainguard-sandbox/darkfiles2
cd darkfiles2
go build -o darkfiles .
Run darkfiles <image> to scan an image. By default it prints a statistics
summary; flags switch on more detailed views.
darkfiles alpine:latest
darkfiles debian:latest
darkfiles cgr.dev/chainguard/wolfi-base:latest
darkfiles --format json cgr.dev/chainguard/static:latest
Example output:
Image: alpine:latest
Distro: alpine
Total files: 416
Total size: 8.0 MiB
Tracked files: 411 (98.8%)
Tracked size: 8.0 MiB (100.0%)
Dark files: 5 (1.2%)
Dark size: 2.1 KiB (0.0%)
--detailed (-d) appends a breakdown of dark files grouped by the layer
(Dockerfile instruction) that introduced them, with size and mode:
darkfiles -d alpine:latest
--paths emits matching file paths, one per line:
darkfiles --paths debian:latest # unknown files (default)
darkfiles --paths --sizes debian:latest # add file sizes
darkfiles --paths --group debian:latest # group by category
Among dark files, executables, libraries, and scripts are the highest-signal subset — an unknown binary is far more concerning than a stray config file. darkfiles identifies them by content (ELF/Mach-O/PE/shebang/ar magic bytes, disambiguated by mode and path) and:
- adds a
Dark code:line to the summary (3 executables, 2 shared libraries), - tags each code file in the detailed/paths views (
[executable],[shared library],[script]), - highlights them in red in the
--detailedview when writing to a terminal.
Use --code to show only code files (composes with --set):
darkfiles -d --code img # dark binaries/libs/scripts, by layer
darkfiles --paths --code img # just their paths, for scripting
The JSON output includes a dark_code object with per-kind counts.
--sbom fetches the image's SPDX SBOM — published by DHI (Docker Hardened
Images) and similar builders as an in-toto attestation attached via the OCI
referrers API — and extracts every file path it records. Dark files that the
SBOM documents are not treated as dark: they are accounted for by the SBOM,
so they move into their own In SBOM bucket between tracked and dark:
darkfiles --sbom dhi.io/vault:2
Image: dhi.io/vault:2
Distro: debian
Total files: 1050
Total size: 432.4 MiB
Tracked files: 287 (27.3%)
Tracked size: 13.2 MiB (3.1%)
In SBOM: 644 (61.3%)
In SBOM size: 418.7 MiB (96.8%)
Dark files: 119 (11.3%)
Dark size: 536.8 KiB (0.1%)
The Dark file breakdown and Dark code lines then describe only the files
that remain unaccounted for — neither tracked by a package nor documented by the
SBOM. Tracked, In SBOM, and Dark partition every file in the image.
A registry-fetched SBOM is only trusted once its cosign signature is verified.
darkfiles verifies the SPDX attestation against Docker's published DHI signing
key. If verification fails — a non-DHI image, a missing
signature, or a bad one — the SBOM is not applied and the files stay dark,
with a warning. Override the key with --sbom-key <pem>, or skip verification
entirely with --insecure-sbom. A local --sbom-file is trusted as supplied and
is not verified.
Use --sbom-file <path> to cross-reference against a local SPDX file (an
in-toto statement or a bare SPDX document) instead of fetching from the
registry; this is required when scanning a --tar image. List the
SBOM-accounted paths with --set in-sbom:
darkfiles --sbom --paths --set in-sbom dhi.io/vault:2
darkfiles --sbom-file ./vault.spdx.json --tar ./vault.tar
The JSON output gains an in_sbom object ({count, bytes}) whenever an SBOM
was applied, and dark_files/dark_bytes exclude the SBOM-accounted files.
Statically linked libraries are often missed from SBOMs, resulting in another
kind of "dark matter". --detect-libs extracts printable strings from each
selected file and matches them against a database of ~450 per-library
signatures (ported from cve-bin-tool) to
recover which libraries — and, where possible, which versions — are baked in:
darkfiles --detect-libs --code img # scan dark code files
darkfiles --detect-libs --code --set all img # scan every code file
darkfiles --detect-libs --format json img # machine-readable results
It operates on the same selection as the other views (--set and --code), so
--detect-libs --code targets dark executables and libraries — usually what you
want. It is not limited to dark files: because it honours --set, you can
detect vendored libraries in package-owned binaries too:
darkfiles --detect-libs --code --set tracked img # only package-owned code
darkfiles --detect-libs --code --set all img # every binary, dark or not
Each detected library is listed under its file as library version(s) vendor(s).
Example:
/usr/bin/busybox
busybox 1.38.0 busybox
/usr/lib/libcrypto.so.3
openssl 3.6.4 openssl
Scanned 29 file(s); 27 with detected libraries.
This is signature-based detection, not a bill of materials: false positives
(e.g. a compiler build-id string reported as gcc) and false negatives are
inherent to the heuristic. Detection is based purely on the file's contents;
presence with no parseable version is reported as UNKNOWN. Tune string
extraction with --detect-libs-min-length.
The feature is off by default and reads full file content (a second pass over the image layers), unlike the metadata-only default scan.
The --set flag controls which files the --detailed and --paths views
operate on (the summary always reports on everything):
darkfiles -d --set unknown img # unrecognised dark files only (default)
darkfiles -d --set dark img # all dark files, incl. expected ones
darkfiles --paths --set tracked img
darkfiles --paths --set all img
--set |
meaning |
|---|---|
unknown |
unrecognised dark files (default) |
dark |
all dark files, including expected (pkg state, /dev, …) |
tracked |
files owned by a package |
all |
every file in the image |
in-sbom |
files accounted for by the SBOM (requires --sbom) |
docker save myimage:latest | darkfiles --tar /dev/stdin
darkfiles --tar ./myimage.tar -d --set dark
A file is dark if:
- It is not listed in any installed-package manifest (
/lib/apk/db/installed,/usr/lib/apk/db/installed,/var/lib/dpkg/info/*.list) - It is not a symlink whose fully-resolved target is a tracked file — this correctly handles multi-call busybox, merged-usr hierarchies, etc.
The percentage shown is dark_files / total_files and dark_bytes / total_bytes
independently, because a single large binary is more concerning than many tiny
config files.
There was an original chainguard-dev/darkfiles project that was archived. This is a rewrite that fixes several shortcomings and improves reporting.
- RPM-based images (RHEL, Fedora, Rocky): there is currently no support for RPM based distros.
- Multi-platform images: the tool pulls the platform that matches the host.