mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
Add APK version constraint parsing (#455)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
This commit is contained in:
parent
dc1f682e4b
commit
637a061532
11 changed files with 197 additions and 0 deletions
1
go.mod
1
go.mod
|
@ -21,6 +21,7 @@ require (
|
|||
github.com/hashicorp/go-getter v1.4.1
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f
|
||||
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
|
|
2
go.sum
2
go.sum
|
@ -524,6 +524,8 @@ github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
|
|||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
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-20190517075300-09fca494f03d h1:X4cedH4Kn3JPupAwwWuo4AzYp16P0OyLO9d7OnMZc/c=
|
||||
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
|
|
@ -57,9 +57,11 @@ func TestSecDBOnlyMatch(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("failed to create a new distro: %+v", err)
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: "libvncserver",
|
||||
Version: "0.9.9",
|
||||
Type: syftPkg.ApkPkg,
|
||||
CPEs: []syftPkg.CPE{
|
||||
must(syftPkg.NewCPE("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*")),
|
||||
},
|
||||
|
@ -140,9 +142,11 @@ func TestBothSecdbAndNvdMatches(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("failed to create a new distro: %+v", err)
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: "libvncserver",
|
||||
Version: "0.9.9",
|
||||
Type: syftPkg.ApkPkg,
|
||||
CPEs: []syftPkg.CPE{
|
||||
must(syftPkg.NewCPE("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*")),
|
||||
},
|
||||
|
@ -227,6 +231,7 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
|
|||
p := pkg.Package{
|
||||
Name: "libvncserver",
|
||||
Version: "0.9.9",
|
||||
Type: syftPkg.ApkPkg,
|
||||
CPEs: []syftPkg.CPE{
|
||||
// Note: the product name is NOT the same as the package name
|
||||
must(syftPkg.NewCPE("cpe:2.3:a:*:libvncumbrellaproject:0.9.9:*:*:*:*:*:*:*")),
|
||||
|
@ -299,6 +304,7 @@ func TestNvdOnlyMatches(t *testing.T) {
|
|||
p := pkg.Package{
|
||||
Name: "libvncserver",
|
||||
Version: "0.9.9",
|
||||
Type: syftPkg.ApkPkg,
|
||||
CPEs: []syftPkg.CPE{
|
||||
must(syftPkg.NewCPE("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*")),
|
||||
},
|
||||
|
@ -375,6 +381,7 @@ func TestNvdMatchesWithSecDBFix(t *testing.T) {
|
|||
p := pkg.Package{
|
||||
Name: "libvncserver",
|
||||
Version: "0.9.11",
|
||||
Type: syftPkg.ApkPkg,
|
||||
CPEs: []syftPkg.CPE{
|
||||
must(syftPkg.NewCPE("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*")),
|
||||
},
|
||||
|
@ -427,6 +434,7 @@ func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) {
|
|||
p := pkg.Package{
|
||||
Name: "libvncserver",
|
||||
Version: "0.9.11",
|
||||
Type: syftPkg.ApkPkg,
|
||||
CPEs: []syftPkg.CPE{
|
||||
must(syftPkg.NewCPE("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*")),
|
||||
},
|
||||
|
@ -469,6 +477,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) {
|
|||
p := pkg.Package{
|
||||
Name: "musl-utils",
|
||||
Version: "1.3.2-r0",
|
||||
Type: syftPkg.ApkPkg,
|
||||
Metadata: pkg.ApkMetadata{OriginPackage: "musl"},
|
||||
}
|
||||
|
||||
|
@ -538,6 +547,7 @@ func TestNVDMatchBySourceIndirection(t *testing.T) {
|
|||
p := pkg.Package{
|
||||
Name: "musl-utils",
|
||||
Version: "1.3.2-r0",
|
||||
Type: syftPkg.ApkPkg,
|
||||
CPEs: []syftPkg.CPE{
|
||||
must(syftPkg.NewCPE("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*")),
|
||||
must(syftPkg.NewCPE("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*")),
|
||||
|
|
73
grype/version/apk_constraint.go
Normal file
73
grype/version/apk_constraint.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
//nolint:dupl
|
||||
package version
|
||||
|
||||
import "fmt"
|
||||
|
||||
type apkConstraint struct {
|
||||
raw string
|
||||
expression constraintExpression
|
||||
}
|
||||
|
||||
func newApkConstraint(raw string) (apkConstraint, error) {
|
||||
if raw == "" {
|
||||
// empy constraints are always satisfied
|
||||
return apkConstraint{}, nil
|
||||
}
|
||||
|
||||
constraints, err := newConstraintExpression(raw, newApkComparator)
|
||||
if err != nil {
|
||||
return apkConstraint{}, fmt.Errorf("unable to parse apk constraint phrase: %w", err)
|
||||
}
|
||||
|
||||
return apkConstraint{
|
||||
raw: raw,
|
||||
expression: constraints,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newApkComparator(unit constraintUnit) (Comparator, error) {
|
||||
ver, err := newApkVersion(unit.version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse constraint version (%s): %w", unit.version, err)
|
||||
}
|
||||
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
func (c apkConstraint) supported(format Format) bool {
|
||||
return format == ApkFormat
|
||||
}
|
||||
|
||||
func (c apkConstraint) Satisfied(version *Version) (bool, error) {
|
||||
if c.raw == "" && version != nil {
|
||||
// empty constraints are always satisfied
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if version == nil {
|
||||
if c.raw != "" {
|
||||
// a non-empty constraint with no version given should always fail
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !c.supported(version.Format) {
|
||||
return false, fmt.Errorf("(apk) unsupported format: %s", version.Format)
|
||||
}
|
||||
|
||||
if version.rich.apkVer == nil {
|
||||
return false, fmt.Errorf("no rich apk version given: %+v", version)
|
||||
}
|
||||
|
||||
return c.expression.satisfied(version)
|
||||
}
|
||||
|
||||
func (c apkConstraint) String() string {
|
||||
if c.raw == "" {
|
||||
return "none (apk)"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s (apk)", c.raw)
|
||||
}
|
60
grype/version/apk_constraint_test.go
Normal file
60
grype/version/apk_constraint_test.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVersionApk(t *testing.T) {
|
||||
tests := []testCase{
|
||||
{version: "2.3.1", constraint: "", satisfied: true},
|
||||
// compound conditions
|
||||
{version: "2.3.1", constraint: "> 1.0.0, < 2.0.0", satisfied: false},
|
||||
{version: "1.3.1", constraint: "> 1.0.0, < 2.0.0", satisfied: true},
|
||||
{version: "2.0.0", constraint: "> 1.0.0, <= 2.0.0", satisfied: true},
|
||||
{version: "2.0.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false},
|
||||
{version: "1.0.0", constraint: ">= 1.0.0, < 2.0.0", satisfied: true},
|
||||
{version: "1.0.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false},
|
||||
{version: "0.9.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false},
|
||||
{version: "1.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true},
|
||||
{version: "0.2.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true},
|
||||
{version: "0.0.1", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false},
|
||||
{version: "0.6.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false},
|
||||
{version: "2.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false},
|
||||
// fixed-in scenarios
|
||||
{version: "2.3.1", constraint: "< 2.0.0", satisfied: false},
|
||||
{version: "2.3.1", constraint: "< 2.0", satisfied: false},
|
||||
{version: "2.3.1", constraint: "< 2", satisfied: false},
|
||||
{version: "2.3.1", constraint: "< 2.3", satisfied: false},
|
||||
{version: "2.3.1", constraint: "< 2.3.1", satisfied: false},
|
||||
{version: "2.3.1", constraint: "< 2.3.2", satisfied: true},
|
||||
{version: "2.3.1", constraint: "< 2.4", satisfied: true},
|
||||
{version: "2.3.1", constraint: "< 3", satisfied: true},
|
||||
{version: "2.3.1", constraint: "< 3.0", satisfied: true},
|
||||
{version: "2.3.1", constraint: "< 3.0.0", satisfied: true},
|
||||
// alpine specific scenarios
|
||||
// https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver
|
||||
{version: "1.5.1-r1", constraint: "< 1.5.1", satisfied: false},
|
||||
{version: "1.5.1-r1", constraint: "> 1.5.1", satisfied: true},
|
||||
{version: "9.3.2-r4", constraint: "< 9.3.4-r2", satisfied: true},
|
||||
{version: "9.3.4-r2", constraint: "> 9.3.4", satisfied: true},
|
||||
{version: "4.2.52_p2-r1", constraint: "< 4.2.52_p4-r2", satisfied: true},
|
||||
{version: "4.2.52_p2-r1", constraint: "> 4.2.52_p4-r2", satisfied: false},
|
||||
{version: "0.1.0_alpha", constraint: "< 0.1.3_alpha", satisfied: true},
|
||||
{version: "0.1.0_alpha2", constraint: "> 0.1.0_alpha", satisfied: true},
|
||||
{version: "1.1", constraint: "> 1.1_alpha1", satisfied: true},
|
||||
{version: "1.1", constraint: "< 1.1_alpha1", satisfied: false},
|
||||
{version: "2.3.0b-r1", constraint: "< 2.3.0b-r2", satisfied: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
constraint, err := newApkConstraint(test.constraint)
|
||||
|
||||
assert.NoError(t, err, "unexpected error from newApkConstraint: %v", err)
|
||||
test.assertVersionConstraint(t, ApkFormat, constraint)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
33
grype/version/apk_version.go
Normal file
33
grype/version/apk_version.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
apk "github.com/knqyf263/go-apk-version"
|
||||
)
|
||||
|
||||
type apkVersion struct {
|
||||
obj apk.Version
|
||||
}
|
||||
|
||||
func newApkVersion(raw string) (*apkVersion, error) {
|
||||
ver, err := apk.NewVersion(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apkVersion{
|
||||
obj: ver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *apkVersion) Compare(other *Version) (int, error) {
|
||||
if other.Format != ApkFormat {
|
||||
return -1, fmt.Errorf("unable to compare apk to given format: %s", other.Format)
|
||||
}
|
||||
if other.rich.apkVer == nil {
|
||||
return -1, fmt.Errorf("given empty apkVersion object")
|
||||
}
|
||||
|
||||
return other.rich.apkVer.obj.Compare(a.obj), nil
|
||||
}
|
|
@ -11,6 +11,8 @@ type Constraint interface {
|
|||
|
||||
func GetConstraint(constStr string, format Format) (Constraint, error) {
|
||||
switch format {
|
||||
case ApkFormat:
|
||||
return newApkConstraint(constStr)
|
||||
case SemanticFormat:
|
||||
return newSemanticConstraint(constStr)
|
||||
case DebFormat:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//nolint:dupl
|
||||
package version
|
||||
|
||||
import "fmt"
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
const (
|
||||
UnknownFormat Format = iota
|
||||
SemanticFormat
|
||||
ApkFormat
|
||||
DebFormat
|
||||
RpmFormat
|
||||
PythonFormat
|
||||
|
@ -20,6 +21,7 @@ type Format int
|
|||
var formatStr = []string{
|
||||
"UnknownFormat",
|
||||
"Semantic",
|
||||
"Apk",
|
||||
"Deb",
|
||||
"RPM",
|
||||
"Python",
|
||||
|
@ -28,6 +30,7 @@ var formatStr = []string{
|
|||
|
||||
var Formats = []Format{
|
||||
SemanticFormat,
|
||||
ApkFormat,
|
||||
DebFormat,
|
||||
RpmFormat,
|
||||
PythonFormat,
|
||||
|
@ -38,6 +41,8 @@ func ParseFormat(userStr string) Format {
|
|||
switch strings.ToLower(userStr) {
|
||||
case strings.ToLower(SemanticFormat.String()), "semver":
|
||||
return SemanticFormat
|
||||
case strings.ToLower(ApkFormat.String()), "apk":
|
||||
return ApkFormat
|
||||
case strings.ToLower(DebFormat.String()), "dpkg":
|
||||
return DebFormat
|
||||
case strings.ToLower(RpmFormat.String()), "rpmdb":
|
||||
|
@ -53,6 +58,8 @@ func ParseFormat(userStr string) Format {
|
|||
func FormatFromPkgType(t pkg.Type) Format {
|
||||
var format Format
|
||||
switch t {
|
||||
case pkg.ApkPkg:
|
||||
format = ApkFormat
|
||||
case pkg.DebPkg:
|
||||
format = DebFormat
|
||||
case pkg.RpmPkg:
|
||||
|
|
|
@ -20,6 +20,7 @@ func (v *kbVersion) Compare(other *Version) (int, error) {
|
|||
if other.Format != KBFormat {
|
||||
return -1, fmt.Errorf("unable to compare kb to given format: %s", other.Format)
|
||||
}
|
||||
|
||||
if other.rich.kbVer == nil {
|
||||
return -1, fmt.Errorf("given empty kbVersion object")
|
||||
}
|
||||
|
@ -32,6 +33,7 @@ func (v kbVersion) compare(v2 kbVersion) int {
|
|||
if reflect.DeepEqual(v, v2) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ type Version struct {
|
|||
type rich struct {
|
||||
cpeVers []syftPkg.CPE
|
||||
semVer *semanticVersion
|
||||
apkVer *apkVersion
|
||||
debVer *debVersion
|
||||
rpmVer *rpmVersion
|
||||
kbVer *kbVersion
|
||||
|
@ -51,6 +52,10 @@ func (v *Version) populate() error {
|
|||
ver, err := newSemanticVersion(v.Raw)
|
||||
v.rich.semVer = ver
|
||||
return err
|
||||
case ApkFormat:
|
||||
ver, err := newApkVersion(v.Raw)
|
||||
v.rich.apkVer = ver
|
||||
return err
|
||||
case DebFormat:
|
||||
ver, err := newDebVersion(v.Raw)
|
||||
v.rich.debVer = ver
|
||||
|
@ -70,6 +75,7 @@ func (v *Version) populate() error {
|
|||
// use the raw string + fuzzy constraint
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("no rich version populated (format=%s)", v.Format)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue