lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-Id: <20260119064731.23879-7-luis.augenstein@tngtech.com>
Date: Mon, 19 Jan 2026 07:47:23 +0100
From: Luis Augenstein <luis.augenstein@...tech.com>
To: nathan@...nel.org,
	nsc@...nel.org
Cc: linux-kbuild@...r.kernel.org,
	linux-kernel@...r.kernel.org,
	akpm@...ux-foundation.org,
	gregkh@...uxfoundation.org,
	maximilian.huber@...tech.com,
	Luis Augenstein <luis.augenstein@...tech.com>
Subject: [PATCH 06/14] tools/sbom: add SPDX classes

Implement Python dataclasses to model the SPDX classes
required within an SPDX document. The class and property
names are consistent with the SPDX 3.0.1 specification.

Co-developed-by: Maximilian Huber <maximilian.huber@...tech.com>
Signed-off-by: Maximilian Huber <maximilian.huber@...tech.com>
Signed-off-by: Luis Augenstein <luis.augenstein@...tech.com>
---
 tools/sbom/sbom/spdx/__init__.py        |   7 +
 tools/sbom/sbom/spdx/build.py           |  17 +++
 tools/sbom/sbom/spdx/core.py            | 182 ++++++++++++++++++++++++
 tools/sbom/sbom/spdx/serialization.py   |  56 ++++++++
 tools/sbom/sbom/spdx/simplelicensing.py |  20 +++
 tools/sbom/sbom/spdx/software.py        |  71 +++++++++
 tools/sbom/sbom/spdx/spdxId.py          |  36 +++++
 7 files changed, 389 insertions(+)
 create mode 100644 tools/sbom/sbom/spdx/__init__.py
 create mode 100644 tools/sbom/sbom/spdx/build.py
 create mode 100644 tools/sbom/sbom/spdx/core.py
 create mode 100644 tools/sbom/sbom/spdx/serialization.py
 create mode 100644 tools/sbom/sbom/spdx/simplelicensing.py
 create mode 100644 tools/sbom/sbom/spdx/software.py
 create mode 100644 tools/sbom/sbom/spdx/spdxId.py

