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/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, 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: 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: 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_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 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 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)))