From: "'Niedermayr, BENEDIKT' via isar-users" <isar-users@googlegroups.com>
To: "MOESSBAUER, Felix" <felix.moessbauer@siemens.com>,
"isar-users@googlegroups.com" <isar-users@googlegroups.com>
Cc: "Kiszka, Jan" <jan.kiszka@siemens.com>,
"Hillier, Gernot" <gernot.hillier@siemens.com>,
"Steiger, Christoph" <christoph.steiger@siemens.com>
Subject: Re: [RFC PATCH 1/1] meta: add CycloneDX/SPDX SBOM generation
Date: Tue, 4 Mar 2025 11:54:53 +0000 [thread overview]
Message-ID: <e331ec02-9594-41b1-b206-0ea10160543c@siemens.com> (raw)
In-Reply-To: <20250220095944.114203-2-felix.moessbauer@siemens.com>
On 20.02.25 10:59, 'Felix Moessbauer' via isar-users wrote:
> From: Christoph Steiger <christoph.steiger@siemens.com>
>
> Add a new class to allow generation of software bill of materials
> (SBOM). Supported are the two standard SBOM formats CycloneDX and SPDX.
> SBOM generation is enabled per default for all images.
>
> Both formats support the minimal usecase of binary packages information
> and their dependencies. Unfortunately there is no proper way to express
> the relationships of debian source packages and their corresponding
> binary packages in the CDX format, so it is left out there.
>
> The information included in the SBOM is parsed from the dpkg status
> file found in the created image.
AFAIK you're using the cyclonedx-python-lib [1], right?
This package is not packaged by debian, yet and the python package needs
to be run in python-virtualenv.
Do you have any plans to support this?
[1] https://pypi.org/project/cyclonedx-python-lib/
>
> Signed-off-by: Christoph Steiger <christoph.steiger@siemens.com>
> ---
> meta/classes/create-sbom.bbclass | 49 ++++
> meta/classes/image.bbclass | 2 +
> meta/lib/sbom.py | 446 +++++++++++++++++++++++++++++++
> meta/lib/sbom_cdx_types.py | 82 ++++++
> meta/lib/sbom_spdx_types.py | 95 +++++++
> 5 files changed, 674 insertions(+)
> create mode 100644 meta/classes/create-sbom.bbclass
> create mode 100644 meta/lib/sbom.py
> create mode 100644 meta/lib/sbom_cdx_types.py
> create mode 100644 meta/lib/sbom_spdx_types.py
>
> diff --git a/meta/classes/create-sbom.bbclass b/meta/classes/create-sbom.bbclass
> new file mode 100644
> index 00000000..8c647699
> --- /dev/null
> +++ b/meta/classes/create-sbom.bbclass
> @@ -0,0 +1,49 @@
> +# This software is a part of ISAR.
> +# Copyright (C) 2025 Siemens AG
> +#
> +# SPDX-License-Identifier: MIT
> +
> +# sbom type to generate, accepted are "cyclonedx" and "spdx"
> +SBOM_TYPE ?= "cyclonedx spdx"
> +
> +# general user variables
> +SBOM_DISTRO_SUPPLIER ?= "ISAR"
> +SBOM_DISTRO_NAME ?= "ISAR-Debian-GNU-Linux"
> +SBOM_DISTRO_VERSION ?= "1.0.0"
> +SBOM_DISTRO_SUMMARY ?= "Linux distribution built with ISAR"
> +SBOM_DOCUMENT_UUID ?= ""
> +
> +# SPDX specific user variables
> +SBOM_SPDX_NAMESPACE_PREFIX ?= "https://spdx.org/spdxdocs"
> +
> +SBOM_DEPLOY_BASE = "${DEPLOY_DIR_IMAGE}/${IMAGE_FULLNAME}"
> +
> +SBOM_GEN_VERSION = "0.1.0"
> +
> +# adapted from the isar-cip-core image_uuid.bbclass
> +def generate_document_uuid(d):
> + import uuid
> +
> + base_hash = d.getVar("BB_TASKHASH")
> + if base_hash is None:
> + bb.warn("no BB_TASKHASH available, SBOM UUID is not reproducible")
> + return uuid.uuid4()
> + return str(uuid.UUID(base_hash[:32], version=4))
> +
> +python do_create_sbom() {
> + import sbom
> +
> + dpkg_status = d.getVar("IMAGE_ROOTFS") + "/var/lib/dpkg/status"
> + packages = sbom.Package.parse_status_file(dpkg_status)
> +
> + if not d.getVar("SBOM_DOCUMENT_UUID"):
> + d.setVar("SBOM_DOCUMENT_UUID", generate_document_uuid(d))
> +
> + sbom_type = d.getVar("SBOM_TYPE")
> + if "cyclonedx" in sbom_type:
> + sbom.generate(d, packages, sbom.SBOMType.CycloneDX, d.getVar("SBOM_DEPLOY_BASE") + ".cyclonedx.json")
> + if "spdx" in sbom_type:
> + sbom.generate(d, packages, sbom.SBOMType.SPDX, d.getVar("SBOM_DEPLOY_BASE") + ".spdx.json")
> +}
Maybe a variable for enabling/disabling this feature would be nice.
(IMAGE_FEATURES, DISTRO_FEATURES).
> +
> +addtask do_create_sbom after do_rootfs before do_build
> diff --git a/meta/classes/image.bbclass b/meta/classes/image.bbclass
> index 56eca202..e9da6a61 100644
> --- a/meta/classes/image.bbclass
> +++ b/meta/classes/image.bbclass
> @@ -81,6 +81,8 @@ inherit image-postproc-extension
> inherit image-locales-extension
> inherit image-account-extension
>
> +inherit create-sbom
> +
> # Extra space for rootfs in MB
> ROOTFS_EXTRA ?= "64"
>
> diff --git a/meta/lib/sbom.py b/meta/lib/sbom.py
> new file mode 100644
> index 00000000..d7c79e43
> --- /dev/null
> +++ b/meta/lib/sbom.py
> @@ -0,0 +1,446 @@
> +# This software is part of ISAR.
> +# Copyright (C) 2025 Siemens AG
> +#
> +# SPDX-License-Identifier: MIT
> +
> +from dataclasses import dataclass
> +from datetime import datetime
> +from enum import Enum
> +from typing import Dict, List, Type
> +import json
> +import re
> +from uuid import uuid4
> +
> +import sbom_cdx_types as cdx
> +import sbom_spdx_types as spdx
> +
> +
> +class SBOMType(Enum):
> + CycloneDX = (0,)
> + SPDX = (1,)
> +
> +
> +@dataclass
> +class SourcePackage:
> + name: str
> + version: str | None
> +
> + def purl(self):
> + """Return the PURL of the package."""
> + return "pkg:deb/debian/{}@{}?arch=source".format(self.name, self.version)
> +
> + def bom_ref(self, sbom_type: SBOMType) -> str:
> + """Return a unique BOM reference."""
> + if sbom_type == SBOMType.CycloneDX:
> + return cdx.CDXREF_PREFIX + "{}-src".format(self.name)
> + elif sbom_type == SBOMType.SPDX:
> + return spdx.SPDX_REF_PREFIX + "{}-src".format(self.name)
> +
> + def parse(s: str) -> Type["SourcePackage"]:
Using "@classmethod" for this factory method would be more idiomatic.
> + split = s.split(" ")
> + name = split[0]
> + try:
> + version = " ".join(split[1:]).strip("()")
> + except IndexError:
> + version = None
> +
> + return SourcePackage(name=name, version=version)
> +
> +
> +@dataclass
> +class Dependency:
> + name: str
> + version: str | None
> +
> + def bom_ref(self, sbom_type: SBOMType) -> str:
> + """Return a unique BOM reference."""
> + if sbom_type == SBOMType.CycloneDX:
> + return cdx.CDX_REF_PREFIX + "{}".format(self.name)
> + elif sbom_type == SBOMType.SPDX:
> + return spdx.SPDX_REF_PREFIX + "{}".format(self.name)
> +
> + def parse_multiple(s: str) -> List[Type["Dependency"]]:
... Using "@classmethod" for this factory method would be more idiomatic.
> + """Parse a 'Depends' line in the dpkg status file."""
> + dependencies = []
> + for entry in s.split(","):
> + entry = entry.strip()
> + for entry in entry.split("|"):
> + split = entry.split("(")
> + name = split[0].strip()
> + try:
> + version = split[1].strip(")")
> + except IndexError:
> + version = None
> + dependencies.append(Dependency(name=name, version=version))
> +
> + return dependencies
> +
> +
> +@dataclass
> +class Package:
> + """Incomplete representation of a debian package."""
> +
> + name: str
> + section: str
> + maintainer: str
> + architecture: str
> + source: SourcePackage
> + version: str
> + depends: List[Dependency]
> + description: str
> + homepage: str
> +
> + def purl(self) -> str:
> + """Return the PURL of the package."""
> + purl = "pkg:deb/debian/{}@{}".format(self.name, self.version)
> + if self.architecture:
> + purl = purl + "?arch={}".format(self.architecture)
> + return purl
> +
> + def bom_ref(self, sbom_type: SBOMType) -> str:
> + """Return a unique BOM reference."""
> + if sbom_type == SBOMType.CycloneDX:
> + return cdx.CDX_REF_PREFIX + self.name
> + elif sbom_type == SBOMType.SPDX:
> + return spdx.SPDX_REF_PREFIX + self.name
> +
> + def parse_status_file(status_file: str) -> List[Type["Package"]]:
...Using "@classmethod" for this factory method would be more idiomatic.
Regards,
Benedikt
> + """Parse a dpkg status file."""
> + packages = []
> + with open(status_file, "r") as f:
> + name = None
> + section = None
> + maintainer = None
> + architecture = None
> + source = None
> + version = None
> + dependencies = None
> + description = None
> + homepage = None
> + for line in f.readlines():
> + if line.strip():
> + if line[0] == " ":
> + # this is a description line, we ignore it
> + continue
> + else:
> + split = line.split(":")
> + key = split[0]
> + value = ":".join(split[1:]).strip()
> + if key == "Package":
> + name = value
> + elif key == "Section":
> + section = value
> + elif key == "Maintainer":
> + maintainer = value
> + elif key == "Architecture":
> + architecture = value
> + elif key == "Source":
> + source = SourcePackage.parse(value)
> + elif key == "Version":
> + version = value
> + elif key == "Depends":
> + dependencies = Dependency.parse_multiple(value)
> + elif key == "Description":
> + description = value
> + elif key == "Homepage":
> + homepage = value
> + else:
> + # fixup source version, if not specified it is the same
> + # as the package version
> + if source and not source.version:
> + source.version = version
> + # empty line means new package, so finish the current one
> + packages.append(
> + Package(
> + name=name,
> + section=section,
> + maintainer=maintainer,
> + architecture=architecture,
> + source=source,
> + version=version,
> + depends=dependencies,
> + description=description,
> + homepage=homepage,
> + )
> + )
> + name = None
> + section = None
> + maintainer = None
> + architecture = None
> + source = None
> + version = None
> + dependencies = None
> + description = None
> + homepage = None
> +
> + return packages
> +
> +
> +def cyclonedx_bom(d, packages: List[Package]) -> Dict:
> + """Return a valid CycloneDX SBOM."""
> + data = []
> + dependencies = []
> +
> + pattern = re.compile("(?P<supplier_name>^[^<]*)(\\<(?P<supplier_email>.*)\\>)?")
> + for package in packages:
> + match = pattern.match(package.maintainer)
> + supplier = cdx.CDXSupplier(name=match["supplier_name"])
> + supplier_email = match["supplier_email"]
> + if supplier_email:
> + supplier.contact = [cdx.CDXSupplierContact(email=supplier_email)]
> + entry = cdx.CDXComponent(
> + type=cdx.CDX_COMPONENT_TYPE_LIBRARY,
> + bom_ref=package.bom_ref(SBOMType.CycloneDX),
> + supplier=supplier,
> + name=package.name,
> + version=package.version,
> + description=package.description,
> + purl=package.purl(),
> + )
> + if package.homepage:
> + entry.externalReferences = (
> + cdx.CDXExternalReference(
> + url=package.homepage,
> + type=cdx.CDX_PACKAGE_EXTREF_TYPE_WEBSITE,
> + comment="homepage",
> + ),
> + )
> + data.append(entry)
> +
> + distro_bom_ref = cdx.CDX_REF_PREFIX + d.getVar("SBOM_DISTRO_NAME")
> + distro_dependencies = []
> + # after we have found all packages we can start to resolve dependencies
> + package_names = [package.name for package in packages]
> + for package in packages:
> + distro_dependencies.append(package.bom_ref(SBOMType.CycloneDX))
> + if package.depends:
> + deps = []
> + for dep in package.depends:
> + dep_bom_ref = dep.bom_ref(SBOMType.CycloneDX)
> + # it is possibe to specify the same package multiple times, but
> + # in different versions
> + if dep.name in package_names and dep_bom_ref not in deps:
> + deps.append(dep_bom_ref)
> + else:
> + # this might happen if we have optional dependencies
> + continue
> + dependency = cdx.CDXDependency(
> + ref=package.bom_ref(SBOMType.CycloneDX),
> + dependsOn=deps,
> + )
> + dependencies.append(dependency)
> + dependency = cdx.CDXDependency(
> + ref=distro_bom_ref,
> + dependsOn=distro_dependencies,
> + )
> + dependencies.append(dependency)
> +
> + doc_uuid = d.getVar("SBOM_DOCUMENT_UUID")
> + distro_component = cdx.CDXComponent(
> + type=cdx.CDX_COMPONENT_TYPE_OS,
> + bom_ref=cdx.CDX_REF_PREFIX + d.getVar("SBOM_DISTRO_NAME"),
> + supplier=cdx.CDXSupplier(name=d.getVar("SBOM_DISTRO_SUPPLIER")),
> + name=d.getVar("SBOM_DISTRO_NAME"),
> + version=d.getVar("SBOM_DISTRO_VERSION"),
> + description=d.getVar("SBOM_DISTRO_SUMMARY"),
> + )
> +
> + timestamp = datetime.fromtimestamp(int(d.getVar("SOURCE_DATE_EPOCH")))
> + bom = cdx.CDXBOM(
> + bomFormat=cdx.CDX_BOM_FORMAT,
> + specVersion=cdx.CDX_SPEC_VERSION,
> + serialNumber="urn:uuid:{}".format(doc_uuid if doc_uuid else uuid4()),
> + version=1,
> + metadata=cdx.CDXBOMMetadata(
> + timestamp=timestamp.strftime("%Y-%m-%dT%H:%M:%SZ"),
> + component=distro_component,
> + tools=cdx.CDXBOMMetadataTool(
> + components=[
> + cdx.CDXComponent(
> + type=cdx.CDX_COMPONENT_TYPE_APPLICATION,
> + name="ISAR SBOM Generator",
> + version=d.getVar("SBOM_GEN_VERSION"),
> + )
> + ],
> + ),
> + ),
> + components=data,
> + dependencies=dependencies,
> + )
> + return bom
> +
> +
> +def spdx_bom(d, packages: List[Package]) -> Dict:
> + "Return a valid SPDX SBOM."
> +
> + data = []
> + # create a "fake" entry for the distribution
> + distro_ref = spdx.SPDX_REF_PREFIX + d.getVar("SBOM_DISTRO_NAME")
> + distro_package = spdx.SPDXPackage(
> + SPDXID=distro_ref,
> + name=d.getVar("SBOM_DISTRO_NAME"),
> + versionInfo=d.getVar("SBOM_DISTRO_VERSION"),
> + primaryPackagePurpose=spdx.SPDX_PACKAGE_PURPOSE_OS,
> + supplier="Organization: {}".format(d.getVar("SBOM_DISTRO_SUPPLIER")),
> + downloadLocation=spdx.SPDX_NOASSERTION,
> + filesAnalyzed=False,
> + licenseConcluded=spdx.SPDX_NOASSERTION,
> + licenseDeclared=spdx.SPDX_NOASSERTION,
> + copyrightText=spdx.SPDX_NOASSERTION,
> + summary=d.getVar("SBOM_DISTRO_SUMMARY"),
> + )
> +
> + data.append(distro_package)
> +
> + pattern = re.compile("(?P<supplier_name>^[^<]*)(\\<(?P<supplier_email>.*)\\>)?")
> + for package in packages:
> + match = pattern.match(package.maintainer)
> + supplier_name = match["supplier_name"]
> + supplier_email = match["supplier_email"]
> + if any([cue in supplier_name.lower() for cue in spdx.SPDX_SUPPLIER_ORG_CUE]):
> + supplier = "Organization: {}".format(supplier_name)
> + else:
> + supplier = "Person: {}".format(supplier_name)
> + if supplier_email:
> + supplier += "({})".format(supplier_email)
> +
> + entry = spdx.SPDXPackage(
> + SPDXID=package.bom_ref(SBOMType.SPDX),
> + name=package.name,
> + versionInfo=package.version,
> + primaryPackagePurpose=spdx.SPDX_PACKAGE_PURPOSE_LIBRARY,
> + supplier=supplier,
> + downloadLocation=spdx.SPDX_NOASSERTION,
> + filesAnalyzed=False,
> + # TODO: it should be possible to conclude license/copyright
> + # information, we could look e.g. in /usr/share/doc/*/copyright
> + licenseConcluded=spdx.SPDX_NOASSERTION,
> + licenseDeclared=spdx.SPDX_NOASSERTION,
> + copyrightText=spdx.SPDX_NOASSERTION,
> + summary=package.description,
> + externalRefs=[
> + spdx.SPDXExternalRef(
> + referenceCategory=spdx.SPDX_REFERENCE_CATEGORY_PKG_MANAGER,
> + referenceType=spdx.SPDX_REFERENCE_TYPE_PURL,
> + referenceLocator=package.purl(),
> + )
> + ],
> + )
> + if package.homepage:
> + entry.homepage = package.homepage
> + data.append(entry)
> +
> + if package.source:
> + src_entry = spdx.SPDXPackage(
> + SPDXID=package.source.bom_ref(SBOMType.SPDX),
> + name=package.source.name,
> + versionInfo=package.source.version,
> + primaryPackagePurpose=spdx.SPDX_PACKAGE_PURPOSE_SRC,
> + supplier=supplier,
> + downloadLocation=spdx.SPDX_NOASSERTION,
> + filesAnalyzed=False,
> + licenseConcluded=spdx.SPDX_NOASSERTION,
> + licenseDeclared=spdx.SPDX_NOASSERTION,
> + copyrightText=spdx.SPDX_NOASSERTION,
> + summary="debian source code package '{}'".format(package.source.name),
> + externalRefs=[
> + spdx.SPDXExternalRef(
> + referenceCategory=spdx.SPDX_REFERENCE_CATEGORY_PKG_MANAGER,
> + referenceType=spdx.SPDX_REFERENCE_TYPE_PURL,
> + referenceLocator=package.source.purl(),
> + )
> + ],
> + )
> + # source packages might be referenced multiple times
> + if src_entry not in data:
> + data.append(src_entry)
> +
> + relationships = []
> + # after we have found all packages we can start to resolve dependencies
> + package_names = [package.name for package in packages]
> + for package in packages:
> + relationships.append(
> + spdx.SPDXRelationship(
> + spdxElementId=package.bom_ref(SBOMType.SPDX),
> + relatedSpdxElement=distro_ref,
> + relationshipType=spdx.SPDX_RELATIONSHIP_PACKAGE_OF,
> + )
> + )
> + if package.depends:
> + for dep in package.depends:
> + if dep.name in package_names:
> + relationship = spdx.SPDXRelationship(
> + spdxElementId=package.bom_ref(SBOMType.SPDX),
> + relatedSpdxElement=dep.bom_ref(SBOMType.SPDX),
> + relationshipType=spdx.SPDX_RELATIONSHIP_DEPENDS_ON,
> + )
> + relationships.append(relationship)
> + else:
> + # this might happen if we have optional dependencies
> + pass
> + if package.source:
> + relationship = spdx.SPDXRelationship(
> + spdxElementId=package.source.bom_ref(SBOMType.SPDX),
> + relatedSpdxElement=package.bom_ref(SBOMType.SPDX),
> + relationshipType=spdx.SPDX_RELATIONSHIP_GENERATES,
> + )
> + relationships.append(relationship)
> + relationships.append(
> + spdx.SPDXRelationship(
> + spdxElementId=spdx.SPDX_REF_DOCUMENT,
> + relatedSpdxElement=distro_ref,
> + relationshipType=spdx.SPDX_RELATIONSHIP_DESCRIBES,
> + )
> + )
> +
> + namespace_uuid = d.getVar("SBOM_DOCUMENT_UUID")
> + timestamp = datetime.fromtimestamp(int(d.getVar("SOURCE_DATE_EPOCH")))
> + bom = spdx.SPDXBOM(
> + SPDXID=spdx.SPDX_REF_DOCUMENT,
> + spdxVersion=spdx.SPDX_VERSION,
> + creationInfo=spdx.SPDXCreationInfo(
> + comment="This document has been generated as part of an ISAR build.",
> + creators=[
> + "Tool: ISAR SBOM Generator - {}".format(d.getVar("SBOM_GEN_VERSION"))
> + ],
> + created=timestamp.strftime("%Y-%m-%dT%H:%M:%SZ"),
> + ),
> + name=d.getVar("SBOM_DISTRO_NAME"),
> + dataLicense="CC0-1.0",
> + documentNamespace="{}/{}-{}".format(
> + d.getVar("SBOM_SPDX_NAMESPACE_PREFIX"),
> + d.getVar("SBOM_DISTRO_NAME"),
> + namespace_uuid if namespace_uuid else uuid4(),
> + ),
> + packages=data,
> + relationships=relationships,
> + )
> + return bom
> +
> +
> +def fixup_dict(o):
> + """Apply fixups for the BOMs.
> +
> + This is necessary for some field names and to remove fields with a None
> + value.
> + """
> + dct = vars(o)
> + new_dct = {}
> + for k, v in dct.items():
> + # remove fields with no content
> + if v is not None:
> + # we can not name our fields with dashes, so convert them
> + k = k.replace("_", "-")
> + new_dct[k] = v
> + return new_dct
> +
> +
> +def generate(d, packages: List[Package], sbom_type: SBOMType, out: str):
> + """Generate a SBOM."""
> + if sbom_type == SBOMType.CycloneDX:
> + bom = cyclonedx_bom(d, packages)
> + elif sbom_type == SBOMType.SPDX:
> + bom = spdx_bom(d, packages)
> +
> + with open(out, "w") as bom_file:
> + json.dump(bom, bom_file, indent=2, default=fixup_dict, sort_keys=True)
> diff --git a/meta/lib/sbom_cdx_types.py b/meta/lib/sbom_cdx_types.py
> new file mode 100644
> index 00000000..4911cc23
> --- /dev/null
> +++ b/meta/lib/sbom_cdx_types.py
> @@ -0,0 +1,82 @@
> +# This software is part of ISAR.
> +# Copyright (C) 2025 Siemens AG
> +#
> +# SPDX-License-Identifier: MIT
> +
> +from dataclasses import dataclass
> +from typing import List, Optional
> +
> +# Minimal implementation of some CycloneDX SBOM types.
> +# Please mind that (almost) none of these types are complete, they only
> +# reflect what was strictly necessary for immediate SBOM creation
> +
> +CDX_BOM_FORMAT = "CycloneDX"
> +CDX_SPEC_VERSION = "1.6"
> +
> +CDX_REF_PREFIX = "CDXRef-"
> +
> +CDX_PACKAGE_EXTREF_TYPE_WEBSITE = "website"
> +
> +CDX_COMPONENT_TYPE_LIBRARY = "library"
> +CDX_COMPONENT_TYPE_APPLICATION = "application"
> +CDX_COMPONENT_TYPE_OS = "operating-system"
> +
> +
> +@dataclass
> +class CDXDependency:
> + ref: str
> + dependsOn: Optional[str]
> +
> +
> +@dataclass
> +class CDXExternalReference:
> + url: str
> + type: str
> + comment: Optional[str] = None
> +
> +
> +@dataclass
> +class CDXSupplierContact:
> + email: Optional[str] = None
> +
> +
> +@dataclass
> +class CDXSupplier:
> + name: Optional[str] = None
> + contact: Optional[CDXSupplierContact] = None
> +
> +
> +@dataclass
> +class CDXComponent:
> + type: str
> + name: str
> + bom_ref: Optional[str] = None
> + supplier: Optional[str] = None
> + version: Optional[CDXSupplier] = None
> + description: Optional[str] = None
> + purl: Optional[str] = None
> + externalReferences: Optional[List[CDXExternalReference]] = None
> + homepage: Optional[str] = None
> +
> +
> +@dataclass
> +class CDXBOMMetadataTool:
> + components: Optional[List[CDXComponent]]
> +
> +
> +@dataclass
> +class CDXBOMMetadata:
> + timestamp: Optional[str] = None
> + component: Optional[str] = None
> + tools: Optional[List[CDXBOMMetadataTool]] = None
> +
> +
> +@dataclass
> +class CDXBOM:
> + bomFormat: str
> + specVersion: str
> + serialNumber: Optional[str] = None
> + version: Optional[str] = None
> + metadata: Optional[CDXBOMMetadata] = None
> + components: Optional[List[CDXComponent]] = None
> + dependencies: Optional[List[CDXDependency]] = None
> diff --git a/meta/lib/sbom_spdx_types.py b/meta/lib/sbom_spdx_types.py
> new file mode 100644
> index 00000000..efd7cc0c
> --- /dev/null
> +++ b/meta/lib/sbom_spdx_types.py
> @@ -0,0 +1,95 @@
> +# This software is part of ISAR.
> +# Copyright (C) 2025 Siemens AG
> +#
> +# SPDX-License-Identifier: MIT
> +
> +from dataclasses import dataclass
> +from typing import List, Optional
> +
> +# Minimal implementation of some SPDX SBOM types.
> +# Please mind that (almost) none of these types are complete, they only
> +# reflect what was strictly necessary for immediate SBOM creation
> +
> +SPDX_VERSION = "SPDX-2.3"
> +
> +SPDX_REF_PREFIX = "SPDXRef-"
> +
> +SPDX_REF_DOCUMENT = "SPDXRef-DOCUMENT"
> +
> +SPDX_PACKAGE_PURPOSE_LIBRARY = "LIBRARY"
> +SPDX_PACKAGE_PURPOSE_OS = "OPERATING_SYSTEM"
> +SPDX_PACKAGE_PURPOSE_SRC = "SOURCE"
> +
> +SPDX_NOASSERTION = "NOASSERTION"
> +
> +SPDX_RELATIONSHIP_DEPENDS_ON = "DEPENDS_ON"
> +SPDX_RELATIONSHIP_PACKAGE_OF = "PACKAGE_OF"
> +SPDX_RELATIONSHIP_GENERATES = "GENERATES"
> +SPDX_RELATIONSHIP_DESCRIBES = "DESCRIBES"
> +
> +SPDX_REFERENCE_CATEGORY_PKG_MANAGER = "PACKAGE_MANAGER"
> +SPDX_REFERENCE_TYPE_PURL = "purl"
> +
> +# cues for an organization in the maintainer name
> +SPDX_SUPPLIER_ORG_CUE = [
> + "maintainers",
> + "group",
> + "developers",
> + "team",
> + "project",
> + "task force",
> + "strike force",
> + "packagers",
> +]
> +
> +
> +@dataclass
> +class SPDXRelationship:
> + spdxElementId: str
> + relatedSpdxElement: str
> + relationshipType: str
> +
> +
> +@dataclass
> +class SPDXExternalRef:
> + referenceCategory: str
> + referenceType: str
> + referenceLocator: str
> +
> +
> +@dataclass
> +class SPDXPackage:
> + SPDXID: str
> + name: str
> + downloadLocation: str
> + filesAnalyzed: Optional[bool] = False
> + versionInfo: Optional[str] = None
> + homepage: Optional[str] = None
> + primaryPackagePurpose: Optional[str] = None
> + supplier: Optional[str] = None
> + licenseConcluded: Optional[str] = None
> + licenseDeclared: Optional[str] = None
> + copyrightText: Optional[str] = None
> + summary: Optional[str] = None
> + externalRefs: Optional[List[SPDXExternalRef]] = None
> +
> +
> +@dataclass
> +class SPDXCreationInfo:
> + created: str
> + comment: Optional[str] = None
> + creators: List[str] = None
> +
> +
> +@dataclass
> +class SPDXBOM:
> + """Incomplete BOM as of SPDX spec v2.3."""
> +
> + SPDXID: str
> + spdxVersion: str
> + creationInfo: SPDXCreationInfo
> + name: str
> + dataLicense: str
> + documentNamespace: str
> + packages: List[SPDXPackage]
> + relationships: List[SPDXRelationship]
--
You received this message because you are subscribed to the Google Groups "isar-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to isar-users+unsubscribe@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/isar-users/e331ec02-9594-41b1-b206-0ea10160543c%40siemens.com.
next prev parent reply other threads:[~2025-03-04 11:55 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-02-20 9:59 [RFC PATCH 0/1] SBOM Generation for isar 'Felix Moessbauer' via isar-users
2025-02-20 9:59 ` [RFC PATCH 1/1] meta: add CycloneDX/SPDX SBOM generation 'Felix Moessbauer' via isar-users
2025-02-20 18:58 ` 'Gernot Hillier' via isar-users
2025-03-04 11:54 ` 'Niedermayr, BENEDIKT' via isar-users [this message]
2025-03-04 12:12 ` 'Christoph Steiger' via isar-users
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=e331ec02-9594-41b1-b206-0ea10160543c@siemens.com \
--to=isar-users@googlegroups.com \
--cc=benedikt.niedermayr@siemens.com \
--cc=christoph.steiger@siemens.com \
--cc=felix.moessbauer@siemens.com \
--cc=gernot.hillier@siemens.com \
--cc=jan.kiszka@siemens.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox