| Message ID | 20260107180951.140895-5-stondo@gmail.com |
|---|---|
| State | Changes Requested |
| Headers | show |
| Series | spdx30: Add SBOM metadata component and supplier support | expand |
On Wed, Jan 7, 2026 at 11:10 AM Stefano Tondo via lists.openembedded.org <stondo=gmail.com@lists.openembedded.org> wrote: > > From: Stefano Tondo <stefano.tondo.ext@siemens.com> > > This commit adds support for including image/product metadata and > supplier information in SPDX 3.0 SBOMs to meet compliance requirements. > > New configuration variables (in spdx-common.bbclass): > > SBOM_COMPONENT_NAME (optional): > - Name of the product/image being documented > - Creates a software_Package element with metadata > - Typically set to IMAGE_BASENAME or product name > > SBOM_COMPONENT_VERSION (optional): > - Version of the product/image > - Falls back to DISTRO_VERSION if not set > > SBOM_COMPONENT_SUMMARY (optional): > - Description of the product/image > - Falls back to IMAGE_SUMMARY if not set > > SBOM_SUPPLIER_NAME (optional): > - Name of the organization supplying the SBOM > - Creates an Organization element > > SBOM_SUPPLIER_URL (optional): > - URL of the supplier organization > - Added as externalIdentifier > > Implementation (in sbom30.py): > > - create_sbom(): Add metadata component and supplier after SBOM > creation but before collection expansion > - Create relationships: > * SBOM --describes--> metadata component > * SBOM --availableFrom--> supplier Organization > > SPDX 3.0 elements created: > > - software_Package (primaryPurpose: operatingSystem) for product > - Organization with optional URL externalIdentifier > - Appropriate relationships per SPDX 3.0 spec > > Usage example in local.conf: > > SBOM_COMPONENT_NAME = "" > SBOM_COMPONENT_VERSION = "1.0.0" > SBOM_COMPONENT_SUMMARY = "Production image for Device X" > SBOM_SUPPLIER_NAME = "Acme Corporation" > SBOM_SUPPLIER_URL = "https://acme.com" > > This enables profile-specific SBOM workflows and compliance validation > tools that require product and supplier metadata. > > Signed-off-by: Stefano Tondo <stefano.tondo.ext@siemens.com> > --- > meta/lib/oe/sbom30.py | 52 +++++++++++++++++++++++++++++++++++++ > meta/lib/oe/spdx30_tasks.py | 10 +++---- > 2 files changed, 57 insertions(+), 5 deletions(-) > > diff --git a/meta/lib/oe/sbom30.py b/meta/lib/oe/sbom30.py > index 227ac51877..197361db4f 100644 > --- a/meta/lib/oe/sbom30.py > +++ b/meta/lib/oe/sbom30.py > @@ -1045,6 +1045,58 @@ def create_sbom(d, name, root_elements, add_objectsets=[]): > ) > ) create_sbom() is a generic function that is used for more than just images. As such, using the SBOM_COMPONENT_VERSION, DISTRO_VERSION, IMAGE_SUMMARY, etc. is not appropriate. You should be able to do the same thing by using the `objset` and `sbom` object returned from the function instead. > > + # Add SBOM metadata component (image/product information) > + sbom_component_name = d.getVar("SBOM_COMPONENT_NAME") > + if sbom_component_name: > + sbom_component_version = d.getVar("SBOM_COMPONENT_VERSION") or d.getVar("DISTRO_VERSION") or "unknown" > + sbom_component_summary = d.getVar("SBOM_COMPONENT_SUMMARY") or d.getVar("IMAGE_SUMMARY") or f"{name} image" > + > + metadata_component = objset.add( > + oe.spdx30.software_Package( > + _id=objset.new_spdxid("metadata", "component"), > + creationInfo=objset.doc.creationInfo, > + name=sbom_component_name, > + software_packageVersion=sbom_component_version, > + summary=sbom_component_summary, > + software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.operatingSystem, > + ) > + ) It's not clear why this is needed over the root elements? You can effectively do the same thing by setting the "suppliedBy" property on all the root elements of the SBoM. If some of them are missing supplier information, we can add properties to set those the same as the SPDX_PACKAGE_SUPPLIER. (e.g. SPDX_IMAGE_SUPPLIER, SPDX_SDK_SUPPLIER, etc.) This seems like it's adding things in the wrong place as-is. > + > + # Link SBOM to metadata component > + objset.new_relationship( > + [sbom], > + oe.spdx30.RelationshipType.describes, > + [metadata_component], > + ) > + > + # Add supplier information if provided > + sbom_supplier_name = d.getVar("SBOM_SUPPLIER_NAME") > + if sbom_supplier_name: > + sbom_supplier_url = d.getVar("SBOM_SUPPLIER_URL") > + > + supplier = objset.add( > + oe.spdx30.Organization( > + _id=objset.new_spdxid("supplier", sbom_supplier_name.replace(" ", "-").lower()), > + creationInfo=objset.doc.creationInfo, > + name=sbom_supplier_name, > + ) > + ) > + > + if sbom_supplier_url: > + supplier.externalIdentifier = [ > + oe.spdx30.ExternalIdentifier( > + externalIdentifierType=oe.spdx30.ExternalIdentifierType.urlScheme, > + identifier=sbom_supplier_url, > + ) > + ] Use objset.new_agent() so that this follows all the other rules for agents; this will allow user to de-duplicate and add all the other metadata using variables > + > + # Link supplier to SBOM (SBOM is available from supplier) > + objset.new_relationship( > + [sbom], > + oe.spdx30.RelationshipType.availableFrom, > + [supplier], FYI, there is a _much_ better way to do this planned in SPDX 3.1 (which is why I've not added it yet): https://spdx.github.io/spdx-spec/v3.1-dev/model/Core/Classes/SupportRelationship/ In the meantime, isn't the "suppliedBy" property sufficient? > + ) > + > missing_spdxids = objset.expand_collection(add_objectsets=add_objectsets) > if missing_spdxids: > bb.warn( > diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py > index c86b088b61..757503cd6b 100644 > --- a/meta/lib/oe/spdx30_tasks.py > +++ b/meta/lib/oe/spdx30_tasks.py > @@ -179,17 +179,17 @@ def add_package_files( > continue > > filename = str(filepath.relative_to(topdir)) > - > + > # Apply file filtering if enabled > if spdx_file_filter == "essential": > file_upper = file.upper() > filename_lower = filename.lower() > - > + > # Skip if matches exclude patterns > skip_file = any(pattern in filename_lower for pattern in exclude_patterns) > if skip_file: > continue > - > + > # Keep only essential files (license/readme/etc) > is_essential = any(pattern in file_upper for pattern in essential_patterns) > if not is_essential: > @@ -198,7 +198,7 @@ def add_package_files( > # Skip all files > continue > # else: spdx_file_filter == "all" or any other value - include all files > - > + > file_purposes = get_purposes(filepath) > > # Check if file is compiled > @@ -245,7 +245,7 @@ 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() > - > + > def file_path_match(file_path, pkg_file): > if file_path.lstrip("/") == pkg_file.name.lstrip("/"): > return True > -- > 2.52.0 > > > -=-=-=-=-=-=-=-=-=-=-=- > Links: You receive all messages sent to this group. > View/Reply Online (#229024): https://lists.openembedded.org/g/openembedded-core/message/229024 > Mute This Topic: https://lists.openembedded.org/mt/117138943/3616693 > Group Owner: openembedded-core+owner@lists.openembedded.org > Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [JPEWhacker@gmail.com] > -=-=-=-=-=-=-=-=-=-=-=- >
diff --git a/meta/lib/oe/sbom30.py b/meta/lib/oe/sbom30.py index 227ac51877..197361db4f 100644 --- a/meta/lib/oe/sbom30.py +++ b/meta/lib/oe/sbom30.py @@ -1045,6 +1045,58 @@ def create_sbom(d, name, root_elements, add_objectsets=[]): ) ) + # Add SBOM metadata component (image/product information) + sbom_component_name = d.getVar("SBOM_COMPONENT_NAME") + if sbom_component_name: + sbom_component_version = d.getVar("SBOM_COMPONENT_VERSION") or d.getVar("DISTRO_VERSION") or "unknown" + sbom_component_summary = d.getVar("SBOM_COMPONENT_SUMMARY") or d.getVar("IMAGE_SUMMARY") or f"{name} image" + + metadata_component = objset.add( + oe.spdx30.software_Package( + _id=objset.new_spdxid("metadata", "component"), + creationInfo=objset.doc.creationInfo, + name=sbom_component_name, + software_packageVersion=sbom_component_version, + summary=sbom_component_summary, + software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.operatingSystem, + ) + ) + + # Link SBOM to metadata component + objset.new_relationship( + [sbom], + oe.spdx30.RelationshipType.describes, + [metadata_component], + ) + + # Add supplier information if provided + sbom_supplier_name = d.getVar("SBOM_SUPPLIER_NAME") + if sbom_supplier_name: + sbom_supplier_url = d.getVar("SBOM_SUPPLIER_URL") + + supplier = objset.add( + oe.spdx30.Organization( + _id=objset.new_spdxid("supplier", sbom_supplier_name.replace(" ", "-").lower()), + creationInfo=objset.doc.creationInfo, + name=sbom_supplier_name, + ) + ) + + if sbom_supplier_url: + supplier.externalIdentifier = [ + oe.spdx30.ExternalIdentifier( + externalIdentifierType=oe.spdx30.ExternalIdentifierType.urlScheme, + identifier=sbom_supplier_url, + ) + ] + + # Link supplier to SBOM (SBOM is available from supplier) + objset.new_relationship( + [sbom], + oe.spdx30.RelationshipType.availableFrom, + [supplier], + ) + missing_spdxids = objset.expand_collection(add_objectsets=add_objectsets) if missing_spdxids: bb.warn( diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py index c86b088b61..757503cd6b 100644 --- a/meta/lib/oe/spdx30_tasks.py +++ b/meta/lib/oe/spdx30_tasks.py @@ -179,17 +179,17 @@ def add_package_files( continue filename = str(filepath.relative_to(topdir)) - + # Apply file filtering if enabled if spdx_file_filter == "essential": file_upper = file.upper() filename_lower = filename.lower() - + # Skip if matches exclude patterns skip_file = any(pattern in filename_lower for pattern in exclude_patterns) if skip_file: continue - + # Keep only essential files (license/readme/etc) is_essential = any(pattern in file_upper for pattern in essential_patterns) if not is_essential: @@ -198,7 +198,7 @@ def add_package_files( # Skip all files continue # else: spdx_file_filter == "all" or any other value - include all files - + file_purposes = get_purposes(filepath) # Check if file is compiled @@ -245,7 +245,7 @@ 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() - + def file_path_match(file_path, pkg_file): if file_path.lstrip("/") == pkg_file.name.lstrip("/"): return True