Skip to content

feat(sbom): generate file-based package SBOMs without docker.sock - #307

Draft
reyreavman wants to merge 6 commits into
mainfrom
feat/sbom/targeted-dir-scan
Draft

reyreavman wants to merge 6 commits into
mainfrom
feat/sbom/targeted-dir-scan

Conversation

@reyreavman

Copy link
Copy Markdown
Collaborator

Summary

SBOM generation for a stapel image with file-based packages no longer mounts
/var/run/docker.sock into the scanner container, so it now runs in environments
where 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

  • For a stapel image with file-based packages, syft runs once per directive against a dir: source containing only that directive's declared spec/lock files; the scanner container receives no /var/run/docker.sock mount.
  • The declared files are read from the built image with the same image-read mechanism os-pm uses, and materialized into a temporary directory with their workdir-relative layout (spec next to lock) so the directory cataloger can link them.
  • Per-directive BOMs are unioned and deduplicated by normalized PURL; the top-level BOM component is restored to the image (type=container, name=repository, version=tag) rather than the temporary scan directory syft reports for a dir: source.
  • Dockerfile images are unchanged: full-image docker: scan, docker.sock still mounted.
  • The SBOM artifact format version is bumped 3 -> 4: an image carrying a legacy full-scan SBOM regenerates it on the next build, while an unchanged rebuild reuses the cache.
  • Removed FilterBOMBySourcePaths and the cataloger filter modes (CatalogerFilterMode): a targeted directory scan surfaces no foreign components, so the post-scan filter is a no-op.
  • Does NOT change: os-pm collection, VEX, SBOM signing, skip-without-packages, multi-platform per-platform reads, or the ecosystem coverage (go-mod, pip, uv, poetry, npm, yarn, pnpm, cargo, lua-rock).
  • UNVERIFIED: component-set equivalence (name+version+PURL) with the previous full-scan+filter output rests on syft's directory-tag catalogers producing identical output on a 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=true and installs are frozen/locked, the spec/lock in
the 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.

@reyreavman

Copy link
Copy Markdown
Collaborator Author

Verification

  • Local (macOS): task build exit 0; task lint scoped to the changed packages (.golangci.yml) reports 0 issues; task test:unit across ./pkg/sbom/..., ./pkg/build/..., ./pkg/container_backend/... — 22 suites green.
  • Mutation: not run on the new tests. To settle them: break relativeToWorkdir so spec/lock no longer land adjacent → materialize_test.go layout assertion must fail; drop the SourceTypeDir branch in mapSbomScanOptionsToDockerRunCommanddocker_server_sbom_test.go "without docker.sock" case must fail.
  • Not run: the e2e sbom suites (test/e2e/sbom: inject/, stage_dependencies, packages_merge) — they assert component sets and are the equivalence oracle, but require Linux + Docker + kind and cannot run on the macOS dev host.

Review focus

  • pkg/build/sbom_step.go scanFileBasedPackages: per-directive union via MergeBOMs plus the explicit container-metadata override — confirm the dir: path does not drop a metadata surface a downstream SBOM consumer relied on from the old docker: scan.

Follow-up

  • BLOCKER: run task test:e2e paths="./test/e2e/sbom/..." labelFilter="sbom" and task test:integration on Linux/CI to settle the UNVERIFIED equivalence claim before merge.
  • Out of scope: the SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_CONTENTS / SYFT_FILE_METADATA_SELECTION env vars passed to the scanner do not exist in syft v1.45.1 (real keys are golang.main-module-version.from-contents / file.metadata.selection); left untouched here.

Comment thread pkg/build/sbom_step.go Outdated
Comment thread pkg/build/sbom_step.go
Comment thread pkg/build/sbom_step.go Outdated
Comment thread pkg/build/sbom_step.go
Comment thread pkg/sbom/managedinput/materialize.go
Comment thread pkg/sbom/managedinput/materialize.go Outdated
Comment thread pkg/sbom/managedinput/materialize.go Outdated
Comment thread pkg/sbom/managedinput/materialize.go Outdated
Comment thread pkg/container_backend/docker_server_backend.go
Comment thread pkg/container_backend/docker_server_backend.go
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
reyreavman force-pushed the feat/sbom/targeted-dir-scan branch from d0bc2a8 to 82ea3f9 Compare September 10, 2026 07:13
Comment thread pkg/build/sbom_step.go Outdated
Comment thread pkg/sbom/managedinput/materialize.go Outdated
Comment thread pkg/container_backend/docker_server_backend.go
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant