diff --git a/server/go.mod b/server/go.mod index d1ccff3..5cfd081 100644 --- a/server/go.mod +++ b/server/go.mod @@ -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 ) diff --git a/server/go.sum b/server/go.sum index b866001..103fb7b 100644 --- a/server/go.sum +++ b/server/go.sum @@ -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= diff --git a/server/internal/vulndb/version.go b/server/internal/vulndb/version.go new file mode 100644 index 0000000..2066304 --- /dev/null +++ b/server/internal/vulndb/version.go @@ -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) + } +}