From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from shymkent.ilbers.de ([unix socket]) by shymkent (Cyrus 2.5.10-Debian-2.5.10-3+deb9u2) with LMTPA; Mon, 14 Jul 2025 14:28:41 +0200 X-Sieve: CMU Sieve 2.4 Received: from mail-qv1-f63.google.com (mail-qv1-f63.google.com [209.85.219.63]) by shymkent.ilbers.de (8.15.2/8.15.2/Debian-8+deb9u1) with ESMTPS id 56ECScjE009187 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT) for ; Mon, 14 Jul 2025 14:28:39 +0200 Received: by mail-qv1-f63.google.com with SMTP id 6a1803df08f44-6fb50d92061sf69778416d6.3 for ; Mon, 14 Jul 2025 05:28:39 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=googlegroups.com; s=20230601; t=1752496113; x=1753100913; darn=ilbers.de; h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post :list-id:mailing-list:precedence:reply-to:x-original-sender :mime-version:subject:references:in-reply-to:message-id:to:from:date :from:to:cc:subject:date:message-id:reply-to; bh=XLcW9QbNLIEfTnXUIc1hs9NBAYHiKEX4LOAzflOMIpA=; b=eqOD2QrbaS/A2ABL6tB5Nk7J7YvsbYGiLz/C2+sdFIe2gNAxY8I0qQRmgqWwaGUmtq oDRFFBNGp0f1H2FA8gSUZRVaDS5QjitducUp4c2B5BWGBdYPe06HvjR7AKYrE5g6yVo0 h9iwkxXNgiXOTEBszRhfNmI2p2R5JCQyPEqd4YT7MjKqFjmxOOz7X/rLx0zU6AR/ODcI IMIBzMhDIXZF3hSkuLLQ3wpbPr+qvKGPcKNoevbvrkbJ7/KJxApN4QkGkZ8pBtd8msee 0WTxjchoOo/grP1E6nDziYvjrSenvIIrPKotgA0qDSRu9TlNZO3pVWkOpzl+XSTVyOpP x73g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1752496113; x=1753100913; h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post :list-id:mailing-list:precedence:reply-to:x-original-sender :mime-version:subject:references:in-reply-to:message-id:to:from:date :x-beenthere:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=XLcW9QbNLIEfTnXUIc1hs9NBAYHiKEX4LOAzflOMIpA=; b=ZOgohvV1gttkOZn9U/oOtqGbeP/0Z45nil+v0dpKCB7R5IOVvRSw8HGDYI4TUFdyB/ PhT7NTlynAd9Dvs6i9f0PHHCK+Kw8s0sAclvMwGudcCJcaDqLISzub0OaCCMuC0ami4H xDfHejBA8b9Zrnceeujq50jHeKo3GYOPSY+IqDfI6hR+sAZSmvAuLiqC1majSi+42avg SaaXPX9i4gRIawjhunoeR8FnoKYfNpD1eOmqUPvDGVLAgV0/1AzP+a6VfHo0v6sih40+ gT/RBzIpX73I/i6srbGFSG777IwN/gOr83MPsCwoVePgmT/f8rPcaVXMIB0hNRC7CrrD U2+w== X-Forwarded-Encrypted: i=1; AJvYcCUUC1S79wlsD6+38k/M/z+2suqHnLnnJBG58PBzCJuirsht/gIyfUil/vYsHLbUa8GCwN6d@ilbers.de X-Gm-Message-State: AOJu0YxcxFPesl6MWIUu4Zgr42iWNkBmX1S+Vbkq1qPk0h8ZZXebQ5PB hciqovZD/d/B5IK2MICTfTdH2QXZzeyEPEBJ7aKSo3gGf8MY2N54cIc7 X-Google-Smtp-Source: AGHT+IEBjvFiPwzCqOXgpUF8rQAmocyICydacnejwPaYXO8Qj9rH3KIM0cdzIjh7sQ/lrYejRz19nQ== X-Received: by 2002:a05:6214:1d2b:b0:704:778c:3b9d with SMTP id 6a1803df08f44-704a3976eb8mr154722846d6.30.1752496112583; Mon, 14 Jul 2025 05:28:32 -0700 (PDT) X-BeenThere: isar-users@googlegroups.com; h=AZMbMZe2HuMhF+6tOPmi/kCBMsb7pKbTiK8Kh5Qp9MoL3QiuXA== Received: by 2002:a05:6214:d6a:b0:6fb:4df4:35dc with SMTP id 6a1803df08f44-704956d384els68386746d6.1.-pod-prod-08-us; Mon, 14 Jul 2025 05:28:32 -0700 (PDT) X-Received: by 2002:a05:6214:5a13:b0:702:ce4a:849b with SMTP id 6a1803df08f44-704a433d056mr159453326d6.43.1752496111940; Mon, 14 Jul 2025 05:28:31 -0700 (PDT) Received: by 2002:a05:620a:400c:b0:7c5:495f:5415 with SMTP id af79cd13be357-7e212edffcbms85a; Mon, 14 Jul 2025 05:16:32 -0700 (PDT) X-Received: by 2002:a05:620a:3182:b0:7d4:5741:98aa with SMTP id af79cd13be357-7de06bbb399mr2008527185a.36.1752495391010; Mon, 14 Jul 2025 05:16:31 -0700 (PDT) Date: Mon, 14 Jul 2025 05:16:30 -0700 (PDT) From: =?UTF-8?Q?=27Simone_Wei=C3=9F=27_via_isar=2Dusers?= To: isar-users Message-Id: <0f4c261e-305d-467d-92a2-2fee7848571fn@googlegroups.com> In-Reply-To: References: <20250220095944.114203-1-felix.moessbauer@siemens.com> <20250220095944.114203-2-felix.moessbauer@siemens.com> Subject: Re: [RFC PATCH 1/1] meta: add CycloneDX/SPDX SBOM generation MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_Part_465176_533674564.1752495390846" X-Original-Sender: simone.weiss@elektrobit.com X-Original-From: =?UTF-8?Q?Simone_Wei=C3=9F?= Reply-To: =?UTF-8?Q?Simone_Wei=C3=9F?= Precedence: list Mailing-list: list isar-users@googlegroups.com; contact isar-users+owners@googlegroups.com List-ID: X-Google-Group-Id: 914930254986 List-Post: , List-Help: , List-Archive: , List-Unsubscribe: , X-Spam-Status: No, score=-4.9 required=5.0 tests=DKIMWL_WL_MED,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,HTML_MESSAGE,MAILING_LIST_MULTI, RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,RCVD_IN_RP_CERTIFIED, RCVD_IN_RP_RNBL,RCVD_IN_RP_SAFE,SPF_PASS autolearn=unavailable autolearn_force=no version=3.4.2 X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on shymkent.ilbers.de X-TUID: oM4piuuqprY/ ------=_Part_465176_533674564.1752495390846 Content-Type: multipart/alternative; boundary="----=_Part_465177_620646560.1752495390846" ------=_Part_465177_620646560.1752495390846 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Hi, Are you planing to add Concludedlicense information as per your comment in= =20 the original? Any plan for SPDX3? On Tuesday, June 10, 2025 at 2:04:03=E2=80=AFPM UTC+2 Christoph Steiger wro= te: > FYI Benjamin, Cedric and Mete: > > We are currently working on a V2 for this with more or less the same=20 > functionality and some internal changes. It might be interesting for you= =20 > too. Maybe you could try this version out in your builds and see if=20 > anything important/nice-to-have is missing in the SBOMs? > > > From: Christoph Steiger > >=20 > > 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. > >=20 > > 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. > >=20 > > The information included in the SBOM is parsed from the dpkg status > > file found in the created image. > >=20 > > Signed-off-by: Christoph Steiger > > --- > > 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 > >=20 > > diff --git a/meta/classes/create-sbom.bbclass=20 > 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 ?=3D "cyclonedx spdx" > > + > > +# general user variables > > +SBOM_DISTRO_SUPPLIER ?=3D "ISAR" > > +SBOM_DISTRO_NAME ?=3D "ISAR-Debian-GNU-Linux" > > +SBOM_DISTRO_VERSION ?=3D "1.0.0" > > +SBOM_DISTRO_SUMMARY ?=3D "Linux distribution built with ISAR" > > +SBOM_DOCUMENT_UUID ?=3D "" > > + > > +# SPDX specific user variables > > +SBOM_SPDX_NAMESPACE_PREFIX ?=3D "https://spdx.org/spdxdocs" > > + > > +SBOM_DEPLOY_BASE =3D "${DEPLOY_DIR_IMAGE}/${IMAGE_FULLNAME}" > > + > > +SBOM_GEN_VERSION =3D "0.1.0" > > + > > +# adapted from the isar-cip-core image_uuid.bbclass > > +def generate_document_uuid(d): > > + import uuid > > + > > + base_hash =3D 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=3D4)) > > + > > +python do_create_sbom() { > > + import sbom > > + > > + dpkg_status =3D d.getVar("IMAGE_ROOTFS") + "/var/lib/dpkg/status" > > + packages =3D 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 =3D d.getVar("SBOM_TYPE") > > + if "cyclonedx" in sbom_type: > > + sbom.generate(d, packages, sbom.SBOMType.CycloneDX,=20 > d.getVar("SBOM_DEPLOY_BASE") + ".cyclonedx.json") > > + if "spdx" in sbom_type: > > + sbom.generate(d, packages, sbom.SBOMType.SPDX,=20 > d.getVar("SBOM_DEPLOY_BASE") + ".spdx.json") > > +} > > + > > +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 > >=20 > > +inherit create-sbom > > + > > # Extra space for rootfs in MB > > ROOTFS_EXTRA ?=3D "64" > >=20 > > 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 =3D (0,) > > + SPDX =3D (1,) > > + > > + > > +@dataclass > > +class SourcePackage: > > + name: str > > + version: str | None > > + > > + def purl(self): > > + """Return the PURL of the package.""" > > + return "pkg:deb/debian/{}@{}?arch=3Dsource".format(self.name,=20 > self.version) > > + > > + def bom_ref(self, sbom_type: SBOMType) -> str: > > + """Return a unique BOM reference.""" > > + if sbom_type =3D=3D SBOMType.CycloneDX: > > + return cdx.CDXREF_PREFIX + "{}-src".format(self.name) > > + elif sbom_type =3D=3D SBOMType.SPDX: > > + return spdx.SPDX_REF_PREFIX + "{}-src".format(self.name) > > + > > + def parse(s: str) -> Type["SourcePackage"]: > > + split =3D s.split(" ") > > + name =3D split[0] > > + try: > > + version =3D " ".join(split[1:]).strip("()") > > + except IndexError: > > + version =3D None > > + > > + return SourcePackage(name=3Dname, version=3Dversion) > > + > > + > > +@dataclass > > +class Dependency: > > + name: str > > + version: str | None > > + > > + def bom_ref(self, sbom_type: SBOMType) -> str: > > + """Return a unique BOM reference.""" > > + if sbom_type =3D=3D SBOMType.CycloneDX: > > + return cdx.CDX_REF_PREFIX + "{}".format(self.name) > > + elif sbom_type =3D=3D SBOMType.SPDX: > > + return spdx.SPDX_REF_PREFIX + "{}".format(self.name) > > + > > + def parse_multiple(s: str) -> List[Type["Dependency"]]: > > + """Parse a 'Depends' line in the dpkg status file.""" > > + dependencies =3D [] > > + for entry in s.split(","): > > + entry =3D entry.strip() > > + for entry in entry.split("|"): > > + split =3D entry.split("(") > > + name =3D split[0].strip() > > + try: > > + version =3D split[1].strip(")") > > + except IndexError: > > + version =3D None > > + dependencies.append(Dependency(name=3Dname, version=3Dversion)) > > + > > + 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 =3D "pkg:deb/debian/{}@{}".format(self.name, self.version) > > + if self.architecture: > > + purl =3D purl + "?arch=3D{}".format(self.architecture) > > + return purl > > + > > + def bom_ref(self, sbom_type: SBOMType) -> str: > > + """Return a unique BOM reference.""" > > + if sbom_type =3D=3D SBOMType.CycloneDX: > > + return cdx.CDX_REF_PREFIX + self.name > > + elif sbom_type =3D=3D SBOMType.SPDX: > > + return spdx.SPDX_REF_PREFIX + self.name > > + > > + def parse_status_file(status_file: str) -> List[Type["Package"]]: > > + """Parse a dpkg status file.""" > > + packages =3D [] > > + with open(status_file, "r") as f: > > + name =3D None > > + section =3D None > > + maintainer =3D None > > + architecture =3D None > > + source =3D None > > + version =3D None > > + dependencies =3D None > > + description =3D None > > + homepage =3D None > > + for line in f.readlines(): > > + if line.strip(): > > + if line[0] =3D=3D " ": > > + # this is a description line, we ignore it > > + continue > > + else: > > + split =3D line.split(":") > > + key =3D split[0] > > + value =3D ":".join(split[1:]).strip() > > + if key =3D=3D "Package": > > + name =3D value > > + elif key =3D=3D "Section": > > + section =3D value > > + elif key =3D=3D "Maintainer": > > + maintainer =3D value > > + elif key =3D=3D "Architecture": > > + architecture =3D value > > + elif key =3D=3D "Source": > > + source =3D SourcePackage.parse(value) > > + elif key =3D=3D "Version": > > + version =3D value > > + elif key =3D=3D "Depends": > > + dependencies =3D Dependency.parse_multiple(value) > > + elif key =3D=3D "Description": > > + description =3D value > > + elif key =3D=3D "Homepage": > > + homepage =3D value > > + else: > > + # fixup source version, if not specified it is the same > > + # as the package version > > + if source and not source.version: > > + source.version =3D version > > + # empty line means new package, so finish the current one > > + packages.append( > > + Package( > > + name=3Dname, > > + section=3Dsection, > > + maintainer=3Dmaintainer, > > + architecture=3Darchitecture, > > + source=3Dsource, > > + version=3Dversion, > > + depends=3Ddependencies, > > + description=3Ddescription, > > + homepage=3Dhomepage, > > + ) > > + ) > > + name =3D None > > + section =3D None > > + maintainer =3D None > > + architecture =3D None > > + source =3D None > > + version =3D None > > + dependencies =3D None > > + description =3D None > > + homepage =3D None > > + > > + return packages > > + > > + > > +def cyclonedx_bom(d, packages: List[Package]) -> Dict: > > + """Return a valid CycloneDX SBOM.""" > > + data =3D [] > > + dependencies =3D [] > > + > > + pattern =3D=20 > re.compile("(?P^[^<]*)(\\<(?P.*)\\>)?") > > + for package in packages: > > + match =3D pattern.match(package.maintainer) > > + supplier =3D cdx.CDXSupplier(name=3Dmatch["supplier_name"]) > > + supplier_email =3D match["supplier_email"] > > + if supplier_email: > > + supplier.contact =3D [cdx.CDXSupplierContact(email=3Dsupplier_email)] > > + entry =3D cdx.CDXComponent( > > + type=3Dcdx.CDX_COMPONENT_TYPE_LIBRARY, > > + bom_ref=3Dpackage.bom_ref(SBOMType.CycloneDX), > > + supplier=3Dsupplier, > > + name=3Dpackage.name, > > + version=3Dpackage.version, > > + description=3Dpackage.description, > > + purl=3Dpackage.purl(), > > + ) > > + if package.homepage: > > + entry.externalReferences =3D ( > > + cdx.CDXExternalReference( > > + url=3Dpackage.homepage, > > + type=3Dcdx.CDX_PACKAGE_EXTREF_TYPE_WEBSITE, > > + comment=3D"homepage", > > + ), > > + ) > > + data.append(entry) > > + > > + distro_bom_ref =3D cdx.CDX_REF_PREFIX + d.getVar("SBOM_DISTRO_NAME") > > + distro_dependencies =3D [] > > + # after we have found all packages we can start to resolve dependenci= es > > + package_names =3D [package.name for package in packages] > > + for package in packages: > > + distro_dependencies.append(package.bom_ref(SBOMType.CycloneDX)) > > + if package.depends: > > + deps =3D [] > > + for dep in package.depends: > > + dep_bom_ref =3D 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 =3D cdx.CDXDependency( > > + ref=3Dpackage.bom_ref(SBOMType.CycloneDX), > > + dependsOn=3Ddeps, > > + ) > > + dependencies.append(dependency) > > + dependency =3D cdx.CDXDependency( > > + ref=3Ddistro_bom_ref, > > + dependsOn=3Ddistro_dependencies, > > + ) > > + dependencies.append(dependency) > > + > > + doc_uuid =3D d.getVar("SBOM_DOCUMENT_UUID") > > + distro_component =3D cdx.CDXComponent( > > + type=3Dcdx.CDX_COMPONENT_TYPE_OS, > > + bom_ref=3Dcdx.CDX_REF_PREFIX + d.getVar("SBOM_DISTRO_NAME"), > > + supplier=3Dcdx.CDXSupplier(name=3Dd.getVar("SBOM_DISTRO_SUPPLIER")), > > + name=3Dd.getVar("SBOM_DISTRO_NAME"), > > + version=3Dd.getVar("SBOM_DISTRO_VERSION"), > > + description=3Dd.getVar("SBOM_DISTRO_SUMMARY"), > > + ) > > + > > + timestamp =3D datetime.fromtimestamp(int(d.getVar("SOURCE_DATE_EPOCH"= ))) > > + bom =3D cdx.CDXBOM( > > + bomFormat=3Dcdx.CDX_BOM_FORMAT, > > + specVersion=3Dcdx.CDX_SPEC_VERSION, > > + serialNumber=3D"urn:uuid:{}".format(doc_uuid if doc_uuid else uuid4()= ), > > + version=3D1, > > + metadata=3Dcdx.CDXBOMMetadata( > > + timestamp=3Dtimestamp.strftime("%Y-%m-%dT%H:%M:%SZ"), > > + component=3Ddistro_component, > > + tools=3Dcdx.CDXBOMMetadataTool( > > + components=3D[ > > + cdx.CDXComponent( > > + type=3Dcdx.CDX_COMPONENT_TYPE_APPLICATION, > > + name=3D"ISAR SBOM Generator", > > + version=3Dd.getVar("SBOM_GEN_VERSION"), > > + ) > > + ], > > + ), > > + ), > > + components=3Ddata, > > + dependencies=3Ddependencies, > > + ) > > + return bom > > + > > + > > +def spdx_bom(d, packages: List[Package]) -> Dict: > > + "Return a valid SPDX SBOM." > > + > > + data =3D [] > > + # create a "fake" entry for the distribution > > + distro_ref =3D spdx.SPDX_REF_PREFIX + d.getVar("SBOM_DISTRO_NAME") > > + distro_package =3D spdx.SPDXPackage( > > + SPDXID=3Ddistro_ref, > > + name=3Dd.getVar("SBOM_DISTRO_NAME"), > > + versionInfo=3Dd.getVar("SBOM_DISTRO_VERSION"), > > + primaryPackagePurpose=3Dspdx.SPDX_PACKAGE_PURPOSE_OS, > > + supplier=3D"Organization: {}".format(d.getVar("SBOM_DISTRO_SUPPLIER")= ), > > + downloadLocation=3Dspdx.SPDX_NOASSERTION, > > + filesAnalyzed=3DFalse, > > + licenseConcluded=3Dspdx.SPDX_NOASSERTION, > > + licenseDeclared=3Dspdx.SPDX_NOASSERTION, > > + copyrightText=3Dspdx.SPDX_NOASSERTION, > > + summary=3Dd.getVar("SBOM_DISTRO_SUMMARY"), > > + ) > > + > > + data.append(distro_package) > > + > > + pattern =3D=20 > re.compile("(?P^[^<]*)(\\<(?P.*)\\>)?") > > + for package in packages: > > + match =3D pattern.match(package.maintainer) > > + supplier_name =3D match["supplier_name"] > > + supplier_email =3D match["supplier_email"] > > + if any([cue in supplier_name.lower() for cue in=20 > spdx.SPDX_SUPPLIER_ORG_CUE]): > > + supplier =3D "Organization: {}".format(supplier_name) > > + else: > > + supplier =3D "Person: {}".format(supplier_name) > > + if supplier_email: > > + supplier +=3D "({})".format(supplier_email) > > + > > + entry =3D spdx.SPDXPackage( > > + SPDXID=3Dpackage.bom_ref(SBOMType.SPDX), > > + name=3Dpackage.name, > > + versionInfo=3Dpackage.version, > > + primaryPackagePurpose=3Dspdx.SPDX_PACKAGE_PURPOSE_LIBRARY, > > + supplier=3Dsupplier, > > + downloadLocation=3Dspdx.SPDX_NOASSERTION, > > + filesAnalyzed=3DFalse, > > + # TODO: it should be possible to conclude license/copyright > > + # information, we could look e.g. in /usr/share/doc/*/copyright > > + licenseConcluded=3Dspdx.SPDX_NOASSERTION, > > + licenseDeclared=3Dspdx.SPDX_NOASSERTION, > > + copyrightText=3Dspdx.SPDX_NOASSERTION, > > + summary=3Dpackage.description, > > + externalRefs=3D[ > > + spdx.SPDXExternalRef( > > + referenceCategory=3Dspdx.SPDX_REFERENCE_CATEGORY_PKG_MANAGER, > > + referenceType=3Dspdx.SPDX_REFERENCE_TYPE_PURL, > > + referenceLocator=3Dpackage.purl(), > > + ) > > + ], > > + ) > > + if package.homepage: > > + entry.homepage =3D package.homepage > > + data.append(entry) > > + > > + if package.source: > > + src_entry =3D spdx.SPDXPackage( > > + SPDXID=3Dpackage.source.bom_ref(SBOMType.SPDX), > > + name=3Dpackage.source.name, > > + versionInfo=3Dpackage.source.version, > > + primaryPackagePurpose=3Dspdx.SPDX_PACKAGE_PURPOSE_SRC, > > + supplier=3Dsupplier, > > + downloadLocation=3Dspdx.SPDX_NOASSERTION, > > + filesAnalyzed=3DFalse, > > + licenseConcluded=3Dspdx.SPDX_NOASSERTION, > > + licenseDeclared=3Dspdx.SPDX_NOASSERTION, > > + copyrightText=3Dspdx.SPDX_NOASSERTION, > > + summary=3D"debian source code package '{}'".format(package.source.nam= e), > > + externalRefs=3D[ > > + spdx.SPDXExternalRef( > > + referenceCategory=3Dspdx.SPDX_REFERENCE_CATEGORY_PKG_MANAGER, > > + referenceType=3Dspdx.SPDX_REFERENCE_TYPE_PURL, > > + referenceLocator=3Dpackage.source.purl(), > > + ) > > + ], > > + ) > > + # source packages might be referenced multiple times > > + if src_entry not in data: > > + data.append(src_entry) > > + > > + relationships =3D [] > > + # after we have found all packages we can start to resolve dependenci= es > > + package_names =3D [package.name for package in packages] > > + for package in packages: > > + relationships.append( > > + spdx.SPDXRelationship( > > + spdxElementId=3Dpackage.bom_ref(SBOMType.SPDX), > > + relatedSpdxElement=3Ddistro_ref, > > + relationshipType=3Dspdx.SPDX_RELATIONSHIP_PACKAGE_OF, > > + ) > > + ) > > + if package.depends: > > + for dep in package.depends: > > + if dep.name in package_names: > > + relationship =3D spdx.SPDXRelationship( > > + spdxElementId=3Dpackage.bom_ref(SBOMType.SPDX), > > + relatedSpdxElement=3Ddep.bom_ref(SBOMType.SPDX), > > + relationshipType=3Dspdx.SPDX_RELATIONSHIP_DEPENDS_ON, > > + ) > > + relationships.append(relationship) > > + else: > > + # this might happen if we have optional dependencies > > + pass > > + if package.source: > > + relationship =3D spdx.SPDXRelationship( > > + spdxElementId=3Dpackage.source.bom_ref(SBOMType.SPDX), > > + relatedSpdxElement=3Dpackage.bom_ref(SBOMType.SPDX), > > + relationshipType=3Dspdx.SPDX_RELATIONSHIP_GENERATES, > > + ) > > + relationships.append(relationship) > > + relationships.append( > > + spdx.SPDXRelationship( > > + spdxElementId=3Dspdx.SPDX_REF_DOCUMENT, > > + relatedSpdxElement=3Ddistro_ref, > > + relationshipType=3Dspdx.SPDX_RELATIONSHIP_DESCRIBES, > > + ) > > + ) > > + > > + namespace_uuid =3D d.getVar("SBOM_DOCUMENT_UUID") > > + timestamp =3D datetime.fromtimestamp(int(d.getVar("SOURCE_DATE_EPOCH"= ))) > > + bom =3D spdx.SPDXBOM( > > + SPDXID=3Dspdx.SPDX_REF_DOCUMENT, > > + spdxVersion=3Dspdx.SPDX_VERSION, > > + creationInfo=3Dspdx.SPDXCreationInfo( > > + comment=3D"This document has been generated as part of an ISAR build.= ", > > + creators=3D[ > > + "Tool: ISAR SBOM Generator - {}".format(d.getVar("SBOM_GEN_VERSION")) > > + ], > > + created=3Dtimestamp.strftime("%Y-%m-%dT%H:%M:%SZ"), > > + ), > > + name=3Dd.getVar("SBOM_DISTRO_NAME"), > > + dataLicense=3D"CC0-1.0", > > + documentNamespace=3D"{}/{}-{}".format( > > + d.getVar("SBOM_SPDX_NAMESPACE_PREFIX"), > > + d.getVar("SBOM_DISTRO_NAME"), > > + namespace_uuid if namespace_uuid else uuid4(), > > + ), > > + packages=3Ddata, > > + relationships=3Drelationships, > > + ) > > + return bom > > + > > + > > +def fixup_dict(o): > > + """Apply fixups for the BOMs. > > + > > + This is necessary for some field names and to remove fields with a No= ne > > + value. > > + """ > > + dct =3D vars(o) > > + new_dct =3D {} > > + 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 =3D k.replace("_", "-") > > + new_dct[k] =3D v > > + return new_dct > > + > > + > > +def generate(d, packages: List[Package], sbom_type: SBOMType, out: str= ): > > + """Generate a SBOM.""" > > + if sbom_type =3D=3D SBOMType.CycloneDX: > > + bom =3D cyclonedx_bom(d, packages) > > + elif sbom_type =3D=3D SBOMType.SPDX: > > + bom =3D spdx_bom(d, packages) > > + > > + with open(out, "w") as bom_file: > > + json.dump(bom, bom_file, indent=3D2, default=3Dfixup_dict, sort_keys= =3DTrue) > > 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 onl= y > > +# reflect what was strictly necessary for immediate SBOM creation > > + > > +CDX_BOM_FORMAT =3D "CycloneDX" > > +CDX_SPEC_VERSION =3D "1.6" > > + > > +CDX_REF_PREFIX =3D "CDXRef-" > > + > > +CDX_PACKAGE_EXTREF_TYPE_WEBSITE =3D "website" > > + > > +CDX_COMPONENT_TYPE_LIBRARY =3D "library" > > +CDX_COMPONENT_TYPE_APPLICATION =3D "application" > > +CDX_COMPONENT_TYPE_OS =3D "operating-system" > > + > > + > > +@dataclass > > +class CDXDependency: > > + ref: str > > + dependsOn: Optional[str] > > + > > + > > +@dataclass > > +class CDXExternalReference: > > + url: str > > + type: str > > + comment: Optional[str] =3D None > > + > > + > > +@dataclass > > +class CDXSupplierContact: > > + email: Optional[str] =3D None > > + > > + > > +@dataclass > > +class CDXSupplier: > > + name: Optional[str] =3D None > > + contact: Optional[CDXSupplierContact] =3D None > > + > > + > > +@dataclass > > +class CDXComponent: > > + type: str > > + name: str > > + bom_ref: Optional[str] =3D None > > + supplier: Optional[str] =3D None > > + version: Optional[CDXSupplier] =3D None > > + description: Optional[str] =3D None > > + purl: Optional[str] =3D None > > + externalReferences: Optional[List[CDXExternalReference]] =3D None > > + homepage: Optional[str] =3D None > > + > > + > > +@dataclass > > +class CDXBOMMetadataTool: > > + components: Optional[List[CDXComponent]] > > + > > + > > +@dataclass > > +class CDXBOMMetadata: > > + timestamp: Optional[str] =3D None > > + component: Optional[str] =3D None > > + tools: Optional[List[CDXBOMMetadataTool]] =3D None > > + > > + > > +@dataclass > > +class CDXBOM: > > + bomFormat: str > > + specVersion: str > > + serialNumber: Optional[str] =3D None > > + version: Optional[str] =3D None > > + metadata: Optional[CDXBOMMetadata] =3D None > > + components: Optional[List[CDXComponent]] =3D None > > + dependencies: Optional[List[CDXDependency]] =3D 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 onl= y > > +# reflect what was strictly necessary for immediate SBOM creation > > + > > +SPDX_VERSION =3D "SPDX-2.3" > > + > > +SPDX_REF_PREFIX =3D "SPDXRef-" > > + > > +SPDX_REF_DOCUMENT =3D "SPDXRef-DOCUMENT" > > + > > +SPDX_PACKAGE_PURPOSE_LIBRARY =3D "LIBRARY" > > +SPDX_PACKAGE_PURPOSE_OS =3D "OPERATING_SYSTEM" > > +SPDX_PACKAGE_PURPOSE_SRC =3D "SOURCE" > > + > > +SPDX_NOASSERTION =3D "NOASSERTION" > > + > > +SPDX_RELATIONSHIP_DEPENDS_ON =3D "DEPENDS_ON" > > +SPDX_RELATIONSHIP_PACKAGE_OF =3D "PACKAGE_OF" > > +SPDX_RELATIONSHIP_GENERATES =3D "GENERATES" > > +SPDX_RELATIONSHIP_DESCRIBES =3D "DESCRIBES" > > + > > +SPDX_REFERENCE_CATEGORY_PKG_MANAGER =3D "PACKAGE_MANAGER" > > +SPDX_REFERENCE_TYPE_PURL =3D "purl" > > + > > +# cues for an organization in the maintainer name > > +SPDX_SUPPLIER_ORG_CUE =3D [ > > + "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] =3D False > > + versionInfo: Optional[str] =3D None > > + homepage: Optional[str] =3D None > > + primaryPackagePurpose: Optional[str] =3D None > > + supplier: Optional[str] =3D None > > + licenseConcluded: Optional[str] =3D None > > + licenseDeclared: Optional[str] =3D None > > + copyrightText: Optional[str] =3D None > > + summary: Optional[str] =3D None > > + externalRefs: Optional[List[SPDXExternalRef]] =3D None > > + > > + > > +@dataclass > > +class SPDXCreationInfo: > > + created: str > > + comment: Optional[str] =3D None > > + creators: List[str] =3D 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] > > --=20 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 e= mail to isar-users+unsubscribe@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/isar-users/= 0f4c261e-305d-467d-92a2-2fee7848571fn%40googlegroups.com. ------=_Part_465177_620646560.1752495390846 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Hi,

