Skip to content

commands/sbom: emit an SPDX 3.0 SBOM of the installed packages - #198

Open
hiagofranco wants to merge 1 commit into
avocado-linux:mainfrom
hiagofranco:hfranco-eng-2405
Open

commands/sbom: emit an SPDX 3.0 SBOM of the installed packages#198
hiagofranco wants to merge 1 commit into
avocado-linux:mainfrom
hiagofranco:hfranco-eng-2405

Conversation

@hiagofranco

Copy link
Copy Markdown
Collaborator

commands/sbom: emit an SPDX 3.0 SBOM of the installed packages

Generate SPDX 3.0 SBOM json output from the installed RPM packages.

The package list is read from each installed sysroot's RPM database, so
it is the transitive closure the device actually holds rather than the
packages avocado.yaml declares. Generate one document with one root per
scope rather than one document per scope.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

@hiagofranco
hiagofranco force-pushed the hfranco-eng-2405 branch 2 times, most recently from 606e8c5 to cd22840 Compare August 12, 2026 14:15

@yoctopidg3 yoctopidg3 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. One thing maybe for the future is a --validate using https://tools.spdx.org/app/validate/

@hiagofranco
hiagofranco force-pushed the hfranco-eng-2405 branch 2 times, most recently from b43ab84 to d2ad341 Compare August 12, 2026 17:46
@hiagofranco

Copy link
Copy Markdown
Collaborator Author

This looks good. One thing maybe for the future is a --validate using https://tools.spdx.org/app/validate/

I played with their API, it works, however it keeps the SBOM json-ld file public for 10 days in their servers. So instead of doing that without the user consent, I just documented this and added as a 'note' output to suggest the user to upload himself, knowing it will be there for 10 days.

@hiagofranco
hiagofranco requested a review from jetm August 12, 2026 18:12
@hiagofranco hiagofranco self-assigned this Aug 12, 2026
@hiagofranco
hiagofranco force-pushed the hfranco-eng-2405 branch 5 times, most recently from 6baabed to 4a2b873 Compare August 12, 2026 20:28
@hiagofranco

Copy link
Copy Markdown
Collaborator Author

Rebased, checks are now passing. Fixed an issues where 'install -f' would fail after the second run trying to generate the sbom output. This is now ready for reviews.

@mobileoverlord

Copy link
Copy Markdown
Contributor

Two things — one standalone, one an interaction with another open PR.

The document isn't byte-stable

namespace_digest is careful about this and says so:

Derived from the content rather than randomly so the document stays byte-stable: the same installed set must produce the same bytes twice running, or a consumer diffing two SBOMs sees churn that is not there.

But build_document then sets

let created = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();

so two runs over an unchanged sysroot still produce different bytes — exactly the churn the comment is guarding against. The namespace work does its job and created undoes it a few lines later.

Honoring SOURCE_DATE_EPOCH when set, falling back to now(), would close it. That also makes the document referenceable by content hash from anything that pins or signs it, which is worth having before consumers start depending on the current behavior.

Interaction with #193

#193 changes the rpmdb seed source from the rootfs to a dependency's sysroot. That breaks the assumption behind base_names here: find(|d| d.scope == "rootfs") stops being the extension's actual base, so a dependency's transactions contain packages the rootfs lacks, fail the all_in_base test, and read as extension content. It compounds up the chain — #193 documents the composition as rootfs ∪ base ∪ mid.

Concretely, #193 measures kiosk-a at 3.5 MB because weston isn't in the image, while this would emit a document saying kiosk-a contains weston — over-reporting by exactly the content #193 deduplicates. The comment here already names that as the direction to avoid: "the opposite error hands every extension the whole base system."

Short version of the fix: have the seed step record its transaction ids at cp -rf time — right after the copy, before anything is installed, every row in the destination db is seed by definition — and drop exactly those here. That replaces base_names and the transaction grouping entirely, and it also closes the two failure modes documented above it (NVRA subtraction dropping 57 of 113 packages; --force replacing the rootfs's ids) plus the acknowledged blind spot where a transaction installs only packages the rootfs already carries by name. All of those are the same root cause: cp -rf of an rpmdb leaves no record of what arrived in the copy, so everything downstream has to infer it.

Whichever of these two lands second should carry the change.

Generate SPDX 3.0 SBOM json output from the installed RPM packages.

The package list is read from each installed sysroot's RPM database, so
it is the transitive closure the device actually holds rather than the
packages avocado.yaml declares. Generate one document with one root per
scope rather than one document per scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hiagofranco

Copy link
Copy Markdown
Collaborator Author

Two things — one standalone, one an interaction with another open PR.

The document isn't byte-stable

namespace_digest is careful about this and says so:

Derived from the content rather than randomly so the document stays byte-stable: the same installed set must produce the same bytes twice running, or a consumer diffing two SBOMs sees churn that is not there.

But build_document then sets

let created = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();

so two runs over an unchanged sysroot still produce different bytes — exactly the churn the comment is guarding against. The namespace work does its job and created undoes it a few lines later.

Honoring SOURCE_DATE_EPOCH when set, falling back to now(), would close it. That also makes the document referenceable by content hash from anything that pins or signs it, which is worth having before consumers start depending on the current behavior.

Interaction with #193

#193 changes the rpmdb seed source from the rootfs to a dependency's sysroot. That breaks the assumption behind base_names here: find(|d| d.scope == "rootfs") stops being the extension's actual base, so a dependency's transactions contain packages the rootfs lacks, fail the all_in_base test, and read as extension content. It compounds up the chain — #193 documents the composition as rootfs ∪ base ∪ mid.

Concretely, #193 measures kiosk-a at 3.5 MB because weston isn't in the image, while this would emit a document saying kiosk-a contains weston — over-reporting by exactly the content #193 deduplicates. The comment here already names that as the direction to avoid: "the opposite error hands every extension the whole base system."

Short version of the fix: have the seed step record its transaction ids at cp -rf time — right after the copy, before anything is installed, every row in the destination db is seed by definition — and drop exactly those here. That replaces base_names and the transaction grouping entirely, and it also closes the two failure modes documented above it (NVRA subtraction dropping 57 of 113 packages; --force replacing the rootfs's ids) plus the acknowledged blind spot where a transaction installs only packages the rootfs already carries by name. All of those are the same root cause: cp -rf of an rpmdb leaves no record of what arrived in the copy, so everything downstream has to infer it.

Whichever of these two lands second should carry the change.

Thanks, fixed the bug and left the second one open, as you mentioned, whatever PR is merged first should fix it. Please take a look.

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.

3 participants