From 432e2ccba3259818d40a6a10e32901804c0ca419 Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Mon, 2 Mar 2026 13:31:55 +0100 Subject: [PATCH 1/6] test: generalize dpkg-minimal tests assume less about file Some tests currently assume that the dpkg-minimal status file only has a single package. To test for more corner cases, we want to add multiple packages to that file in the future, breaking the assumption that the file only has one. We now adjust the tests so that they do not assume that there is only a single package. Signed-off-by: Felix Moessbauer --- tests/test_download.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_download.py b/tests/test_download.py index 05a98ac8..4e03e568 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -143,12 +143,12 @@ def test_package_resolver_resolve_spdx(spdx_bomfile, tmpdir, sdl): rs_cache = PersistentResolverCache(cachedir) urs = UpstreamResolver(sdl, rs_cache) - files = list(urs.resolve(next(prs))) + files = list(urs.resolve(next(filter(lambda p: p.name == "binutils", prs)))) assert "binutils" in files[0].filename # resolve with cache prs = PackageResolver.create(spdx_bomfile) - files = list(urs.resolve(next(prs))) + files = list(urs.resolve(next(filter(lambda p: p.name == "binutils", prs)))) assert "binutils" in files[0].filename @@ -210,7 +210,7 @@ def test_repack(tmpdir, spdx_bomfile, cdx_bomfile, http_session, sdl): # download a single package dl = PackageDownloader(dl_dir, session=http_session) - for p in filter_sources(pkgs): + for p in filter(lambda p: p.name == "binutils", filter_sources(pkgs)): dl.register(urs.resolve(p), p) files = list(dl.download()) assert len(files) == 3 From 8fbcefec08f38e8634bca5bfef0be281eb5b3f7e Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Mon, 2 Mar 2026 13:03:45 +0100 Subject: [PATCH 2/6] feat(package): track if binary packages are essential This information is useful for information routing, as in debian essential packages are assumed to be always installed, hence do not need to be added to package dependencies. Signed-off-by: Felix Moessbauer --- src/debsbom/dpkg/package.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/debsbom/dpkg/package.py b/src/debsbom/dpkg/package.py index 4b069795..03ad5fb7 100644 --- a/src/debsbom/dpkg/package.py +++ b/src/debsbom/dpkg/package.py @@ -570,6 +570,7 @@ class BinaryPackage(Package): provides: list[VirtualPackage] built_using: list[Dependency] description: str | None + essential: bool manually_installed: bool status: DpkgStatus _locator: str | None = None @@ -587,6 +588,7 @@ def __init__( provides: list[VirtualPackage] = [], built_using: list[Dependency] = [], description: str | None = None, + essential: bool = False, homepage: str | None = None, checksums: dict[ChecksumAlgo, str] | None = None, manually_installed: bool = True, @@ -603,6 +605,7 @@ def __init__( self.provides = provides self.built_using = built_using self.description = description + self.essential = essential self.homepage = homepage self.checksums = checksums or {} self.manually_installed = manually_installed @@ -662,6 +665,7 @@ def merge_with(self, other: "BinaryPackage"): self.source = other.source if not self.description: self.description = other.description + self.essential |= other.essential self.manually_installed |= other.manually_installed # we cannot merge the status, but if the other package is # marked as installed, consider all as installed. @@ -775,6 +779,7 @@ def from_deb822(cls, package) -> "BinaryPackage": provides=provides, built_using=sdepends, description=cls._cleanup_description(package.get("Description")), + essential=package.get("Essential") == "yes", homepage=package.get("Homepage"), checksums=checksums_from_package(package), status=status, From 738ae8374f18f41dc866607747154bfd57ba8a7d Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Mon, 2 Mar 2026 13:04:57 +0100 Subject: [PATCH 3/6] feat(cdx): denote if a package is essential In CycloneDX we denote if a package is an essential package. Downstream tooling can use this information for improved dependency analysis. As SPDX does not offer a field for this information, this remains CDX only for now. Signed-off-by: Felix Moessbauer --- src/debsbom/generate/cdx.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/debsbom/generate/cdx.py b/src/debsbom/generate/cdx.py index 3e4b85f1..fe374866 100644 --- a/src/debsbom/generate/cdx.py +++ b/src/debsbom/generate/cdx.py @@ -79,6 +79,7 @@ def cdx_package_repr( if package.is_binary(): entry.description = package.description entry.properties.add(cdx_model.Property(name="section", value=package.section)) + entry.properties.add(cdx_model.Property(name="essential", value=package.essential)) logger.debug(f"Created binary component: {entry}") elif package.is_source(): if package.vcs: From 1f1986b4d3b144b940009428c49b98d8fb72dd4a Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Mon, 2 Mar 2026 13:21:28 +0100 Subject: [PATCH 4/6] test: check if essential field of bin packages is extracted Signed-off-by: Felix Moessbauer --- tests/data/dpkg-status-minimal | 14 ++++++++++++++ tests/test_dpkg.py | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/tests/data/dpkg-status-minimal b/tests/data/dpkg-status-minimal index 2246f79e..ece0c9f9 100644 --- a/tests/data/dpkg-status-minimal +++ b/tests/data/dpkg-status-minimal @@ -17,3 +17,17 @@ Description: GNU assembler, linker and binary utilities and various libraries to build programs. Homepage: https://www.gnu.org/software/binutils/ +Package: coreutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 17994 +Maintainer: Michael Stone +Architecture: amd64 +Multi-Arch: foreign +Version: 9.10-1 +Pre-Depends: libacl1 (>= 2.2.23), libattr1 (>= 1:2.4.48), libc6 (>= 2.42), libgmp10 (>= 2:6.3.0+dfsg), libselinux1 (>= 3.1~), libssl3t64 (>= 3.0.0), libsystemd0 (>= 254) +Description: GNU core utilities + This package contains the basic file, shell and text manipulation + utilities which are expected to exist on every operating system. diff --git a/tests/test_dpkg.py b/tests/test_dpkg.py index 3d8bb93e..55250626 100644 --- a/tests/test_dpkg.py +++ b/tests/test_dpkg.py @@ -45,6 +45,7 @@ def test_parse_minimal_status_file(mode): assert bpkg.name == "binutils" assert bpkg.section == "devel" + assert bpkg.essential is False assert bpkg.maintainer == "Matthias Klose " assert bpkg.source == Dependency(bpkg.name, None, ("=", bpkg.version), arch="source") assert bpkg.version == "2.40-2" @@ -64,6 +65,10 @@ def test_parse_minimal_status_file(mode): assert bpkg.homepage == "https://www.gnu.org/software/binutils/" assert bpkg.status == DpkgStatus.INSTALLED + coreutils = [p for p in packages if isinstance(p, BinaryPackage)][1] + assert coreutils.name == "coreutils" + assert coreutils.essential is True + spkg = packages[1] assert spkg.name == "binutils" assert spkg.version == bpkg.version From b452cfaba659376dc837f2048792564ac48888a0 Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Mon, 2 Mar 2026 13:11:02 +0100 Subject: [PATCH 5/6] feat(exporter): add essential information to graph This is a CycloneDX only feature, as SPDX does not encode this information. Signed-off-by: Felix Moessbauer --- src/debsbom/commands/export.py | 3 ++- src/debsbom/export/cdx.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/debsbom/commands/export.py b/src/debsbom/commands/export.py index 5aaefbe7..d3c65c71 100644 --- a/src/debsbom/commands/export.py +++ b/src/debsbom/commands/export.py @@ -10,7 +10,8 @@ class ExportCmd(SbomInput): """ Processes an SBOM and converts it to various graph formats. Note, that SPDX SBOMs lead to better results, as they describes inter - package relations more precisely. + package relations more precisely. However, some properties like the + package section and essential can only be tracked in CycloneDX. """ @classmethod diff --git a/src/debsbom/export/cdx.py b/src/debsbom/export/cdx.py index a1334e26..5d013c75 100644 --- a/src/debsbom/export/cdx.py +++ b/src/debsbom/export/cdx.py @@ -43,6 +43,7 @@ def add_key(name, _type, _for): add_key("purl", "string", "node") add_key("type", "string", "node") add_key("section", "string", "node") + add_key("essential", "boolean", "node") def add_packages(self, graph: ET.Element): for p in self.document.components: @@ -58,11 +59,15 @@ def add_packages(self, graph: ET.Element): ET.SubElement(node, "data", {"key": "d_purl"}).text = str(p.purl) ET.SubElement(node, "data", {"key": "d_type"}).text = p.type section = "unknown" + essential = "false" for prop in p.properties: if prop.name == "section": section = prop.value - break + elif prop.name == "essential": + essential = str(prop.value).lower() + ET.SubElement(node, "data", {"key": "d_section"}).text = section + ET.SubElement(node, "data", {"key": "d_essential"}).text = essential def add_dependencies(self, graph: ET.Element): for r in self.document.dependencies: From 3ab73c5cd1299e09195469582c69b81c2af9a26f Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Mon, 2 Mar 2026 13:41:28 +0100 Subject: [PATCH 6/6] test(exporter): check if CycloneDX graph has essential property Signed-off-by: Felix Moessbauer --- tests/test_export.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_export.py b/tests/test_export.py index a62e0c6b..ebdbced1 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -37,6 +37,12 @@ def get_name(node): return data.text return None + def get_essential(node): + for data in node.findall(f"{{{NAMESPACE}}}data"): + if data.get("key") == "d_essential": + return data.text + return None + dbom = sbom_generator("tests/root/tree", sbom_types=[sbom_type]) outdir = Path(tmpdir) dbom.generate(str(outdir / "sbom"), validate=False) @@ -55,6 +61,9 @@ def get_name(node): assert root.tag == f"{{{NAMESPACE}}}graphml" node_tag = f"{{{NAMESPACE}}}node" assert any(map(lambda n: get_name(n) == "binutils", root.iter(node_tag))) + if sbom_type == SBOMType.CycloneDX: + jansson = next(filter(lambda n: get_name(n) == "libjansson4", root.iter(node_tag))) + assert get_essential(jansson) == "false" edge_tag = f"{{{NAMESPACE}}}edge" assert any(map(lambda n: "binutils" in n.get("id"), root.iter(edge_tag)))