diff --git a/tools/sbom/sbom/spdx/__init__.py b/tools/sbom/sbom/spdx/__init__.py
new file mode 100644
index 000000000..4097b59f8
--- /dev/null
+++ b/tools/sbom/sbom/spdx/__init__.py
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+# Copyright (C) 2025 TNG Technology Consulting GmbH
+
+from .spdxId import SpdxId, SpdxIdGenerator
+from .serialization import JsonLdSpdxDocument
+
+__all__ = ["JsonLdSpdxDocument", "SpdxId", "SpdxIdGenerator"]
diff --git a/tools/sbom/sbom/spdx/build.py b/tools/sbom/sbom/spdx/build.py
new file mode 100644
index 000000000..180a8f1e8
--- /dev/null
+++ b/tools/sbom/sbom/spdx/build.py
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+# Copyright (C) 2025 TNG Technology Consulting GmbH
+
+from dataclasses import dataclass, field
+from sbom.spdx.core import DictionaryEntry, Element, Hash
+
+
+@...aclass(kw_only=True)
+class Build(Element):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Build/Classes/Build/"""
+
+    type: str = field(init=False, default="build_Build")
+    build_buildType: str
+    build_buildId: str
+    build_environment: list[DictionaryEntry] = field(default_factory=list[DictionaryEntry])
+    build_configSourceUri: list[str] = field(default_factory=list[str])
+    build_configSourceDigest: list[Hash] = field(default_factory=list[Hash])
diff --git a/tools/sbom/sbom/spdx/core.py b/tools/sbom/sbom/spdx/core.py
new file mode 100644
index 000000000..c5de9194b
--- /dev/null
+++ b/tools/sbom/sbom/spdx/core.py
@@ -0,0 +1,182 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+# Copyright (C) 2025 TNG Technology Consulting GmbH
+
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+from typing import Any, Literal
+from sbom.spdx.spdxId import SpdxId
+
+SPDX_SPEC_VERSION = "3.0.1"
+
+ExternalIdentifierType = Literal["email", "gitoid", "urlScheme"]
+HashAlgorithm = Literal["sha256", "sha512"]
+ProfileIdentifierType = Literal["core", "software", "build", "lite", "simpleLicensing"]
+RelationshipType = Literal[
+    "contains",
+    "generates",
+    "hasDeclaredLicense",
+    "hasInput",
+    "hasOutput",
+    "ancestorOf",
+    "hasDistributionArtifact",
+    "dependsOn",
+]
+RelationshipCompleteness = Literal["complete", "incomplete", "noAssertion"]
+
+
+@...aclass
+class SpdxObject:
+    def to_dict(self) -> dict[str, Any]:
+        def _to_dict(v: Any):
+            return v.to_dict() if hasattr(v, "to_dict") else v
+
+        d: dict[str, Any] = {}
+        for field_name in self.__dataclass_fields__:
+            value = getattr(self, field_name)
+            if not value:
+                continue
+
+            if isinstance(value, Element):
+                d[field_name] = value.spdxId
+            elif isinstance(value, list) and len(value) > 0 and isinstance(value[0], Element):  # type: ignore
+                value: list[Element] = value
+                d[field_name] = [v.spdxId for v in value]
+            else:
+                d[field_name] = [_to_dict(v) for v in value] if isinstance(value, list) else _to_dict(value)  # type: ignore
+        return d
+
+
+@...aclass(kw_only=True)
+class IntegrityMethod(SpdxObject):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/IntegrityMethod/"""
+
+
+@...aclass(kw_only=True)
+class Hash(IntegrityMethod):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Hash/"""
+
+    type: str = field(init=False, default="Hash")
+    hashValue: str
+    algorithm: HashAlgorithm
+
+
+@...aclass(kw_only=True)
+class Element(SpdxObject):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Element/"""
+
+    type: str = field(init=False, default="Element")
+    spdxId: SpdxId
+    creationInfo: str = "_:creationinfo"
+    name: str | None = None
+    verifiedUsing: list[Hash] = field(default_factory=list[Hash])
+    comment: str | None = None
+
+
+@...aclass(kw_only=True)
+class ExternalMap(SpdxObject):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/ExternalMap/"""
+
+    type: str = field(init=False, default="ExternalMap")
+    externalSpdxId: SpdxId
+
+
+@...aclass(kw_only=True)
+class NamespaceMap(SpdxObject):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/NamespaceMap/"""
+
+    type: str = field(init=False, default="NamespaceMap")
+    prefix: str
+    namespace: str
+
+
+@...aclass(kw_only=True)
+class ElementCollection(Element):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/ElementCollection/"""
+
+    type: str = field(init=False, default="ElementCollection")
+    element: list[Element] = field(default_factory=list[Element])
+    rootElement: list[Element] = field(default_factory=list[Element])
+    profileConformance: list[ProfileIdentifierType] = field(default_factory=list[ProfileIdentifierType])
+
+
+@...aclass(kw_only=True)
+class SpdxDocument(ElementCollection):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/SpdxDocument/"""
+
+    type: str = field(init=False, default="SpdxDocument")
+    import_: list[ExternalMap] = field(default_factory=list[ExternalMap])
+    namespaceMap: list[NamespaceMap] = field(default_factory=list[NamespaceMap])
+
+    def to_dict(self) -> dict[str, Any]:
+        return {("import" if k == "import_" else k): v for k, v in super().to_dict().items()}
+
+
+@...aclass(kw_only=True)
+class ExternalIdentifier(SpdxObject):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/ExternalIdentifier/"""
+
+    type: str = field(init=False, default="ExternalIdentifier")
+    externalIdentifierType: ExternalIdentifierType
+    identifier: str
+
+
+@...aclass(kw_only=True)
+class Agent(Element):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Agent/"""
+
+    type: str = field(init=False, default="Agent")
+    externalIdentifier: list[ExternalIdentifier] = field(default_factory=list[ExternalIdentifier])
+
+
+@...aclass(kw_only=True)
+class SoftwareAgent(Agent):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/SoftwareAgent/"""
+
+    type: str = field(init=False, default="SoftwareAgent")
+
+
+@...aclass(kw_only=True)
+class CreationInfo(SpdxObject):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/CreationInfo/"""
+
+    type: str = field(init=False, default="CreationInfo")
+    id: SpdxId = "_:creationinfo"
+    specVersion: str = SPDX_SPEC_VERSION
+    createdBy: list[Agent]
+    created: str = field(default_factory=lambda: datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))
+    comment: str | None = None
+
+    def to_dict(self) -> dict[str, Any]:
+        return {("@id" if k == "id" else k): v for k, v in super().to_dict().items()}
+
+
+@...aclass(kw_only=True)
+class Relationship(Element):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Relationship/"""
+
+    type: str = field(init=False, default="Relationship")
+    relationshipType: RelationshipType
+    from_: Element  # underscore because 'from' is a reserved keyword
+    to: list[Element]
+    completeness: RelationshipCompleteness | None = None
+
+    def to_dict(self) -> dict[str, Any]:
+        return {("from" if k == "from_" else k): v for k, v in super().to_dict().items()}
+
+
+@...aclass(kw_only=True)
+class Artifact(Element):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Artifact/"""
+
+    type: str = field(init=False, default="Artifact")
+    builtTime: str | None = None
+    originatedBy: list[Agent] = field(default_factory=list[Agent])
+
+
+@...aclass(kw_only=True)
+class DictionaryEntry(SpdxObject):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/DictionaryEntry/"""
+
+    type: str = field(init=False, default="DictionaryEntry")
+    key: str
+    value: str
diff --git a/tools/sbom/sbom/spdx/serialization.py b/tools/sbom/sbom/spdx/serialization.py
new file mode 100644
index 000000000..c830d6b3c
--- /dev/null
+++ b/tools/sbom/sbom/spdx/serialization.py
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+# Copyright (C) 2025 TNG Technology Consulting GmbH
+
+import json
+from typing import Any
+from sbom.path_utils import PathStr
+from sbom.spdx.core import SPDX_SPEC_VERSION, SpdxDocument, SpdxObject
+
+
+class JsonLdSpdxDocument:
+    """Represents an SPDX document in JSON-LD format for serialization."""
+
+    context: list[str | dict[str, str]]
+    graph: list[SpdxObject]
+
+    def __init__(self, graph: list[SpdxObject]) -> None:
+        """
+        Initialize a JSON-LD SPDX document from a graph of SPDX objects.
+        The graph must contain a single SpdxDocument element.
+
+        Args:
+            graph: List of SPDX objects representing the complete SPDX document.
+        """
+        self.graph = graph
+        spdx_document = next(element for element in graph if isinstance(element, SpdxDocument))
+        self.context = [
+            f"https://spdx.org/rdf/{SPDX_SPEC_VERSION}/spdx-context.jsonld",
+            {namespaceMap.prefix: namespaceMap.namespace for namespaceMap in spdx_document.namespaceMap},
+        ]
+        spdx_document.namespaceMap = []
+
+    def to_dict(self) -> dict[str, Any]:
+        """
+        Convert the SPDX document to a dictionary representation suitable for JSON serialization.
+
+        Returns:
+            Dictionary with @context and @graph keys following JSON-LD format.
+        """
+        return {
+            "@context": self.context,
+            "@graph": [item.to_dict() for item in self.graph],
+        }
+
+    def save(self, path: PathStr, prettify: bool) -> None:
+        """
+        Save the SPDX document to a JSON file.
+
+        Args:
+            path: File path where the document will be saved.
+            prettify: Whether to pretty-print the JSON with indentation.
+        """
+        with open(path, "w", encoding="utf-8") as f:
+            if prettify:
+                json.dump(self.to_dict(), f, indent=2)
+            else:
+                json.dump(self.to_dict(), f, separators=(",", ":"))
diff --git a/tools/sbom/sbom/spdx/simplelicensing.py b/tools/sbom/sbom/spdx/simplelicensing.py
new file mode 100644
index 000000000..750ddd24a
--- /dev/null
+++ b/tools/sbom/sbom/spdx/simplelicensing.py
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+# Copyright (C) 2025 TNG Technology Consulting GmbH
+
+from dataclasses import dataclass, field
+from sbom.spdx.core import Element
+
+
+@...aclass(kw_only=True)
+class AnyLicenseInfo(Element):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/SimpleLicensing/Classes/AnyLicenseInfo/"""
+
+    type: str = field(init=False, default="simplelicensing_AnyLicenseInfo")
+
+
+@...aclass(kw_only=True)
+class LicenseExpression(AnyLicenseInfo):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/SimpleLicensing/Classes/LicenseExpression/"""
+
+    type: str = field(init=False, default="simplelicensing_LicenseExpression")
+    simplelicensing_licenseExpression: str
diff --git a/tools/sbom/sbom/spdx/software.py b/tools/sbom/sbom/spdx/software.py
new file mode 100644
index 000000000..208e0168b
--- /dev/null
+++ b/tools/sbom/sbom/spdx/software.py
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+# Copyright (C) 2025 TNG Technology Consulting GmbH
+
+from dataclasses import dataclass, field
+from typing import Literal
+from sbom.spdx.core import Artifact, ElementCollection, IntegrityMethod
+
+
+SbomType = Literal["source", "build"]
+FileKindType = Literal["file", "directory"]
+SoftwarePurpose = Literal[
+    "source",
+    "archive",
+    "library",
+    "file",
+    "data",
+    "configuration",
+    "executable",
+    "module",
+    "application",
+    "documentation",
+    "other",
+]
+ContentIdentifierType = Literal["gitoid", "swhid"]
+
+
+@...aclass(kw_only=True)
+class Sbom(ElementCollection):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/Sbom/"""
+
+    type: str = field(init=False, default="software_Sbom")
+    software_sbomType: list[SbomType] = field(default_factory=list[SbomType])
+
+
+@...aclass(kw_only=True)
+class ContentIdentifier(IntegrityMethod):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/ContentIdentifier/"""
+
+    type: str = field(init=False, default="software_ContentIdentifier")
+    software_contentIdentifierType: ContentIdentifierType
+    software_contentIdentifierValue: str
+
+
+@...aclass(kw_only=True)
+class SoftwareArtifact(Artifact):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/SoftwareArtifact/"""
+
+    type: str = field(init=False, default="software_Artifact")
+    software_primaryPurpose: SoftwarePurpose | None = None
+    software_additionalPurpose: list[SoftwarePurpose] = field(default_factory=list[SoftwarePurpose])
+    software_copyrightText: str | None = None
+    software_contentIdentifier: list[ContentIdentifier] = field(default_factory=list[ContentIdentifier])
+
+
+@...aclass(kw_only=True)
+class Package(SoftwareArtifact):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/Package/"""
+
+    type: str = field(init=False, default="software_Package")
+    name: str  # type: ignore
+    software_packageVersion: str | None = None
+    software_downloadLocation: str | None = None
+
+
+@...aclass(kw_only=True)
+class File(SoftwareArtifact):
+    """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/File/"""
+
+    type: str = field(init=False, default="software_File")
+    name: str  # type: ignore
+    software_fileKind: FileKindType | None = None
diff --git a/tools/sbom/sbom/spdx/spdxId.py b/tools/sbom/sbom/spdx/spdxId.py
new file mode 100644
index 000000000..589e85c5f
--- /dev/null
+++ b/tools/sbom/sbom/spdx/spdxId.py
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+# Copyright (C) 2025 TNG Technology Consulting GmbH
+
+from itertools import count
+from typing import Iterator
+
+SpdxId = str
+
+
+class SpdxIdGenerator:
+    _namespace: str
+    _prefix: str | None = None
+    _counter: Iterator[int]
+
+    def __init__(self, namespace: str, prefix: str | None = None) -> None:
+        """
+        Initialize the SPDX ID generator with a namespace.
+
+        Args:
+            namespace: The full namespace to use for generated IDs.
+            prefix: Optional. If provided, generated IDs will use this prefix instead of the full namespace.
+        """
+        self._namespace = namespace
+        self._prefix = prefix
+        self._counter = count(0)
+
+    def generate(self) -> SpdxId:
+        return f"{f'{self._prefix}:' if self._prefix else self._namespace}{next(self._counter)}"
+
+    @property
+    def prefix(self) -> str | None:
+        return self._prefix
+
+    @property
+    def namespace(self) -> str:
+        return self._namespace
-- 
2.34.1


Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