sbom: add opt-in SBOM generation and SBOM-based vulnerability scanning#27455
Conversation
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
6f8e36f to
22032d4
Compare
|
/azpw run |
|
Retrying failed(or canceled) jobs... |
|
No Azure DevOps builds found for #27455. |
22032d4 to
82a7905
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
82a7905 to
afa0659
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
1950da6 to
f36637c
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azpw run |
|
Retrying failed(or canceled) jobs... |
|
No failed(or canceled) stages or jobs found in the most recent build 1118358. |
f36637c to
b8e93cc
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
b8e93cc to
decd2be
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
decd2be to
a4ba30e
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
securely1g
left a comment
There was a problem hiding this comment.
Review on behalf of @lguohan:
Summary
Comprehensive SBOM framework for SONiC — CycloneDX 1.6 + SLSA provenance + OpenVEX + vulnerability scanning via grype. This is a significant infrastructure addition.
Architecture Impressions
The layered approach is well-designed:
- Per-artifact fragments (
sbom_fragment.py) emitted at recipe-build-time - Aggregator (
build_sbom.py) merges fragments + scanner cross-check - Post-build vuln scan (
sbom_vuln_scan.py) — deliberately decoupled from build - VEX for patch-fix suppression — both auto-extracted and curated
- Lockfile parsing for transitive Rust/Go/npm deps
- License resolution from DEP-5 copyright files
The separation of SBOM (reproducible, build-time) from vulnerability report (time-dependent, post-build) is the right architectural choice.
Strengths
- Opt-in via
ENABLE_SBOM=y— zero overhead for non-SBOM builds - cargo-auditable integration — embeds resolved Cargo.lock in ELF binaries, then extracts per-.deb
- Multi-source merge with recipe-emit priority > observation > lockfile > scanner — correct precedence
- Fail-soft by default (
SBOM_STRICT=n) — SBOM issues never break a build unless explicitly opted in sbom_diff.pyfor reproducibility verification between build hosts- Thorough documentation in code comments and
vex/README.md
Questions / Concerns
-
Build time impact — Even with
ENABLE_SBOM=y, how much wall-clock time does this add? The per-.debdpkg-deb -x+ ELF walk + rust-audit-info could be significant across ~120 debs. Any benchmark data? -
Slave Dockerfile changes across all distros (buster/bullseye/bookworm/trixie) — the cargo-auditable + rust-audit-info + cargo-wrapper addition increases slave image size and build time. Is this gated by ENABLE_SBOM or always installed?
-
cargo-wrappershim (/usr/local/bin/cargo) — This intercepts ALL cargo invocations. What happens duringcargo test,cargo clippy,cargo fmt? Does it only wrapcargo build? -
Scanner tool pinning (
install_sbom_tool.sh) — SHA-256 verification is good. Who owns version bumps? Should these be in a separate versions file for easier tracking? -
License map completeness —
sbom_license_map.jsoncovers common licenses but will inevitably miss exotic ones. Is there a process for community contributions to this map? -
syft OCI-archive scanning — The comment says "SONiC docker .gz files are OCI archives" — but SONiC uses
docker save | gzip, which produces docker-archive format, not OCI. This may cause syft to silently return zero components. Please verify. -
VEX auto-extraction confidence —
sbom_extract_vex_from_patches.pymarks loose CVE mentions asunder_investigation. How do you prevent false-positive VEX suppressions from leaking into production scans? Is there a review gate?
Minor Issues
sbom_fragment.pyline ~1000:dict[str, Any]type hint requires Python 3.9+ — fine for bookworm+ but breaks on buster (3.7). ConsiderDictfrom typing.- The
prepare_docker_buildinfo.shchange copiescargo-wrapperonly forsonic-slave-*images — the condition check looks correct.
Overall
This is production-quality work with excellent documentation. The architecture is sound and extensible. Main concern is verifying build-time overhead and the docker-archive vs OCI-archive distinction for scanner accuracy.
LGTM with the above clarifications addressed.
|
Thanks for the detailed review. Going through the questions: 1. Build time impact — Good question. We have instrumentation in place (per- 2. Slave Dockerfile gating — 3. cargo-wrapper scope — Only wraps 4. Scanner tool pinning — Versions + SHA-256s are inlined in 5. License map — 6. syft OCI vs docker-archive — SONiC's docker 7. VEX auto-extraction confidence — Two-tier in Minor — `dict[str, Any]` PEP 585 — Not relevant in practice: only bookworm (3.11) and trixie (3.13) slaves are in active use; buster/bullseye Dockerfile edits were added for completeness but the SBOM emission paths don't run there. Will follow up with the benchmark numbers. |
263fc34 to
197ceb5
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Follow-up on the build-time question — we measured the SBOM-induced cost directly via temporary per-phase timing instrumentation added during development. That instrumentation was removed before final submission (only the optimizations themselves are shipping); the numbers below are from those measurement runs. Per-phase attribution:
Notes:
|
…ation
Adds opt-in CycloneDX 1.6 SBOM generation, SPDX 2.3 conversion,
SLSA v1.0 in-toto provenance, and SBOM-based vulnerability scanning
for SONiC images. Default builds are unchanged; SBOM emission is
opt-in via ENABLE_SBOM=y at build time.
Design summary
==============
Hybrid SBOM aggregation pulls from four independent sources and
deduplicates by PURL + (name, version, arch) + (name, normalized
version, arch). Recipe-emit wins ties because it carries pedigree
and patch-set provenance that scanners can't recover from the
shipped binary alone.
* Recipe-emit fragments: slave.mk hooks every recipe that produces
an artifact in scope (SONIC_DPKG_DEBS, SONIC_MAKE_DEBS,
SONIC_DERIVED_DEBS, SONIC_EXTRA_DEBS, SONIC_ONLINE_DEBS,
SONIC_PYTHON_STDEB_DEBS, SONIC_PYTHON_WHEELS, docker-image-save).
scripts/sbom_fragment.py detects four fork-of-upstream patterns
(sonic-net submodules, dget+patches, nested submodule a la FRR,
direct-upstream-submodule + sidecar patches) and emits
pedigree.ancestors[] accordingly. Each fragment also carries the
recipe's $(pkg)_DEPENDS and $(pkg)_RDEPENDS declarations as
sonic:build_depends / sonic:runtime_depends string properties.
* Observation harvest: src/sonic-build-hooks/scripts/collect_version_files
is extended (gated on ENABLE_SBOM=y) to emit a 9-column TSV per
container (Package, Version, Architecture, Source, SourceVersion,
Maintainer, Homepage, Filename, SHA256) using one bulk
`xargs apt-cache show` per container, plus snapshots of
/usr/share/doc/*/copyright and language lockfiles (go.sum,
package-lock.json, pnpm-lock.yaml, yarn.lock). Cargo.lock is NOT
harvested here — see Rust crate visibility below for the
authoritative source.
* Rust + Go + Python attribution at the .deb level: every SONiC-
built .deb gets `dpkg-deb -x` extracted into a tempdir at
fragment-emit time, with three harvesters running against the
unpacked tree:
- Rust: rust-audit-info reads the .dep-v0 ELF section that
cargo-auditable embedded at compile time. Per-binary precision
on the crate set actually linked into the final binary; emits
pkg:cargo/<name>@<version> components.
- Go: `go version -m <binary>` parses the .go.buildinfo section
embedded by `go build`. Replacement directives (`=>`) honored.
Emits pkg:golang/<module>@<version> components.
- Python: walk for *.dist-info/METADATA files (the modern wheel-
install marker), parse Name+Version, emit pkg:pypi/<name>@<ver>
with PEP 503 name normalization. SONiC .debs today install
Python content into /usr/lib/python3/dist-packages/ without
dist-info, so this harvester is dormant on current builds —
it activates automatically when any recipe starts bundling a
venv or pip-installing wheels into debian/<pkg>/.
Each emitted component carries sonic:fragment_kind=recipe-emit-
{rust,go,python} and sonic:source_deb=<deb filename>, so a
consumer can trace each crate/module/distribution back to the
exact artifact that shipped it.
* Lockfile expansion: scripts/sbom_parse_lockfiles.py parses
lockfiles harvested by collect_version_files (go.sum,
package-lock.json, pnpm-lock.yaml, yarn.lock) for ecosystems
the per-.deb introspection above doesn't already cover.
* Image-level aggregator: scripts/build_sbom.py merges fragments +
observations + lockfiles + scanner output. Version normalization
strips epoch prefixes (1:) and downstream suffixes (+fips,
+sonic, +bN, +debNuM) so the recipe-emit and observation entries
collapse to one component. Emits CycloneDX `dependencies[]`
edges from each fragment's bom-ref to its declared build/runtime
deps (resolved through sonic:build_depends / sonic:runtime_depends
→ sibling fragment bom-refs), and from each .deb to its
attributed Rust crates / Go modules / Python distributions.
Unresolved declared-deps land on the component as a
sonic:unresolved_deps property for audit.
* License resolution: scripts/sbom_resolve_licenses.py parses DEP-5
debian/copyright; falls back to licensecheck for free-form
copyrights. A bundled scripts/sbom_license_map.json (~100
entries) translates Debian License header strings to SPDX
identifiers. Maintenance instructions in README.sbom.md.
* SOURCE_DATE_EPOCH is set from the HEAD git commit time in
Makefile.work and exported through to the slave container, so
two builds of the same source produce byte-identical SBOMs.
* Strict mode (SBOM_STRICT=y, default when ENABLE_SBOM=y) fails
the build when a critical input is missing (host rootfs, declared
installer dockers, scanner binary). Per-recipe sbom_emit_fragment
and sbom_emit_per_container honor SBOM_STRICT consistently: when
set, fragment-emit failures abort the recipe; when n, failures
are swallowed (defensive against script bugs only).
Output filenames track the actual installer artifact via the
SBOM_TARGET_ARTIFACT env var. The slave.mk recipe iterates over
$*_MACHINE + $*_DEPENDENT_MACHINE and derives the per-variant
artifact name as $(subst $($*_MACHINE),$(dep_machine),$*), so each
asic variant (and each installer format) gets its own sidecar:
target/sonic-broadcom.bin.cdx.json + .spdx.json + .intoto.json
target/sonic-broadcom-dnx.bin.cdx.json + ...
target/sonic-broadcom-legacy-th.bin.cdx.json + ...
target/sonic-aboot-broadcom.swi.cdx.json + ...
target/sonic-vs.img.gz.cdx.json + ...
Vulnerability scanning
======================
scripts/sbom_vuln_scan.py runs grype against a CycloneDX SBOM,
applies VEX statements under vex/, and emits a CycloneDX
VEX-annotated report plus a human-readable table. Policy is
configurable (--min-severity, --fail-on, --ignore-unfixed).
Standalone Python; not a make target.
* scripts/sbom_extract_vex_from_patches.py scans src/*/patches/
for CVE markers and auto-emits OpenVEX v0.2.0 statements with
the patch as evidence. vex/auto/ is gitignored and regenerated
by scripts/build_sbom.sh so the suppression set always tracks
src/*/patches/.
* The human-readable table lists only actionable findings (those
with an upstream fix available); not-fixed and wont-fix entries
are summarized at the top so the table is action-oriented. The
--fail-on gate is restricted to actionable findings so CI
doesn't fail on things the team can't act on. The CycloneDX
JSON sidecar preserves grype's exact fix.state per finding so
downstream tooling can still distinguish not-fixed from
wont-fix.
Azure Pipelines integration
===========================
* .azure-pipelines/azure-pipelines-build.yml: ENABLE_SBOM=y
appended to BUILD_OPTIONS so existing make $(BUILD_OPTIONS) ...
lines pick it up without per-line edits.
* .azure-pipelines/build-template.yml: ENABLE_SBOM=y propagation
+ a new inline SBOM vulnerability scan step iterating over each
aggregate SBOM (sonic-*.bin / .swi / .img.gz, docker-*.gz.sbom)
with results published as sbom-vuln-scan-results.<platform>.
Uses ##[group]/##[endgroup] for collapsible per-SBOM log
sections.
* azure-pipelines.yml (top level): the prior Trivy vulnerability
scan (docker-ptf) job from PR sonic-net#27079 is replaced with a unified
SBOM-based vulnerability scan (all artifacts) running in its
own VulnScan stage that dependsOn both BuildVS and Build, so
the scan only fires once every *.cdx.json sidecar this pipeline
produces is on the artifact server. Downloads only *.cdx.json
sidecars (a few MB instead of multi-GB images) and scans every
aggregate SBOM the build produced. continueOnError: true mirrors
the original.
* .azure-pipelines/docker-sonic-mgmt.yml: same trivy -> sbom
replacement for the docker-sonic-mgmt pipeline.
Slave Dockerfiles
=================
Every slave installs two cargo-derived tools, both with the same
CARGO_HOME=$RUST_ROOT install pattern so they land in
/usr/.cargo/bin/ (PATH for all users) rather than /root/.cargo/bin/:
cargo-auditable@0.6.4 embeds resolved Cargo.lock into the
.dep-v0 ELF section of every cargo-built
binary. Reached via the transparent
/usr/local/bin/cargo shim
(files/build/cargo-wrapper, staged into
each slave's buildinfo/ directory by
scripts/prepare_docker_buildinfo.sh).
The shim fails hard with an actionable
error if cargo-auditable is missing.
rust-audit-info@0.5.4 reads .dep-v0 back out at SBOM-emit
time. Invoked by sbom_fragment.py on
every ELF inside every SONiC-built .deb.
Go and Python need no new tool installs (golang-go is already
present for SONiC's Go builds; Python dist-info parsing is pure
Python).
Build caching
=============
Two optimizations avoid redundant work in the ENABLE_SBOM=y build
path. Both preserve full scanner coverage and SBOM completeness —
only redundant scans are skipped.
* Slave-image cache stability: prepare_docker_buildinfo.sh emits
the slave's Dockerfile from a constant `ARG ENABLE_SBOM=n` /
`ENV ENABLE_SBOM=$ENABLE_SBOM` pattern, with --build-arg
ENABLE_SBOM=$(ENABLE_SBOM) passed at every docker build site that
uses the prelude (Makefile.work slave-base build, slave.mk
container build, container -dbg build). The Dockerfile text is
invariant of the ENABLE_SBOM value, so SLAVE_BASE_TAG
(a sha1 of Dockerfile + sources.list.* + buildinfo/versions/,
Makefile.work:262-267) stays stable across runs that toggle
ENABLE_SBOM. Without this, `make configure PLATFORM=<vendor>`
(ENABLE_SBOM typically unset) followed by `make ENABLE_SBOM=y
target/sonic-<vendor>.bin` ran the slave docker build twice with
different ENV ENABLE_SBOM values, adding ~30 min of redundant
work per two-step invocation. After this change the runtime
value still flows in via --build-arg, so pre_run_buildinfo /
post_run_buildinfo / collect_version_files inside the image see
the operator's chosen value whether sourced from rules/config or
a command-line override.
* Scanner cross-check cache: build_sbom.py memoizes syft's output
per archive under target/sbom-tools/syft-cache/<tool>-<sha256>.json,
keyed by SHA-256 of the input archive. Across the per-variant
aggregator runs (broadcom / broadcom-dnx / broadcom-legacy-th)
the ~28 docker .gz files that are identical across variants are
now scanned once each; variants 2 and 3 short-circuit on cache
hit. dir: scans (host rootfs) are bypassed because
fsroot-<machine>/ differs per variant and a directory-tree hash
would be expensive. The cache lives under target/, so `make
reset` invalidates it naturally — no stale entries possible
across resets. SHA-256 keying + syft's deterministic output for
identical input mean cached and freshly-scanned results are
byte-identical; sbom_diff.py's existing reproducibility check
covers this.
Documentation
=============
README.sbom.md (~570 lines) covers configuration, scope,
architecture, license resolution and license-map maintenance,
tools, reproducibility, attestation/signing notes, vulnerability
scanning quick start, VEX workflow, verification, known
limitations, and a file map. Linked from README.md.
Verification
============
End-to-end Broadcom build (make ENABLE_SBOM=y target/sonic-broadcom.bin),
exit 0, all 3 asic variants produced with full SBOM triplet:
Variant .bin .cdx.json components
sonic-broadcom.bin 1.31 GB 46 MB 56,605
sonic-broadcom-dnx.bin 1.18 GB 46 MB 56,600
sonic-broadcom-legacy-th.bin 1.18 GB 46 MB 56,579
Phase spot-checks on broadcom.bin:
188 CycloneDX dependencies[] entries (kernel-module + .deb
dependency graph)
576 recipe-emit-rust components across 7 SONiC Rust .debs
(swss=190, dash-ha=166, supervisord-utilities-rs=77,
ctrmgrd-rs=61, host-services-rs=47, nettools=32,
syslog-counter=3)
67 recipe-emit-go components across 2 .debs (sonic-mgmt-
framework=39, sonic-gnmi=28)
0 recipe-emit-python components (no SONiC .deb ships dist-info;
harvester dormant, activates automatically once a recipe does)
Scanner cache hits firing as expected: ~44 HITs per build across
variants 2+3, dropping the aggregator phase from ~230s (uncached
primary) to ~110s (cached). Measured during development via
temporary per-phase timing instrumentation that was removed before
final submission — only the cache itself is shipping.
Signed-off-by: Brad House <bhouse@nexthop.ai>
Gate all three CI vuln-scan sites behind `condition: false` so the SBOM-based vulnerability scan does not run in the build pipelines for now. SBOM artifacts (.cdx.json / .spdx.json / .intoto.json sidecars) continue to be produced by every ENABLE_SBOM=y build — only the post-build vulnerability scan and report publication is gated. Sites gated: * azure-pipelines.yml: the top-level VulnScan stage that scans every *.cdx.json sidecar from Build + BuildVS. * .azure-pipelines/build-template.yml: the per-platform inline scan step that runs after each platform's `Build sonic image` step. * .azure-pipelines/docker-sonic-mgmt.yml: the standalone job that scans the docker-sonic-mgmt container's SBOM. Each gate carries an inline comment explaining what to flip to re-enable the scan (typically removing `condition: false` or restoring the prior `succeededOrFailed()` / `not(canceled())` predicate). `scripts/sbom_vuln_scan.py` itself is unchanged and can still be run standalone against any committed SBOM: python3 scripts/sbom_vuln_scan.py --vex vex/ target/<artifact>.cdx.json Signed-off-by: Brad House <bhouse@nexthop.ai>
197ceb5 to
64e05d3
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azpw run |
|
Retrying failed(or canceled) jobs... |
|
Retrying failed(or canceled) stages in build 1123889: ✅Stage Test:
|
…r runs Memoize the two SBOM aggregator sub-steps whose output is byte- identical across the per-variant invocations of `build_sbom.py` that the original SBOM PR (sonic-net#27455) introduced. For a multi-ASIC platform like broadcom (broadcom / broadcom-dnx / broadcom-legacy-th), the aggregator runs three times per build and currently redoes the same deterministic work twice. License resolver cache ====================== `build_sbom.py:resolve_licenses()` calls `scripts/sbom_resolve_licenses.py` against every `copyrights.tar.gz` under `target/versions/`. The copyrights tarballs are emitted per-container by `collect_version_files`, and most containers are byte-identical across ASIC variants — so the resolver was producing the same name→SPDX map roughly three times at ~75s each, ~210s of cumulative work deterministic from identical inputs. The cache mirrors the syft scanner cache pattern from sonic-net#27455: * Cache dir: `target/sbom-tools/license-cache/<sha>.json`, sibling to `target/sbom-tools/syft-cache/`. Lives under `target/`, so `make reset` invalidates it automatically. * Key: SHA-256 over the sorted SHA-256s of every input `copyrights.tar.gz`. The resolver's output is a pure function of those tarballs' content, so this is the right invalidator — any drift in any tarball forces a re-resolve. * Hit path: load cached resolver output and return; skips the python3 subprocess + DEP-5 walk + licensecheck fallback + SPDX-map lookup entirely. * Miss path: run the resolver normally, persist its full output dict if it produced a non-empty resolved map. Empty maps are not cached because they're indistinguishable from resolver failures. Expected savings: ~150s (~2.5 min) on a 3-variant build. VEX auto-extraction self-cache ============================== `scripts/sbom_extract_vex_from_patches.py` scans `src/**/*.patch` for CVE markers and emits OpenVEX `not_affected` statements under `vex/auto/`. It's invoked by `build_sbom.sh` at the start of every aggregator run; for a 3-variant build that's three identical passes producing byte-identical output (~13s + ~13s + ~4s today). The fix is a script-level self-cache rather than a separate cache directory: * Fingerprint: SHA-256 over the sorted (path, content-sha256) pairs of every discovered `*.patch` under `src/`. * Marker: `<output-dir>/.input_hash`. Lives inside `vex/auto/` so `rm -rf vex/auto/` (and `make reset`'s `git clean -xfdf`) invalidate it naturally — no separate cache dir, no separate cleanup story. * Hit: log "inputs unchanged" and exit 0 without rescanning. * Miss / first run: do the full walk and write the marker only after a successful pass. A failed run can't poison the cache. * Dry-run mode (`--dry-run`) is unaffected — it neither reads nor writes the marker. Expected savings: ~17-20s on a 3-variant build (variants 2+3 each skip ~10s of patch walking + file writing). Reproducibility =============== Both caches preserve byte-exact output: the SHA-256 keys are content-addressed and the underlying scripts produce deterministic output for identical input. `sbom_diff.py`'s reproducibility check continues to apply. Verification ============ Local smoke test on the VEX cache: two consecutive runs against the same tree produce one full scan + one "inputs unchanged since last run; vex/auto/ is fresh, skipping rescan." Touching any patch's content changes the fingerprint and forces a rescan on the next run. The license cache hits naturally in any 3-variant ENABLE_SBOM=y build — the first variant populates `target/sbom-tools/license-cache/<sha>.json`, the next two read it and skip the resolver subprocess. Cache files are JSON with a `resolved: {pkg → SPDX}` shape identical to what the resolver writes to `target/sbom-licenses.json`. Total expected savings: ~170s (~2.8 min) on a 3-variant build. Combined with the existing syft scanner cache from sonic-net#27455 (~3.5 min savings), the three caches eliminate roughly half of the per- variant aggregator redundancy. Signed-off-by: Brad House <bhouse@nexthop.ai>
…r runs (#27589) Why I did it PR #27455 (the opt-in SBOM PR) shipped a per-archive syft scanner cache that prevents the aggregator from re-scanning identical docker .gz files across the per-variant runs that fire for multi-ASIC platforms (e.g. broadcom invokes build_sbom.py three times — once each for broadcom, broadcom-dnx, broadcom-legacy-th). That cache addressed the largest single source of cross-variant duplication. Two smaller sources of cross-variant duplication remained: License resolution — scripts/sbom_resolve_licenses.py parses every per-container copyrights.tar.gz and emits a {package_name → SPDX expression} map. The input tarballs are byte-identical for the ~28 containers shared across ASIC variants, so the resolver was producing the same map roughly three times per build at ~75s each (~210s cumulative). VEX auto-extraction — scripts/sbom_extract_vex_from_patches.py walks src/**/*.patch for CVE markers and emits OpenVEX not_affected statements. It's invoked by build_sbom.sh at the start of every aggregator pass; the output is a pure function of the patch contents on disk, so variants 2 and 3 redid the same walk (~13s + ~4s per variant, ~30s cumulative). This PR adds caches for both. Same SHA-256-keyed content-addressed pattern as the syft cache, same make reset invalidation semantics, no new user-visible knobs. Total expected savings on a 3-variant build: ~170s (~2.8 min), which compounds with the existing ~3.5 min savings from the syft cache.
sonic-net#27455) Why I did it SONiC currently ships no Software Bill of Materials. There is no inventory of what is in each .bin, no record of the patches and forks SONiC carries on top of upstream sources, no per-artifact vulnerability surface, and no machine-readable provenance. This makes CVE response, license attribution, supply-chain audits (SLSA, SBOM regulations like EO 14028 / CRA), and reproducibility checks expensive or impossible. This PR adds opt-in SBOM generation and SBOM-based vulnerability scanning. When ENABLE_SBOM=y is passed at build time, every artifact (.bin, .img, .swi, plus the standalone test-container .gzs) gets a CycloneDX 1.6 SBOM sidecar covering Debian packages, Python wheels, language-ecosystem lockfile contents (Rust, Go, npm, pnpm, yarn), Docker image layers, vendor blobs, and SONiC-built sources. The SBOMs preserve fork-of-upstream provenance (e.g. the FRR submodule pinned to a specific upstream tag plus SONiC's patch set) via pedigree.ancestors[], so a CVE in upstream FRR can be tied back to the SONiC build that carries the corresponding patch. A separate set of standalone scripts consume the SBOMs to produce vulnerability reports (grype + OpenVEX suppressions) and reproducibility diffs. The default build path is unchanged. With ENABLE_SBOM unset (the default), all SBOM-related work is short-circuited in slave.mk via a make-level $(if) guard, so non-SBOM builds incur zero overhead — no extra subprocesses, no Python startup cost, no apt-cache calls. The 5 added build dependencies (syft, grype, cyclonedx-cli, plus their installer) are only fetched when SBOM is enabled, through the existing sonic-build-hooks wget shim so versions-web tracking still applies. The Azure Pipelines side replaces the recently-merged Trivy scans (PR sonic-net#27079) with the new SBOM-based scanner so CI vulnerability reports come from the same data that ships in the artifact, not a separate re-scan of the image tarball. Comparison with the trivy-based PRs this would supersede The Azure Pipelines half of this PR replaces / overlaps with two recent trivy PRs: PR ci: add Trivy vulnerability scan for docker-ptf and docker-sonic-mgmt sonic-net#27079 — informational trivy scan of docker-ptf and docker-sonic-mgmt. Already merged. This PR removes those two trivy_scan jobs. PR ci: add Trivy vulnerability scan for broadcom and mellanox platform containers and host rootfs sonic-net#27322 — informational trivy scan of all docker-*.gz containers and the host rootfs for broadcom and mellanox platforms. Not yet merged. This PR addresses the same goal more thoroughly. Trivy is a perfectly reasonable image scanner. But trivy alone solves only the vulnerability scanning slice of the supply-chain problem, and it does so by re-discovering what's in the artifact at scan time rather than capturing what the build actually put there. Crucially, the SBOM-based approach finds more CVEs, not the same set — because the input components carry richer provenance, the scanner can match them against CVE databases that trivy can't reach from a plain rootfs scan.
…r runs (sonic-net#27589) Why I did it PR sonic-net#27455 (the opt-in SBOM PR) shipped a per-archive syft scanner cache that prevents the aggregator from re-scanning identical docker .gz files across the per-variant runs that fire for multi-ASIC platforms (e.g. broadcom invokes build_sbom.py three times — once each for broadcom, broadcom-dnx, broadcom-legacy-th). That cache addressed the largest single source of cross-variant duplication. Two smaller sources of cross-variant duplication remained: License resolution — scripts/sbom_resolve_licenses.py parses every per-container copyrights.tar.gz and emits a {package_name → SPDX expression} map. The input tarballs are byte-identical for the ~28 containers shared across ASIC variants, so the resolver was producing the same map roughly three times per build at ~75s each (~210s cumulative). VEX auto-extraction — scripts/sbom_extract_vex_from_patches.py walks src/**/*.patch for CVE markers and emits OpenVEX not_affected statements. It's invoked by build_sbom.sh at the start of every aggregator pass; the output is a pure function of the patch contents on disk, so variants 2 and 3 redid the same walk (~13s + ~4s per variant, ~30s cumulative). This PR adds caches for both. Same SHA-256-keyed content-addressed pattern as the syft cache, same make reset invalidation semantics, no new user-visible knobs. Total expected savings on a 3-variant build: ~170s (~2.8 min), which compounds with the existing ~3.5 min savings from the syft cache.
sonic-net#27455) Why I did it SONiC currently ships no Software Bill of Materials. There is no inventory of what is in each .bin, no record of the patches and forks SONiC carries on top of upstream sources, no per-artifact vulnerability surface, and no machine-readable provenance. This makes CVE response, license attribution, supply-chain audits (SLSA, SBOM regulations like EO 14028 / CRA), and reproducibility checks expensive or impossible. This PR adds opt-in SBOM generation and SBOM-based vulnerability scanning. When ENABLE_SBOM=y is passed at build time, every artifact (.bin, .img, .swi, plus the standalone test-container .gzs) gets a CycloneDX 1.6 SBOM sidecar covering Debian packages, Python wheels, language-ecosystem lockfile contents (Rust, Go, npm, pnpm, yarn), Docker image layers, vendor blobs, and SONiC-built sources. The SBOMs preserve fork-of-upstream provenance (e.g. the FRR submodule pinned to a specific upstream tag plus SONiC's patch set) via pedigree.ancestors[], so a CVE in upstream FRR can be tied back to the SONiC build that carries the corresponding patch. A separate set of standalone scripts consume the SBOMs to produce vulnerability reports (grype + OpenVEX suppressions) and reproducibility diffs. The default build path is unchanged. With ENABLE_SBOM unset (the default), all SBOM-related work is short-circuited in slave.mk via a make-level $(if) guard, so non-SBOM builds incur zero overhead — no extra subprocesses, no Python startup cost, no apt-cache calls. The 5 added build dependencies (syft, grype, cyclonedx-cli, plus their installer) are only fetched when SBOM is enabled, through the existing sonic-build-hooks wget shim so versions-web tracking still applies. The Azure Pipelines side replaces the recently-merged Trivy scans (PR sonic-net#27079) with the new SBOM-based scanner so CI vulnerability reports come from the same data that ships in the artifact, not a separate re-scan of the image tarball. Comparison with the trivy-based PRs this would supersede The Azure Pipelines half of this PR replaces / overlaps with two recent trivy PRs: PR ci: add Trivy vulnerability scan for docker-ptf and docker-sonic-mgmt sonic-net#27079 — informational trivy scan of docker-ptf and docker-sonic-mgmt. Already merged. This PR removes those two trivy_scan jobs. PR ci: add Trivy vulnerability scan for broadcom and mellanox platform containers and host rootfs sonic-net#27322 — informational trivy scan of all docker-*.gz containers and the host rootfs for broadcom and mellanox platforms. Not yet merged. This PR addresses the same goal more thoroughly. Trivy is a perfectly reasonable image scanner. But trivy alone solves only the vulnerability scanning slice of the supply-chain problem, and it does so by re-discovering what's in the artifact at scan time rather than capturing what the build actually put there. Crucially, the SBOM-based approach finds more CVEs, not the same set — because the input components carry richer provenance, the scanner can match them against CVE databases that trivy can't reach from a plain rootfs scan.
…r runs (sonic-net#27589) Why I did it PR sonic-net#27455 (the opt-in SBOM PR) shipped a per-archive syft scanner cache that prevents the aggregator from re-scanning identical docker .gz files across the per-variant runs that fire for multi-ASIC platforms (e.g. broadcom invokes build_sbom.py three times — once each for broadcom, broadcom-dnx, broadcom-legacy-th). That cache addressed the largest single source of cross-variant duplication. Two smaller sources of cross-variant duplication remained: License resolution — scripts/sbom_resolve_licenses.py parses every per-container copyrights.tar.gz and emits a {package_name → SPDX expression} map. The input tarballs are byte-identical for the ~28 containers shared across ASIC variants, so the resolver was producing the same map roughly three times per build at ~75s each (~210s cumulative). VEX auto-extraction — scripts/sbom_extract_vex_from_patches.py walks src/**/*.patch for CVE markers and emits OpenVEX not_affected statements. It's invoked by build_sbom.sh at the start of every aggregator pass; the output is a pure function of the patch contents on disk, so variants 2 and 3 redid the same walk (~13s + ~4s per variant, ~30s cumulative). This PR adds caches for both. Same SHA-256-keyed content-addressed pattern as the syft cache, same make reset invalidation semantics, no new user-visible knobs. Total expected savings on a 3-variant build: ~170s (~2.8 min), which compounds with the existing ~3.5 min savings from the syft cache.
sonic-net#27455) Why I did it SONiC currently ships no Software Bill of Materials. There is no inventory of what is in each .bin, no record of the patches and forks SONiC carries on top of upstream sources, no per-artifact vulnerability surface, and no machine-readable provenance. This makes CVE response, license attribution, supply-chain audits (SLSA, SBOM regulations like EO 14028 / CRA), and reproducibility checks expensive or impossible. This PR adds opt-in SBOM generation and SBOM-based vulnerability scanning. When ENABLE_SBOM=y is passed at build time, every artifact (.bin, .img, .swi, plus the standalone test-container .gzs) gets a CycloneDX 1.6 SBOM sidecar covering Debian packages, Python wheels, language-ecosystem lockfile contents (Rust, Go, npm, pnpm, yarn), Docker image layers, vendor blobs, and SONiC-built sources. The SBOMs preserve fork-of-upstream provenance (e.g. the FRR submodule pinned to a specific upstream tag plus SONiC's patch set) via pedigree.ancestors[], so a CVE in upstream FRR can be tied back to the SONiC build that carries the corresponding patch. A separate set of standalone scripts consume the SBOMs to produce vulnerability reports (grype + OpenVEX suppressions) and reproducibility diffs. The default build path is unchanged. With ENABLE_SBOM unset (the default), all SBOM-related work is short-circuited in slave.mk via a make-level $(if) guard, so non-SBOM builds incur zero overhead — no extra subprocesses, no Python startup cost, no apt-cache calls. The 5 added build dependencies (syft, grype, cyclonedx-cli, plus their installer) are only fetched when SBOM is enabled, through the existing sonic-build-hooks wget shim so versions-web tracking still applies. The Azure Pipelines side replaces the recently-merged Trivy scans (PR sonic-net#27079) with the new SBOM-based scanner so CI vulnerability reports come from the same data that ships in the artifact, not a separate re-scan of the image tarball. Comparison with the trivy-based PRs this would supersede The Azure Pipelines half of this PR replaces / overlaps with two recent trivy PRs: PR ci: add Trivy vulnerability scan for docker-ptf and docker-sonic-mgmt sonic-net#27079 — informational trivy scan of docker-ptf and docker-sonic-mgmt. Already merged. This PR removes those two trivy_scan jobs. PR ci: add Trivy vulnerability scan for broadcom and mellanox platform containers and host rootfs sonic-net#27322 — informational trivy scan of all docker-*.gz containers and the host rootfs for broadcom and mellanox platforms. Not yet merged. This PR addresses the same goal more thoroughly. Trivy is a perfectly reasonable image scanner. But trivy alone solves only the vulnerability scanning slice of the supply-chain problem, and it does so by re-discovering what's in the artifact at scan time rather than capturing what the build actually put there. Crucially, the SBOM-based approach finds more CVEs, not the same set — because the input components carry richer provenance, the scanner can match them against CVE databases that trivy can't reach from a plain rootfs scan.
…r runs (sonic-net#27589) Why I did it PR sonic-net#27455 (the opt-in SBOM PR) shipped a per-archive syft scanner cache that prevents the aggregator from re-scanning identical docker .gz files across the per-variant runs that fire for multi-ASIC platforms (e.g. broadcom invokes build_sbom.py three times — once each for broadcom, broadcom-dnx, broadcom-legacy-th). That cache addressed the largest single source of cross-variant duplication. Two smaller sources of cross-variant duplication remained: License resolution — scripts/sbom_resolve_licenses.py parses every per-container copyrights.tar.gz and emits a {package_name → SPDX expression} map. The input tarballs are byte-identical for the ~28 containers shared across ASIC variants, so the resolver was producing the same map roughly three times per build at ~75s each (~210s cumulative). VEX auto-extraction — scripts/sbom_extract_vex_from_patches.py walks src/**/*.patch for CVE markers and emits OpenVEX not_affected statements. It's invoked by build_sbom.sh at the start of every aggregator pass; the output is a pure function of the patch contents on disk, so variants 2 and 3 redid the same walk (~13s + ~4s per variant, ~30s cumulative). This PR adds caches for both. Same SHA-256-keyed content-addressed pattern as the syft cache, same make reset invalidation semantics, no new user-visible knobs. Total expected savings on a 3-variant build: ~170s (~2.8 min), which compounds with the existing ~3.5 min savings from the syft cache.
sonic-net#27455) Why I did it SONiC currently ships no Software Bill of Materials. There is no inventory of what is in each .bin, no record of the patches and forks SONiC carries on top of upstream sources, no per-artifact vulnerability surface, and no machine-readable provenance. This makes CVE response, license attribution, supply-chain audits (SLSA, SBOM regulations like EO 14028 / CRA), and reproducibility checks expensive or impossible. This PR adds opt-in SBOM generation and SBOM-based vulnerability scanning. When ENABLE_SBOM=y is passed at build time, every artifact (.bin, .img, .swi, plus the standalone test-container .gzs) gets a CycloneDX 1.6 SBOM sidecar covering Debian packages, Python wheels, language-ecosystem lockfile contents (Rust, Go, npm, pnpm, yarn), Docker image layers, vendor blobs, and SONiC-built sources. The SBOMs preserve fork-of-upstream provenance (e.g. the FRR submodule pinned to a specific upstream tag plus SONiC's patch set) via pedigree.ancestors[], so a CVE in upstream FRR can be tied back to the SONiC build that carries the corresponding patch. A separate set of standalone scripts consume the SBOMs to produce vulnerability reports (grype + OpenVEX suppressions) and reproducibility diffs. The default build path is unchanged. With ENABLE_SBOM unset (the default), all SBOM-related work is short-circuited in slave.mk via a make-level $(if) guard, so non-SBOM builds incur zero overhead — no extra subprocesses, no Python startup cost, no apt-cache calls. The 5 added build dependencies (syft, grype, cyclonedx-cli, plus their installer) are only fetched when SBOM is enabled, through the existing sonic-build-hooks wget shim so versions-web tracking still applies. The Azure Pipelines side replaces the recently-merged Trivy scans (PR sonic-net#27079) with the new SBOM-based scanner so CI vulnerability reports come from the same data that ships in the artifact, not a separate re-scan of the image tarball. Comparison with the trivy-based PRs this would supersede The Azure Pipelines half of this PR replaces / overlaps with two recent trivy PRs: PR ci: add Trivy vulnerability scan for docker-ptf and docker-sonic-mgmt sonic-net#27079 — informational trivy scan of docker-ptf and docker-sonic-mgmt. Already merged. This PR removes those two trivy_scan jobs. PR ci: add Trivy vulnerability scan for broadcom and mellanox platform containers and host rootfs sonic-net#27322 — informational trivy scan of all docker-*.gz containers and the host rootfs for broadcom and mellanox platforms. Not yet merged. This PR addresses the same goal more thoroughly. Trivy is a perfectly reasonable image scanner. But trivy alone solves only the vulnerability scanning slice of the supply-chain problem, and it does so by re-discovering what's in the artifact at scan time rather than capturing what the build actually put there. Crucially, the SBOM-based approach finds more CVEs, not the same set — because the input components carry richer provenance, the scanner can match them against CVE databases that trivy can't reach from a plain rootfs scan.
…r runs (sonic-net#27589) Why I did it PR sonic-net#27455 (the opt-in SBOM PR) shipped a per-archive syft scanner cache that prevents the aggregator from re-scanning identical docker .gz files across the per-variant runs that fire for multi-ASIC platforms (e.g. broadcom invokes build_sbom.py three times — once each for broadcom, broadcom-dnx, broadcom-legacy-th). That cache addressed the largest single source of cross-variant duplication. Two smaller sources of cross-variant duplication remained: License resolution — scripts/sbom_resolve_licenses.py parses every per-container copyrights.tar.gz and emits a {package_name → SPDX expression} map. The input tarballs are byte-identical for the ~28 containers shared across ASIC variants, so the resolver was producing the same map roughly three times per build at ~75s each (~210s cumulative). VEX auto-extraction — scripts/sbom_extract_vex_from_patches.py walks src/**/*.patch for CVE markers and emits OpenVEX not_affected statements. It's invoked by build_sbom.sh at the start of every aggregator pass; the output is a pure function of the patch contents on disk, so variants 2 and 3 redid the same walk (~13s + ~4s per variant, ~30s cumulative). This PR adds caches for both. Same SHA-256-keyed content-addressed pattern as the syft cache, same make reset invalidation semantics, no new user-visible knobs. Total expected savings on a 3-variant build: ~170s (~2.8 min), which compounds with the existing ~3.5 min savings from the syft cache.
Why I did it
SONiC currently ships no Software Bill of Materials. There is no inventory of what is in each
.bin, no record of the patches and forks SONiC carries on top of upstream sources, no per-artifact vulnerability surface, and no machine-readable provenance. This makes CVE response, license attribution, supply-chain audits (SLSA, SBOM regulations like EO 14028 / CRA), and reproducibility checks expensive or impossible.This PR adds opt-in SBOM generation and SBOM-based vulnerability scanning. When
ENABLE_SBOM=yis passed at build time, every artifact (.bin,.img,.swi, plus the standalone test-container.gzs) gets a CycloneDX 1.6 SBOM sidecar covering Debian packages, Python wheels, language-ecosystem lockfile contents (Rust, Go, npm, pnpm, yarn), Docker image layers, vendor blobs, and SONiC-built sources. The SBOMs preserve fork-of-upstream provenance (e.g. the FRR submodule pinned to a specific upstream tag plus SONiC's patch set) viapedigree.ancestors[], so a CVE in upstream FRR can be tied back to the SONiC build that carries the corresponding patch. A separate set of standalone scripts consume the SBOMs to produce vulnerability reports (grype + OpenVEX suppressions) and reproducibility diffs.The default build path is unchanged. With
ENABLE_SBOMunset (the default), all SBOM-related work is short-circuited inslave.mkvia a make-level$(if)guard, so non-SBOM builds incur zero overhead — no extra subprocesses, no Python startup cost, no apt-cache calls. The 5 added build dependencies (syft, grype, cyclonedx-cli, plus their installer) are only fetched when SBOM is enabled, through the existingsonic-build-hookswget shim so versions-web tracking still applies.The Azure Pipelines side replaces the recently-merged Trivy scans (PR #27079) with the new SBOM-based scanner so CI vulnerability reports come from the same data that ships in the artifact, not a separate re-scan of the image tarball.
Comparison with the trivy-based PRs this would supersede
The Azure Pipelines half of this PR replaces / overlaps with two recent trivy PRs:
docker-ptfanddocker-sonic-mgmt. Already merged. This PR removes those twotrivy_scanjobs.docker-*.gzcontainers and the host rootfs forbroadcomandmellanoxplatforms. Not yet merged. This PR addresses the same goal more thoroughly.Trivy is a perfectly reasonable image scanner. But trivy alone solves only the vulnerability scanning slice of the supply-chain problem, and it does so by re-discovering what's in the artifact at scan time rather than capturing what the build actually put there. Crucially, the SBOM-based approach finds more CVEs, not the same set — because the input components carry richer provenance, the scanner can match them against CVE databases that trivy can't reach from a plain rootfs scan.
name@versionagainst the trivy DBname@version. SONiC-built debs carry custom version strings (frr_10.5.4-sonic-0,openssh-server_1:9.2p1-2+fips, etc.) that don't match upstream CVE DB entries. CVEs in those packages are silently missedpedigree.ancestors[]records the upstream tag/commit. Grype matches against the upstream identity, and OpenVEX statements (auto-extracted fromsrc/*/patches/) suppress the CVEs that SONiC's carried patches have already fixed. So real unfixed CVEs surface; already-patched CVEs don't generate noise.deband cannot enumerate crates compiled into the binary. Any CVE in a Rust crate (RustSec advisories, GHSA-Rust entries) is unreachablecargo-auditableand routecargo buildthrough a wrapper, so every Rust binary embeds its full resolvedCargo.lockin a.dep-v0ELF section. Grype's cargo cataloger reads it and CVE-matches every crate. This is the largest CVE-coverage gain in the PR for SONiC's increasingly-Rust-heavy components (sonic-swss, sonic-swss-common, sonic-dash-ha, sonic-host-services, sonic-ctrmgrd-rs, …)collect_version_filesharvests lockfiles inside each build container before cleanup phases run, capturing deps that were pulled, used, and discarded during the buildpkg:generic/<vendor>/...PURLs and explicit license metadata. Vendors that publish CVE advisories can be matched; for the rest, the component is at least visible in the inventory for triage.trivyignore— per-rule, per-line, not signed, detached from the source code that justifies the suppressionstatus,justification,impact_statement, and a reference to the source patch that fixes the CVE. Consumed by grype natively, machine-readable, and the auto-extractor regenerates them fromsrc/*/patches/on every build (gitignored, can't drift)scripts/sbom_diff.pydoes build-vs-build diffs to detect non-deterministic build inputs.bin(subject SHA-256 + materials). Release engineering signs with cosign laterpkg:deb/sonic/...vspkg:deb/debian/...), pluspkg:github/,pkg:cargo/,pkg:golang/,pkg:npm/,pkg:oci/,pkg:generic/<vendor>/debian/copyrightparser +licensecheckfallback + 105-entry SPDX header→identifier map. Matches the structured format SONiC actually ships.bin. Customers can run their own scans, do license audits, satisfy EU CRA / EO 14028 reportingsonic-buildimage.<platform>per platform, extracts squashfs, scans everydocker-*.gzin parallel — measured in many minutes per platform)*.cdx.jsonsidecars, scans them in one parallel job. Cheap enough to cover all platforms, not just broadcom + mellanoxENABLE_SBOM=n$(if $(filter y,$(ENABLE_SBOM)),...,:)short-circuit means no Python startup, no apt-cache calls, no extra subprocesses on non-SBOM buildsENABLE_SBOM=y.deblang-deps extraction ~1m 45s, per-container/slavecollect_version_filesSBOM harvest ~4m 04s, aggregator × 3 ASIC variants ~7m 30s, VEX patch-scan ~32s. The aggregator's syftscanner_containersstep dominates the primary variant (~141s) but the SHA-256-keyed cache drops it to ~15s on variants 2 and 3. Measured during development via temporary per-phase timing instrumentation that was removed before final submission — only the cache itself is shipping.bin,.img,.swi, plus standalone SBOMs fordocker-ptf,docker-ptf-sai,docker-sonic-mgmtTL;DR: trivy answers "what CVEs match the packages I can identify by name+version in this rootfs?" SONiC's SBOM tooling answers a strictly larger question — "what CVEs match the components actually built into this artifact, including SONiC-patched forks (with already-fixed CVEs suppressed via VEX), Rust crates embedded in binaries, and build-time deps that were consumed and discarded?" — at lower CI cost, and produces shippable CycloneDX/SPDX/SLSA artifacts as a side effect.
Work item tracking
How I did it
The implementation is two commits on this branch (rebased onto latest master): the SBOM feature itself, plus a small follow-up that stages the CI vulnerability scan in disabled form (see Azure Pipelines integration below). High-level structure:
1. SBOM generation
ENABLE_SBOM,SBOM_FORMAT,SBOM_SCAN_TOOL,SBOM_INCLUDE_LICENSESadded torules/config.Makefile.workandbuild_debian.shpropagate the flag into the slave container and the chroot.prepare_docker_buildinfo.shinjectsENV ENABLE_SBOMinto every container's Dockerfile so per-container post-build hooks can branch on it.sbom_emit_fragmenthelper inslave.mkis invoked from every recipe site that produces an artifact in scope:SONIC_DPKG_DEBS,SONIC_MAKE_DEBS,SONIC_DERIVED_DEBS,SONIC_EXTRA_DEBS,SONIC_ONLINE_DEBS,SONIC_PYTHON_STDEB_DEBS,SONIC_PYTHON_WHEELS, and the docker-image-save step. The helper is wrapped in a make-level$(if $(filter y,$(ENABLE_SBOM)),...,:)short-circuit so non-SBOM builds never invoke Python. Each recipe produces a small per-component CycloneDX fragment;scripts/sbom_fragment.pydetects four fork-of-upstream patterns (sonic-net submodules, dget+patches, nested submodule à la FRR, direct-upstream-submodule + sidecar patches) and emitspedigree.ancestors[]accordingly.src/sonic-build-hooks/scripts/collect_version_filesis extended (gated onENABLE_SBOM=y) to emit a 9-column TSV per container (Package, Version, Architecture, Source, SourceVersion, Maintainer, Homepage, Filename, SHA256) using one bulkxargs apt-cache showcall plus an awk parser — one fork per container rather than one per package — and to snapshot/usr/share/doc/*/copyrightplus any language lockfiles (Cargo.lock,go.sum,package-lock.json,pnpm-lock.yaml,yarn.lock) found in the container.cargo-auditable: every slave Dockerfile (sonic-slave-{bookworm,trixie,bullseye,buster}) installscargo-auditableand drops a wrapper at/usr/local/bin/cargothat routescargo buildthroughcargo auditable build. The resulting binaries embed the resolvedCargo.lockinto a.dep-v0ELF section. Syft'srust-binarycataloger reads that section at scan time, so the SBOM lists exactly the crates the active feature set actually pulls in — not aCargo.locklockfile superset. Zero changes required to any individualdebian/rules.scripts/sbom_parse_lockfiles.pyparses lockfiles harvested from runtime containers and emitspkg:cargo/,pkg:golang/,pkg:npm/PURLs so transitive dependencies compiled into the image are visible. Mostly relevant for ecosystems other than Rust (which is already covered precisely by cargo-auditable) and Go (already covered natively by syft).scripts/build_sbom.pymerges recipe fragments + observation TSVs + lockfile components + scanner output. Dedupe runs on three keys (PURL,name+version+arch,name+normalized-version+arch); the normalizer strips epoch prefixes (1:) and suffixes (+fips,+sonic,+bN,+debNuM) so e.g.openssh-server@10.0p1-7andopenssh-server@1:10.0p1-7+fipscollapse to one component. The aggregator runs once per.bin/.img/.swi, and again in--containermode to produce standalone sidecars fordocker-ptf,docker-ptf-sai, anddocker-sonic-mgmt.scripts/sbom_resolve_licenses.pyparses DEP-5debian/copyrightfiles (the structured format) and falls back tolicensecheckheuristic scanning for free-form headers. A bundledscripts/sbom_license_map.jsontranslates 104 common Debian License header strings to SPDX identifiers.cyclonedx-cli convert.scripts/sbom_emit_provenance.pyproduces an unsigned in-toto/SLSA v1.0 provenance attestation per artifact.scripts/sbom_diff.pyis provided for two-build reproducibility checks.scripts/install_sbom_tool.shpins syft 1.44.0, grype 0.112.0, and cyclonedx-cli 0.32.0 with SHA-256 checksums for amd64 and arm64, fetched through the existing wget shim so versions-web records the downloads.2. Vulnerability scanning + VEX
scripts/sbom_vuln_scan.pyruns grype against a CycloneDX SBOM, applies VEX statements undervex/, and emits a CycloneDX VEX-annotated report plus a human-readable table. Policy is configurable (--min-severity,--fail-on,--ignore-unfixed). Standalone Python — not amaketarget — so engineers and CI can run it against any published SBOM without re-driving the build system.scripts/sbom_vuln_diff.pyshows CVE drift between two scan reports (added / fixed / unchanged).scripts/sbom_extract_vex_from_patches.pyscanssrc/*/patches/for CVE markers (CVE-YYYY-NNNN) in patch headers and emits OpenVEX v0.2.0 JSON statements claimingfixedstatus with the patch as evidence. The output directoryvex/auto/is gitignored; the extractor runs automatically at the start ofscripts/build_sbom.shso the suppression set always reflects the current state ofsrc/*/patches/.vex/README.mddocuments the OpenVEX schema, the auto-vs-manual directory split, and the engineer workflow for addingnot_affected/false_positive/under_investigationstatements withjustificationandimpact_statementfields.3. Azure Pipelines integration
.azure-pipelines/azure-pipelines-build.yml: appendsENABLE_SBOM=ytoBUILD_OPTIONSso every existingmake $BUILD_OPTIONS …line picks it up..azure-pipelines/build-template.yml: sameENABLE_SBOM=ypropagation, plus a new inlineSBOM vulnerability scanstep that iterates over everytarget/sonic-*.bin.cdx.jsonandtarget/docker-*.gz.sbom.cdx.jsonproduced by the build, runssbom_vuln_scan.pyagainst each, and publishessbom-vuln-scan-results.<platform>.azure-pipelines.yml(top-level): the Test-stage[OPTIONAL] Trivy vulnerability scan (docker-ptf)job from PR ci: add Trivy vulnerability scan for docker-ptf and docker-sonic-mgmt #27079 is replaced with a unified[OPTIONAL] SBOM-based vulnerability scan (all artifacts)job that downloads only the*.cdx.jsonsidecars from everysonic-buildimage.<platform>artifact (a few MB instead of multi-GB images) and scans them in parallel groups.continueOnError: truemirrors the original — informational, doesn't gate merges..azure-pipelines/docker-sonic-mgmt.yml: same trivy → sbom replacement for the docker-sonic-mgmt pipeline.All three SBOM vulnerability-scan sites are staged but not active: each carries a
condition: falsegate (with an inline comment giving the exact predicate to restore). The wiring is in place —sbom_vuln_scan.py, the artifact-download steps, the result-publishing — but the scans don't fire on CI runs yet. SBOM artifact production itself is unaffected: everyENABLE_SBOM=ybuild still emits the full.cdx.json/.spdx.json/.intoto.jsontriplet, andscripts/sbom_vuln_scan.pyis fully usable as a standalone tool against any published SBOM.Documentation:
README.sbom.md(~150 lines, with table of contents) covers the build flag, the four data sources, dedupe priority, vulnerability scanning quick-start, VEX workflow, reproducibility, and limitations.How to verify it
Default path — non-SBOM builds are unchanged:
There should be no SBOM artifacts, no extra Python invocations during the build, and no increase in build wall time.
Enable SBOM:
For per-container SBOMs (the test containers not embedded in any
.bin):Vulnerability scan:
python3 scripts/sbom_vuln_scan.py \ --vex vex \ --min-severity medium \ target/sonic-broadcom.bin.cdx.jsonExpected: a table of MEDIUM+ findings with VEX-suppressed CVEs filtered out.
Reproducibility:
# Build twice with identical inputs python3 scripts/sbom_diff.py build1/sonic-broadcom.bin.cdx.json build2/sonic-broadcom.bin.cdx.jsonExpected: an empty diff (any differences indicate non-deterministic build inputs).
VEX auto-extraction:
python3 scripts/sbom_extract_vex_from_patches.py --output vex/auto ls vex/auto/*.jsonExpected: one OpenVEX JSON file per CVE referenced in any
src/*/patches/patch header.Azure Pipelines: the existing
[OPTIONAL] Trivy vulnerability scan (docker-ptf)and[OPTIONAL] Trivy vulnerability scan (docker-sonic-mgmt)jobs are gone from the Test stage; replacement[OPTIONAL] SBOM-based vulnerability scanjobs are wired up but currently gated withcondition: falseat all three sites (top-levelVulnScanstage, per-platform inline scan step, docker-sonic-mgmt scan job). Each gate has an inline comment giving the predicate to restore. To validate the wiring locally, runpython3 scripts/sbom_vuln_scan.py --vex vex/ target/<artifact>.cdx.jsonagainst any SBOM the build produces.Which release branch to backport (provide reason below if selected)
This is a feature, not a fix — no backport requested.
Tested branch (Please provide the tested image version)
Description for the changelog
Add opt-in SBOM generation and SBOM-based vulnerability scanning (
ENABLE_SBOM=y); replaces the Trivy CI jobs from PR #27079 with the new scanner.Link to config_db schema for YANG module changes
N/A — no YANG schema changes.
A picture of a cute animal (not mandatory but encouraged)