diff --git a/napalm_linux/linux.py b/napalm_linux/linux.py index 941e1ee..f12e5fd 100644 --- a/napalm_linux/linux.py +++ b/napalm_linux/linux.py @@ -835,11 +835,13 @@ class LinuxDriver(OSDriver): def _get_packages_apt(self) -> list[PackageDict]: out = self._send( "dpkg-query -W -f='${Package}\\t${Version}\\t${Installed-Size}" - "\\t${source:Package}\\t${binary:Summary}\\n' 2>/dev/null" + "\\t${source:Package}\\t${source:Version}\\t${binary:Summary}\\n' 2>/dev/null" ) packages: list[PackageDict] = [] for line in out.splitlines(): - parts = line.split("\t", 4) + # Summary stays last and keeps whatever it contains: maxsplit must + # equal the number of tabs the format writes, not the field count. + parts = line.split("\t", 5) if len(parts) < 2: continue name = parts[0].strip() @@ -847,7 +849,21 @@ class LinuxDriver(OSDriver): size = int(parts[2].strip()) * 1024 if len(parts) > 2 and parts[2].strip().isdigit() else 0 # Debian source package (e.g. openssh-server → openssh) for OSV matching. source_package = parts[3].strip() if len(parts) > 3 and parts[3].strip() else name - description = parts[4].strip() if len(parts) > 4 else "" + # And its version, which is a different number from this package's. + # + # OSV states Debian ranges in *source* versions. A source package + # that ships several binaries gives each its own upstream version: + # libldb2 is 2:2.11.0+samba4.22.11+dfsg-… while its source, samba, + # is 2:4.22.11+dfsg-…. A consumer matching on source_package and + # comparing `version` compares two unrelated numbers — dpkg reads + # ldb's 2.11.0 as older than the 2:4.17.4+dfsg-1 that fixed + # CVE-2022-44640, and a host five releases past the fix was reported + # vulnerable on four packages at once. + # + # dpkg leaves this empty when it equals `Version`; so does an older + # dpkg that does not know the field at all. + source_version = parts[4].strip() if len(parts) > 4 and parts[4].strip() else version + description = parts[5].strip() if len(parts) > 5 else "" packages.append({ "name": name, "version": version, @@ -856,6 +872,7 @@ class LinuxDriver(OSDriver): "size": size, "source": "apt", "source_package": source_package, + "source_version": source_version, }) return packages diff --git a/tests/test_linux.py b/tests/test_linux.py index c3f58f6..edbf7d9 100644 --- a/tests/test_linux.py +++ b/tests/test_linux.py @@ -153,9 +153,22 @@ def test_parse_uptime_invalid(driver): # --------------------------------------------------------------------------- +#: What `dpkg-query` actually returns for the format this driver asks for. +#: +#: The previous fixture carried four fields against a format string asking for +#: five, so `source_package` was silently receiving the description and no +#: assertion noticed. A fixture simpler than the data cannot fail the way the +#: data does. APT_PKG_OUTPUT = ( - "openssh-server\t1:9.2p1-2+deb12u2\t512\tsecure shell server\n" - "curl\t7.88.1-10+deb12u5\t1024\tcommand line tool for transferring data\n" + "openssh-server\t1:9.2p1-2+deb12u2\t512\topenssh\t1:9.2p1-2+deb12u2" + "\tsecure shell server\n" + "curl\t7.88.1-10+deb12u5\t1024\tcurl\t7.88.1-10+deb12u5" + "\tcommand line tool for transferring data\n" + # The shape that matters: a binary package whose own upstream version has + # nothing to do with its source package's. ldb 2.11.0 is built from samba + # 4.22.11, and OSV states Debian ranges in source versions. + "libldb2\t2:2.11.0+samba4.22.11+dfsg-0+deb13u1\t2048\tsamba" + "\t2:4.22.11+dfsg-0+deb13u1\tLDB shared library\n" ) @@ -163,11 +176,45 @@ def test_get_packages_apt(driver): driver._pkg_manager = "apt" with patch.object(driver, "_send", return_value=APT_PKG_OUTPUT): pkgs = driver.get_packages() - assert len(pkgs) == 2 + assert len(pkgs) == 3 assert pkgs[0]["name"] == "openssh-server" assert pkgs[0]["version"] == "1:9.2p1-2+deb12u2" assert pkgs[0]["installed"] is True assert pkgs[0]["source"] == "apt" + assert pkgs[0]["description"] == "secure shell server" + + +def test_get_packages_apt_keeps_the_source_package_and_its_version(driver): + """OSV states Debian ranges in *source* package versions. + + A consumer that matches on the source package and then compares the binary + package's version is comparing two unrelated numbers. On a Debian 13 host + that reported four Samba libraries as vulnerable to CVE-2022-44640 while + running samba 4.22.11 — five releases past the fix — because dpkg reads + ldb's own `2.11.0` as older than samba's `2:4.17.4+dfsg-1`. + + The driver cannot fix the comparison, but it is the only place that can + supply the number to compare. + """ + driver._pkg_manager = "apt" + with patch.object(driver, "_send", return_value=APT_PKG_OUTPUT): + pkgs = {p["name"]: p for p in driver.get_packages()} + + assert pkgs["libldb2"]["source_package"] == "samba" + assert pkgs["libldb2"]["source_version"] == "2:4.22.11+dfsg-0+deb13u1" + assert pkgs["libldb2"]["version"] == "2:2.11.0+samba4.22.11+dfsg-0+deb13u1" + assert pkgs["libldb2"]["description"] == "LDB shared library" + + +def test_get_packages_apt_falls_back_when_dpkg_gives_no_source(driver): + """`source:Package` is empty for a package whose source name equals its own. + Older dpkg builds leave `source:Version` empty in that case too.""" + driver._pkg_manager = "apt" + with patch.object(driver, "_send", return_value="curl\t7.88.1-10\t1024\t\t\ttool\n"): + (pkg,) = driver.get_packages() + + assert pkg["source_package"] == "curl" + assert pkg["source_version"] == "7.88.1-10" # ---------------------------------------------------------------------------