Skip to content

Commit

Permalink
feat: add custom maven comparator (#1571)
Browse files Browse the repository at this point in the history
This PR takes the recommendation from #1526 and adapts the go-mvn-version to be used as a custom comparator for matching against packages that have the JavaPkg type. Packages of type JavaPkg will no longer use the stock matcher.
---------
Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
  • Loading branch information
spiffcs committed Oct 27, 2023
1 parent 1ab051b commit 401d67c
Show file tree
Hide file tree
Showing 19 changed files with 327 additions and 13 deletions.
6 changes: 2 additions & 4 deletions .github/actions/bootstrap/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ inputs:
cache-key-prefix:
description: "Prefix all cache keys with this value"
required: true
default: "831180ac25"
default: "831180ac26"
build-cache-key-prefix:
description: "Prefix build cache key with this value"
required: true
Expand Down Expand Up @@ -40,9 +40,7 @@ runs:
path: |
test/quality/venv
test/quality/vulnerability-match-labels/venv
key: ${{ runner.os }}-python-${{ inputs.python-version }}-${{ hashFiles('**/test/quality/**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-python-${{ env.python-version }}-
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-python-${{ inputs.python-version }}-${{ hashFiles('**/test/quality/**/requirements.txt') }}

- name: Restore tool cache
id: tool-cache
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/validations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ jobs:
run: make quality
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GRYPE_BY_CVE: "true"

- name: Archive the provider state
if: ${{ failure() }}
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ require (
modernc.org/sqlite v1.26.0 // indirect
)

require github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554
require (
github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08
github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554
)

require (
cloud.google.com/go v0.110.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,8 @@ github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc8
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08 h1:AevUBW4cc99rAF8q8vmddIP8qd/0J5s/UyltGbp66dg=
github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08/go.mod h1:JOkBRrE1HvgTyjk6diFtNGgr8XJMtIfiBzkL5krqzVk=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
Expand Down
2 changes: 1 addition & 1 deletion grype/version/apk_constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type apkConstraint struct {

func newApkConstraint(raw string) (apkConstraint, error) {
if raw == "" {
// empy constraints are always satisfied
// empty constraints are always satisfied
return apkConstraint{}, nil
}

Expand Down
2 changes: 2 additions & 0 deletions grype/version/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ func GetConstraint(constStr string, format Format) (Constraint, error) {
return newSemanticConstraint(constStr)
case DebFormat:
return newDebConstraint(constStr)
case MavenFormat:
return newMavenConstraint(constStr)
case RpmFormat:
return newRpmConstraint(constStr)
case PythonFormat:
Expand Down
7 changes: 7 additions & 0 deletions grype/version/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
SemanticFormat
ApkFormat
DebFormat
MavenFormat
RpmFormat
PythonFormat
KBFormat
Expand All @@ -25,6 +26,7 @@ var formatStr = []string{
"Semantic",
"Apk",
"Deb",
"Maven",
"RPM",
"Python",
"KB",
Expand All @@ -36,6 +38,7 @@ var Formats = []Format{
SemanticFormat,
ApkFormat,
DebFormat,
MavenFormat,
RpmFormat,
PythonFormat,
KBFormat,
Expand All @@ -51,6 +54,8 @@ func ParseFormat(userStr string) Format {
return ApkFormat
case strings.ToLower(DebFormat.String()), "dpkg":
return DebFormat
case strings.ToLower(MavenFormat.String()), "maven":
return MavenFormat
case strings.ToLower(RpmFormat.String()), "rpm":
return RpmFormat
case strings.ToLower(PythonFormat.String()), "python":
Expand All @@ -72,6 +77,8 @@ func FormatFromPkgType(t pkg.Type) Format {
format = ApkFormat
case pkg.DebPkg:
format = DebFormat
case pkg.JavaPkg:
format = MavenFormat
case pkg.RpmPkg:
format = RpmFormat
case pkg.GemPkg:
Expand Down
8 changes: 8 additions & 0 deletions grype/version/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ func TestParseFormat(t *testing.T) {
input: "dpkg",
format: DebFormat,
},
{
input: "maven",
format: MavenFormat,
},
{
input: "gem",
format: GemFormat,
Expand Down Expand Up @@ -54,6 +58,10 @@ func TestFormatFromPkgType(t *testing.T) {
pkgType: pkg.DebPkg,
format: DebFormat,
},
{
pkgType: pkg.JavaPkg,
format: MavenFormat,
},
{
pkgType: pkg.GemPkg,
format: GemFormat,
Expand Down
13 changes: 13 additions & 0 deletions grype/version/fuzzy_constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ func (f *fuzzyConstraint) Satisfied(verObj *Version) (bool, error) {

version := verObj.Raw

// rebuild temp constraint based off of ver obj
if verObj.Format != UnknownFormat {
newConstaint, err := GetConstraint(f.rawPhrase, verObj.Format)
// check if constraint is not fuzzyConstraint
_, ok := newConstaint.(*fuzzyConstraint)
if err == nil && !ok {
satisfied, err := newConstaint.Satisfied(verObj)
if err == nil {
return satisfied, nil
}
}
}

// attempt semver first, then fallback to fuzzy part matching...
if f.semanticConstraint != nil {
if pseudoSemverPattern.Match([]byte(version)) {
Expand Down
72 changes: 72 additions & 0 deletions grype/version/maven_constraint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package version

import "fmt"

type mavenConstraint struct {
raw string
expression constraintExpression
}

func newMavenConstraint(raw string) (mavenConstraint, error) {
if raw == "" {
// empty constraints are always satisfied
return mavenConstraint{}, nil
}

constraints, err := newConstraintExpression(raw, newMavenComparator)
if err != nil {
return mavenConstraint{}, fmt.Errorf("unable to parse maven constraint phrase: %w", err)
}

return mavenConstraint{
raw: raw,
expression: constraints,
}, nil
}

func newMavenComparator(unit constraintUnit) (Comparator, error) {
ver, err := newMavenVersion(unit.version)
if err != nil {
return nil, fmt.Errorf("unable to parse constraint version (%s): %w", unit.version, err)
}

return ver, nil
}

func (c mavenConstraint) supported(format Format) bool {
return format == MavenFormat
}

func (c mavenConstraint) Satisfied(version *Version) (satisfied bool, err 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("(maven) unsupported format: %s", version.Format)
}

if version.rich.mavenVer == nil {
return false, fmt.Errorf("no rich apk version given: %+v", version)
}

return c.expression.satisfied(version)
}

func (c mavenConstraint) String() string {
if c.raw == "" {
return "none (maven)"
}

return fmt.Sprintf("%s (maven)", c.raw)
}
104 changes: 104 additions & 0 deletions grype/version/maven_constraint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package version

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestVersionConstraintJava(t *testing.T) {
tests := []testCase{
{version: "1", constraint: "< 2.5", satisfied: true},
{version: "1.0", constraint: "< 1.1", satisfied: true},
{version: "1.1", constraint: "< 1.2", satisfied: true},
{version: "1.0.0", constraint: "< 1.1", satisfied: true},
{version: "1.0.1", constraint: "< 1.1", satisfied: true},
{version: "1.1", constraint: "> 1.2.0", satisfied: false},
{version: "1.0-alpha-1", constraint: "> 1.0", satisfied: false},
{version: "1.0-alpha-1", constraint: "> 1.0-alpha-2", satisfied: false},
{version: "1.0-alpha-1", constraint: "< 1.0-beta-1", satisfied: true},
{version: "1.0-beta-1", constraint: "< 1.0-SNAPSHOT", satisfied: true},
{version: "1.0-SNAPSHOT", constraint: "< 1.0", satisfied: true},
{version: "1.0-alpha-1-SNAPSHOT", constraint: "> 1.0-alpha-1", satisfied: false},
{version: "1.0", constraint: "< 1.0-1", satisfied: true},
{version: "1.0-1", constraint: "< 1.0-2", satisfied: true},
{version: "1.0.0", constraint: "< 1.0-1", satisfied: true},
{version: "2.0-1", constraint: "> 2.0.1", satisfied: false},
{version: "2.0.1-klm", constraint: "> 2.0.1-lmn", satisfied: false},
{version: "2.0.1", constraint: "< 2.0.1-xyz", satisfied: true},
{version: "2.0.1", constraint: "< 2.0.1-123", satisfied: true},
{version: "2.0.1-xyz", constraint: "< 2.0.1-123", satisfied: true},
{version: "2.414.2-cb-5", constraint: "> 2.414.2", satisfied: true},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
constraint, err := newMavenConstraint(test.constraint)

assert.NoError(t, err, "unexpected error from newMavenConstraint %s: %v", test.version, err)
test.assertVersionConstraint(t, MavenFormat, constraint)

})
}
}

func TestVersionEqualityJava(t *testing.T) {
tests := []testCase{
{version: "1", constraint: "1", satisfied: true},
{version: "1", constraint: "1.0", satisfied: true},
{version: "1", constraint: "1.0.0", satisfied: true},
{version: "1.0", constraint: "1.0.0", satisfied: true},
{version: "1", constraint: "1-0", satisfied: true},
{version: "1", constraint: "1.0-0", satisfied: true},
{version: "1.0", constraint: "1.0-0", satisfied: true},
{version: "1a", constraint: "1-a", satisfied: true},
{version: "1a", constraint: "1.0-a", satisfied: true},
{version: "1a", constraint: "1.0.0-a", satisfied: true},
{version: "1.0a", constraint: "1-a", satisfied: true},
{version: "1.0.0a", constraint: "1-a", satisfied: true},
{version: "1x", constraint: "1-x", satisfied: true},
{version: "1x", constraint: "1.0-x", satisfied: true},
{version: "1x", constraint: "1.0.0-x", satisfied: true},
{version: "1.0x", constraint: "1-x", satisfied: true},
{version: "1.0.0x", constraint: "1-x", satisfied: true},
{version: "1ga", constraint: "1", satisfied: true},
{version: "1release", constraint: "1", satisfied: true},
{version: "1final", constraint: "1", satisfied: true},
{version: "1cr", constraint: "1rc", satisfied: true},
{version: "1a1", constraint: "1-alpha-1", satisfied: true},
{version: "1b2", constraint: "1-beta-2", satisfied: true},
{version: "1m3", constraint: "1-milestone-3", satisfied: true},
{version: "1X", constraint: "1x", satisfied: true},
{version: "1A", constraint: "1a", satisfied: true},
{version: "1B", constraint: "1b", satisfied: true},
{version: "1M", constraint: "1m", satisfied: true},
{version: "1Ga", constraint: "1", satisfied: true},
{version: "1GA", constraint: "1", satisfied: true},
{version: "1RELEASE", constraint: "1", satisfied: true},
{version: "1release", constraint: "1", satisfied: true},
{version: "1RELeaSE", constraint: "1", satisfied: true},
{version: "1Final", constraint: "1", satisfied: true},
{version: "1FinaL", constraint: "1", satisfied: true},
{version: "1FINAL", constraint: "1", satisfied: true},
{version: "1Cr", constraint: "1Rc", satisfied: true},
{version: "1cR", constraint: "1rC", satisfied: true},
{version: "1m3", constraint: "1Milestone3", satisfied: true},
{version: "1m3", constraint: "1MileStone3", satisfied: true},
{version: "1m3", constraint: "1MILESTONE3", satisfied: true},
{version: "1", constraint: "01", satisfied: true},
{version: "1", constraint: "001", satisfied: true},
{version: "1.1", constraint: "1.01", satisfied: true},
{version: "1.1", constraint: "1.001", satisfied: true},
{version: "1-1", constraint: "1-01", satisfied: true},
{version: "1-1", constraint: "1-001", satisfied: true},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
constraint, err := newMavenConstraint(test.constraint)

assert.NoError(t, err, "unexpected error from newMavenConstraint %s: %v", test.version, err)
test.assertVersionConstraint(t, MavenFormat, constraint)
})
}
}
51 changes: 51 additions & 0 deletions grype/version/maven_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package version

import (
"fmt"

mvnv "github.com/masahiro331/go-mvn-version"
)

type mavenVersion struct {
raw string
version mvnv.Version
}

func newMavenVersion(raw string) (*mavenVersion, error) {
ver, err := mvnv.NewVersion(raw)
if err != nil {
return nil, fmt.Errorf("could not generate new java version from: %s; %w", raw, err)
}

return &mavenVersion{
raw: raw,
version: ver,
}, nil
}

// Compare returns 0 if j2 == j, 1 if j2 > j, and -1 if j2 < j.
// If an error returns the int value is -1
func (j *mavenVersion) Compare(j2 *Version) (int, error) {
if j2.Format != MavenFormat {
return -1, fmt.Errorf("unable to compare java to given format: %s", j2.Format)
}
if j2.rich.mavenVer == nil {
return -1, fmt.Errorf("given empty mavenVersion object")
}

submittedVersion := j2.rich.mavenVer.version
if submittedVersion.Equal(j.version) {
return 0, nil
}
if submittedVersion.LessThan(j.version) {
return -1, nil
}
if submittedVersion.GreaterThan(j.version) {
return 1, nil
}

return -1, fmt.Errorf(
"could not compare java versions: %v with %v",
submittedVersion.String(),
j.version.String())
}
Loading

0 comments on commit 401d67c

Please sign in to comment.