[PATCH v2 02/18] spdx30: Add supplier support for image and SDK SBOMs

Stefano Tondo
2026-02-21T05:10:23Z

This commit adds support for setting supplier information on image and SDK
SBOMs using the suppliedBy property on root elements.

New configuration variables:

SPDX_IMAGE_SUPPLIER (optional):
- Base variable name to describe the Agent supplying the image SBOM
- Follows the same Agent variable pattern as SPDX_PACKAGE_SUPPLIER
- Sets suppliedBy on all root elements of the image SBOM

SPDX_SDK_SUPPLIER (optional):
- Base variable name to describe the Agent supplying the SDK SBOM
- Follows the same Agent variable pattern as SPDX_PACKAGE_SUPPLIER
- Sets suppliedBy on all root elements of the SDK SBOM

Implementation:

- create_image_sbom_spdx(): After create_sbom() returns, uses
objset.new_agent() to create supplier and sets suppliedBy on
sbom.rootElement

- create_sdk_sbom(): After create_sbom() returns, uses objset.new_agent()
to create supplier and sets suppliedBy on sbom.rootElement

- Uses existing agent infrastructure (objset.new_agent()) for proper
de-duplication and metadata handling

- No changes to generic create_sbom() function which is used for recipes,
images, and SDKs

Usage example in local.conf:

SPDX_IMAGE_SUPPLIER = "acme"
SPDX_IMAGE_SUPPLIER_acme_name = "Acme Corporation"
SPDX_IMAGE_SUPPLIER_acme_type = "organization"
SPDX_IMAGE_SUPPLIER_acme_id_email = "sbom@acme.com"

This enables compliance workflows that require supplier metadata on image
and SDK SBOMs while following existing OpenEmbedded SPDX patterns.

Signed-off-by: Stefano Tondo <stefano.tondo.ext@siemens.com>
---
meta/classes/create-spdx-3.0.bbclass | 10 +++++
meta/lib/oe/spdx30_tasks.py | 59 +++++++++++++++++++++++++---
2 files changed, 63 insertions(+), 6 deletions(-)

diff --git a/meta/classes/create-spdx-3.0.bbclass b/meta/classes/create-spdx-3.0.bbclass
index d4575d61c4..def2dacbc3 100644
--- a/meta/classes/create-spdx-3.0.bbclass
+++ b/meta/classes/create-spdx-3.0.bbclass
@@ -124,6 +124,16 @@ SPDX_ON_BEHALF_OF[doc] = "The base variable name to describe the Agent on who's
SPDX_PACKAGE_SUPPLIER[doc] = "The base variable name to describe the Agent who \
is supplying artifacts produced by the build"

