public inbox for isar-users@googlegroups.com
 help / color / mirror / Atom feed
From: "'Christoph' via isar-users" <isar-users@googlegroups.com>
To: isar-users <isar-users@googlegroups.com>
Subject: Re: [RFC PATCH 1/1] meta: add CycloneDX/SPDX SBOM generation
Date: Tue, 29 Jul 2025 01:49:55 -0700 (PDT)	[thread overview]
Message-ID: <dc9b7cec-3bb6-4ae7-9d74-e7b496d592f3n@googlegroups.com> (raw)
In-Reply-To: <8e74ae25f4e8add72fd17ce2284a48df7994edaa.camel@siemens.com>


[-- Attachment #1.1: Type: text/plain, Size: 28026 bytes --]



On Thu, 2025-02-20 at 10:59 +0100, 'Felix Moessbauer' via isar-users
wrote:
> From: Christoph Steiger <christop...@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.

Hi,

while discussing various use-cases of the SBOM, I noticed, that the
component description might not be precise enough to identify a debian
package: while the purl SHOULD be sufficient to globally identify the
package, there is no cryptographic hash provided that guarantees the
integrity of the package. By that, different mirrors could
theoretically offer different packages under the same purl. This works,
as long as the repositories themselves are signed correctly and the key
is added to the ISAR build.

How about adding the "hashes" field for each binary package with the
data from apt?

Best regards,
Felix

Do you mean the "hashes" field in a CycloneDX component? It is a very
good idea to add that. There is a checksum field for SPDX packages too
which should semantically be the same.

I will add it to the (long) list of features we want ;)

> 
> 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
> 
> 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")
> +}
> +
> +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"]:
> +        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"]]:
> +        """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"]]:
> +        """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]
> -- 
> 2.39.5

-- 

Siemens AG
Linux Expert Center
Friedrich-Ludwig-Bauer-Str. 3
85748 Garching, Germany

-- 
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/dc9b7cec-3bb6-4ae7-9d74-e7b496d592f3n%40googlegroups.com.

[-- Attachment #1.2: Type: text/html, Size: 41038 bytes --]

  reply	other threads:[~2025-07-29  8:50 UTC|newest]

Thread overview: 15+ 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
2025-03-04 12:12     ` 'Christoph Steiger' via isar-users
2025-06-10 12:03   ` 'Christoph Steiger' via isar-users
2025-07-14 12:16     ` 'Simone Weiß' via isar-users
2025-07-28 15:13       ` 'MOESSBAUER, Felix' via isar-users
2025-07-29  9:08       ` 'Christoph' via isar-users
2025-07-28 15:24   ` 'MOESSBAUER, Felix' via isar-users
2025-07-29  8:49     ` 'Christoph' via isar-users [this message]
2025-08-01  7:53   ` 'MOESSBAUER, Felix' via isar-users
2025-07-31  5:38 ` [RFC PATCH 0/1] SBOM Generation for isar Syeda Shagufta Naaz
2025-07-31  6:50   ` 'Christoph Steiger' via isar-users
2025-07-31  7:24   ` 'Jan Kiszka' 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=dc9b7cec-3bb6-4ae7-9d74-e7b496d592f3n@googlegroups.com \
    --to=isar-users@googlegroups.com \
    --cc=christoph.steiger@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