feat(sbom): generate file-based package SBOMs without docker.sock - #307
Draft
reyreavman wants to merge 6 commits into
Draft
reyreavman wants to merge 6 commits into
reyreavman wants to merge 6 commits into
Conversation
Collaborator
Author
Verification
Review focus
Follow-up
|
reyreavman
commented
Sep 10, 2026
Previously the SBOM of a stapel image with file-based packages was produced by running syft against the whole image filesystem via a docker: source. That required mounting /var/run/docker.sock into the scanner container and then post-filtering the resulting BOM down to the declared spec/lock files, so SBOM generation could not run where the docker socket is unavailable or forbidden. Now every packages directive is scanned on its own: its declared spec and lock files are read from the built image (the same image-read mechanism os-pm uses), materialized into a temporary directory with their workdir-relative layout, and scanned with a dir: source and the directive's cataloger — one scanner run per directive, with no docker.sock mount. The per-directive BOMs are then unioned. Because the targeted scan only ever sees the declared files, the redundant post-scan FilterBOMBySourcePaths pass and its cataloger filter modes are removed. Dockerfile images keep the full-image docker: scan. The SBOM artifact format version is bumped 3 -> 4 so images carrying a legacy full-scan SBOM regenerate it, while unchanged rebuilds still reuse the cache. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
Addresses review findings on the targeted directory-scan SBOM path. Materialize each declared spec/lock under its full in-image path instead of a workdir-relative one, so a dir source records the real location (/app/api/go.mod) rather than a path that never existed in the image (/go.mod). The rebase is anchored and cleaned so a "..", a leading slash or a relative source path cannot escape the scan directory. The scan layout now depends only on the source paths, so Cataloger.Workdir — and the cache-key ambiguity of it being absent from the scan-command checksum — is removed. Restore only the container component of the scanned BOM's metadata and keep syft's own tools and timestamp; replacing the whole metadata dropped the timestamp, and a per-image SBOM without one is rejected by the ISPRAS validator. Stamp the skip-scan branch with a timestamp too. A directory source makes syft emit a PURL-less type=file component for each scanned manifest, which dedup never removes; drop these after every per-directive scan so only real packages remain, which is what keeps the post-scan source-path filter safely removed. Record that SYFT_FILE_METADATA_SELECTION=none is required for this — the scanner honors it, contrary to the task note. Wrap the materialize error, guard the per-directive union against an empty slice, and document its first-directive-wins dedup order. Add a unit test for scanFileBasedPackages, and document the directory scan and the build-fails-on-a -missing-declared-file behavior in the SBOM docs (en/ru). Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
reyreavman
force-pushed
the
feat/sbom/targeted-dir-scan
branch
from
September 10, 2026 07:13
d0bc2a8 to
82ea3f9
Compare
reyreavman
commented
Sep 10, 2026
Second review pass follow-ups on the directory-scan SBOM path. MkdirTemp, MkdirAll and WriteFile are all umask-subject, so under a restrictive umask (e.g. 077) the scan root and the nested per-path directories materialized for every scan were 0700 — not traversable by a non-root scanner container, contradicting the function's own world-readable contract. Force the whole materialized tree world-readable (directories also executable) in one pass after writing, instead of the piecemeal chmods that missed the MkdirAll directories. A test under umask 077 pins the intermediate app/, app/api/ directories, which the previous permission assertions (scan root and file only) passed for the wrong reason — a default 022 umask already yields 0755. Extract the image-metadata restoration into restoreImageMetadata and cover it with a unit test: replacing only the container component while keeping syft's tools and timestamp, and stamping a timestamp when the BOM has none. The previous test asserted timestamp survival only through the per-directive union, leaving the actual restoration in ConvergeWithMerge unpinned; reverting it to replace the whole metadata now fails a test. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
A stapel image with file-based `packages` failed the build when a declared lock file was not present in the image. go-mod always declares go.sum (the ecosystem's DefaultLockFile), but a module with no dependencies produces none, so the targeted directory scan aborted with: materialize inputs for cataloger "go-module-file-cataloger": read /app/go.sum ...: Could not find the file /app/go.sum in container ... The old full-image scan tolerated this — it simply did not catalog a file that was not there. The targeted scan read every declared path and hard-failed on absence. Split a cataloger's inputs into required and optional: the spec (e.g. go.mod) must be present and still fails the build with a directive-and-path error when missing, while the lock (e.g. go.sum) is best-effort — an absent one is skipped, restoring the previous behavior. Optional paths join the scan cache key so a declared lock still contributes to cache identity. A unit test covers a go-mod directive whose image has go.mod but no go.sum: materialization succeeds and skips the lock. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
The "Data Source Connection Method" and "Path in Source Image" rows described a Dockerfile full-image scan (daemon + socket via volume, OS root). SBOM is not supported for Dockerfile images — validateSbomOnlyWithStapelImages rejects them — so that path never runs. Describe what actually happens: for stapel images with file-based packages, a directory scan of the spec/lock files extracted from the built image, without the Docker socket. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
An absent lock file is tolerated so that a module with no dependencies builds, but the same skip also fires when a lock that should exist is gone — removed by a later stage, or present only as a symlink. In that case the scan sees the spec alone and transitive dependencies silently drop out of the SBOM, with the only trace at debug level. Surface the skip as a warning naming the lock path, image and cataloger, so a missing lock is visible in the build output. The message states that the absence is expected for a project without dependencies, since for Go that is the normal state (go.sum is only written when there are modules to verify), so the warning stays truthful for both cases. A unit test pins the warning. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SBOM generation for a stapel image with file-based
packagesno longer mounts/var/run/docker.sockinto the scanner container, so it now runs in environmentswhere the docker socket is unavailable or forbidden. syft is invoked per packages
directive against a directory source built from only the declared spec/lock files,
instead of walking the whole image filesystem and post-filtering the result.
What
packages, syft runs once per directive against adir:source containing only that directive's declared spec/lock files; the scanner container receives no/var/run/docker.sockmount.dir:source.docker:scan, docker.sock still mounted.FilterBOMBySourcePathsand the cataloger filter modes (CatalogerFilterMode): a targeted directory scan surfaces no foreign components, so the post-scan filter is a no-op.dir:source; the e2e sbom suites are the oracle and did not run on the macOS dev host.Why
The
docker:source forced a docker.sock mount and a whole-image filesystem walk,then discarded everything outside the declared spec/lock with a post-filter — costly,
and impossible where the socket is forbidden. Because network is disabled for shell
stages under
build.sbom.enable=trueand installs are frozen/locked, the spec/lock inthe image are byte-identical to source, so scanning just those files is equivalent to
the old scan-then-filter. The rejected alternative was syft-as-a-library, which would
add an external dependency; this keeps the same pinned scanner image and the existing
image-read path, adding no new dependency.