Skip to content

Commit 2c3e800

Browse files
committed
added parsing spdx-license-headers
Signed-off-by: Luis Augenstein <luis.augenstein@tngtech.com>
1 parent 9bbfac0 commit 2c3e800

2 files changed

Lines changed: 81 additions & 37 deletions

File tree

sbom/lib/sbom/spdx_graph/kernel_file.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import hashlib
66
import logging
77
import os
8+
import re
89
from typing import Any, Literal
910
from sbom.path_utils import PathStr, is_relative_to
1011
from sbom.spdx.core import Hash
@@ -15,17 +16,22 @@
1516

1617
@dataclass(kw_only=True)
1718
class KernelFile(File):
18-
"""SPDX File Element annotated with additional metadata"""
19+
"""SPDX file element with kernel-specific metadata."""
1920

2021
absolute_path: PathStr
22+
"""Absolute file system path."""
2123

2224
tree: Literal["src_tree", "output_tree"] | None
23-
"""Determines whether the file lives in the source tree, the output tree or outside of both"""
25+
"""File location: source tree, output tree, or external."""
26+
27+
license_identifier: str | None
28+
"""SPDX license ID if from source tree; otherwise None."""
2429

2530
def to_dict(self) -> dict[str, Any]:
2631
d = super().to_dict()
2732
d.pop("absolute_path", None)
2833
d.pop("tree", None)
34+
d.pop("license_identifier", None)
2935
return d
3036

3137

