From 68ab2280ddc467e257d1bfa8300cbc24580f05f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Baumann?= Date: Mon, 27 Jul 2026 11:56:59 -0400 Subject: [PATCH 1/2] fix(ecosystems): use lineage-aware version comparison for Docker Hardened Images Docker Hardened Images was registered as a SemverEcosystem, but DHI OS packages are repackaged Alpine (apk) and Debian (dpkg) packages that keep their upstream version syntax (e.g. "8.4.0-r0", "7.88.1-10+deb13u2"). Semver comparison mis-orders these -- it compares the apk "-rN" release lexically (so r10 < r2) and does not understand Debian epochs/revisions. Add a DHIEcosystem helper that delegates version handling to the lineage ecosystem named in the ":" suffix (Alpine/APK for "...:Alpine:", Debian for "...:Debian:"), mirroring the TuxCareEcosystem wrap-an-inner pattern. --- osv/ecosystems/_ecosystems.py | 7 ++++- osv/ecosystems/_ecosystems_test.py | 36 +++++++++++++++++++++ osv/ecosystems/dhi.py | 50 ++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 osv/ecosystems/dhi.py diff --git a/osv/ecosystems/_ecosystems.py b/osv/ecosystems/_ecosystems.py index d0f7c45e34c..5c228b43d58 100644 --- a/osv/ecosystems/_ecosystems.py +++ b/osv/ecosystems/_ecosystems.py @@ -20,6 +20,7 @@ from .bioconductor import Bioconductor from .cran import CRAN from .debian import Debian +from .dhi import DHIEcosystem from .echo import Echo from .haskell import Hackage, GHC from .hex import Hex @@ -51,7 +52,7 @@ 'CRAN': CRAN, 'crates.io': SemverEcosystem, 'Debian': Debian, - 'Docker Hardened Images': SemverEcosystem, + 'Docker Hardened Images': DHIEcosystem, 'Echo': Echo, 'GHC': GHC, 'Go': SemverEcosystem, @@ -148,6 +149,10 @@ def get(name: str) -> OrderedEcosystem | EnumerableEcosystem | None: return None if ecosys is TuxCareEcosystem: return TuxCareEcosystem(suffix, inner=get(suffix)) + if ecosys is DHIEcosystem: + # The suffix is the ":" pair (e.g. "Alpine:3.23"), itself + # a resolvable ecosystem, so version handling delegates to that lineage. + return DHIEcosystem(suffix, inner=get(suffix)) return ecosys(suffix) diff --git a/osv/ecosystems/_ecosystems_test.py b/osv/ecosystems/_ecosystems_test.py index 8c012eef4b5..95983f268d5 100644 --- a/osv/ecosystems/_ecosystems_test.py +++ b/osv/ecosystems/_ecosystems_test.py @@ -178,6 +178,42 @@ def test_root_ecosystem(self): root_debian.sort_key('1.0.0.root.io.1'), root_debian.sort_key('1.0.0.root.io.2')) + def test_dhi_ecosystem(self): + """Docker Hardened Images delegates to its lineage ecosystem.""" + # Known when the base name matches; the lineage:release suffix passes + # through to the delegated ecosystem. + self.assertTrue(ecosystems.is_known('Docker Hardened Images:Alpine:3.23')) + self.assertTrue(ecosystems.is_known('Docker Hardened Images:Debian:trixie')) + + # DHI is not semver: it delegates to apk/dpkg version handling. + self.assertFalse(ecosystems.is_semver('Docker Hardened Images:Alpine:3.23')) + + # Alpine lineage sorts with apk semantics, matching the plain Alpine parser. + dhi_alpine = ecosystems.get('Docker Hardened Images:Alpine:3.23') + self.assertIsNotNone(dhi_alpine) + alpine = ecosystems.get('Alpine:3.23') + self.assertEqual( + dhi_alpine.sort_key('8.4.0-r0'), alpine.sort_key('8.4.0-r0')) + self.assertLess( + dhi_alpine.sort_key('8.4.0-r0'), dhi_alpine.sort_key('8.5.0-r0')) + # apk orders the -rN package release numerically (r2 < r10); a semver + # comparison would invert this by comparing "r10" and "r2" lexically. + self.assertLess( + dhi_alpine.sort_key('1.2.3-r2'), dhi_alpine.sort_key('1.2.3-r10')) + + # Debian lineage sorts with dpkg semantics, matching the plain Debian + # parser (epoch/revision aware). + dhi_debian = ecosystems.get('Docker Hardened Images:Debian:trixie') + self.assertIsNotNone(dhi_debian) + debian = ecosystems.get('Debian:trixie') + self.assertEqual( + dhi_debian.sort_key('7.88.1-10+deb13u2'), + debian.sort_key('7.88.1-10+deb13u2')) + + # The resolved lineage suffix is exposed on the inner ecosystem. + self.assertEqual(dhi_alpine.inner.suffix, '3.23') + self.assertEqual(dhi_debian.inner.suffix, 'trixie') + def test_tuxcare_ecosystem(self): """Test TuxCare ecosystem delegates to inner ecosystem parsers.""" # TuxCare: should be recognized when the inner ecosystem is. diff --git a/osv/ecosystems/dhi.py b/osv/ecosystems/dhi.py new file mode 100644 index 00000000000..16604cc4c38 --- /dev/null +++ b/osv/ecosystems/dhi.py @@ -0,0 +1,50 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Docker Hardened Images ecosystem helper.""" + +from typing import Any + +from .ecosystems_base import OrderedEcosystem + + +class DHIEcosystem(OrderedEcosystem): + """Docker Hardened Images advisories use the form + "Docker Hardened Images::" (e.g. + "Docker Hardened Images:Alpine:3.23", "Docker Hardened Images:Debian:trixie"). + + DHI OS packages are repackaged Alpine (apk) and Debian (dpkg) packages that + keep their upstream version syntax (e.g. "8.4.0-r0", "7.88.1-10+deb13u2"), so + version handling must delegate to the lineage ecosystem rather than to semver. + + The caller (``_ecosystems.get``) resolves the inner ecosystem from the + ":" suffix (which is itself a resolvable ecosystem such as + "Alpine:3.23" or "Debian:trixie") and passes it in. A bare + "Docker Hardened Images" or an unknown lineage yields no inner; the sort and + coarse methods then raise ValueError, which OrderedEcosystem.sort_key surfaces + as an invalid version. + """ + + def __init__(self, suffix: str | None, inner: OrderedEcosystem | None = None): + super().__init__(suffix) + self.inner = inner + + def _sort_key(self, version: str) -> Any: + if self.inner is None: + raise ValueError('Docker Hardened Images has no lineage ecosystem') + return self.inner._sort_key(version) # pylint: disable=protected-access + + def coarse_version(self, version: str) -> str: + if self.inner is None: + raise ValueError('Docker Hardened Images has no lineage ecosystem') + return self.inner.coarse_version(version) From 5425e89ad5a3019c5384d47fc3e4e3c8ea1d283a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Baumann?= Date: Tue, 28 Jul 2026 09:08:07 -0400 Subject: [PATCH 2/2] refactor(ecosystem): move DHI version comparison to Go per review Per review feedback on #5716, the ecosystem logic has migrated to Go, so this implements Docker Hardened Images version comparison in go/osv/ecosystem and removes the earlier Python implementation. Register DHI with a lineage-aware factory that delegates version handling to the ecosystem named in the :Alpine:/:Debian: suffix (apk / dpkg), mirroring tuxcareFactory. IsSemver is false: DHI uses ECOSYSTEM ranges and apk/dpkg version ordering, not SemVer. --- go/osv/ecosystem/dhi.go | 83 ++++++++++++++ go/osv/ecosystem/dhi_test.go | 175 +++++++++++++++++++++++++++++ go/osv/ecosystem/ecosystem.go | 2 +- osv/ecosystems/_ecosystems.py | 7 +- osv/ecosystems/_ecosystems_test.py | 36 ------ osv/ecosystems/dhi.py | 50 --------- 6 files changed, 260 insertions(+), 93 deletions(-) create mode 100644 go/osv/ecosystem/dhi.go create mode 100644 go/osv/ecosystem/dhi_test.go delete mode 100644 osv/ecosystems/dhi.py diff --git a/go/osv/ecosystem/dhi.go b/go/osv/ecosystem/dhi.go new file mode 100644 index 00000000000..7bee4eb76e5 --- /dev/null +++ b/go/osv/ecosystem/dhi.go @@ -0,0 +1,83 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ecosystem + +import ( + "fmt" + "strings" + + "github.com/ossf/osv-schema/bindings/go/osvconstants" +) + +// dhiEcosystem represents "Docker Hardened Images::" +// advisories (e.g. "Docker Hardened Images:Alpine:3.23", +// "Docker Hardened Images:Debian:trixie"). DHI OS packages are repackaged +// Alpine (apk) and Debian (dpkg) packages that keep their upstream version +// syntax (e.g. "8.4.0-r0", "7.88.1-10+deb13u2"), so version handling delegates +// to the lineage ecosystem named in the suffix, resolved lazily via the +// Provider to avoid a package-init cycle. +type dhiEcosystem struct { + p *Provider + suffix string +} + +var _ Ecosystem = dhiEcosystem{} + +func dhiFactory(p *Provider, suffix string) Ecosystem { + lineage, _, _ := strings.Cut(suffix, ":") + if suffix == "" || lineage == string(osvconstants.EcosystemDockerHardenedImages) { + // Bare "Docker Hardened Images" or a self-referential suffix is malformed. + return nil + } + + return dhiEcosystem{p: p, suffix: suffix} +} + +// resolve looks up the lineage ecosystem named by the suffix (e.g. "Alpine:3.23" +// or "Debian:trixie") on demand. Inner is unwrapped to avoid double-wrapping the +// resulting Version (which would fail to compare against a singly-wrapped +// Version from the same inner ecosystem). +func (e dhiEcosystem) resolve() (Ecosystem, error) { + inner, ok := e.p.Get(e.suffix) + if !ok { + return nil, fmt.Errorf("unknown Docker Hardened Images lineage ecosystem %q", e.suffix) + } + + return unwrap(inner), nil +} + +func (e dhiEcosystem) Parse(version string) (Version, error) { + inner, err := e.resolve() + if err != nil { + return nil, err + } + + return inner.Parse(version) +} + +func (e dhiEcosystem) Coarse(version string) (string, error) { + inner, err := e.resolve() + if err != nil { + return "", err + } + + return inner.Coarse(version) +} + +// IsSemver always returns false: DHI advisories use ECOSYSTEM ranges, and DHI +// versions follow apk/dpkg ordering rather than SemVer. +func (e dhiEcosystem) IsSemver() bool { + return false +} diff --git a/go/osv/ecosystem/dhi_test.go b/go/osv/ecosystem/dhi_test.go new file mode 100644 index 00000000000..ba8bbc45cb5 --- /dev/null +++ b/go/osv/ecosystem/dhi_test.go @@ -0,0 +1,175 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ecosystem + +import ( + "testing" +) + +func TestDHIEcosystem_DelegatesToInner(t *testing.T) { + p := NewProvider(nil) + + cases := []struct { + name string + ecosystem string + }{ + {"Alpine", "Docker Hardened Images:Alpine:3.23"}, + {"Debian", "Docker Hardened Images:Debian:trixie"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if _, ok := p.Get(tc.ecosystem); !ok { + t.Fatalf("Provider.Get(%q) = ok=false, want true", tc.ecosystem) + } + }) + } +} + +func TestDHIEcosystem_Malformed(t *testing.T) { + p := NewProvider(nil) + cases := []string{ + // Bare "Docker Hardened Images" with no lineage suffix. + "Docker Hardened Images", + "Docker Hardened Images:", + // Self-referential suffix. + "Docker Hardened Images:Docker Hardened Images", + } + for _, ecosystem := range cases { + t.Run(ecosystem, func(t *testing.T) { + if e, ok := p.Get(ecosystem); ok { + t.Errorf("Provider.Get(%q) = (%v, true), want (_, false)", ecosystem, e) + } + }) + } +} + +// IsSemver is false: DHI uses ECOSYSTEM ranges, not SEMVER. +func TestDHIEcosystem_IsSemverFalse(t *testing.T) { + p := NewProvider(nil) + e, ok := p.Get("Docker Hardened Images:Alpine:3.23") + if !ok { + t.Fatalf("Docker Hardened Images:Alpine:3.23 not found") + } + if e.IsSemver() { + t.Errorf("IsSemver() = true, want false") + } +} + +// The Alpine lineage sorts with apk semantics, not SemVer. In particular the +// "-rN" package release is ordered numerically (r2 < r10); a SemVer comparison +// would invert this by comparing the prerelease identifiers "r10" and "r2" +// lexically. Results also match the plain Alpine parser. +func TestDHIEcosystem_AlpineLineage(t *testing.T) { + p := NewProvider(nil) + + dhi, ok := p.Get("Docker Hardened Images:Alpine:3.23") + if !ok { + t.Fatalf("Docker Hardened Images:Alpine:3.23 not found") + } + alpine, ok := p.Get("Alpine:3.23") + if !ok { + t.Fatalf("Alpine:3.23 not found") + } + + v1, err := dhi.Parse("8.4.0-r0") + if err != nil { + t.Fatalf("dhi.Parse(8.4.0-r0): %v", err) + } + v2, err := dhi.Parse("8.5.0-r0") + if err != nil { + t.Fatalf("dhi.Parse(8.5.0-r0): %v", err) + } + if c, err := v1.Compare(v2); err != nil || c != -1 { + t.Errorf("Compare(8.4.0-r0, 8.5.0-r0) = (%d, %v), want (-1, nil)", c, err) + } + + // apk orders the -rN release numerically, unlike SemVer prerelease ordering. + r2, err := dhi.Parse("1.2.3-r2") + if err != nil { + t.Fatalf("dhi.Parse(1.2.3-r2): %v", err) + } + r10, err := dhi.Parse("1.2.3-r10") + if err != nil { + t.Fatalf("dhi.Parse(1.2.3-r10): %v", err) + } + if c, err := r2.Compare(r10); err != nil || c != -1 { + t.Errorf("Compare(1.2.3-r2, 1.2.3-r10) = (%d, %v), want (-1, nil)", c, err) + } + + // Delegation matches the plain Alpine parser. + dv, err := dhi.Parse("8.4.0-r0") + if err != nil { + t.Fatalf("dhi.Parse: %v", err) + } + av, err := alpine.Parse("8.4.0-r0") + if err != nil { + t.Fatalf("alpine.Parse: %v", err) + } + if c, err := dv.Compare(av); err != nil || c != 0 { + t.Errorf("Compare(dhi, alpine) = (%d, %v), want (0, nil)", c, err) + } +} + +// The Debian lineage sorts with dpkg semantics (epoch/revision aware) and +// matches the plain Debian parser. +func TestDHIEcosystem_DebianLineage(t *testing.T) { + p := NewProvider(nil) + + dhi, ok := p.Get("Docker Hardened Images:Debian:trixie") + if !ok { + t.Fatalf("Docker Hardened Images:Debian:trixie not found") + } + debian, ok := p.Get("Debian:trixie") + if !ok { + t.Fatalf("Debian:trixie not found") + } + + v1, err := dhi.Parse("7.88.1-10+deb13u1") + if err != nil { + t.Fatalf("dhi.Parse(7.88.1-10+deb13u1): %v", err) + } + v2, err := dhi.Parse("7.88.1-10+deb13u2") + if err != nil { + t.Fatalf("dhi.Parse(7.88.1-10+deb13u2): %v", err) + } + if c, err := v1.Compare(v2); err != nil || c != -1 { + t.Errorf("Compare(deb13u1, deb13u2) = (%d, %v), want (-1, nil)", c, err) + } + + dv, err := dhi.Parse("7.88.1-10+deb13u2") + if err != nil { + t.Fatalf("dhi.Parse: %v", err) + } + bv, err := debian.Parse("7.88.1-10+deb13u2") + if err != nil { + t.Fatalf("debian.Parse: %v", err) + } + if c, err := dv.Compare(bv); err != nil || c != 0 { + t.Errorf("Compare(dhi, debian) = (%d, %v), want (0, nil)", c, err) + } +} + +// An unknown lineage is accepted by Get (resolved lazily, mirroring TuxCare); +// the failure surfaces at Parse time. +func TestDHIEcosystem_UnknownLineageFailsAtParse(t *testing.T) { + p := NewProvider(nil) + e, ok := p.Get("Docker Hardened Images:NotARealEcosystem") + if !ok { + t.Fatalf("Provider.Get(Docker Hardened Images:NotARealEcosystem) = ok=false, want true") + } + if _, err := e.Parse("1.0.0"); err == nil { + t.Errorf("Parse on unknown lineage returned nil error, want non-nil") + } +} diff --git a/go/osv/ecosystem/ecosystem.go b/go/osv/ecosystem/ecosystem.go index ab33883eb7a..9ae50baf1a5 100644 --- a/go/osv/ecosystem/ecosystem.go +++ b/go/osv/ecosystem/ecosystem.go @@ -51,7 +51,7 @@ var ecosystems = map[osvconstants.Ecosystem]ecosystemFactory{ osvconstants.EcosystemCRAN: func(p *Provider, _ string) Ecosystem { return cranEcosystem{p: p} }, osvconstants.EcosystemCratesIO: statelessFactory[semverEcosystem], osvconstants.EcosystemDebian: debianFactory, - osvconstants.EcosystemDockerHardenedImages: statelessFactory[semverEcosystem], + osvconstants.EcosystemDockerHardenedImages: dhiFactory, osvconstants.EcosystemEcho: echoFactory, osvconstants.EcosystemGHC: func(p *Provider, _ string) Ecosystem { return ghcEcosystem{p: p} }, osvconstants.EcosystemGo: statelessFactory[semverEcosystem], diff --git a/osv/ecosystems/_ecosystems.py b/osv/ecosystems/_ecosystems.py index 5c228b43d58..d0f7c45e34c 100644 --- a/osv/ecosystems/_ecosystems.py +++ b/osv/ecosystems/_ecosystems.py @@ -20,7 +20,6 @@ from .bioconductor import Bioconductor from .cran import CRAN from .debian import Debian -from .dhi import DHIEcosystem from .echo import Echo from .haskell import Hackage, GHC from .hex import Hex @@ -52,7 +51,7 @@ 'CRAN': CRAN, 'crates.io': SemverEcosystem, 'Debian': Debian, - 'Docker Hardened Images': DHIEcosystem, + 'Docker Hardened Images': SemverEcosystem, 'Echo': Echo, 'GHC': GHC, 'Go': SemverEcosystem, @@ -149,10 +148,6 @@ def get(name: str) -> OrderedEcosystem | EnumerableEcosystem | None: return None if ecosys is TuxCareEcosystem: return TuxCareEcosystem(suffix, inner=get(suffix)) - if ecosys is DHIEcosystem: - # The suffix is the ":" pair (e.g. "Alpine:3.23"), itself - # a resolvable ecosystem, so version handling delegates to that lineage. - return DHIEcosystem(suffix, inner=get(suffix)) return ecosys(suffix) diff --git a/osv/ecosystems/_ecosystems_test.py b/osv/ecosystems/_ecosystems_test.py index 95983f268d5..8c012eef4b5 100644 --- a/osv/ecosystems/_ecosystems_test.py +++ b/osv/ecosystems/_ecosystems_test.py @@ -178,42 +178,6 @@ def test_root_ecosystem(self): root_debian.sort_key('1.0.0.root.io.1'), root_debian.sort_key('1.0.0.root.io.2')) - def test_dhi_ecosystem(self): - """Docker Hardened Images delegates to its lineage ecosystem.""" - # Known when the base name matches; the lineage:release suffix passes - # through to the delegated ecosystem. - self.assertTrue(ecosystems.is_known('Docker Hardened Images:Alpine:3.23')) - self.assertTrue(ecosystems.is_known('Docker Hardened Images:Debian:trixie')) - - # DHI is not semver: it delegates to apk/dpkg version handling. - self.assertFalse(ecosystems.is_semver('Docker Hardened Images:Alpine:3.23')) - - # Alpine lineage sorts with apk semantics, matching the plain Alpine parser. - dhi_alpine = ecosystems.get('Docker Hardened Images:Alpine:3.23') - self.assertIsNotNone(dhi_alpine) - alpine = ecosystems.get('Alpine:3.23') - self.assertEqual( - dhi_alpine.sort_key('8.4.0-r0'), alpine.sort_key('8.4.0-r0')) - self.assertLess( - dhi_alpine.sort_key('8.4.0-r0'), dhi_alpine.sort_key('8.5.0-r0')) - # apk orders the -rN package release numerically (r2 < r10); a semver - # comparison would invert this by comparing "r10" and "r2" lexically. - self.assertLess( - dhi_alpine.sort_key('1.2.3-r2'), dhi_alpine.sort_key('1.2.3-r10')) - - # Debian lineage sorts with dpkg semantics, matching the plain Debian - # parser (epoch/revision aware). - dhi_debian = ecosystems.get('Docker Hardened Images:Debian:trixie') - self.assertIsNotNone(dhi_debian) - debian = ecosystems.get('Debian:trixie') - self.assertEqual( - dhi_debian.sort_key('7.88.1-10+deb13u2'), - debian.sort_key('7.88.1-10+deb13u2')) - - # The resolved lineage suffix is exposed on the inner ecosystem. - self.assertEqual(dhi_alpine.inner.suffix, '3.23') - self.assertEqual(dhi_debian.inner.suffix, 'trixie') - def test_tuxcare_ecosystem(self): """Test TuxCare ecosystem delegates to inner ecosystem parsers.""" # TuxCare: should be recognized when the inner ecosystem is. diff --git a/osv/ecosystems/dhi.py b/osv/ecosystems/dhi.py deleted file mode 100644 index 16604cc4c38..00000000000 --- a/osv/ecosystems/dhi.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Docker Hardened Images ecosystem helper.""" - -from typing import Any - -from .ecosystems_base import OrderedEcosystem - - -class DHIEcosystem(OrderedEcosystem): - """Docker Hardened Images advisories use the form - "Docker Hardened Images::" (e.g. - "Docker Hardened Images:Alpine:3.23", "Docker Hardened Images:Debian:trixie"). - - DHI OS packages are repackaged Alpine (apk) and Debian (dpkg) packages that - keep their upstream version syntax (e.g. "8.4.0-r0", "7.88.1-10+deb13u2"), so - version handling must delegate to the lineage ecosystem rather than to semver. - - The caller (``_ecosystems.get``) resolves the inner ecosystem from the - ":" suffix (which is itself a resolvable ecosystem such as - "Alpine:3.23" or "Debian:trixie") and passes it in. A bare - "Docker Hardened Images" or an unknown lineage yields no inner; the sort and - coarse methods then raise ValueError, which OrderedEcosystem.sort_key surfaces - as an invalid version. - """ - - def __init__(self, suffix: str | None, inner: OrderedEcosystem | None = None): - super().__init__(suffix) - self.inner = inner - - def _sort_key(self, version: str) -> Any: - if self.inner is None: - raise ValueError('Docker Hardened Images has no lineage ecosystem') - return self.inner._sort_key(version) # pylint: disable=protected-access - - def coarse_version(self, version: str) -> str: - if self.inner is None: - raise ValueError('Docker Hardened Images has no lineage ecosystem') - return self.inner.coarse_version(version)