* [PATCH] spdx-common: Clarify documentation and make SPDX_LICENSES extensible
@ 2026-01-07 18:09 stondo
2026-01-07 18:09 ` [PATCH 2/4] spdx30_tasks: Add PURL generation for package identification stondo
` (3 more replies)
0 siblings, 4 replies; 9+ messages in thread
From: stondo @ 2026-01-07 18:09 UTC (permalink / raw)
To: openembedded-core
Cc: stondo, stefano.tondo.ext, peter.marko, adrian.freihofer
From: Stefano Tondo <stefano.tondo.ext@siemens.com>
This commit improves the SPDX variable documentation and enhances
SPDX_LICENSES to support layer-based license extensions.
1. SPDX_NAMESPACE_PREFIX documentation clarification:
- Clarify that this should be organization-specific
- Explain the default is for compatibility only
- Provide example of production override
- Make it consistent with SPDX_UUID_NAMESPACE guidance
2. SPDX_LICENSES documentation enhancement:
- Clarify when this variable needs to be set
- Document the new list behavior
- Provide example usage with += operator
3. SPDX_LICENSES implementation as extensible list:
- Change from single file to space-separated list of files
- Support layer-based license extensions without file copying
- Later files override earlier ones for duplicate license IDs
- Backward compatible (single file path still works)
- Add error handling for missing/invalid files
This enhancement allows layers to add custom licenses without
maintaining a copy of the base spdx-licenses.json file:
SPDX_LICENSES += "${LAYERDIR}/files/custom-licenses.json"
This is particularly useful for organizations with proprietary or
custom licenses that need to be tracked in SBOMs.
Signed-off-by: Stefano Tondo <stefano.tondo.ext@siemens.com>
---
meta/classes/spdx-common.bbclass | 13 +++++++++----
meta/lib/oe/spdx_common.py | 31 +++++++++++++++++++++++++++----
2 files changed, 36 insertions(+), 8 deletions(-)
diff --git a/meta/classes/spdx-common.bbclass b/meta/classes/spdx-common.bbclass
index 6bd1b56d96..fbb096c528 100644
--- a/meta/classes/spdx-common.bbclass
+++ b/meta/classes/spdx-common.bbclass
@@ -42,7 +42,10 @@ SPDX_UUID_NAMESPACE[doc] = "The namespace used for generating UUIDs in SPDX \
SPDX_NAMESPACE_PREFIX ??= "http://spdx.org/spdxdocs"
SPDX_NAMESPACE_PREFIX[doc] = "The URI prefix used for SPDX document namespaces. \
- Combined with other identifiers to create unique document URIs."
+ This should be a domain name or URI prefix unique to your organization to ensure \
+ globally unique document URIs. The default 'http://spdx.org/spdxdocs' is provided \
+ for compatibility but should be overridden in production environments (e.g., \
+ 'https://sbom.example.com')."
SPDX_PRETTY ??= "0"
SPDX_PRETTY[doc] = "If set to '1', generate human-readable formatted JSON output \
@@ -50,9 +53,11 @@ SPDX_PRETTY[doc] = "If set to '1', generate human-readable formatted JSON output
Pretty formatting makes files larger but easier to read."
SPDX_LICENSES ??= "${COREBASE}/meta/files/spdx-licenses.json"
-SPDX_LICENSES[doc] = "Path to the JSON file containing SPDX license identifier \
- mappings. This file maps common license names to official SPDX license \
- identifiers."
+SPDX_LICENSES[doc] = "Space-separated list of JSON files containing SPDX license \
+ identifier mappings. Files are processed in order, with later entries overriding \
+ earlier ones. This allows layers to extend the base license set without copying \
+ the entire file. Set this variable in your layer when using licenses not known \
+ to oe-core (e.g., 'SPDX_LICENSES += \"${LAYERDIR}/files/custom-licenses.json\"')."
SPDX_CUSTOM_ANNOTATION_VARS ??= ""
SPDX_CUSTOM_ANNOTATION_VARS[doc] = "Space-separated list of variable names whose \
diff --git a/meta/lib/oe/spdx_common.py b/meta/lib/oe/spdx_common.py
index 72c24180d5..8a6cf70fc1 100644
--- a/meta/lib/oe/spdx_common.py
+++ b/meta/lib/oe/spdx_common.py
@@ -42,10 +42,33 @@ def is_work_shared_spdx(d):
def load_spdx_license_data(d):
- with open(d.getVar("SPDX_LICENSES"), "r") as f:
- data = json.load(f)
- # Transform the license array to a dictionary
- data["licenses"] = {l["licenseId"]: l for l in data["licenses"]}
+ """
+ Load SPDX license data from one or more JSON files.
+ SPDX_LICENSES can be a space-separated list of files.
+ Later files override earlier ones for duplicate license IDs.
+ """
+ license_files = d.getVar("SPDX_LICENSES").split()
+
+ # Initialize with empty structure
+ data = {"licenses": {}}
+
+ # Load and merge each file
+ for license_file in license_files:
+ try:
+ with open(license_file, "r") as f:
+ file_data = json.load(f)
+ # Transform the license array to a dictionary and merge
+ if "licenses" in file_data:
+ for lic in file_data["licenses"]:
+ data["licenses"][lic["licenseId"]] = lic
+ # Copy over other top-level keys from the last file
+ for key in file_data:
+ if key != "licenses":
+ data[key] = file_data[key]
+ except FileNotFoundError:
+ bb.warn(f"SPDX license file not found: {license_file}")
+ except json.JSONDecodeError as e:
+ bb.warn(f"Invalid JSON in SPDX license file {license_file}: {e}")
return data
--
2.52.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH 2/4] spdx30_tasks: Add PURL generation for package identification
2026-01-07 18:09 [PATCH] spdx-common: Clarify documentation and make SPDX_LICENSES extensible stondo
@ 2026-01-07 18:09 ` stondo
2026-01-07 20:14 ` [OE-core] " Joshua Watt
2026-01-07 18:09 ` [PATCH 3/4] spdx30_tasks: Use recipe metadata for dependency PURL generation stondo
` (2 subsequent siblings)
3 siblings, 1 reply; 9+ messages in thread
From: stondo @ 2026-01-07 18:09 UTC (permalink / raw)
To: openembedded-core
Cc: stondo, stefano.tondo.ext, peter.marko, adrian.freihofer
From: Stefano Tondo <stefano.tondo.ext@siemens.com>
Add automatic Package URL (PURL) generation according to the Yocto PURL
specification to enable package identification in vulnerability databases
and improve NTIA SBOM compliance.
Field added:
- software_packageUrl: Auto-generates Package URLs per Yocto PURL spec
Format: pkg:yocto/<LAYERNAME>/<BPN>@<PV>
See: https://github.com/package-url/purl-spec/pull/372
PURL Implementation:
- Type: yocto (official PURL type for Yocto recipes, per PR #372)
- Namespace: Layer name from FILE_LAYERNAME variable
- Name: BPN (base package name with prefixes/suffixes removed)
- Version: PV (package version from recipe)
- Normalization: Lowercase per PURL spec
New BitBake variable:
- SPDX_PACKAGE_URL: Override auto-generated PURL
The Yocto PURL type specification (purl-spec PR #372) has been approved
by the PURL maintainers and is ready for implementation. This follows
the agreed format from JPEWdev (Joshua Watt) and petermarko.
Signed-off-by: Stefano Tondo <stefano.tondo.ext@siemens.com>
---
meta/lib/oe/spdx30_tasks.py | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
index f731a709e3..86430c7008 100644
--- a/meta/lib/oe/spdx30_tasks.py
+++ b/meta/lib/oe/spdx30_tasks.py
@@ -474,6 +474,36 @@ def create_spdx(d):
if val:
setattr(obj, name, val)
+ def generate_purl(d, package=None):
+ """
+ Generate Package URL (purl) for a package according to Yocto PURL spec.
+ Format: pkg:yocto/<LAYERNAME>/<BPN>@<PV>
+
+ See: https://github.com/package-url/purl-spec/pull/372
+ """
+ bpn = d.getVar("BPN")
+ pv = d.getVar("PV")
+
+ # Get layer name using FILE_LAYERNAME
+ # This is the correct variable that contains the layer name from BBFILE_COLLECTIONS
+ # (BBFILE_COLLECTIONS itself is not available outside of layer.conf)
+ layer = d.getVar("FILE_LAYERNAME")
+
+ if not layer:
+ layer = "core" # Default to core if layer detection fails
+
+ # For sub-packages, use BPN (base package name)
+ # Per spec: BPN has prefixes/suffixes removed
+ name = bpn
+
+ # Normalize name per PURL spec (lowercase only)
+ # Note: Underscores are not allowed in recipe names
+ name = name.lower()
+
+ purl = f"pkg:yocto/{layer}/{name}@{pv}"
+
+ return purl
+
license_data = oe.spdx_common.load_spdx_license_data(d)
deploydir = Path(d.getVar("SPDXDEPLOY"))
@@ -646,6 +676,10 @@ def create_spdx(d):
"software_packageUrl",
package=package
)
+ else:
+ # Auto-generate PURL if not manually specified
+ auto_purl = generate_purl(d, package)
+ spdx_package.software_packageUrl = auto_purl
pkg_objset.new_scoped_relationship(
[oe.sbom30.get_element_link_id(build)],
--
2.52.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH 3/4] spdx30_tasks: Use recipe metadata for dependency PURL generation
2026-01-07 18:09 [PATCH] spdx-common: Clarify documentation and make SPDX_LICENSES extensible stondo
2026-01-07 18:09 ` [PATCH 2/4] spdx30_tasks: Add PURL generation for package identification stondo
@ 2026-01-07 18:09 ` stondo
2026-01-07 18:09 ` [PATCH 4/4] spdx30_tasks: Add source package PURL support stondo
2026-01-07 18:09 ` [PATCH] spdx30: Add SBOM metadata component and supplier support stondo
3 siblings, 0 replies; 9+ messages in thread
From: stondo @ 2026-01-07 18:09 UTC (permalink / raw)
To: openembedded-core
Cc: stondo, stefano.tondo.ext, peter.marko, adrian.freihofer
From: Stefano Tondo <stefano.tondo.ext@siemens.com>
Use recipe metadata (PV, inherited classes) to determine package ecosystem
and version instead of unreliable filename parsing.
Previous implementation used greedy regex patterns matching any
name-version.tar.gz file, causing false positives:
zlib-1.3.1.tar.gz → pkg:pypi/zlib (WRONG - zlib is not from PyPI)
Changes:
- Always use d.getVar("PV") for version (addresses review feedback)
- Determine ecosystem via inherits_class() checks (pypi, npm, cpan, etc.)
- Only parse filenames for unambiguous cases (.crate extension)
- Support all major ecosystems: Rust, Go, PyPI, NPM, CPAN, NuGet, Maven
- Use pkg:generic for C/C++ libraries and other non-ecosystem sources
Example results:
- zlib source: pkg:generic/zlib@1.3.1
- zlib built package: pkg:yocto/core/zlib@1.3.1
- Python with pypi class: pkg:pypi/requests@2.31.0
- Rust crate: pkg:cargo/serde@1.0.0
This approach aligns with Yocto's metadata system and ensures every
source download gets a PURL for supply chain tracking.
Signed-off-by: Stefano Tondo <stefano.tondo.ext@siemens.com>
---
meta/lib/oe/spdx30_tasks.py | 160 ++++++++++++++++++++++++++++++++++++
1 file changed, 160 insertions(+)
diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
index 86430c7008..c685b649b3 100644
--- a/meta/lib/oe/spdx30_tasks.py
+++ b/meta/lib/oe/spdx30_tasks.py
@@ -357,6 +357,155 @@ def collect_dep_sources(dep_objsets, dest):
index_sources_by_hash(e.to, dest)
+def extract_dependency_metadata(d, file_name):
+ """
+ Extract version and generate PURL for dependency packages.
+
+ Uses recipe metadata (PV, inherited classes) to determine package ecosystem
+ rather than guessing from filenames. Only parses filenames for unambiguous
+ cases where the file extension definitively identifies the ecosystem.
+
+ Supported ecosystems:
+ - Rust crates (.crate extension is unambiguous)
+ - Go modules (when GO_IMPORT is set or domain pattern is explicit)
+ - PyPI packages (when recipe inherits pypi class)
+ - NPM packages (when recipe inherits npm class)
+ - CPAN packages (when recipe inherits cpan class)
+ - NuGet packages (when recipe inherits nuget/dotnet class)
+ - Maven packages (when recipe inherits maven class)
+
+ Returns: (version, purl) tuple, or (None, None) if cannot determine
+ """
+ import re
+
+ # Get version from recipe PV (always prefer recipe metadata over filename parsing)
+ pv = d.getVar("PV")
+ version = pv if pv else None
+ purl = None
+
+ # Case 1: Rust crate - .crate extension is unambiguous
+ if file_name.endswith('.crate'):
+ crate_match = re.match(r'^(.+?)-(\d+\.\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)\.crate$', file_name)
+ if crate_match:
+ name = crate_match.group(1)
+ # Use filename version for crates (they embed version in filename)
+ version = crate_match.group(2)
+ purl = f"pkg:cargo/{name}@{version}"
+ return (version, purl)
+
+ # Case 2: Go module - check if GO_IMPORT is set (most reliable)
+ go_import = d.getVar("GO_IMPORT")
+ if go_import and version:
+ # GO_IMPORT contains the module path (e.g., github.com/containers/storage)
+ purl = f"pkg:golang/{go_import}@{version}"
+ return (version, purl)
+
+ # Case 3: Go module from filename - only for explicit hosting domains with version in filename
+ # Patterns like github.com.user.repo-v1.2.3.tar.gz where the domain is explicit
+ go_match = re.match(
+ r'^((?:github|gitlab|gopkg|golang|go\.googlesource)\.com\.[\w.]+(?:\.[\w-]+)*?)-(v?\d+\.\d+\.\d+(?:[-+][\w.]+)?)\.',
+ file_name
+ )
+ if go_match:
+ # Convert dots to slashes for proper Go module path
+ # github.com.containers.storage → github.com/containers/storage
+ module_path = go_match.group(1).replace('.', '/', 1) # First dot only
+ parts = module_path.split('/', 1)
+ if len(parts) == 2:
+ domain = parts[0]
+ path = parts[1].replace('.', '/')
+ module_path = f"{domain}/{path}"
+
+ version = go_match.group(2)
+ purl = f"pkg:golang/{module_path}@{version}"
+ return (version, purl)
+
+ # Case 4: PyPI package - check if recipe inherits pypi class
+ if bb.data.inherits_class("pypi", d) and version:
+ # Get the PyPI package name from PYPI_PACKAGE variable (handles python3- prefix removal)
+ pypi_package = d.getVar("PYPI_PACKAGE")
+ if pypi_package:
+ # Normalize package name per PEP 503
+ name = re.sub(r"[-_.]+", "-", pypi_package).lower()
+ purl = f"pkg:pypi/{name}@{version}"
+ return (version, purl)
+
+ # Case 5: NPM package - check if recipe inherits npm class
+ if bb.data.inherits_class("npm", d) and version:
+ # Get package name from recipe
+ bpn = d.getVar("BPN")
+ if bpn:
+ # Remove npm- prefix if present
+ name = bpn[4:] if bpn.startswith('npm-') else bpn
+ purl = f"pkg:npm/{name}@{version}"
+ return (version, purl)
+
+ # Case 6: CPAN package - check if recipe inherits cpan class
+ if bb.data.inherits_class("cpan", d) and version:
+ # Get package name from recipe
+ bpn = d.getVar("BPN")
+ if bpn:
+ # Remove perl- or libperl- prefixes if present
+ if bpn.startswith('perl-'):
+ name = bpn[5:]
+ elif bpn.startswith('libperl-'):
+ name = bpn[8:]
+ else:
+ name = bpn
+ purl = f"pkg:cpan/{name}@{version}"
+ return (version, purl)
+
+ # Case 7: NuGet package - check if recipe inherits nuget/dotnet class
+ if (bb.data.inherits_class("nuget", d) or bb.data.inherits_class("dotnet", d)) and version:
+ bpn = d.getVar("BPN")
+ if bpn:
+ # Remove dotnet- or nuget- prefix if present
+ if bpn.startswith('dotnet-'):
+ name = bpn[7:]
+ elif bpn.startswith('nuget-'):
+ name = bpn[6:]
+ else:
+ name = bpn
+ purl = f"pkg:nuget/{name}@{version}"
+ return (version, purl)
+
+ # Case 8: Maven package - check if recipe inherits maven class
+ if bb.data.inherits_class("maven", d) and version:
+ # Maven PURLs require group:artifact format
+ # Check for MAVEN_GROUP_ID and MAVEN_ARTIFACT_ID variables
+ group_id = d.getVar("MAVEN_GROUP_ID")
+ artifact_id = d.getVar("MAVEN_ARTIFACT_ID")
+
+ if group_id and artifact_id:
+ # Proper Maven PURL: pkg:maven/group.id/artifact@version
+ purl = f"pkg:maven/{group_id}/{artifact_id}@{version}"
+ return (version, purl)
+ else:
+ # Fallback: use BPN as artifact name without group
+ bpn = d.getVar("BPN")
+ if bpn:
+ # Remove maven- or java- prefix if present
+ if bpn.startswith('maven-'):
+ name = bpn[6:]
+ elif bpn.startswith('java-'):
+ name = bpn[5:]
+ else:
+ name = bpn
+ purl = f"pkg:maven/{name}@{version}"
+ return (version, purl)
+
+ # Fallback: use pkg:generic for source downloads without specific ecosystem
+ # This covers C/C++ libraries and other non-ecosystem packages
+ bpn = d.getVar("BPN")
+ if version and bpn:
+ # Generic PURL for source tarballs (e.g., zlib, openssl, curl)
+ # The built package will have pkg:yocto/... PURL
+ purl = f"pkg:generic/{bpn}@{version}"
+ return (version, purl)
+
+ return (version, None)
+
+
def add_download_files(d, objset):
inputs = set()
@@ -408,6 +557,9 @@ def add_download_files(d, objset):
inputs.add(file)
else:
+ # Extract version and PURL for dependency packages using recipe metadata
+ dep_version, dep_purl = extract_dependency_metadata(d, file_name)
+
dl = objset.add(
oe.spdx30.software_Package(
_id=objset.new_spdxid("source", str(download_idx + 1)),
@@ -420,6 +572,14 @@ def add_download_files(d, objset):
)
)
+ # Add version if extracted
+ if dep_version:
+ dl.software_packageVersion = dep_version
+
+ # Add PURL if generated
+ if dep_purl:
+ dl.software_packageUrl = dep_purl
+
if fd.method.supports_checksum(fd):
# TODO Need something better than hard coding this
for checksum_id in ["sha256", "sha1"]:
--
2.52.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH 4/4] spdx30_tasks: Add source package PURL support
2026-01-07 18:09 [PATCH] spdx-common: Clarify documentation and make SPDX_LICENSES extensible stondo
2026-01-07 18:09 ` [PATCH 2/4] spdx30_tasks: Add PURL generation for package identification stondo
2026-01-07 18:09 ` [PATCH 3/4] spdx30_tasks: Use recipe metadata for dependency PURL generation stondo
@ 2026-01-07 18:09 ` stondo
2026-01-07 18:09 ` [PATCH] spdx30: Add SBOM metadata component and supplier support stondo
3 siblings, 0 replies; 9+ messages in thread
From: stondo @ 2026-01-07 18:09 UTC (permalink / raw)
To: openembedded-core
Cc: stondo, stefano.tondo.ext, peter.marko, adrian.freihofer
From: Stefano Tondo <stefano.tondo.ext@siemens.com>
- Move generate_purl() to module level for broader accessibility
(fixes NameError when called from add_download_files)
- Add PURL generation for source packages with type=source qualifier
- Follows ECMA-427 PURL spec (similar to Maven's classifier=sources)
This allows source packages to be properly identified with PURLs
like pkg:yocto/core/glibc@2.42+git?type=source, distinguishing
them from runtime packages while maintaining compatibility with
the PURL specification.
---
meta/lib/oe/spdx30_tasks.py | 68 ++++++++++++++++++++-----------------
1 file changed, 37 insertions(+), 31 deletions(-)
diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
index c685b649b3..95ec4ebafb 100644
--- a/meta/lib/oe/spdx30_tasks.py
+++ b/meta/lib/oe/spdx30_tasks.py
@@ -576,9 +576,14 @@ def add_download_files(d, objset):
if dep_version:
dl.software_packageVersion = dep_version
- # Add PURL if generated
+ # Add PURL if generated for dependencies, otherwise generate recipe PURL for source
if dep_purl:
dl.software_packageUrl = dep_purl
+ elif primary_purpose == oe.spdx30.software_SoftwarePurpose.source:
+ # For the main recipe source, generate PURL with type=source qualifier
+ recipe_purl = generate_purl(d)
+ # Add type=source qualifier to distinguish from runtime packages
+ dl.software_packageUrl = f"{recipe_purl}?type=source"
if fd.method.supports_checksum(fd):
# TODO Need something better than hard coding this
@@ -601,6 +606,37 @@ def add_download_files(d, objset):
return inputs
+def generate_purl(d, package=None):
+ """
+ Generate Package URL (purl) for a package according to Yocto PURL spec.
+ Format: pkg:yocto/<LAYERNAME>/<BPN>@<PV>
+
+ See: https://github.com/package-url/purl-spec/pull/372
+ """
+ bpn = d.getVar("BPN")
+ pv = d.getVar("PV")
+
+ # Get layer name using FILE_LAYERNAME
+ # This is the correct variable that contains the layer name from BBFILE_COLLECTIONS
+ # (BBFILE_COLLECTIONS itself is not available outside of layer.conf)
+ layer = d.getVar("FILE_LAYERNAME")
+
+ if not layer:
+ layer = "core" # Default to core if layer detection fails
+
+ # For sub-packages, use BPN (base package name)
+ # Per spec: BPN has prefixes/suffixes removed
+ name = bpn
+
+ # Normalize name per PURL spec (lowercase only)
+ # Note: Underscores are not allowed in recipe names
+ name = name.lower()
+
+ purl = f"pkg:yocto/{layer}/{name}@{pv}"
+
+ return purl
+
+
def set_purposes(d, element, *var_names, force_purposes=[]):
purposes = force_purposes[:]
@@ -634,36 +670,6 @@ def create_spdx(d):
if val:
setattr(obj, name, val)
- def generate_purl(d, package=None):
- """
- Generate Package URL (purl) for a package according to Yocto PURL spec.
- Format: pkg:yocto/<LAYERNAME>/<BPN>@<PV>
-
- See: https://github.com/package-url/purl-spec/pull/372
- """
- bpn = d.getVar("BPN")
- pv = d.getVar("PV")
-
- # Get layer name using FILE_LAYERNAME
- # This is the correct variable that contains the layer name from BBFILE_COLLECTIONS
- # (BBFILE_COLLECTIONS itself is not available outside of layer.conf)
- layer = d.getVar("FILE_LAYERNAME")
-
- if not layer:
- layer = "core" # Default to core if layer detection fails
-
- # For sub-packages, use BPN (base package name)
- # Per spec: BPN has prefixes/suffixes removed
- name = bpn
-
- # Normalize name per PURL spec (lowercase only)
- # Note: Underscores are not allowed in recipe names
- name = name.lower()
-
- purl = f"pkg:yocto/{layer}/{name}@{pv}"
-
- return purl
-
license_data = oe.spdx_common.load_spdx_license_data(d)
deploydir = Path(d.getVar("SPDXDEPLOY"))
--
2.52.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH] spdx30: Add SBOM metadata component and supplier support
2026-01-07 18:09 [PATCH] spdx-common: Clarify documentation and make SPDX_LICENSES extensible stondo
` (2 preceding siblings ...)
2026-01-07 18:09 ` [PATCH 4/4] spdx30_tasks: Add source package PURL support stondo
@ 2026-01-07 18:09 ` stondo
2026-01-07 20:05 ` [OE-core] " Joshua Watt
2026-01-08 10:34 ` [PATCH] spdx30: Add supplier support for image and SDK SBOMs stondo
3 siblings, 2 replies; 9+ messages in thread
From: stondo @ 2026-01-07 18:09 UTC (permalink / raw)
To: openembedded-core
Cc: stondo, stefano.tondo.ext, peter.marko, adrian.freihofer
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=[]):
)
)
+ # 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
--
2.52.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [OE-core] [PATCH] spdx30: Add SBOM metadata component and supplier support
2026-01-07 18:09 ` [PATCH] spdx30: Add SBOM metadata component and supplier support stondo
@ 2026-01-07 20:05 ` Joshua Watt
2026-01-08 10:34 ` [PATCH] spdx30: Add supplier support for image and SDK SBOMs stondo
1 sibling, 0 replies; 9+ messages in thread
From: Joshua Watt @ 2026-01-07 20:05 UTC (permalink / raw)
To: stondo; +Cc: openembedded-core, stefano.tondo.ext, peter.marko,
adrian.freihofer
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]
> -=-=-=-=-=-=-=-=-=-=-=-
>
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [OE-core] [PATCH 2/4] spdx30_tasks: Add PURL generation for package identification
2026-01-07 18:09 ` [PATCH 2/4] spdx30_tasks: Add PURL generation for package identification stondo
@ 2026-01-07 20:14 ` Joshua Watt
2026-01-08 11:14 ` Tondo, Stefano
0 siblings, 1 reply; 9+ messages in thread
From: Joshua Watt @ 2026-01-07 20:14 UTC (permalink / raw)
To: stondo; +Cc: openembedded-core, stefano.tondo.ext, peter.marko,
adrian.freihofer
I do plan on implementing this once the PURL spec PR is merged. I
already have a proof of concept:
https://git.yoctoproject.org/poky-contrib/commit/?h=jpew/purls&id=538c31488e7300a4ad3320e861324a4858a6f148
The "extra" repository URLs that it generates are done terribly ATM
and it needs a few other things to be cleaned up. However, it will
also correctly allow end users to add multiple purls (so they can have
their own PURLs in addition to the Yocto ones).
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>
>
> Add automatic Package URL (PURL) generation according to the Yocto PURL
> specification to enable package identification in vulnerability databases
> and improve NTIA SBOM compliance.
>
> Field added:
> - software_packageUrl: Auto-generates Package URLs per Yocto PURL spec
> Format: pkg:yocto/<LAYERNAME>/<BPN>@<PV>
> See: https://github.com/package-url/purl-spec/pull/372
>
> PURL Implementation:
> - Type: yocto (official PURL type for Yocto recipes, per PR #372)
> - Namespace: Layer name from FILE_LAYERNAME variable
> - Name: BPN (base package name with prefixes/suffixes removed)
> - Version: PV (package version from recipe)
> - Normalization: Lowercase per PURL spec
>
> New BitBake variable:
> - SPDX_PACKAGE_URL: Override auto-generated PURL
>
> The Yocto PURL type specification (purl-spec PR #372) has been approved
> by the PURL maintainers and is ready for implementation. This follows
> the agreed format from JPEWdev (Joshua Watt) and petermarko.
>
> Signed-off-by: Stefano Tondo <stefano.tondo.ext@siemens.com>
> ---
> meta/lib/oe/spdx30_tasks.py | 34 ++++++++++++++++++++++++++++++++++
> 1 file changed, 34 insertions(+)
>
> diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
> index f731a709e3..86430c7008 100644
> --- a/meta/lib/oe/spdx30_tasks.py
> +++ b/meta/lib/oe/spdx30_tasks.py
> @@ -474,6 +474,36 @@ def create_spdx(d):
> if val:
> setattr(obj, name, val)
>
> + def generate_purl(d, package=None):
> + """
> + Generate Package URL (purl) for a package according to Yocto PURL spec.
> + Format: pkg:yocto/<LAYERNAME>/<BPN>@<PV>
> +
> + See: https://github.com/package-url/purl-spec/pull/372
> + """
> + bpn = d.getVar("BPN")
> + pv = d.getVar("PV")
> +
> + # Get layer name using FILE_LAYERNAME
> + # This is the correct variable that contains the layer name from BBFILE_COLLECTIONS
> + # (BBFILE_COLLECTIONS itself is not available outside of layer.conf)
> + layer = d.getVar("FILE_LAYERNAME")
> +
> + if not layer:
> + layer = "core" # Default to core if layer detection fails
> +
> + # For sub-packages, use BPN (base package name)
> + # Per spec: BPN has prefixes/suffixes removed
> + name = bpn
> +
> + # Normalize name per PURL spec (lowercase only)
> + # Note: Underscores are not allowed in recipe names
> + name = name.lower()
> +
> + purl = f"pkg:yocto/{layer}/{name}@{pv}"
> +
> + return purl
> +
> license_data = oe.spdx_common.load_spdx_license_data(d)
>
> deploydir = Path(d.getVar("SPDXDEPLOY"))
> @@ -646,6 +676,10 @@ def create_spdx(d):
> "software_packageUrl",
> package=package
> )
> + else:
> + # Auto-generate PURL if not manually specified
> + auto_purl = generate_purl(d, package)
> + spdx_package.software_packageUrl = auto_purl
>
> pkg_objset.new_scoped_relationship(
> [oe.sbom30.get_element_link_id(build)],
> --
> 2.52.0
>
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#229021): https://lists.openembedded.org/g/openembedded-core/message/229021
> Mute This Topic: https://lists.openembedded.org/mt/117138938/3616693
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [JPEWhacker@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
>
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH] spdx30: Add supplier support for image and SDK SBOMs
2026-01-07 18:09 ` [PATCH] spdx30: Add SBOM metadata component and supplier support stondo
2026-01-07 20:05 ` [OE-core] " Joshua Watt
@ 2026-01-08 10:34 ` stondo
1 sibling, 0 replies; 9+ messages in thread
From: stondo @ 2026-01-08 10:34 UTC (permalink / raw)
To: openembedded-core
Cc: stondo, stefano.tondo.ext, peter.marko, adrian.freihofer,
JPEWhacker
From: Stefano Tondo <stefano.tondo.ext@siemens.com>
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 | 24 ++++++++++++++++++++++++
2 files changed, 34 insertions(+)
diff --git a/meta/classes/create-spdx-3.0.bbclass b/meta/classes/create-spdx-3.0.bbclass
index 96c0b9722b..9f9066eeb5 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 c86b088b61..2883e2202a 100644
--- a/meta/lib/oe/spdx30_tasks.py
+++ b/meta/lib/oe/spdx30_tasks.py
@@ -1305,6 +1305,18 @@ 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
+ supplier = objset.new_agent("SPDX_IMAGE_SUPPLIER", 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):
@@ -1416,6 +1428,18 @@ 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
+ supplier = objset.new_agent("SPDX_SDK_SUPPLIER", 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.52.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [OE-core] [PATCH 2/4] spdx30_tasks: Add PURL generation for package identification
2026-01-07 20:14 ` [OE-core] " Joshua Watt
@ 2026-01-08 11:14 ` Tondo, Stefano
0 siblings, 0 replies; 9+ messages in thread
From: Tondo, Stefano @ 2026-01-08 11:14 UTC (permalink / raw)
To: Joshua Watt, stondo@gmail.com
Cc: openembedded-core@lists.openembedded.org, Marko, Peter,
Freihofer, Adrian
[-- Attachment #1: Type: text/plain, Size: 6764 bytes --]
Hi Joshua,
Thank you for the feedback and for sharing your proof of concept.
I have reviewed your jpew/purls branch and agree that your implementation is the better approach. It is more comprehensive and the support for multiple PURLs (both Yocto and custom) is a significant improvement over my proposal.
Since you plan to move forward with that implementation once PURL spec PR #372 is merged, I am withdrawing patches 2, 3, and 4 from this series to avoid duplication.
Regarding the rest of my series:
*
I will keep patch 1/4 (documentation clarification) as it is independent.
*
I have already submitted a v2 for the supplier support patch (referencing your earlier feedback).
Please feel free to cherry-pick or adapt any logic from my withdrawn patches if useful. I am happy to help test your implementation once it lands.
Best regards,
Stefano
________________________________
From: Joshua Watt <jpewhacker@gmail.com>
Sent: Wednesday, January 7, 2026 21:14
To: stondo@gmail.com <stondo@gmail.com>
Cc: openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org>; Tondo, Stefano (ext) (SI B PRO AUT PD ZUG SW 2) <stefano.tondo.ext@siemens.com>; Marko, Peter (FT D EU SK BFS1) <Peter.Marko@siemens.com>; Freihofer, Adrian (SI B PRO TI EAC CCP) <adrian.freihofer@siemens.com>
Subject: Re: [OE-core] [PATCH 2/4] spdx30_tasks: Add PURL generation for package identification
I do plan on implementing this once the PURL spec PR is merged. I
already have a proof of concept:
https://eur01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgit.yoctoproject.org%2Fpoky-contrib%2Fcommit%2F%3Fh%3Djpew%2Fpurls%26id%3D538c31488e7300a4ad3320e861324a4858a6f148&data=05%7C02%7Cstefano.tondo.ext%40siemens.com%7Cf7d0c35415234e07f7f808de4e295947%7C38ae3bcd95794fd4addab42e1495d55a%7C1%7C0%7C639034136656442825%7CUnknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=sZo30nhs%2FY%2BTdbci4I9GQYAr0sCBwrrJqhXD%2FrAUdsE%3D&reserved=0<https://git.yoctoproject.org/poky-contrib/commit/?h=jpew/purls&id=538c31488e7300a4ad3320e861324a4858a6f148>
The "extra" repository URLs that it generates are done terribly ATM
and it needs a few other things to be cleaned up. However, it will
also correctly allow end users to add multiple purls (so they can have
their own PURLs in addition to the Yocto ones).
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>
>
> Add automatic Package URL (PURL) generation according to the Yocto PURL
> specification to enable package identification in vulnerability databases
> and improve NTIA SBOM compliance.
>
> Field added:
> - software_packageUrl: Auto-generates Package URLs per Yocto PURL spec
> Format: pkg:yocto/<LAYERNAME>/<BPN>@<PV>
> See: https://eur01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fpackage-url%2Fpurl-spec%2Fpull%2F372&data=05%7C02%7Cstefano.tondo.ext%40siemens.com%7Cf7d0c35415234e07f7f808de4e295947%7C38ae3bcd95794fd4addab42e1495d55a%7C1%7C0%7C639034136656477188%7CUnknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=wpryQd4H8mx1f%2B6APIoqrXvboWghjhxDjFDNrXliqxI%3D&reserved=0<https://github.com/package-url/purl-spec/pull/372>
>
> PURL Implementation:
> - Type: yocto (official PURL type for Yocto recipes, per PR #372)
> - Namespace: Layer name from FILE_LAYERNAME variable
> - Name: BPN (base package name with prefixes/suffixes removed)
> - Version: PV (package version from recipe)
> - Normalization: Lowercase per PURL spec
>
> New BitBake variable:
> - SPDX_PACKAGE_URL: Override auto-generated PURL
>
> The Yocto PURL type specification (purl-spec PR #372) has been approved
> by the PURL maintainers and is ready for implementation. This follows
> the agreed format from JPEWdev (Joshua Watt) and petermarko.
>
> Signed-off-by: Stefano Tondo <stefano.tondo.ext@siemens.com>
> ---
> meta/lib/oe/spdx30_tasks.py | 34 ++++++++++++++++++++++++++++++++++
> 1 file changed, 34 insertions(+)
>
> diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
> index f731a709e3..86430c7008 100644
> --- a/meta/lib/oe/spdx30_tasks.py
> +++ b/meta/lib/oe/spdx30_tasks.py
> @@ -474,6 +474,36 @@ def create_spdx(d):
> if val:
> setattr(obj, name, val)
>
> + def generate_purl(d, package=None):
> + """
> + Generate Package URL (purl) for a package according to Yocto PURL spec.
> + Format: pkg:yocto/<LAYERNAME>/<BPN>@<PV>
> +
> + See: https://eur01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fpackage-url%2Fpurl-spec%2Fpull%2F372&data=05%7C02%7Cstefano.tondo.ext%40siemens.com%7Cf7d0c35415234e07f7f808de4e295947%7C38ae3bcd95794fd4addab42e1495d55a%7C1%7C0%7C639034136656499697%7CUnknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=NqHnK0zl6ASj8lKc%2BpV6LaDoozpgpugPMmDWslCA62w%3D&reserved=0<https://github.com/package-url/purl-spec/pull/372>
> + """
> + bpn = d.getVar("BPN")
> + pv = d.getVar("PV")
> +
> + # Get layer name using FILE_LAYERNAME
> + # This is the correct variable that contains the layer name from BBFILE_COLLECTIONS
> + # (BBFILE_COLLECTIONS itself is not available outside of layer.conf)
> + layer = d.getVar("FILE_LAYERNAME")
> +
> + if not layer:
> + layer = "core" # Default to core if layer detection fails
> +
> + # For sub-packages, use BPN (base package name)
> + # Per spec: BPN has prefixes/suffixes removed
> + name = bpn
> +
> + # Normalize name per PURL spec (lowercase only)
> + # Note: Underscores are not allowed in recipe names
> + name = name.lower()
> +
> + purl = f"pkg:yocto/{layer}/{name}@{pv}"
> +
> + return purl
> +
> license_data = oe.spdx_common.load_spdx_license_data(d)
>
> deploydir = Path(d.getVar("SPDXDEPLOY"))
> @@ -646,6 +676,10 @@ def create_spdx(d):
> "software_packageUrl",
> package=package
> )
> + else:
> + # Auto-generate PURL if not manually specified
> + auto_purl = generate_purl(d, package)
> + spdx_package.software_packageUrl = auto_purl
>
> pkg_objset.new_scoped_relationship(
> [oe.sbom30.get_element_link_id(build)],
> --
> 2.52.0
>
>
>
>
[-- Attachment #2: Type: text/html, Size: 11991 bytes --]
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-01-08 11:54 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-01-07 18:09 [PATCH] spdx-common: Clarify documentation and make SPDX_LICENSES extensible stondo
2026-01-07 18:09 ` [PATCH 2/4] spdx30_tasks: Add PURL generation for package identification stondo
2026-01-07 20:14 ` [OE-core] " Joshua Watt
2026-01-08 11:14 ` Tondo, Stefano
2026-01-07 18:09 ` [PATCH 3/4] spdx30_tasks: Use recipe metadata for dependency PURL generation stondo
2026-01-07 18:09 ` [PATCH 4/4] spdx30_tasks: Add source package PURL support stondo
2026-01-07 18:09 ` [PATCH] spdx30: Add SBOM metadata component and supplier support stondo
2026-01-07 20:05 ` [OE-core] " Joshua Watt
2026-01-08 10:34 ` [PATCH] spdx30: Add supplier support for image and SDK SBOMs stondo
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox