Repository navigation
feat(sbom): read which modules and crates a produced binary carries - #442
Conversation
A Go module and a Rust crate keep no files of their own in an image: they are compiled into a binary, so the binary's own metadata is the only place that knows they are there. Shipped-scope needs that answer per file to tell a component that is really shipped from one that only passed through the build. Classify a single binary from its bytes. The branches are ordered, and the order carries the correctness: - a test binary first. `go test -c` leaves no mark in the build info — not in the module path, and not in an empty dependency list, since the test binary inherits whatever the package under test imports. The path of the testing package's own source is in every one of them and a strip does not touch it; - wiped build info next, before the dependency list is read at all. An obfuscator leaves an empty list behind on a binary whose dependencies are alive, so reading that as knowledge would drop every one of them. Hold them all instead; - a non-empty list names the modules the linker put in, both sides of a replace among them; - an empty list on live build info is a main-only binary, and it holds nothing. Match modules by path, never by version: build info describes go.mod as it was when the binary was linked, and the SBOM describes it as it is now. Matching on version would turn that skew into a lost component. Split the error from reading build info by section: a binary carrying .gopclntab or .go.buildinfo is a Go binary that lost its metadata, so hold every module; anything else is not Go and holds no module at all. For Rust, read the .dep-v0 section cargo auditable writes and keep the runtime dependencies. The kind field is absent for a runtime dependency — the serialiser omits its default — so filter out build dependencies rather than filtering in runtime ones, which would silently drop the common case. Without that section nothing can be told about crates, so hold them all. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
Verification
Review focus
Follow-up
|
Review and a cross-check against the measurements behind the design found two ways a binary could silently lose every component it carries, which is the one failure direction this reader exists to avoid. The test-binary check matched the path of the testing package's own source. That string is in every binary linking the testing package, not only in one `go test -c` produced: gopls, mockgen and werf itself all carry it, and each would have been read as a test binary, holding nothing. Match the source path of the main `go test -c` generates instead — it is absent from all three and survives -trimpath and -ldflags="-s -w". The old needle does not survive -trimpath, so it also missed real test binaries. The wiped-build-info branch required an empty GoVersion. An obfuscator fills that field with a version string rather than clearing it, so the branch never fired and an obfuscated binary fell through to "no dependencies, holds nothing" with its dependencies alive. Test the module path and the settings, which are empty in that case and populated in every honest binary. Read Go and Rust independently rather than returning on the first match: a cgo binary linking an audited Rust archive carries the evidence of both, and whichever was read second was being dropped. A Go binary built without cgo now holds no crate instead of all of them, since it can link no Rust object. Resolve a file that claims to be an ELF but cannot be parsed, and a read that fails outright, to holding every component rather than none. Reject audit data whose recorded size exceeds the cap before reading it, because a compressed section is inflated inside Data() and the later limit would not apply. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
Second round — two silent-drop defects, both fixed in
|
reyreavman
left a comment
There was a problem hiding this comment.
Code Review Report
Base: main (ba4cc59c), head 756666c6
Diff: 3 files, +838/-0 — ClassifyBinary in pkg/sbom/shippedscope, no callers yet.
Context: shipped-scope concept §4 / Kaiten card D (71736124).
Verdict
- Technical: Branch order matches card D exactly and every "I don't know" resolves to holding, as the body promises. Three of four mutations fail the spec written for them. The fourth — overlap removed from the chunked scan — survives: the straddle spec places the needle at
chunk+8, fully inside the second read, so it guards the stride and not the overlap. A probe with the needle atchunk-5shows the overlap is load-bearing and untested. One perf gap: the whole-file scan runs on every ELF, including everylibc.so, before anything says the file is Go. - Product: The
_testmain.goneedle survives-trimpath -ldflags="-s -w"— verified by rebuilding fixtures with those flags. It is a string literal in thegotool itself (2 hits inbin/go), so an image shipping the Go toolchain readsgoas a test binary; acceptable, but should be in the godoc. Rust fail-open is per concept, but its aggregate effect needs stating for E: any non-Go, non-audited ELF in the image —bash,libc.so— saysUnknownfor crates, so every glibc image holds every crate forever. - Risk: No user surface. Risks are deferred to E and listed below.
DoD Criteria
From card D "Готово, когда" + PR body.
| Criteria | Inferred? | Met? | Evidence |
|---|---|---|---|
| Test binary recognised before anything else; mark survives strip | no | ✅ | binary.go:102-111; probe: -trimpath -s -w test binary → TestBinary=true, app → false |
Wiped build info → fail-open on whole go.mod, checked before Deps |
no | ✅ | binary.go:175; M2 fails spec :62 |
Deps ≠ ∅ → modules by path, both sides of replace |
no | ✅ | binary.go:179-188; spec :27 |
Deps = ∅ → main-only, holds nothing |
no | ✅ | spec :44 |
buildinfo.Read error split by .gopclntab/.go.buildinfo |
no | ✅ | binary.go:122-134; specs :225,:237 |
Rust: .dep-v0, runtime only, absent → fail-open |
no | ✅ | binary.go:136-157; M3 fails 2 specs |
| Mutation "test binary checked after Deps" fails a spec | no | ✅ | tool-linking-testing spec :129 would catch it (M1 analogue) |
Mutation "Deps=∅ as knowledge for garble" fails a spec |
no | ✅ | M2 |
| Mutation "match by version" fails a spec | no | goEvidence returns paths only; a version-matching mutation would change the return type — not a runtime-mutable claim. Fine. |
|
| Unit specs on fixture binaries per branch | no | ✅ | fixtures built by the suite from source; synthetic ELF for the rest |
task format/build/lint/test:unit green |
no | ✅ | build OK, lint 0, 45/45 |
Issues
Major
- Overlap in
carriesTestMainis untested; the spec that claims to test it cannot fail on its removal —binary_test.go:164-182. Inline.
Minor
- Whole-file scan on every ELF, Go or not —
binary.go:102. Inline. - Rust fail-open on every non-Rust ELF —
binary.go:144-146. Inline. Design note for E, not a defect here. gotool is read as a test binary —binary.go:55-57. Inline. Godoc.
Mutation log
| Mutation | Result |
|---|---|
M1: needle reverted to testing/testing.go |
✅ fails lets a tool that merely links the testing package… |
M2: wiped check on GoVersion == "" |
✅ fails …obfuscator left a version string behind |
M3: filter kind == "runtime" in, instead of build out |
✅ 2 fail |
M4: buffer = chunk (overlap removed) |
❌ 45/45 green — Major #1 |
Probes: -trimpath -ldflags="-s -w" fixtures classify correctly; needle at chunk-5 → false under M4, true unmutated. Tree restored after each; final 45/45; lint 0 issues.
Risks
| № | Risk | Type | Likelihood | Severity | Location | Circumstances | Consequences | Recommendation |
|---|---|---|---|---|---|---|---|---|
| 1 | Real test binary missed when _testmain.go lands on a 1 MiB boundary |
Technical | Unlikely | Medium | binary.go:224-233 |
Only if overlap regresses; today it is correct but unguarded | Test binary holds its modules → false KEEP in E | Major #1: fix the spec |
| 2 | Rust shipped-scope effectively off on every glibc image | UX/Product | Likely | Medium | binary.go:144-146 |
Any ELF that is neither Go-without-cgo nor audited Rust | E keeps every crate; SBOM as bloated for Rust as today | Minor #3: an "is this Rust at all" test, or document for E |
| 3 | Index time dominated by scanning C binaries for a Go needle | Operational | Likely | Low | binary.go:102 |
Image with many ELFs (/usr/lib/*.so) |
Seconds per image, scales with rootfs size | Minor #2: gate on carriesGoSections |
Not verified
- garble output (
Path=="" && Settings==∅) — no garble here; taken from the commit body's measurement. - Real
cargo auditablebinary — synthetic.dep-v0only; the JSON shape matches theauditable-serdeschema I know. - Unscoped
task lint/task test:unit— scoped to the package.
| It("finds the test main when it straddles two reads", func(ctx SpecContext) { | ||
| // The first read covers [0, chunk+overlap); a needle crossing its far edge is | ||
| // found only because the next read starts at chunk rather than where it ended. | ||
| const ( | ||
| chunk = 1 << 20 | ||
| overlap = len(testMainSource) - 1 | ||
| sectionHead = 64 + 3*64 | ||
| ) | ||
|
|
||
| padding := bytes.Repeat([]byte{'x'}, chunk+overlap-sectionHead-4) | ||
| binary := elfWithSections(map[string][]byte{ | ||
| ".rodata": append(padding, []byte(testMainSource)...), | ||
| }) |
There was a problem hiding this comment.
Major. This spec does not test what its name and comment say. With one section, data starts at sectionHead = 256; padding = chunk+overlap-256-4, so the needle starts at chunk + overlap - 4 = chunk + 7 and lies entirely inside the second read [chunk, 2·chunk+overlap). It therefore passes whether or not the buffer has an overlap:
M4: buffer := make([]byte, chunk) // overlap removed
→ 45/45 green
The overlap is load-bearing — probe with the needle actually crossing the edge:
needle at [chunk-5, chunk+7): unmutated → TestBinary=true, M4 → false
What this spec currently guards is the stride (offset += chunk, not chunk+overlap) — useful, but a different property, and the comment at :165-166 describes that one. Fix: put the needle so it straddles, e.g. padding := bytes.Repeat('x', chunk-sectionHead-len(testMainSource)/2), and keep the current placement as a second entry if you want the stride guarded too. A DescribeTable over needle offsets {chunk-1, chunk-len+1, chunk+7} reads well.
| } | ||
| defer file.Close() | ||
|
|
||
| testBinary, err := carriesTestMain(binary, size) |
There was a problem hiding this comment.
Minor (perf). The full-file scan runs before anything has said the file is Go. For an image rootfs that means every libc.so.6, bash, libssl.so — every ELF — is read end to end looking for a Go-only string. On a typical glibc image that is hundreds of MB of pure overhead for files that cannot be test binaries.
A go test -c binary always carries .gopclntab (-s -w strips symtab and DWARF, not pclntab — the runtime needs it), so gate the scan:
testBinary := false
if carriesGoSections(file) {
testBinary, err = carriesTestMain(binary, size)
…
}The truncated-ELF and read-failure specs still hold (they fail before or inside this branch respectively). Branch order from card D is preserved — the scan still precedes buildinfo.Read.
| // Nothing can be told about crates from a Rust binary built without cargo | ||
| // auditable, and a binary of another language is indistinguishable from one. | ||
| return VerdictUnknown, nil |
There was a problem hiding this comment.
Minor (design note for E, not a defect here). Per concept this is right — no .dep-v0, nothing can be said. But think about the aggregate: rule E will see one verdict per produced file, and every bash, dash, libc.so.6, libstdc++.so in the image answers Unknown for Rust. Unless E restricts classification to the produced set (card D's diff(install) ∪ …) and that set never contains a C binary, every glibc image holds every crate forever and Rust shipped-scope is a no-op.
Two cheap ways to narrow it, either now or in E:
- an "is this Rust at all" test analogous to
goSections: release Rust binaries keep/rustc/<hash>/library/paths in panic strings and a.rustc-less but recognisablerust_eh_personality/rust_begin_unwindsymbol even after strip (-skeeps dynamic symbols; static binaries keep neither — fallback to the path needle); - or state in the godoc that
Rust == Unknownfrom a file without Go sections means "not evidence either way" and let E count only files that returnedNamesfor Rust.
Either way, write down which one E is expected to do; otherwise the first measurement on a real image will look like a bug in this reader.
| // The generated main of a `go test -c` binary. Its own source path is the only mark | ||
| // that tells a test binary from a tool that merely links the testing package. | ||
| testMainSource = "_testmain.go" |
There was a problem hiding this comment.
Minor. The needle is a string literal in cmd/go/internal/load — the go tool itself carries it:
$ grep -c -a _testmain.go $(go env GOROOT)/bin/go
2
(gofmt, golangci-lint, werf — 0.) So an image shipping the Go toolchain reads /usr/local/go/bin/go as a test binary and it holds none of its modules. For a builder image that is harmless — the toolchain is not something E should keep modules for anyway — but it is a known false positive of a whole-file substring scan, and the next person to hit it will think the reader is broken. One sentence in the godoc of ClassifyBinary or on this constant.
The spec that claimed to cover the overlapping reads placed the mark across the far edge of the first read, which the next read covers whole. Removing the overlap left it green: it guarded the stride and not the overlap. Place the mark across both boundaries — the stride, which only a buffer longer than the stride spans, and the far edge, which only a read starting back at the stride spans. Each boundary now has a mutation that fails it. The scan read every ELF end to end before anything established the file was Go, so an image full of shared libraries paid for a Go-only question on each of them. Only a Go binary can be a test binary, so gate the scan on the Go sections. A binary of another language that happens to quote the mark is no longer read as a test binary either. Record in the godoc what the mark cannot distinguish: the `go` tool names the file it generates, so it reads as a test binary. Record what the Rust fail-open costs in aggregate — one unaudited ELF makes an image hold every crate — since that is the part task E has to live with. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
Third round —
|
reyreavman
left a comment
There was a problem hiding this comment.
Code Review — follow-up on ef6e9335
Delta since 756666c6: 3 files, +67/-25.
Verdict
No findings. All four items from the previous round are closed and each is now guarded by a mutation that fails for the right reason.
| # | Finding | Status |
|---|---|---|
| Major 1 | overlap untested | ✅ DescribeTable with the mark at chunk-4 and chunk+overlap-4; elfWithTestMainAt places it by absolute offset |
| Minor 2 | scan on every ELF | ✅ gated on carriesGoSections (binary.go:102); new spec does not read a binary of another language as a test binary |
| Minor 3 | Rust fail-open in aggregate | ✅ stated in the comment at binary.go:148-152 for E to live with |
| Minor 4 | go tool reads as a test binary |
✅ godoc on carriesTestMain |
Mutation log
| Mutation | Result |
|---|---|
buffer = chunk (overlap removed) |
✅ fails across the stride boundary |
stride = chunk+overlap |
✅ fails across the far edge of the first read |
scan gate if true |
✅ fails does not read a binary of another language… on TestBinary being true |
47/47 otherwise; tree restored after each.
Nothing outstanding on this PR from my side.
…less ELF The `go` tool writes the generated test main's path as a literal, so the one mark that told a test binary apart also matched the toolchain itself, and an image shipping it would hold no module on its account. That is a drop, not the safe direction the godoc called it. Require the test runner's own function alongside: measured on go1.24.9, the toolchain carries the path and not the runner, a test binary carries both in every build mode, and no production tool at hand carries either. Sections were the only thing telling "not Go" from "Go I cannot read", so a binary stripped of its section headers was read as not Go and held nothing. Scan it for the test marks regardless, and hold every module when its build info cannot be read, as the Rust side already did. Reject a non-positive size instead of silently skipping the scan, wrap the errors leaving ClassifyBinary, and cap the inflated side of the audit data as well as the stored one: the two sizes are unrelated, and the previous cap truncated silently. Confirm the garble branch against a binary garble built here on go1.27: GoVersion="unknown", Path="", Settings=0, Deps=0. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
Fourth round —
|
| binary | _testmain.go |
testing.tRunner |
|---|---|---|
go test -c output, plain / -trimpath / -s -w / both |
1 | 1–2 |
bin/go |
2 | 0 |
werf, gopls, mockgen, ginkgo, golangci-lint, dlv |
0 | 0 |
Each mark has a witness that carries it alone — the toolchain for the path, a binary embedding a test framework for the runner — so neither suffices by itself, and both have a spec that fails when the condition degrades to one mark.
Major — an ELF stripped of its section headers dropped all Go modules. goEvidenceOf used the Go sections as the only thing telling "not Go" from "Go I cannot read"; a packed or objcopy --strip-section-headers'd binary parses fine with zero sections and fell into "not Go → holds nothing", while the Rust side in the same situation already returned Unknown. A sectionless binary now holds every module and is scanned for the test marks regardless. The remaining asymmetry is named in the godoc: a sectionless test binary whose build info is still readable holds too much, not too little.
Minors. size <= 0 is rejected instead of silently skipping the scan; errors leaving ClassifyBinary are wrapped; the audit-data cap now covers the inflated side as well as the stored one — the two are unrelated, and the previous LimitReader truncated silently so a bomb and data that fits were indistinguishable. A fixture whose stored size is well under the cap and inflates past it has a spec, and if false in place of the check fails it.
Open question from the review — garble, settled by measurement rather than by the concept's table. Installed go1.27, built the fixture with garble build, read it with debug/buildinfo: GoVersion="unknown" Path="" Main="" Settings=0 Deps=0. The branch condition Path == "" && len(Settings) == 0 matches the real output.
Open question — CGO_ENABLED=0 → no crates. Holds for linkage: a pure-Go binary cannot link a Rust object. A pure-Go binary can go:embed a Rust-built artifact; that artifact is then a file of its own in the image, or inside a Go binary we do not unpack. Either way it is not this classifier's call — recorded as a limit for card E, not changed here.
Verification
- Eight mutations, all killed with compiling mutants — the previous round's harness read a compile failure as a kill, so each mutation here was re-formed until it compiled: one-mark-suffices (×2), gate without the sectionless case, gate removed, inflated cap removed,
size < 0, overlap zeroed, stride without overlap. task build, unscopedtask lint— 0 issues../pkg/sbom/...on buildah-commons (linux/amd64, cgo): green;shippedscope53/53. The fulltask test:uniton the VM has one failure inpkg/build/image(build_context_lfs_test.go:51); it fails identically onorigin/mainin a fresh worktree, so it is pre-existing on that host and not this diff.
Review focus
carriesTestMarks: both needles are looked for in one pass over the same overlapping buffer;overlapis the longer needle's length minus one, so each boundary spec still guards what it says.withoutSectionHeaderszeroese_shoff/e_shnum/e_shstrndxthe way a packer leaves them;debug/elfopens the result with zero sections, which is the real-world input the branch is for.
reyreavman
left a comment
There was a problem hiding this comment.
Code Review — follow-up on 0e21c6f6
Delta since ef6e9335: 3 files, +149/-26.
Verdict
No findings. The commit turns my Minor #4 (go tool reads as a test binary, "safe direction") into a proper fix — and it is right to: holding nothing for a shipped toolchain is a drop. The two-mark rule is verified against real binaries and a stripped test build; the sectionless-ELF and inflated-cap changes are each guarded by a spec that fails on reversal.
Verified
| Claim | How | Result |
|---|---|---|
go carries the path and not the runner |
grep -c -a on $GOROOT/bin/go |
_testmain.go=2 testing.tRunner=0 |
| no production tool carries either | same on werf, golangci-lint, gofmt |
all 0 / 0 |
both marks survive -trimpath -ldflags="-s -w" |
probe: go test -c with those flags, GOOS=linux |
main=true runner=true TestBinary=true |
Mutation log
| Mutation | Result |
|---|---|
only _testmain.go required |
✅ fails lets the go tool keep its modules… |
len(file.Sections) == 0 removed from both sites |
✅ 2 fail — sectionless test binary, sectionless unreadable build info |
| inflated-size cap removed | ✅ fails refuses audit data that inflates over the cap… |
53/53 otherwise; tree restored after each.
Note for E
A sectionless ELF of any language is now scanned end to end and holds every module when its build info is unreadable. That is the right fail-open, but it means a UPX-packed or sstrip-ed C binary in the produced set costs a full read and keeps the whole go.mod. Rare in practice; mentioning it so E's measurements have the explanation ready.
Nothing outstanding on this PR from my side — I would approve.
## Summary Shipped-scope now has its decision: given what an image's SBOM says each component ships, what every binary the build produced carries, and what files the image actually holds, the rule says per component whether the image ships it, and why. Nothing calls it yet; it is the judgment that marking (card F) will apply. ## What ### Binary-backed components (Go modules, Rust crates) - A module is held when any produced binary names its import path; a crate when any produced binary names its name and version. One binary legitimately names many, so no exclusivity applies. - Any produced binary whose Go (or Rust) evidence reads `unknown` holds every module (or crate): "I don't know" is never "not shipped". - A test binary contributes nothing, whatever it names, and does not fail anything open either. - A produced set that could not be established (`nil`) holds every binary-backed component as `no-data`; a produced set known to be empty holds none — an image that declares `go.mod` but never runs `go build` has all its modules dropped as `not-linked`. - Go and Rust evidence are read independently: a cgo binary naming its modules while carrying no audit data holds its modules exactly and every crate. ### File-backed components (os-pm, Python, JavaScript, Lua) - A component is held as `content` when a file of its own is in the image byte for byte, provided no other component of the same ecosystem claims those bytes. - Ownership is counted by `(name, version)`: five identical copies of `debug@2.6.9` under different `node_modules` are one owner and do not block each other. - Ownership is counted within an ecosystem, not across: a deb shipping an npm tree and the npm packages in that tree both hold their files. - A component whose every matching file is shared is held as `content-shared` only when no co-owner proved itself by a file of its own; otherwise it is dropped. A stale `openssl` sharing `c_rehash` with the fresh one is dropped once the fresh one is proven by `libssl.so.3`. - A zero-size file proves nothing. - A component none of whose bytes match is held as `path` when one of its declared paths still exists in the image and no proven component explains it: a certificate bundle regenerated after install stays held. - Failing that, as `name` when a file named like one of its exists unexplained, no other component declares that name, and the name is not one every package carries (`LICENSE`, `README`, `package.json`, `__init__.py`, …). - Path and name look only at files no proven component explains, the symlinks of proven components included: a stale version is not rescued by the path or symlink the fresh version now owns. - A component nothing is known about (`Occurrences == nil`) is held as `no-data`; one whose files were looked for and none found (`Occurrences` empty) is dropped as `no-match` — these are different answers and the rule keeps them apart. - Every drop is one debug log line: `not shipped <ecosystem> <name>@<version>: checked N files, 0 matches`. ### Output - Decisions come back in the order of components, each with the reason and the image paths that proved it, so a consumer can record where the component is in this image rather than where it was in the donor. - No user-visible surface changes: no flag, no log at default level, no change to any SBOM werf produces today. - VERIFIED: the concept's end-to-end stand (17 components across Go, Python, JavaScript and os-pm, built from a builder image into a distroless final) reproduces its expected verdicts as a unit spec over the same components and files. ## Why Shipped-scope cuts an SBOM to what the final image ships. The index of the image's files (#433), each installed component's own files (#435) and what every produced binary carries (#442) are the evidence; this is the first place the evidence meets a component and yields an answer. Without it the SBOM lists everything the build touched, build-only tooling included, and every vulnerability in that tooling is reported against an image that does not contain it. Three decisions here depart from the Python prototype the concept was validated on. The prototype held a component with no exclusive file unconditionally; here it is held only when no co-owner is proven, because the real source of such components is a stale version next to a fresh one, and a fail-open there hides exactly what marking before deletion is meant to surface. The prototype matched Go modules by binary hash; here by name, because the classifier (#442) reads the module list out of the binary and the binary is read live from the image being matched, so no donor hash is needed. The prototype had no path or name tier; here both, because a file rewritten in place after install — `update-ca-certificates`, `compileall` — defeats the content check for a component that is unquestionably shipped. --------- Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
🤖 I have created a release *beep* *boop* --- ## [3.11.0-dk.1](v3.10.0-dk.3...v3.11.0-dk.1) (2026-10-20) ### Features * **build:** publish built images to multiple final repositories ([werf#8087](https://github.com/deckhouse/delivery-kit/issues/8087)) ([9a30573](9a30573)) * **build:** publish cached images with require-built-images ([93776dc](93776dc)) * **sbom:** decide which components an image actually ships ([#452](#452)) ([69de584](69de584)) * **sbom:** read which modules and crates a produced binary carries ([#442](#442)) ([11a39f6](11a39f6)) * **sbom:** record which files each component of an image consists of ([#453](#453)) ([fcc8e93](fcc8e93)) ### Bug Fixes * **build:** avoid panic on cache-repo errors without final-repo ([werf#8089](https://github.com/deckhouse/delivery-kit/issues/8089)) ([f553a94](f553a94)) * **build:** reduce native build delays on populated image stores ([werf#8064](https://github.com/deckhouse/delivery-kit/issues/8064)) ([d7e8f38](d7e8f38)) * **build:** retain actionable check-mode diagnostics ([93dc456](93dc456)) * **cleanup:** limit registry credentials to deletion commands ([werf#8086](https://github.com/deckhouse/delivery-kit/issues/8086)) ([5fe00b3](5fe00b3)) * **deploy, plan:** respect `--releases-history-max` with `--use-plan` ([werf#8085](https://github.com/deckhouse/delivery-kit/issues/8085)) ([9bb995b](9bb995b)) * **deploy, tracking:** honor `$WERF_NO_POD_LOGS` ([werf#8091](https://github.com/deckhouse/delivery-kit/issues/8091)) ([97866b8](97866b8)) * **deploy:** respect `--no-pod-logs` with `--use-plan` ([werf#8092](https://github.com/deckhouse/delivery-kit/issues/8092)) ([ffe489e](ffe489e)) ### Miscellaneous Chores * **release:** pin upstream base to 3.11.0-dk.1 ([e955d96](e955d96)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Shipped-scope needs to know which components a produced binary carries, because a Go module and a Rust crate keep no files of their own in an image — they are compiled in, and the binary's own metadata is the only witness. This adds that reader: given the bytes of one file, it answers which modules and crates that file holds, or says it cannot tell. Nothing calls it yet; it is the input the matching rule will consume.
What
go test -cbinary speaks for no component at all, even when its dependency list is full: it is recognized before anything else is read, by two marks together — the source path of the main thatgo test -cgenerates, and the test runner's own function. Each mark alone has a false witness: thegotool writes the path as a literal, and a binary embedding a test framework reaches the runner; so a shipped Go toolchain, a tool that merely links the testing package — gopls, mockgen, werf itself — and a framework-embedding binary all keep their modules. Both marks survive-trimpathand-ldflags="-s -w".go.mod, not none: an empty dependency list is checked only after that case is ruled out, so an obfuscated binary with live dependencies does not drop them. The test is an empty module path and empty settings, because an obfuscator writes a version string rather than clearing it.replaceamong them, so a forked module answers under the upstream path and the fork path alike.go.modas of the link, the SBOM describes it as of now, and matching versions would turn that skew into a component silently dropped from the SBOM..gopclntabor.go.buildinfoholds every module; a binary carrying neither is not Go and holds no module. A binary stripped of its section headers altogether says neither, so it holds every module and is scanned for the test marks regardless.cargo auditableholds the crates its.dep-v0section calls runtime dependencies; build dependencies are excluded. Absent, unreadable or corrupt audit data holds every crate instead, as does audit data over the 8 MiB cap by stored or inflated size — except on a Go binary built without cgo, which can link no Rust object and so holds none.Why
The shipped-scope rule cuts an SBOM down to what a final image actually ships. For file-backed ecosystems the files themselves are the evidence, but a compiled binary erases that: after
go build, nothing on disk says which modules went in. Without this reader the rule has two bad options — keep every module ofgo.mod, which leaves the SBOM as bloated as it is today, or drop modules on a guess, which is worse than bloat because a vulnerable component disappears from the report.The alternative worth arguing for is matching binaries by version as well as path, which looks stricter. It is not: the binary records the dependency graph frozen at link time, while the SBOM is generated from the current source tree, so any rebuild skew — a bumped dependency, a
+dirtysuffix, a replace pointing at a local path — makes the version differ and the component vanish. Path alone is the stable key; the version in the SBOM is already authoritative.