sbom: add opt-in SBOM generation and SBOM-based vulnerability scanning#27455
sbom: add opt-in SBOM generation and SBOM-based vulnerability scanning#27455bhouse-nexthop wants to merge 3 commits into
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 |
25debd0 to
9f81eb4
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
9f81eb4 to
673f611
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). |
94c2afd to
673f611
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
673f611 to
239d27c
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
c8fbb94 to
0654d98
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
0654d98 to
d950167
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.
Thorough and well-documented PR — the comparison table against trivy alone justifies the approach. A few observations:
Structure
5 commits — consider squashing. SONiC convention is single-commit PRs. At minimum, the three [sbom] commits should be squashed into one.
The [build] and [temp] commits should be separate PRs. The build_mirror_config.sh race fix and the Micas plat_sysfs ordering fix are real bugs worth fixing, but bundling them here makes this PR harder to review and merge. Split them out — they can land independently and faster.
Design (positive)
- Zero-overhead opt-in via make-level
$(if)short-circuit is the right call. Non-SBOM builds pay nothing. cargo-auditablewrapper that transparently embedsCargo.lockinto ELF.dep-v0sections without touching anydebian/rules— clever.- Recipe-emit-wins dedupe with version normalization (stripping epoch,
+fips,+sonicsuffixes) is well thought out. - VEX auto-extraction from
src/*/patches/gives you CVE suppression that tracks source state rather than a manually-maintained ignore file. - CI cost: downloading ~5 MB of
.cdx.jsonsidecars vs multi-GB image tarballs is a big win.
Concerns
cargo-wrapper error path
The wrapper does exec "$REAL" auditable "$@" — if cargo-auditable is not installed (e.g. a custom slave image without the Dockerfile change), this silently fails the build rather than falling back. Consider checking for cargo-auditable on PATH and falling through to plain cargo build if absent:
build)
if command -v cargo-auditable >/dev/null 2>&1; then
export CARGO_AUDITABLE_WRAPPED=1
exec "$REAL" auditable "$@"
fi
exec "$REAL" "$@"
;;cargo-wrapper duplication
The same cargo-wrapper file is copied into all 4 slave dirs (bookworm, bullseye, buster, trixie). Consider putting it in a shared location (e.g. files/) and COPYing from there to avoid 4-way drift.
|| true in slave.mk vs SBOM_STRICT
Every sbom_emit_fragment call in slave.mk is wrapped with || true, which means even with SBOM_STRICT=y, fragment emission failures are silently swallowed at the make level before build_sbom.py ever sees them. This is fine for the "SBOM cannot break a build" philosophy, but worth documenting that SBOM_STRICT only governs the aggregation phase, not the per-recipe emit phase.
find / in collect_version_files
The lockfile harvest does find / ... with prune exclusions. This is broad — on a large slave container it could be slow or pick up unexpected lockfiles. Consider scoping to known source paths (/sonic/src/, /usr/, etc.) rather than root.
Reproducibility of SBOM timestamps
now_iso() in build_sbom.py respects SOURCE_DATE_EPOCH — good. But is SOURCE_DATE_EPOCH actually set during SONiC builds? If not, SBOMs will have non-deterministic timestamps and sbom_diff.py will report false diffs. Worth noting in the README or setting it from the build system.
Minor
README.sbom.mdat 507 lines is excellent documentation. Consider linking it from the mainREADME.md.- The
scripts/sbom_license_map.jsonwith 104 entries — is this manually maintained? If so, consider documenting how to add entries (or auto-generating from a source).
Overall this is solid work. The main ask is splitting out the unrelated build fixes and squashing the SBOM commits.
Yes, these are separate PRs I filed after running into them:
I needed them in there for building and testing and they will be removed from this PR once those PRs merge. |
This PR 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. * 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 and snapshot /usr/share/doc/*/copyright + language lockfiles found in known source roots (/sonic /usr /etc /root /home /opt). Avoids walking the entire filesystem. * Rust crate visibility via cargo-auditable: every slave Dockerfile installs cargo-auditable and a transparent /usr/local/bin/cargo shim (files/build/cargo-wrapper, staged into each slave's build context by scripts/prepare_docker_buildinfo.sh). Every Rust binary embeds its resolved Cargo.lock in a .dep-v0 ELF section; syft reads it at scan time. The shim fails hard with an actionable error if cargo-auditable is missing, rather than silently degrading to plain cargo build. * Lockfile expansion: scripts/sbom_parse_lockfiles.py parses lockfiles harvested from runtime containers (Cargo.lock, go.sum, package-lock.json, pnpm-lock.yaml, yarn.lock) and emits the appropriate PURL types. * 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. * 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 ($* from the slave.mk recipe), so .bin, .swi, and .img.gz all get correctly-named sidecars: target/sonic-broadcom.bin.cdx.json + .spdx.json + .intoto.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 Test-stage [OPTIONAL] Trivy vulnerability scan (docker-ptf) job from PR sonic-net#27079 is replaced with a unified [OPTIONAL] SBOM-based vulnerability scan (all artifacts) job. 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. 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. Signed-off-by: Brad House <bhouse@nexthop.ai>
scripts/build_mirror_config.sh writes $CONFIG_PATH/sources.list.<arch> with `j2 $TEMPLATE | sed ... > path`. The `>` redirect truncates the destination first, so during the j2|sed pipeline lifetime the file is in a 0-byte / partial state. build_debian.sh:117 runs this with CONFIG_PATH=files/apt, a path shared across the whole tree. SONiC builds multiple rootfs variants in parallel under `make -j` (e.g. broadcom, broadcom-dnx, broadcom-legacy-th — all amd64, all writing files/apt/sources.list.amd64). One parallel build_debian.sh can `cp` the file at the exact instant another's `>` has just truncated it, and end up with an empty sources.list in its chroot. The chroot's apt-get update then succeeds trivially (no sources to fetch, exit 0, no Get:/Hit:/Err: output). apt-get -y install eatmydata later fails with `E: Unable to locate package eatmydata`. The failure is flaky — only one of the three rootfs builds hits the window per affected run. Container builds are immune because prepare_docker_buildinfo.sh:47 calls build_mirror_config.sh with a per-container DOCKERFILE_PATH, so each container has its own non-shared sources.list.amd64. Fix: write via mktemp + atomic mv. Apply the same pattern to the apt-retries-count file write that follows, which has the same race shape. mv within the same filesystem is atomic — readers either see the old content or the new content, never a torn write. Signed-off-by: Brad House <bhouse@nexthop.ai>
The plat_sysfs/Makefile lists dev_cfg and dev_sysfs as prerequisites of 'all' but declares no ordering between them. Under make -j (SONiC uses -j12 by default) the two subdirs can build in parallel and the modpost step in dev_sysfs fails with: dev_cfg/Module.symvers: No such file or directory because dev_sysfs/Makefile's KBUILD_EXTRA_SYMBOLS references that file. Adding 'dev_sysfs: dev_cfg' forces dev_cfg to finish first. Temporary patch on this branch so verification builds for the SBOM work complete reliably. Drop this commit (git rebase --onto) once an equivalent upstream fix lands. Signed-off-by: Brad House <bhouse@nexthop.ai>
d950167 to
369da06
Compare
|
/azp run Azure.sonic-buildimage |
|
Azure Pipelines successfully started running 1 pipeline(s). |
I'm going to reject this as since the SBOM is for security it would be bad if we couldn't extract this. Instead I made a change to provide a clear error message to the user.
done, though had to do a temporary copy elsewhere during the build where docker could access it.
Fixed, different behavior for SBOM_STRICT=y vs SBOM_STRICT=n
done
timestamp now taken from git commit time automatically
done
Its manual intentionally, added a section to README.sbom.md |
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 + 104-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 total, 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.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 split across three commits.
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.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.Out of scope — transient build fixes (two trailing
[temp]:commits)These are not part of the SBOM feature; they unblock builds in my local environment and are independent of this work. They are included so the branch builds end-to-end and can be dropped once the upstream fixes land:
[temp]: Fix sonic-sysmgr build race (upstream PR #27447)— backport of a fix already submitted upstream for a protoc-generated.pb.ccrace insrc/sonic-sysmgr/Makefile.am. Remove once PR Add a dependency in sonic-sysmgr makefile #27447 merges.[temp]: Fix Micas plat_sysfs parallel build race— adds adev_sysfs: dev_cfgordering dependency inplatform/broadcom/sonic-platform-modules-micas/common/modules/plat_sysfs/Makefilesodev_sysfs's modpost can seedev_cfg/Module.symversunder-j. Belongs upstream in the Micas platform repo.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 run on the cheapsonic-ubuntu-1cpool with the samecontinueOnErrorsemantics, and emitsbom-vuln-scan-results.*artifacts.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)