Add APK version constraint parsing (#455)

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
This commit is contained in:
Christopher Angelo Phillips 2021-10-18 13:27:02 -04:00 committed by GitHub
parent dc1f682e4b
commit 637a061532
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 197 additions and 0 deletions

1
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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:*:*:*:*:*:*:*:*")),

View 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)
}

View 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)
})
}
}

View 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
}

View file

@ -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:

View file

@ -1,3 +1,4 @@
//nolint:dupl
package version
import "fmt"

View file

@ -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:

View file

@ -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
}

View file

@ -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)
}