@@ -53,12 +59,16 @@ def build_kernel_file_element(absolute_path: PathStr, output_tree: PathStr, src_
5359
else:
5460
logging.warning(f"Cannot compute hash for {absolute_path} because file does not exist.")
5561

62+
# parse spdx license identifier
63+
license_identifier = _parse_spdx_license_identifier(absolute_path) if tree == "src_tree" else None
64+
5665
file_element = KernelFile(
5766
spdxId=generate_spdx_id("software_File", file_element_name),
5867
name=file_element_name,
5968
verifiedUsing=verifiedUsing,
6069
absolute_path=absolute_path,
6170
tree=tree,
71+
license_identifier=license_identifier,
6272
)
6373

6474
return file_element
@@ -69,3 +79,29 @@ def _sha256(path: PathStr) -> str:
6979
with open(path, "rb") as f:
7080
data = f.read()
7181
return hashlib.sha256(data).hexdigest()
82+
83+
84+
SPDX_LICENSE_IDENTIFIER_PATTERN = re.compile(
85+
r"SPDX-License-Identifier:\s*(?P<id>[^\s]+(?:\s+(?:AND|OR|WITH)\s+[^\s]+)*)",
86+
re.IGNORECASE,
87+
)
88+
89+
90+
def _parse_spdx_license_identifier(absolute_path: str, max_lines: int = 5) -> str | None:
91+
"""
92+
Extracts the SPDX-License-Identifier from the first few lines of a source file.
93+
94+
Args:
95+
absolute_path: Path to the source file.
96+
max_lines: Number of lines to scan from the top (default: 5).
97+
98+
Returns:
99+
The license identifier string (e.g., 'GPL-2.0-only') if found, otherwise None.
100+
"""
101+
with open(absolute_path, "r") as f:
102+
for _ in range(max_lines):
103+
match = SPDX_LICENSE_IDENTIFIER_PATTERN.search(f.readline())
104+
if match:
105+
return match.group("id")
106+
logging.warning(f"Failed to parse SPDX-License-Identifier from {absolute_path}")
107+
return None

sbom/lib/sbom/spdx_graph/spdx_graph.py

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# SPDX-License-Identifier: GPL-2.0-only
22
# SPDX-FileCopyrightText: 2025 TNG Technology Consulting GmbH
33

4+
from typing import Mapping
45
from sbom.cmd_graph.cmd_graph import CmdGraph, iter_cmd_graph
56
from sbom.path_utils import PathStr
67
from sbom.spdx.build import Build
78
from sbom.spdx.core import (
8-
Element,
99
SoftwareAgent,
1010
SpdxObject,
1111
CreationInfo,
@@ -27,6 +27,7 @@ def build_spdx_graph(
2727
package_license: str,
2828
build_version: str,
2929
) -> list[SpdxObject]:
30+
# Main Skeletton
3031
spdx_document = SpdxDocument(profileConformance=["core", "software", "build", "simpleLicensing"])
3132
agent = SoftwareAgent(name="KernelSbom")
3233
creation_info = CreationInfo(createdBy=[agent])
@@ -53,16 +54,6 @@ def build_spdx_graph(
5354
from_=output_tree_element,
5455
to=[],
5556
)
56-
src_and_output_tree_elements: list[Element] = (
57-
[
58-
src_tree_element,
59-
src_tree_contains_relationship,
60-
output_tree_element,
61-
output_tree_contains_relationship,
62-
]
63-
if src_tree != output_tree
64-
else []
65-
)
6657

6758
# package elements
6859
package = Package(
@@ -82,29 +73,55 @@ def build_spdx_graph(
8273
to=[],
8374
)
8475

85-
file_elements, build_and_relationship_elements = _build_kernel_file_graph(
86-
cmd_graph, output_tree, src_tree, spdx_uri_prefix
87-
)
76+
# file elements
77+
files: dict[PathStr, KernelFile] = {
78+
node.absolute_path: build_kernel_file_element(node.absolute_path, output_tree, src_tree)
79+
for node in iter_cmd_graph(cmd_graph)
80+
}
81+
file_relationships = _build_file_relationships(cmd_graph, files, spdx_uri_prefix)
82+
file_license_identifiers = {
83+
file.license_identifier: LicenseExpression(simplelicensing_licenseExpression=file.license_identifier)
84+
for file in files.values()
85+
if file.license_identifier is not None
86+
}
87+
file_license_relationships = [
88+
Relationship(
89+
relationshipType="hasDeclaredLicense",
90+
from_=file,
91+
to=[file_license_identifiers[file.license_identifier]],
92+
)
93+
for file in files.values()
94+
if file.license_identifier is not None
95+
]
8896

8997
# update relationships
9098
spdx_document.rootElement = [sbom]
9199

92100
sbom.rootElement = [package]
93101
sbom.element = [
94-
*src_and_output_tree_elements,
95102
package,
96103
package_license_expression,
97104
package_hasDeclaredLicense_relationship,
98105
package_contains_roots_relationship,
99-
*file_elements,
100-
*build_and_relationship_elements,
106+
*files.values(),
107+
*file_relationships,
108+
*file_license_identifiers.values(),
109+
*file_license_relationships,
101110
]
102111

103-
src_tree_contains_relationship.to = [file for file in file_elements if file.tree == "src_tree"]
104-
output_tree_contains_relationship.to = [file for file in file_elements if file.tree == "output_tree"]
105-
106112
root_paths = {node.absolute_path for node in cmd_graph.roots}
107-
package_contains_roots_relationship.to = [file for file in file_elements if file.absolute_path in root_paths]
113+
package_contains_roots_relationship.to = [file for file in files.values() if file.absolute_path in root_paths]
114+
115+
if src_tree != output_tree:
116+
sbom.element = [
117+
src_tree_element,
118+
src_tree_contains_relationship,
119+
output_tree_element,
120+
output_tree_contains_relationship,
121+
*sbom.element,
122+
]
123+
src_tree_contains_relationship.to = [file for file in files.values() if file.tree == "src_tree"]
124+
output_tree_contains_relationship.to = [file for file in files.values() if file.tree == "output_tree"]
108125

109126
return [
110127
spdx_document,
@@ -115,19 +132,10 @@ def build_spdx_graph(
115132
]
116133

117134

118-
def _build_kernel_file_graph(
119-
cmd_graph: CmdGraph,
120-
output_tree: PathStr,
121-
src_tree: PathStr,
122-
spdx_uri_prefix: str,
123-
) -> tuple[list[KernelFile], list[Build | Relationship]]:
124-
# First cmd graph traversal: create a file element for each node
125-
file_elements: dict[PathStr, KernelFile] = {
126-
node.absolute_path: build_kernel_file_element(node.absolute_path, output_tree, src_tree)
127-
for node in iter_cmd_graph(cmd_graph)
128-
}
129-
130-
# Second cmd graph traversal: create a relationship for each (child)node representing the generation of all its parents
135+
def _build_file_relationships(
136+
cmd_graph: CmdGraph, file_elements: Mapping[PathStr, File], spdx_uri_prefix: str
137+
) -> list[Build | Relationship]:
138+
# create a relationship between each node (output file) and its children (input files)
131139
build_and_relationship_elements: list[Build | Relationship] = []
132140
for node in iter_cmd_graph(cmd_graph):
133141
if len(node.children) == 0:
@@ -152,4 +160,4 @@ def _build_kernel_file_graph(
152160
)
153161
build_and_relationship_elements += [build_element, hasInput_relationship, hasOutput_relationship]
154162

155-
return list(file_elements.values()), build_and_relationship_elements
163+
return build_and_relationship_elements

0 commit comments

Comments
 (0)