Skip to content

[sbom] cache license resolver + VEX auto-extraction across aggregator runs#27589

Merged
qiluo-msft merged 1 commit into
sonic-net:masterfrom
bhouse-nexthop:bhouse.sbom-license-cache
May 29, 2026
Merged

[sbom] cache license resolver + VEX auto-extraction across aggregator runs#27589
qiluo-msft merged 1 commit into
sonic-net:masterfrom
bhouse-nexthop:bhouse.sbom-license-cache

Conversation

@bhouse-nexthop

Copy link
Copy Markdown
Collaborator

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:

  1. License resolutionscripts/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).
  2. VEX auto-extractionscripts/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.

Work item tracking
  • Microsoft ADO (number only):

How I did it

License resolver cache (scripts/build_sbom.py:resolve_licenses)

  • Cache dir: target/sbom-tools/license-cache/<sha>.json, sibling to target/sbom-tools/syft-cache/ from sbom: add opt-in SBOM generation and SBOM-based vulnerability scanning #27455. Lives under target/, so make reset invalidates it naturally.
  • Cache key: SHA-256 over the sorted SHA-256s of every input copyrights.tar.gz. Content-addressed — any drift in any tarball forces a re-resolve.
  • Hit path: load cached resolver JSON output and return.
  • Miss path: run sbom_resolve_licenses.py normally, persist the full output dict if it produced a non-empty resolved map. Empty maps are not cached because they're indistinguishable from resolver failures and would otherwise poison subsequent variant runs.
  • Two new helpers: _license_cache_dir() and _license_cache_key(), mirroring the existing _scanner_cache_dir() / _scanner_cache_lookup() helpers from sbom: add opt-in SBOM generation and SBOM-based vulnerability scanning #27455 for consistency.

VEX auto-extraction self-cache (scripts/sbom_extract_vex_from_patches.py)

  • Marker: <output-dir>/.input_hash inside vex/auto/ itself. Co-locating the marker with the output it certifies means rm -rf vex/auto/ (and make reset's git clean -xfdf) invalidate the cache naturally — no separate cache directory, no separate cleanup story.
  • Fingerprint: SHA-256 over the sorted (path, content-sha256) pairs of every discovered *.patch under src/.
  • Hit: log inputs unchanged since last run; vex/auto/ is fresh, skipping rescan. and exit 0.
  • 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.

Reproducibility: both caches are content-addressed. The cached and freshly-computed outputs are byte-identical, so scripts/sbom_diff.py's existing reproducibility check covers them.

No new build flags, no new tools, no new dependencies. The caches activate automatically on any ENABLE_SBOM=y build; they are transparent on non-SBOM builds (the caches never get touched).

How to verify it

License cache:

# First ENABLE_SBOM=y build populates the cache
make configure PLATFORM=broadcom
make ENABLE_SBOM=y target/sonic-broadcom.bin

# Cache files should be present, one entry per unique tarball-set fingerprint
ls target/sbom-tools/license-cache/
# Expected: one or more <64-hex-char>.json files

# Subsequent variants in the same build reuse them automatically.
# Component count and license resolution percentage should be unchanged.
jq '.components | length' target/sonic-broadcom.bin.cdx.json
jq '[.components[] | select(.licenses)] | length' target/sonic-broadcom.bin.cdx.json

VEX auto-extraction self-cache:

# Run twice in a row — second run should short-circuit
python3 scripts/sbom_extract_vex_from_patches.py --output-dir vex/auto
python3 scripts/sbom_extract_vex_from_patches.py --output-dir vex/auto
# Expected on second run: "inputs unchanged since last run; vex/auto/ is fresh, skipping rescan."

# Marker should match the fingerprint
cat vex/auto/.input_hash   # 64-hex-char sha256

# Touch any *.patch under src/ to invalidate
touch src/sonic-frr/patch/*.patch
python3 scripts/sbom_extract_vex_from_patches.py --output-dir vex/auto
# Expected: full rescan, no short-circuit message.

Reproducibility unchanged:

# Build twice with identical inputs (with and without the caches present)
python3 scripts/sbom_diff.py build1/sonic-broadcom.bin.cdx.json build2/sonic-broadcom.bin.cdx.json
# Expected: empty diff

End-to-end: make ENABLE_SBOM=y target/sonic-broadcom.bin on master + this PR should produce the same 3 ASIC variants × full SBOM triplet (.cdx.json + .spdx.json + .intoto.json) as master alone, with no change in component counts or SBOM content. Only the aggregator wall-clock should differ.

Which release branch to backport (provide reason below if selected)

  • 202305
  • 202311
  • 202405
  • 202411
  • 202505
  • 202511

This is a performance optimization on top of an opt-in feature, not a fix — no backport requested.

Tested branch (Please provide the tested image version)

  • master + this PR / sonic-broadcom.bin (3 variants × .bin + .cdx.json + .spdx.json + .intoto.json; component counts and license-resolution percentages match runs without the caches).

Description for the changelog

Cache license-resolver and VEX auto-extraction output across per-variant build_sbom.py runs (~170s savings on a 3-variant ENABLE_SBOM=y build).

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)

@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run Azure.sonic-buildimage

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

…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>
@bhouse-nexthop
bhouse-nexthop force-pushed the bhouse.sbom-license-cache branch from 2f10047 to e7a08e1 Compare May 28, 2026 22:57
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run Azure.sonic-buildimage

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@yxieca

yxieca commented May 28, 2026

Copy link
Copy Markdown
Contributor

/azpw ms_conflict

@qiluo-msft
qiluo-msft merged commit 007f4fb into sonic-net:master May 29, 2026
27 of 28 checks passed
securely1g pushed a commit to securely1g/sonic-buildimage that referenced this pull request Jun 4, 2026
…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.
purush-nexthop pushed a commit to nexthop-ai/sonic-buildimage that referenced this pull request Jun 10, 2026
…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.
roger530-ho pushed a commit to roger530-ho/sonic-buildimage that referenced this pull request Jun 23, 2026
…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.
xdqi pushed a commit to canonical/sonic-buildimage that referenced this pull request Jul 6, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants