feat: version comparators for distro package ordering

This commit is contained in:
2026-08-06 11:54:13 +01:00
parent 3afc4ab012
commit bd24b03cac
3 changed files with 84 additions and 0 deletions
+3
View File
@@ -16,6 +16,9 @@ require (
require (
github.com/hyperboloide/lk v0.0.0-20251220053519-b291812e3216 // indirect
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f // indirect
github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 // indirect
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
)
+8
View File
@@ -52,6 +52,12 @@ github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6K
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg=
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8=
github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 h1:dWzdsqjh1p2gNtRKqNwuBvKqMNwnLOPLzVZT1n6DK7s=
github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23/go.mod h1:lUaIXCWzf7BRKTY5iEcrYy1TfgbYLYVIS/B2vPkJzOc=
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g=
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -81,6 +87,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -158,6 +165,7 @@ google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6h
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+73
View File
@@ -0,0 +1,73 @@
// Package vulndb matches installed packages against distribution security
// advisories.
//
// Version comparison is bought rather than written. Distribution version
// ordering is subtle in ways that are invisible until they are wrong: dpkg has
// epochs and sorts "~" before the empty string, rpmvercmp has its own segment
// rules and treats "~" and "^" differently again, and any ordering that falls
// back on string comparison puts 1.10 before 1.9. Every one of those mistakes
// produces a false negative — a vulnerable host reported clean — which is the
// failure nobody notices.
package vulndb
import (
"errors"
"fmt"
apk "github.com/knqyf263/go-apk-version"
deb "github.com/knqyf263/go-deb-version"
rpm "github.com/knqyf263/go-rpm-version"
)
// ErrUnsupportedFamily means we hold no comparator for this distribution, and
// therefore cannot answer whether it is vulnerable. Callers must surface this
// as "unsupported" and must never treat it as "not vulnerable".
var ErrUnsupportedFamily = errors.New("unsupported OS family")
// LessThan reports whether version a sorts before version b under the ordering
// rules of the given OS family.
//
// An unparseable or empty version is an error, never a quiet false. False here
// means "not vulnerable", which is the dangerous direction to guess in.
func LessThan(family, a, b string) (bool, error) {
switch family {
case "debian", "ubuntu":
if a == "" || b == "" {
return false, fmt.Errorf("empty deb version (a=%q b=%q)", a, b)
}
va, err := deb.NewVersion(a)
if err != nil {
return false, fmt.Errorf("parse deb version %q: %w", a, err)
}
vb, err := deb.NewVersion(b)
if err != nil {
return false, fmt.Errorf("parse deb version %q: %w", b, err)
}
return va.LessThan(vb), nil
case "redhat", "centos", "rocky", "alma", "amazon", "oracle", "suse", "opensuse", "sles":
// go-rpm-version does not error; rpmvercmp is defined over arbitrary
// strings. Guard empties so a missing version cannot read as equal.
if a == "" || b == "" {
return false, fmt.Errorf("empty rpm version (a=%q b=%q)", a, b)
}
return rpm.NewVersion(a).LessThan(rpm.NewVersion(b)), nil
case "alpine":
if a == "" || b == "" {
return false, fmt.Errorf("empty apk version (a=%q b=%q)", a, b)
}
va, err := apk.NewVersion(a)
if err != nil {
return false, fmt.Errorf("parse apk version %q: %w", a, err)
}
vb, err := apk.NewVersion(b)
if err != nil {
return false, fmt.Errorf("parse apk version %q: %w", b, err)
}
return va.LessThan(vb), nil
default:
return false, fmt.Errorf("%w: %s", ErrUnsupportedFamily, family)
}
}