Are you planing to add Concludedlicense= information as per your comment in the original? Any plan for SPDX3?
=


On Tuesday, June 10, 2025 at 2:04:03=E2=80=AFPM UT= C+2 Christoph Steiger wrote:
FYI Benjamin, Cedric and Mete:

We are currently working on a V2 for this with more or less the same=20
functionality and some internal changes. It might be interesting for yo= u=20
too. Maybe you could try this version out in your builds and see if=20
anything important/nice-to-have is missing in the SBOMs?

> From: Christoph Steiger <christop...@siemens.com>
>=20
> 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.
>=20
> Both formats support the minimal usecase of binary packages inform= ation
> and their dependencies. Unfortunately there is no proper way to ex= press
> the relationships of debian source packages and their correspondin= g
> binary packages in the CDX format, so it is left out there.
>=20
> The information included in the SBOM is parsed from the dpkg statu= s
> file found in the created image.
>=20
> Signed-off-by: Christoph Steiger <christop...@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
>=20
> diff --git a/meta/classes/create-sbom.bbclass b/meta/classes/creat= e-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 &= quot;spdx"
> +SBOM_TYPE ?=3D "cyclonedx spdx"
> +
> +# general user variables
> +SBOM_DISTRO_SUPPLIER ?=3D "ISAR"
> +SBOM_DISTRO_NAME ?=3D "ISAR-Debian-GNU-Linux"
> +SBOM_DISTRO_VERSION ?=3D "1.0.0"
> +SBOM_DISTRO_SUMMARY ?=3D "Linux distribution built with ISAR= "
> +SBOM_DOCUMENT_UUID ?=3D ""
> +
> +# SPDX specific user variables
> +SBOM_SPDX_NAMESPACE_PREFIX ?=3D "https://spdx.org/spdxdocs"
> +
> +SBOM_DEPLOY_BASE =3D "${DEPLOY_DIR_IMAGE}/${IMAGE_FULLNAME}&= quot;
> +
> +SBOM_GEN_VERSION =3D "0.1.0"
> +
> +# adapted from the isar-cip-core image_uuid.bbclass
> +def generate_document_uuid(d):
> + import uuid
> +
> + base_hash =3D 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=3D4))
> +
> +python do_create_sbom() {
> + import sbom
> +
> + dpkg_status =3D d.getVar("IMAGE_ROOTFS") + "/v= ar/lib/dpkg/status"
> + packages =3D sbom.Package.parse_status_file(dpkg_status)
> +
> + if not d.getVar("SBOM_DOCUMENT_UUID"):
> + d.setVar("SBOM_DOCUMENT_UUID", generate_documen= t_uuid(d))
> +
> + sbom_type =3D d.getVar("SBOM_TYPE")
> + if "cyclonedx" in sbom_type:
> + sbom.generate(d, packages, sbom.SBOMType.CycloneDX, d.get= Var("SBOM_DEPLOY_BASE") + ".cyclonedx.json")
> + if "spdx" in sbom_type:
> + sbom.generate(d, packages, sbom.SBOMType.SPDX, d.getVar(&= quot;SBOM_DEPLOY_BASE") + ".spdx.json")
> +}
> +
> +addtask do_create_sbom after do_rootfs before do_build
> diff --git a/meta/classes/image.bbclass b/meta/classes/image.bbcla= ss
> 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
> =20
> +inherit create-sbom
> +
> # Extra space for rootfs in MB
> ROOTFS_EXTRA ?=3D "64"
> =20
> 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 =3D (0,)
> + SPDX =3D (1,)
> +
> +
> +@dataclass
> +class SourcePackage:
> + name: str
> + version: str | None
> +
> + def purl(self):
> + """Return the PURL of the package."&q= uot;"
> + return "pkg:deb/debian/{}@{}?arch=3Dsource".for= mat(self.name, self.version)
> +
> + def bom_ref(self, sbom_type: SBOMType) -> str:
> + """Return a unique BOM reference."&qu= ot;"
> + if sbom_type =3D=3D SBOMType.CycloneDX:
> + return cdx.CDXREF_PREFIX + "{}-src".format(= self.name)
> + elif sbom_type =3D=3D SBOMType.SPDX:
> + return spdx.SPDX_REF_PREFIX + "{}-src".form= at(self.name)
> +
> + def parse(s: str) -> Type["SourcePackage"]:
> + split =3D s.split(" ")
> + name =3D split[0]
> + try:
> + version =3D " ".join(split[1:]).strip("= ;()")
> + except IndexError:
> + version =3D None
> +
> + return SourcePackage(name=3Dname, version=3Dversion)
> +
> +
> +@dataclass
> +class Dependency:
> + name: str
> + version: str | None
> +
> + def bom_ref(self, sbom_type: SBOMType) -> str:
> + """Return a unique BOM reference."&qu= ot;"
> + if sbom_type =3D=3D SBOMType.CycloneDX:
> + return cdx.CDX_REF_PREFIX + "{}".format(self.name)
> + elif sbom_type =3D=3D SBOMType.SPDX:
> + return spdx.SPDX_REF_PREFIX + "{}".format(<= a href=3D"http://self.name" target=3D"_blank" rel=3D"nofollow" data-safered= irecturl=3D"https://www.google.com/url?hl=3Den&q=3Dhttp://self.name&= ;source=3Dgmail&ust=3D1752573565741000&usg=3DAOvVaw1emKvfcqgaSd6osp= Sz-mDB">self.name)
> +
> + def parse_multiple(s: str) -> List[Type["Dependency&q= uot;]]:
> + """Parse a 'Depends' line in the d= pkg status file."""
> + dependencies =3D []
> + for entry in s.split(","):
> + entry =3D entry.strip()
> + for entry in entry.split("|"):
> + split =3D entry.split("(")
> + name =3D split[0].strip()
> + try:
> + version =3D split[1].strip(")")
> + except IndexError:
> + version =3D None
> + dependencies.append(Dependency(name=3Dname, versi= on=3Dversion))
> +
> + return dependencies
> +
> +
> +@dataclass
> +class Package:
> + """Incomplete representation of a debian packa= ge."""
> +
> + 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."&q= uot;"
> + purl =3D "pkg:deb/debian/{}@{}".format(self.name, self.version)
> + if self.architecture:
> + purl =3D purl + "?arch=3D{}".format(self.ar= chitecture)
> + return purl
> +
> + def bom_ref(self, sbom_type: SBOMType) -> str:
> + """Return a unique BOM reference."&qu= ot;"
> + if sbom_type =3D=3D SBOMType.CycloneDX:
> + return cdx.CDX_REF_PREFIX + self.name
> + elif sbom_type =3D=3D SBOMType.SPDX:
> + return spdx.SPDX_REF_PREFIX + self.name
> +
> + def parse_status_file(status_file: str) -> List[Type["= ;Package"]]:
> + """Parse a dpkg status file.""&q= uot;
> + packages =3D []
> + with open(status_file, "r") as f:
> + name =3D None
> + section =3D None
> + maintainer =3D None
> + architecture =3D None
> + source =3D None
> + version =3D None
> + dependencies =3D None
> + description =3D None
> + homepage =3D None
> + for line in f.readlines():
> + if line.strip():
> + if line[0] =3D=3D " ":
> + # this is a description line, we ignore i= t
> + continue
> + else:
> + split =3D line.split(":")
> + key =3D split[0]
> + value =3D ":".join(split[1:]).s= trip()
> + if key =3D=3D "Package":
> + name =3D value
> + elif key =3D=3D "Section":
> + section =3D value
> + elif key =3D=3D "Maintainer":
> + maintainer =3D value
> + elif key =3D=3D "Architecture":
> + architecture =3D value
> + elif key =3D=3D "Source":
> + source =3D SourcePackage.parse(value)
> + elif key =3D=3D "Version":
> + version =3D value
> + elif key =3D=3D "Depends":
> + dependencies =3D Dependency.parse_mul= tiple(value)
> + elif key =3D=3D "Description":
> + description =3D value
> + elif key =3D=3D "Homepage":
> + homepage =3D value
> + else:
> + # fixup source version, if not specified it i= s the same
> + # as the package version
> + if source and not source.version:
> + source.version =3D version
> + # empty line means new package, so finish the= current one
> + packages.append(
> + Package(
> + name=3Dname,
> + section=3Dsection,
> + maintainer=3Dmaintainer,
> + architecture=3Darchitecture,
> + source=3Dsource,
> + version=3Dversion,
> + depends=3Ddependencies,
> + description=3Ddescription,
> + homepage=3Dhomepage,
> + )
> + )
> + name =3D None
> + section =3D None
> + maintainer =3D None
> + architecture =3D None
> + source =3D None
> + version =3D None
> + dependencies =3D None
> + description =3D None
> + homepage =3D None
> +
> + return packages
> +
> +
> +def cyclonedx_bom(d, packages: List[Package]) -> Dict:
> + """Return a valid CycloneDX SBOM.""&= quot;
> + data =3D []
> + dependencies =3D []
> +
> + pattern =3D re.compile("(?P<supplier_name>^[^<]= *)(\\<(?P<supplier_email>.*)\\>)?")
> + for package in packages:
> + match =3D pattern.match(package.maintainer)
> + supplier =3D cdx.CDXSupplier(name=3Dmatch["supplier_= name"])
> + supplier_email =3D match["supplier_email"]
> + if supplier_email:
> + supplier.contact =3D [cdx.CDXSupplierContact(email=3D= supplier_email)]
> + entry =3D cdx.CDXComponent(
> + type=3Dcdx.CDX_COMPONENT_TYPE_LIBRARY,
> + bom_ref=3Dpackage.bom_ref(SBOMType.CycloneDX),
> + supplier=3Dsupplier,
> + name=3Dpackage.name,
> + version=3Dpackage.version,
> + description=3Dpackage.description,
> + purl=3Dpackage.purl(),
> + )
> + if package.homepage:
> + entry.externalReferences =3D (
> + cdx.CDXExternalReference(
> + url=3Dpackage.homepage,
> + type=3Dcdx.CDX_PACKAGE_EXTREF_TYPE_WEBSITE,
> + comment=3D"homepage",
> + ),
> + )
> + data.append(entry)
> +
> + distro_bom_ref =3D cdx.CDX_REF_PREFIX + d.getVar("SBOM_D= ISTRO_NAME")
> + distro_dependencies =3D []
> + # after we have found all packages we can start to resolve de= pendencies
> + package_names =3D [package.name for packag= e in packages]
> + for package in packages:
> + distro_dependencies.append(package.bom_ref(SBOMType.Cyclo= neDX))
> + if package.depends:
> + deps =3D []
> + for dep in package.depends:
> + dep_bom_ref =3D dep.bom_ref(SBOMType.CycloneDX)
> + # it is possibe to specify the same package multi= ple times, but
> + # in different versions
> + if dep.name in package_names and dep_b= om_ref not in deps:
> + deps.append(dep_bom_ref)
> + else:
> + # this might happen if we have optional depen= dencies
> + continue
> + dependency =3D cdx.CDXDependency(
> + ref=3Dpackage.bom_ref(SBOMType.CycloneDX),
> + dependsOn=3Ddeps,
> + )
> + dependencies.append(dependency)
> + dependency =3D cdx.CDXDependency(
> + ref=3Ddistro_bom_ref,
> + dependsOn=3Ddistro_dependencies,
> + )
> + dependencies.append(dependency)
> +
> + doc_uuid =3D d.getVar("SBOM_DOCUMENT_UUID")
> + distro_component =3D cdx.CDXComponent(
> + type=3Dcdx.CDX_COMPONENT_TYPE_OS,
> + bom_ref=3Dcdx.CDX_REF_PREFIX + d.getVar("SBOM_DISTRO= _NAME"),
> + supplier=3Dcdx.CDXSupplier(name=3Dd.getVar("SBOM_DIS= TRO_SUPPLIER")),
> + name=3Dd.getVar("SBOM_DISTRO_NAME"),
> + version=3Dd.getVar("SBOM_DISTRO_VERSION"),
> + description=3Dd.getVar("SBOM_DISTRO_SUMMARY"),
> + )
> +
> + timestamp =3D datetime.fromtimestamp(int(d.getVar("SOURC= E_DATE_EPOCH")))
> + bom =3D cdx.CDXBOM(
> + bomFormat=3Dcdx.CDX_BOM_FORMAT,
> + specVersion=3Dcdx.CDX_SPEC_VERSION,
> + serialNumber=3D"urn:uuid:{}".format(doc_uuid if= doc_uuid else uuid4()),
> + version=3D1,
> + metadata=3Dcdx.CDXBOMMetadata(
> + timestamp=3Dtimestamp.strftime("%Y-%m-%dT%H:%M:%= SZ"),
> + component=3Ddistro_component,
> + tools=3Dcdx.CDXBOMMetadataTool(
> + components=3D[
> + cdx.CDXComponent(
> + type=3Dcdx.CDX_COMPONENT_TYPE_APPLICATION= ,
> + name=3D"ISAR SBOM Generator",
> + version=3Dd.getVar("SBOM_GEN_VERSION= "),
> + )
> + ],
> + ),
> + ),
> + components=3Ddata,
> + dependencies=3Ddependencies,
> + )
> + return bom
> +
> +
> +def spdx_bom(d, packages: List[Package]) -> Dict:
> + "Return a valid SPDX SBOM."
> +
> + data =3D []
> + # create a "fake" entry for the distribution
> + distro_ref =3D spdx.SPDX_REF_PREFIX + d.getVar("SBOM_DIS= TRO_NAME")
> + distro_package =3D spdx.SPDXPackage(
> + SPDXID=3Ddistro_ref,
> + name=3Dd.getVar("SBOM_DISTRO_NAME"),
> + versionInfo=3Dd.getVar("SBOM_DISTRO_VERSION"),
> + primaryPackagePurpose=3Dspdx.SPDX_PACKAGE_PURPOSE_OS,
> + supplier=3D"Organization: {}".format(d.getVar(&= quot;SBOM_DISTRO_SUPPLIER")),
> + downloadLocation=3Dspdx.SPDX_NOASSERTION,
> + filesAnalyzed=3DFalse,
> + licenseConcluded=3Dspdx.SPDX_NOASSERTION,
> + licenseDeclared=3Dspdx.SPDX_NOASSERTION,
> + copyrightText=3Dspdx.SPDX_NOASSERTION,
> + summary=3Dd.getVar("SBOM_DISTRO_SUMMARY"),
> + )
> +
> + data.append(distro_package)
> +
> + pattern =3D re.compile("(?P<supplier_name>^[^<]= *)(\\<(?P<supplier_email>.*)\\>)?")
> + for package in packages:
> + match =3D pattern.match(package.maintainer)
> + supplier_name =3D match["supplier_name"]
> + supplier_email =3D match["supplier_email"]
> + if any([cue in supplier_name.lower() for cue in spdx.SPDX= _SUPPLIER_ORG_CUE]):
> + supplier =3D "Organization: {}".format(supp= lier_name)
> + else:
> + supplier =3D "Person: {}".format(supplier_n= ame)
> + if supplier_email:
> + supplier +=3D "({})".format(supplier_email)
> +
> + entry =3D spdx.SPDXPackage(
> + SPDXID=3Dpackage.bom_ref(SBOMType.SPDX),
> + name=3Dpackage.name,
> + versionInfo=3Dpackage.version,
> + primaryPackagePurpose=3Dspdx.SPDX_PACKAGE_PURPOSE_LIB= RARY,
> + supplier=3Dsupplier,
> + downloadLocation=3Dspdx.SPDX_NOASSERTION,
> + filesAnalyzed=3DFalse,
> + # TODO: it should be possible to conclude license/cop= yright
> + # information, we could look e.g. in /usr/share/doc/*= /copyright
> + licenseConcluded=3Dspdx.SPDX_NOASSERTION,
> + licenseDeclared=3Dspdx.SPDX_NOASSERTION,
> + copyrightText=3Dspdx.SPDX_NOASSERTION,
> + summary=3Dpackage.description,
> + externalRefs=3D[
> + spdx.SPDXExternalRef(
> + referenceCategory=3Dspdx.SPDX_REFERENCE_CATEG= ORY_PKG_MANAGER,
> + referenceType=3Dspdx.SPDX_REFERENCE_TYPE_PURL= ,
> + referenceLocator=3Dpackage.purl(),
> + )
> + ],
> + )
> + if package.homepage:
> + entry.homepage =3D package.homepage
> + data.append(entry)
> +
> + if package.source:
> + src_entry =3D spdx.SPDXPackage(
> + SPDXID=3Dpackage.source.bom_ref(SBOMType.SPDX),
> + name=3Dpackage.sourc= e.name,
> + versionInfo=3Dpackage.source.version,
> + primaryPackagePurpose=3Dspdx.SPDX_PACKAGE_PURPOSE= _SRC,
> + supplier=3Dsupplier,
> + downloadLocation=3Dspdx.SPDX_NOASSERTION,
> + filesAnalyzed=3DFalse,
> + licenseConcluded=3Dspdx.SPDX_NOASSERTION,
> + licenseDeclared=3Dspdx.SPDX_NOASSERTION,
> + copyrightText=3Dspdx.SPDX_NOASSERTION,
> + summary=3D"debian source code package '{= }'".format(package.source.name),
> + externalRefs=3D[
> + spdx.SPDXExternalRef(
> + referenceCategory=3Dspdx.SPDX_REFERENCE_C= ATEGORY_PKG_MANAGER,
> + referenceType=3Dspdx.SPDX_REFERENCE_TYPE_= PURL,
> + referenceLocator=3Dpackage.source.purl(),
> + )
> + ],
> + )
> + # source packages might be referenced multiple times
> + if src_entry not in data:
> + data.append(src_entry)
> +
> + relationships =3D []
> + # after we have found all packages we can start to resolve de= pendencies
> + package_names =3D [package.name for packag= e in packages]
> + for package in packages:
> + relationships.append(
> + spdx.SPDXRelationship(
> + spdxElementId=3Dpackage.bom_ref(SBOMType.SPDX),
> + relatedSpdxElement=3Ddistro_ref,
> + relationshipType=3Dspdx.SPDX_RELATIONSHIP_PACKAGE= _OF,
> + )
> + )
> + if package.depends:
> + for dep in package.depends:
> + if dep.name in package_names:
> + relationship =3D spdx.SPDXRelationship(
> + spdxElementId=3Dpackage.bom_ref(SBOMType.= SPDX),
> + relatedSpdxElement=3Ddep.bom_ref(SBOMType= .SPDX),
> + relationshipType=3Dspdx.SPDX_RELATIONSHIP= _DEPENDS_ON,
> + )
> + relationships.append(relationship)
> + else:
> + # this might happen if we have optional depen= dencies
> + pass
> + if package.source:
> + relationship =3D spdx.SPDXRelationship(
> + spdxElementId=3Dpackage.source.bom_ref(SBOMType.S= PDX),
> + relatedSpdxElement=3Dpackage.bom_ref(SBOMType.SPD= X),
> + relationshipType=3Dspdx.SPDX_RELATIONSHIP_GENERAT= ES,
> + )
> + relationships.append(relationship)
> + relationships.append(
> + spdx.SPDXRelationship(
> + spdxElementId=3Dspdx.SPDX_REF_DOCUMENT,
> + relatedSpdxElement=3Ddistro_ref,
> + relationshipType=3Dspdx.SPDX_RELATIONSHIP_DESCRIBES,
> + )
> + )
> +
> + namespace_uuid =3D d.getVar("SBOM_DOCUMENT_UUID")
> + timestamp =3D datetime.fromtimestamp(int(d.getVar("SOURC= E_DATE_EPOCH")))
> + bom =3D spdx.SPDXBOM(
> + SPDXID=3Dspdx.SPDX_REF_DOCUMENT,
> + spdxVersion=3Dspdx.SPDX_VERSION,
> + creationInfo=3Dspdx.SPDXCreationInfo(
> + comment=3D"This document has been generated as p= art of an ISAR build.",
> + creators=3D[
> + "Tool: ISAR SBOM Generator - {}".format= (d.getVar("SBOM_GEN_VERSION"))
> + ],
> + created=3Dtimestamp.strftime("%Y-%m-%dT%H:%M:%SZ= "),
> + ),
> + name=3Dd.getVar("SBOM_DISTRO_NAME"),
> + dataLicense=3D"CC0-1.0",
> + documentNamespace=3D"{}/{}-{}".format(
> + d.getVar("SBOM_SPDX_NAMESPACE_PREFIX"),
> + d.getVar("SBOM_DISTRO_NAME"),
> + namespace_uuid if namespace_uuid else uuid4(),
> + ),
> + packages=3Ddata,
> + relationships=3Drelationships,
> + )
> + return bom
> +
> +
> +def fixup_dict(o):
> + """Apply fixups for the BOMs.
> +
> + This is necessary for some field names and to remove fields w= ith a None
> + value.
> + """
> + dct =3D vars(o)
> + new_dct =3D {}
> + 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 =3D k.replace("_", "-")
> + new_dct[k] =3D v
> + return new_dct
> +
> +
> +def generate(d, packages: List[Package], sbom_type: SBOMType, out= : str):
> + """Generate a SBOM."""
> + if sbom_type =3D=3D SBOMType.CycloneDX:
> + bom =3D cyclonedx_bom(d, packages)
> + elif sbom_type =3D=3D SBOMType.SPDX:
> + bom =3D spdx_bom(d, packages)
> +
> + with open(out, "w") as bom_file:
> + json.dump(bom, bom_file, indent=3D2, default=3Dfixup_dict= , sort_keys=3DTrue)
> 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, the= y only
> +# reflect what was strictly necessary for immediate SBOM creation
> +
> +CDX_BOM_FORMAT =3D "CycloneDX"
> +CDX_SPEC_VERSION =3D "1.6"
> +
> +CDX_REF_PREFIX =3D "CDXRef-"
> +
> +CDX_PACKAGE_EXTREF_TYPE_WEBSITE =3D "website"
> +
> +CDX_COMPONENT_TYPE_LIBRARY =3D "library"
> +CDX_COMPONENT_TYPE_APPLICATION =3D "application"
> +CDX_COMPONENT_TYPE_OS =3D "operating-system"
> +
> +
> +@dataclass
> +class CDXDependency:
> + ref: str
> + dependsOn: Optional[str]
> +
> +
> +@dataclass
> +class CDXExternalReference:
> + url: str
> + type: str
> + comment: Optional[str] =3D None
> +
> +
> +@dataclass
> +class CDXSupplierContact:
> + email: Optional[str] =3D None
> +
> +
> +@dataclass
> +class CDXSupplier:
> + name: Optional[str] =3D None
> + contact: Optional[CDXSupplierContact] =3D None
> +
> +
> +@dataclass
> +class CDXComponent:
> + type: str
> + name: str
> + bom_ref: Optional[str] =3D None
> + supplier: Optional[str] =3D None
> + version: Optional[CDXSupplier] =3D None
> + description: Optional[str] =3D None
> + purl: Optional[str] =3D None
> + externalReferences: Optional[List[CDXExternalReference]] =3D = None
> + homepage: Optional[str] =3D None
> +
> +
> +@dataclass
> +class CDXBOMMetadataTool:
> + components: Optional[List[CDXComponent]]
> +
> +
> +@dataclass
> +class CDXBOMMetadata:
> + timestamp: Optional[str] =3D None
> + component: Optional[str] =3D None
> + tools: Optional[List[CDXBOMMetadataTool]] =3D None
> +
> +
> +@dataclass
> +class CDXBOM:
> + bomFormat: str
> + specVersion: str
> + serialNumber: Optional[str] =3D None
> + version: Optional[str] =3D None
> + metadata: Optional[CDXBOMMetadata] =3D None
> + components: Optional[List[CDXComponent]] =3D None
> + dependencies: Optional[List[CDXDependency]] =3D None
> diff --git a/meta/lib/sbom_spdx_types.py b/meta/lib/sbom_spdx_type= s.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, the= y only
> +# reflect what was strictly necessary for immediate SBOM creation
> +
> +SPDX_VERSION =3D "SPDX-2.3"
> +
> +SPDX_REF_PREFIX =3D "SPDXRef-"
> +
> +SPDX_REF_DOCUMENT =3D "SPDXRef-DOCUMENT"
> +
> +SPDX_PACKAGE_PURPOSE_LIBRARY =3D "LIBRARY"
> +SPDX_PACKAGE_PURPOSE_OS =3D "OPERATING_SYSTEM"
> +SPDX_PACKAGE_PURPOSE_SRC =3D "SOURCE"
> +
> +SPDX_NOASSERTION =3D "NOASSERTION"
> +
> +SPDX_RELATIONSHIP_DEPENDS_ON =3D "DEPENDS_ON"
> +SPDX_RELATIONSHIP_PACKAGE_OF =3D "PACKAGE_OF"
> +SPDX_RELATIONSHIP_GENERATES =3D "GENERATES"
> +SPDX_RELATIONSHIP_DESCRIBES =3D "DESCRIBES"
> +
> +SPDX_REFERENCE_CATEGORY_PKG_MANAGER =3D "PACKAGE_MANAGER&quo= t;
> +SPDX_REFERENCE_TYPE_PURL =3D "purl"
> +
> +# cues for an organization in the maintainer name
> +SPDX_SUPPLIER_ORG_CUE =3D [
> + "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] =3D False
> + versionInfo: Optional[str] =3D None
> + homepage: Optional[str] =3D None
> + primaryPackagePurpose: Optional[str] =3D None
> + supplier: Optional[str] =3D None
> + licenseConcluded: Optional[str] =3D None
> + licenseDeclared: Optional[str] =3D None
> + copyrightText: Optional[str] =3D None
> + summary: Optional[str] =3D None
> + externalRefs: Optional[List[SPDXExternalRef]] =3D None
> +
> +
> +@dataclass
> +class SPDXCreationInfo:
> + created: str
> + comment: Optional[str] =3D None
> + creators: List[str] =3D None
> +
> +
> +@dataclass
> +class SPDXBOM:
> + """Incomplete BOM as of SPDX spec v2.3."&= quot;"
> +
> + 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 &= quot;isar-users" group.
To unsubscribe from this group and stop receiving emails from it, send an e= mail to isar-use= rs+unsubscribe@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/isar-use= rs/0f4c261e-305d-467d-92a2-2fee7848571fn%40googlegroups.com.
------=_Part_465177_620646560.1752495390846-- ------=_Part_465176_533674564.1752495390846--