+SPDX_IMAGE_SUPPLIER[doc] = "The base variable name to describe the Agent who \
+ is supplying the image SBOM. The supplier will be set on all root elements \
+ of the image SBOM using the suppliedBy property. If not set, no supplier \
+ information will be added to the image SBOM."
+
+SPDX_SDK_SUPPLIER[doc] = "The base variable name to describe the Agent who \
+ is supplying the SDK SBOM. The supplier will be set on all root elements \
+ of the SDK SBOM using the suppliedBy property. If not set, no supplier \
+ information will be added to the SDK SBOM."
+
SPDX_PACKAGE_VERSION ??= "${PV}"
SPDX_PACKAGE_VERSION[doc] = "The version of a package, software_packageVersion \
in software_Package"
diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
index bd703b5bec..789b39bd93 100644
--- a/meta/lib/oe/spdx30_tasks.py
+++ b/meta/lib/oe/spdx30_tasks.py
@@ -162,7 +162,7 @@ def add_package_files(
bb.debug(1, f"Total compiled files: {len(compiled_sources)}")

# File filtering configuration
- spdx_file_filter = (d.getVar("SPDX_FILE_FILTER") or "all").lower()
+ spdx_file_filter = (d.getVar("SPDX_FILES_INCLUDED") or "all").lower()
essential_patterns = (d.getVar("SPDX_FILE_ESSENTIAL_PATTERNS") or "").split()
exclude_patterns = (d.getVar("SPDX_FILE_EXCLUDE_PATTERNS") or "").split()

@@ -244,7 +244,7 @@ def add_package_files(
def get_package_sources_from_debug(
d, package, package_files, sources, source_hash_cache
):
- spdx_file_filter = (d.getVar("SPDX_FILE_FILTER") or "all").lower()
+ spdx_file_filter = (d.getVar("SPDX_FILES_INCLUDED") or "all").lower()

def file_path_match(file_path, pkg_file):
if file_path.lstrip("/") == pkg_file.name.lstrip("/"):
@@ -283,7 +283,7 @@ def get_package_sources_from_debug(
if spdx_file_filter in ("none", "essential"):
bb.debug(
1,
- f"Skipping debug source lookup for {file_path} in {package} (filtered by SPDX_FILE_FILTER={spdx_file_filter})",
+ f"Skipping debug source lookup for {file_path} in {package} (filtered by SPDX_FILES_INCLUDED={spdx_file_filter})",
)
continue
else:
@@ -663,7 +663,13 @@ def create_spdx(d):
force_purposes=["install"],
)

- supplier = build_objset.new_agent("SPDX_PACKAGE_SUPPLIER")
+ # Follow the same pattern as SPDX_AUTHORS: get identifier, build varname, then call new_agent
+ supplier_id_val = d.getVar("SPDX_PACKAGE_SUPPLIER")
+ if supplier_id_val:
+ supplier_varname = f"SPDX_PACKAGE_SUPPLIER_{supplier_id_val}"
+ supplier = build_objset.new_agent(supplier_varname)
+ else:
+ supplier = None
if supplier is not None:
spdx_package.suppliedBy = (
supplier if isinstance(supplier, str) else supplier._id
@@ -1006,8 +1012,17 @@ def write_bitbake_spdx(d):
objset = oe.sbom30.ObjectSet.new_objset(d, "bitbake", False)

host_import_key = d.getVar("SPDX_BUILD_HOST")
- invoked_by = objset.new_agent("SPDX_INVOKED_BY", add=False)
- on_behalf_of = objset.new_agent("SPDX_ON_BEHALF_OF", add=False)
+ invoked_by = None
+ invoked_by_id_val = d.getVar("SPDX_INVOKED_BY")
+ if invoked_by_id_val:
+ invoked_by_varname = f"SPDX_INVOKED_BY_{invoked_by_id_val}"
+ invoked_by = objset.new_agent(invoked_by_varname, add=False)
+
+ on_behalf_of = None
+ on_behalf_of_id_val = d.getVar("SPDX_ON_BEHALF_OF")
+ if on_behalf_of_id_val:
+ on_behalf_of_varname = f"SPDX_ON_BEHALF_OF_{on_behalf_of_id_val}"
+ on_behalf_of = objset.new_agent(on_behalf_of_varname, add=False)

if d.getVar("SPDX_INCLUDE_BITBAKE_PARENT_BUILD") == "1":
# Since the Build objects are unique, we may as well set the creation
@@ -1330,6 +1345,22 @@ def create_image_sbom_spdx(d):

objset, sbom = oe.sbom30.create_sbom(d, image_name, root_elements)

+ # Set supplier on root elements if SPDX_IMAGE_SUPPLIER is defined
+ # Follow the same pattern as SPDX_AUTHORS: get identifier, build varname, then call new_agent
+ supplier_id_val = d.getVar("SPDX_IMAGE_SUPPLIER")
+ if supplier_id_val:
+ supplier_varname = f"SPDX_IMAGE_SUPPLIER_{supplier_id_val}"
+ supplier = objset.new_agent(supplier_varname, add=False)
+ if supplier is not None:
+ supplier_id = supplier if isinstance(supplier, str) else supplier._id
+ # Add supplier to objset if it's not already there
+ if not isinstance(supplier, str):
+ objset.add(supplier)
+ # Set suppliedBy on all root elements
+ for elem in sbom.rootElement:
+ if hasattr(elem, "suppliedBy"):
+ elem.suppliedBy = supplier_id
+
oe.sbom30.write_jsonld_doc(d, objset, spdx_path)

def make_image_link(target_path, suffix):
@@ -1441,6 +1472,22 @@ def create_sdk_sbom(d, sdk_deploydir, spdx_work_dir, toolchain_outputname):
d, toolchain_outputname, sorted(list(files)), [rootfs_objset]
)

+ # Set supplier on root elements if SPDX_SDK_SUPPLIER is defined
+ # Follow the same pattern as SPDX_AUTHORS: get identifier, build varname, then call new_agent
+ supplier_id_val = d.getVar("SPDX_SDK_SUPPLIER")
+ if supplier_id_val:
+ supplier_varname = f"SPDX_SDK_SUPPLIER_{supplier_id_val}"
+ supplier = objset.new_agent(supplier_varname, add=False)
+ if supplier is not None:
+ supplier_id = supplier if isinstance(supplier, str) else supplier._id
+ # Add supplier to objset if it's not already there
+ if not isinstance(supplier, str):
+ objset.add(supplier)
+ # Set suppliedBy on all root elements
+ for elem in sbom.rootElement:
+ if hasattr(elem, "suppliedBy"):
+ elem.suppliedBy = supplier_id
+
oe.sbom30.write_jsonld_doc(
d, objset, sdk_deploydir / (toolchain_outputname + ".spdx.json")
)
--
2.53.0