From f898c75ef48ca0ec320fc7169ceda54dc098084a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Baumann?= Date: Mon, 27 Jul 2026 11:51:59 -0400 Subject: [PATCH 1/3] fix(purl): resolve Docker Hardened Images apk/dhi and deb/dhi PURLs DHI advisories publish release-lineage PURLs (pkg:apk/dhi/ for the Alpine lineage, pkg:deb/dhi/ for the Debian lineage), but the reverse PURL map only knew pkg:dhi/, so PURL-form queries never resolved to the Docker Hardened Images ecosystem. Add both (apk,dhi) and (deb,dhi) to the reverse map; the dhi namespace disambiguates from Alpine (apk,alpine) and Debian (deb,debian). Forward generation is intentionally left as pkg:dhi/ because package_to_purl only sees the normalized (lineage-stripped) ecosystem name, and DHI records always ship their own PURL. --- osv/purl_helpers.py | 10 ++++++++++ osv/purl_helpers_test.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/osv/purl_helpers.py b/osv/purl_helpers.py index 3c7beb64ca4..d5940e16981 100644 --- a/osv/purl_helpers.py +++ b/osv/purl_helpers.py @@ -117,6 +117,16 @@ # Add Gradle PURL support - Gradle packages should map to Maven ecosystem PURL_ECOSYSTEM_MAP[EcosystemPURL('gradle', None)] = 'Maven' +# Docker Hardened Images publishes release-lineage PURLs -- pkg:apk/dhi/ +# for the Alpine lineage and pkg:deb/dhi/ for the Debian lineage -- so map +# both back to the ecosystem for PURL queries. The forward direction stays +# pkg:dhi/ (see ECOSYSTEM_PURL_DATA): package_to_purl receives the +# ecosystem name after ecosystems.normalize() has stripped the :Alpine:/:Debian: +# lineage, so it cannot choose apk vs deb, and DHI feed records always carry +# their own PURL, so the generation path is not exercised for DHI in practice. +PURL_ECOSYSTEM_MAP[EcosystemPURL('apk', 'dhi')] = 'Docker Hardened Images' +PURL_ECOSYSTEM_MAP[EcosystemPURL('deb', 'dhi')] = 'Docker Hardened Images' + def _url_encode(package_name): """URL encode a PURL `namespace/name` or `name`.""" diff --git a/osv/purl_helpers_test.py b/osv/purl_helpers_test.py index 139ed41704d..cca3c907e49 100644 --- a/osv/purl_helpers_test.py +++ b/osv/purl_helpers_test.py @@ -338,6 +338,21 @@ def test_parse_purl(self): ('Wolfi', 'test-package', '1.2.3'), purl_helpers.parse_purl('pkg:apk/wolfi/test-package@1.2.3')) + # Docker Hardened Images: release-lineage PURLs (apk/dhi for the Alpine + # lineage, deb/dhi for Debian) resolve to the ecosystem. Qualifiers are + # ignored, and versionless (OSV affected) and versioned (VEX product) + # forms both parse. + self.assertEqual( + ('Docker Hardened Images', 'curl', None), + purl_helpers.parse_purl( + 'pkg:apk/dhi/curl?os_distro=alpine&os_name=dhi&os_version=3.23')) + + self.assertEqual(('Docker Hardened Images', 'curl', '8.4.0-r0'), + purl_helpers.parse_purl('pkg:apk/dhi/curl@8.4.0-r0')) + + self.assertEqual(('Docker Hardened Images', 'openssl', '3.0.11-1'), + purl_helpers.parse_purl('pkg:deb/dhi/openssl@3.0.11-1')) + self.assertIsNone(purl_helpers.parse_purl('pkg:bad/ubuntu/pygments')) with self.assertRaises(ValueError): From 3c3bb347bb8d12c9c0863575c02244b30d5b283d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Baumann?= Date: Tue, 28 Jul 2026 09:10:28 -0400 Subject: [PATCH 2/3] refactor(purl): move DHI PURL conversion to Go per review Per review feedback on #5715, purl conversion has migrated to Go, so this implements the Docker Hardened Images mapping in go/purl and removes the earlier Python implementation. DHI advisories publish release-lineage PURLs (pkg:apk/dhi/ for the Alpine lineage, pkg:deb/dhi/ for Debian). Register parsers for both so PURL-form queries resolve to the ecosystem; the dhi namespace disambiguates from Alpine (apk/alpine) and Debian (deb/debian). Generation stays pkg:dhi/. --- go/purl/ecosystems_simple.go | 7 +++++++ go/purl/purl_test.go | 8 ++++++++ osv/purl_helpers.py | 10 ---------- osv/purl_helpers_test.py | 15 --------------- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/go/purl/ecosystems_simple.go b/go/purl/ecosystems_simple.go index c2258a9f4b2..48c19d4535e 100644 --- a/go/purl/ecosystems_simple.go +++ b/go/purl/ecosystems_simple.go @@ -68,6 +68,13 @@ func init() { registerSimple(osvconstants.EcosystemCRAN, "cran", "", nil) registerSimple(osvconstants.EcosystemConanCenter, "conan", "", nil) registerSimple(osvconstants.EcosystemDockerHardenedImages, "dhi", "", nil) + // DHI advisories also publish release-lineage PURLs -- pkg:apk/dhi/ + // for the Alpine lineage and pkg:deb/dhi/ for Debian -- so map both + // back to the ecosystem for PURL queries. Generation stays pkg:dhi/ (the + // registerSimple above); the dhi namespace disambiguates from Alpine + // (apk/alpine) and Debian (deb/debian), so there is no parser collision. + registerParser("apk", "dhi", simpleParser{ecosystem: osvconstants.EcosystemDockerHardenedImages}) + registerParser("deb", "dhi", simpleParser{ecosystem: osvconstants.EcosystemDockerHardenedImages}) registerSimple(osvconstants.EcosystemHackage, "hackage", "", nil) registerSlash(osvconstants.EcosystemHex, "hex") registerSimple(osvconstants.EcosystemJulia, "julia", "", nil) diff --git a/go/purl/purl_test.go b/go/purl/purl_test.go index f7f5d248a91..56aa73d3a51 100644 --- a/go/purl/purl_test.go +++ b/go/purl/purl_test.go @@ -25,6 +25,7 @@ func TestGenerate(t *testing.T) { {"npm", "@babel/core", "pkg:npm/%40babel/core", false}, {"Hex", "acme/foo", "pkg:hex/acme/foo", false}, {"SwiftURL", "github.com/apple/swift-markdown", "pkg:swift/github.com/apple/swift-markdown", false}, + {"Docker Hardened Images", "curl", "pkg:dhi/curl", false}, // Error cases {"UnknownEcosystem", "package", "", true}, } @@ -66,6 +67,13 @@ func TestParse(t *testing.T) { {"pkg:npm/%40babel/core@1.2.3", "npm", "@babel/core", "1.2.3", false}, {"pkg:hex/acme/foo@1.2.3", "Hex", "acme/foo", "1.2.3", false}, {"pkg:swift/github.com/apple/swift-markdown@1.2.3", "SwiftURL", "github.com/apple/swift-markdown", "1.2.3", false}, + // Docker Hardened Images: release-lineage PURLs resolve to the ecosystem. + // Qualifiers are ignored; versionless (OSV affected) and versioned (VEX + // product) forms both parse. pkg:dhi/ also still resolves. + {"pkg:apk/dhi/curl?os_distro=alpine&os_name=dhi&os_version=3.23", "Docker Hardened Images", "curl", "", false}, + {"pkg:apk/dhi/curl@8.4.0-r0", "Docker Hardened Images", "curl", "8.4.0-r0", false}, + {"pkg:deb/dhi/openssl@3.0.11-1", "Docker Hardened Images", "openssl", "3.0.11-1", false}, + {"pkg:dhi/curl@8.4.0-r0", "Docker Hardened Images", "curl", "8.4.0-r0", false}, // Error cases {"invalid-purl", "", "", "", true}, {"pkg:unknown/package@1.0.0", "", "", "", true}, diff --git a/osv/purl_helpers.py b/osv/purl_helpers.py index d5940e16981..3c7beb64ca4 100644 --- a/osv/purl_helpers.py +++ b/osv/purl_helpers.py @@ -117,16 +117,6 @@ # Add Gradle PURL support - Gradle packages should map to Maven ecosystem PURL_ECOSYSTEM_MAP[EcosystemPURL('gradle', None)] = 'Maven' -# Docker Hardened Images publishes release-lineage PURLs -- pkg:apk/dhi/ -# for the Alpine lineage and pkg:deb/dhi/ for the Debian lineage -- so map -# both back to the ecosystem for PURL queries. The forward direction stays -# pkg:dhi/ (see ECOSYSTEM_PURL_DATA): package_to_purl receives the -# ecosystem name after ecosystems.normalize() has stripped the :Alpine:/:Debian: -# lineage, so it cannot choose apk vs deb, and DHI feed records always carry -# their own PURL, so the generation path is not exercised for DHI in practice. -PURL_ECOSYSTEM_MAP[EcosystemPURL('apk', 'dhi')] = 'Docker Hardened Images' -PURL_ECOSYSTEM_MAP[EcosystemPURL('deb', 'dhi')] = 'Docker Hardened Images' - def _url_encode(package_name): """URL encode a PURL `namespace/name` or `name`.""" diff --git a/osv/purl_helpers_test.py b/osv/purl_helpers_test.py index cca3c907e49..139ed41704d 100644 --- a/osv/purl_helpers_test.py +++ b/osv/purl_helpers_test.py @@ -338,21 +338,6 @@ def test_parse_purl(self): ('Wolfi', 'test-package', '1.2.3'), purl_helpers.parse_purl('pkg:apk/wolfi/test-package@1.2.3')) - # Docker Hardened Images: release-lineage PURLs (apk/dhi for the Alpine - # lineage, deb/dhi for Debian) resolve to the ecosystem. Qualifiers are - # ignored, and versionless (OSV affected) and versioned (VEX product) - # forms both parse. - self.assertEqual( - ('Docker Hardened Images', 'curl', None), - purl_helpers.parse_purl( - 'pkg:apk/dhi/curl?os_distro=alpine&os_name=dhi&os_version=3.23')) - - self.assertEqual(('Docker Hardened Images', 'curl', '8.4.0-r0'), - purl_helpers.parse_purl('pkg:apk/dhi/curl@8.4.0-r0')) - - self.assertEqual(('Docker Hardened Images', 'openssl', '3.0.11-1'), - purl_helpers.parse_purl('pkg:deb/dhi/openssl@3.0.11-1')) - self.assertIsNone(purl_helpers.parse_purl('pkg:bad/ubuntu/pygments')) with self.assertRaises(ValueError): From d2fdf30e31ad45fee162b09ad38e3a1140ccda23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Baumann?= Date: Fri, 31 Jul 2026 09:22:37 -0400 Subject: [PATCH 3/3] refactor(purl): group DHI registrations --- go/purl/ecosystems_simple.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/go/purl/ecosystems_simple.go b/go/purl/ecosystems_simple.go index 48c19d4535e..14225de31e3 100644 --- a/go/purl/ecosystems_simple.go +++ b/go/purl/ecosystems_simple.go @@ -67,14 +67,6 @@ func init() { registerSimple(osvconstants.EcosystemBitnami, "bitnami", "", nil) registerSimple(osvconstants.EcosystemCRAN, "cran", "", nil) registerSimple(osvconstants.EcosystemConanCenter, "conan", "", nil) - registerSimple(osvconstants.EcosystemDockerHardenedImages, "dhi", "", nil) - // DHI advisories also publish release-lineage PURLs -- pkg:apk/dhi/ - // for the Alpine lineage and pkg:deb/dhi/ for Debian -- so map both - // back to the ecosystem for PURL queries. Generation stays pkg:dhi/ (the - // registerSimple above); the dhi namespace disambiguates from Alpine - // (apk/alpine) and Debian (deb/debian), so there is no parser collision. - registerParser("apk", "dhi", simpleParser{ecosystem: osvconstants.EcosystemDockerHardenedImages}) - registerParser("deb", "dhi", simpleParser{ecosystem: osvconstants.EcosystemDockerHardenedImages}) registerSimple(osvconstants.EcosystemHackage, "hackage", "", nil) registerSlash(osvconstants.EcosystemHex, "hex") registerSimple(osvconstants.EcosystemJulia, "julia", "", nil) @@ -110,4 +102,14 @@ func init() { registerSimple(osvconstants.EcosystemAlpaquita, "apk", "alpaquita", sourceArchQualifiers) registerSimple(osvconstants.EcosystemAlpine, "apk", "alpine", sourceArchQualifiers) registerSimple(osvconstants.EcosystemBellSoftHardenedContainers, "apk", "bellsoft-hardened-containers", sourceArchQualifiers) + + // Docker Hardened Images + registerSimple(osvconstants.EcosystemDockerHardenedImages, "dhi", "", nil) + // DHI advisories also publish release-lineage PURLs -- pkg:apk/dhi/ + // for the Alpine lineage and pkg:deb/dhi/ for Debian -- so map both + // back to the ecosystem for PURL queries. Generation stays pkg:dhi/ (the + // registerSimple above); the dhi namespace disambiguates from Alpine + // (apk/alpine) and Debian (deb/debian), so there is no parser collision. + registerParser("apk", "dhi", simpleParser{ecosystem: osvconstants.EcosystemDockerHardenedImages}) + registerParser("deb", "dhi", simpleParser{ecosystem: osvconstants.EcosystemDockerHardenedImages}) }