Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/sbom_cve_check/sbom/lib_spdx3.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,47 @@ def iterate_install_spec_packages(
}
)

def has_software_agent(self, name: str) -> bool:
"""
Match SBOM with a SoftwareAgent (tool used to generate the SBOM).

:param name: Agent name.
:return: True if found.
"""
return any(
a.name == name for a in self.foreach_type(spdx30.SoftwareAgent)
)

def iterate_application_packages(
self,
) -> Generator[spdx30.software_Package, None, None]:
"""
Iterate over packages with 'application' purpose.

:return: Yield software_Package objects with application purpose.
"""
yield from (
p
for p in self.foreach_type(spdx30.software_Package)
if p.software_primaryPurpose == spdx30.software_SoftwarePurpose.application
)

def get_source_file_paths(self) -> set[str]:
"""
Collect source file paths from this object set.

:return: Set of source file path strings.
"""
return {
f.name
for f in self.foreach_type(spdx30.software_File)
if (
f.software_primaryPurpose == spdx30.software_SoftwarePurpose.source
and isinstance(f.name, str)
and f.name
)
}

def find_package_by_name(
self, name: str, purposes: set[str] | None = None
) -> Generator[spdx30.software_Package, None, None]:
Expand Down
237 changes: 234 additions & 3 deletions src/sbom_cve_check/sbom/sbom_spdx3.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from ..export.config import ExportConfig
from ..utils.compat import override
from ..utils.compress import filename_without_compress_ext
from ..vuln.comp_id import CompId
from ..vuln.comp_id import LINUX_KERNEL_COMP_IDS, CompId
from ..vuln.cpe import Cpe23
from ..vuln.vuln import CveId, VulnInfo
from .component import CompBuild, Component, CompType
Expand Down Expand Up @@ -389,6 +389,159 @@ def add_vulnerability(
update_vuln_info = False


class KernelComponent(Component):
"""
Fill CPE/PURL for KernelSbom SBOM.
"""

def __init__(
self,
pkg: spdx30.software_Package,
version: str,
) -> None:
"""
Initialize KernelComponent.

:param pkg: The SPDX3 package object.
:param version: The kernel version string.
"""
self._pkg = pkg
self._version = version

@property
@override
def name(self) -> str:
"""Component name."""
return str(self._pkg.name or "linux")

@property
@override
def version(self) -> str | None:
"""Component version."""
return self._version

@property
@override
def pkg_version(self) -> str | None:
"""Package version."""
return self._pkg.software_packageVersion

@property
@override
def supplier(self) -> str | None:
"""Supplier name (not available in kernel SBOM)."""
return None

@property
@override
def comp_type(self) -> CompType | None:
"""Component type (OS for the Linux kernel)."""
return CompType.OS

@property
@override
def identifiers(self) -> set[CompId]:
"""Well-known Linux kernel component identifiers."""
return set(LINUX_KERNEL_COMP_IDS)

@property
@override
def purls(self) -> set[str]:
"""PURLs (none available in kernel SBOM)."""
return set()

@property
@override
def cpes(self) -> set[Cpe23]:
"""CPEs (none available in kernel SBOM)."""
return set()


class KernelCompBuild(CompBuild):
"""
CompBuild for a kernel SBOM ``application``-purpose package.
"""

def __init__(
self,
objset: ObjectSet,
pkg: spdx30.software_Package,
version: str,
source_files: set[str],
) -> None:
"""
Initialize KernelCompBuild.

:param objset: The ObjectSet containing the SBOM graph.
:param pkg: The application-purpose kernel package.
:param version: The kernel version string.
:param source_files: Set of source file paths compiled into this kernel.
"""
super().__init__()
self._objset = objset
self._pkg = pkg
self._version = version
self._source_files = source_files
self._component = KernelComponent(pkg, version)

@property
@override
def version(self) -> str:
"""The kernel version string."""
return self._version

@property
@override
def identifiers(self) -> tuple[CompId, ...]:
"""Linux kernel component identifiers (sorted)."""
return tuple(sorted(LINUX_KERNEL_COMP_IDS))

@property
@override
def components(self) -> Iterable[Component]:
"""The single kernel component."""
return (self._component,)

@property
@override
def build_name(self) -> str | None:
"""Name derived from the kernel package."""
return str(self._pkg.name) if self._pkg.name else "linux-kernel"

@property
@override
def is_native(self) -> bool:
"""The kernel is always a target (non-native) build."""
return False

@override
def _get_compiled_sources(self) -> set[str]:
"""Return source file paths collected from the kernel source SBOM."""
return self._source_files

@override
def add_vulnerability(
self,
vuln_info: VulnInfo,
*,
filter_components: Callable[[CompBuild], Iterable[Component]],
update_vuln_info: bool = True,
export_config: ExportConfig | None = None,
) -> None:
"""
Add a vulnerability associated with the kernel package.

:param vuln_info: Vulnerability info to add.
:param filter_components: Filter function (not used; there is only one
component per kernel build).
:param update_vuln_info: Whether to update existing vulnerability info.
:param export_config: Export configuration (optional).
"""
self._objset.add_vulnerability(
self._pkg, vuln_info, update_vuln_info=update_vuln_info
)


@register_sbom("spdx3")
class Spdx3Sbom(Sbom):
"""
Expand All @@ -406,6 +559,13 @@ def can_handle_sbom(cls, path: pathlib.Path) -> bool:
"""
return filename_without_compress_ext(path.name).endswith(".spdx.json")

# kernel's scripts/sbom
_KERNEL_SBOM_AGENT = "KernelSbom"
_KERNEL_SOURCE_SBOM_NAMES = (
"sbom-source.spdx.json",
"sbom-build.spdx.json",
)

def __init__(self, path: pathlib.Path) -> None:
"""
Initialize Spdx3Sbom.
Expand All @@ -414,7 +574,8 @@ def __init__(self, path: pathlib.Path) -> None:
"""
super().__init__(path)
self._map_comp_build: dict[spdx30.Element, Spdx3CompBuild] = {}
self._comp_builds: list[Spdx3CompBuild] = []
self._comp_builds: list[CompBuild] = []
self._kernel_source_files: set[str] | None = None

_logger.info("Loading SPDX3 SBOM: %s", path)
self._objset: ObjectSet = ObjectSet.parse_jsonld(path)
Expand Down Expand Up @@ -475,7 +636,70 @@ def __add_spec_pkg(self, spec: spdx30.software_Package) -> None:
self._comp_builds.append(comp_build)
self._map_comp_build[spec] = comp_build

def _create_comp_builds(self) -> list[Spdx3CompBuild]:
def _load_kernel_source_files(self) -> set[str]:
"""
Load source file paths from kernel SBOM documents.

The kernel's scripts/sbom writes ``sbom-output.spdx.json``,
``sbom-source.spdx.json`` and ``sbom-build.spdx.json``.

:return: Set of source file path strings.
"""
if self._kernel_source_files is not None:
return self._kernel_source_files

source_files: set[str] = set()
sbom_dir = self._sbom_path.parent

source_files.update(self._objset.get_source_file_paths())

for sibling_name in self._KERNEL_SOURCE_SBOM_NAMES:
sibling_path = sbom_dir / sibling_name
if sibling_path == self._sbom_path or not sibling_path.exists():
continue
_logger.debug(
"Loading kernel source files from sibling SBOM: %s", sibling_path
)
try:
sibling_objset = ObjectSet.parse_jsonld(sibling_path)
source_files.update(sibling_objset.get_source_file_paths())
except Exception as exc: # noqa: BLE001
_logger.warning(
"Failed to load kernel source SBOM %s: %s", sibling_path, exc
)

self._kernel_source_files = source_files
return source_files

def _create_kernel_comp_builds(self) -> list[KernelCompBuild]:
"""
Create KernelCompBuild objects from application-purpose packages.

:return: List of KernelCompBuild objects.
"""
kernel_builds: list[KernelCompBuild] = []
source_files = self._load_kernel_source_files()

for pkg in self._objset.iterate_application_packages():
version: str | None = pkg.software_packageVersion
if not version:
_logger.warning(
"Kernel SBOM package '%s' has no version. "
"Skipping CVE matching for this package.",
pkg.name,
)
continue

_logger.debug(
"Detected kernel SBOM package: name=%s, version=%s", pkg.name, version
)
kernel_builds.append(
KernelCompBuild(self._objset, pkg, version, source_files)
)

return kernel_builds

def _create_comp_builds(self) -> list[CompBuild]:
"""
Create the SPDX3 component build objects associated with this SBOM.

Expand All @@ -485,6 +709,13 @@ def _create_comp_builds(self) -> list[Spdx3CompBuild]:
if self._comp_builds:
return self._comp_builds

if self._objset.has_software_agent(self._KERNEL_SBOM_AGENT):
self._comp_builds = sorted(
self._create_kernel_comp_builds(),
key=lambda cb: cb.build_name or "",
)
return self._comp_builds

for pkg in self._objset.iterate_install_spec_packages():
if pkg.software_primaryPurpose == spdx30.software_SoftwarePurpose.install:
self.__add_install_pkg(pkg)
Expand Down