diff mbox series

spdx30: Add SBOM metadata component and supplier support

Message ID 20260107180951.140895-5-stondo@gmail.com
State Changes Requested
Headers show
Series spdx30: Add SBOM metadata component and supplier support | expand

Commit Message

Stefano Tondo Jan. 7, 2026, 6:09 p.m. UTC
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(-)

Comments

Joshua Watt Jan. 7, 2026, 8:05 p.m. UTC | #1
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 mbox series

Patch

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