diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index 3fada0a417b..a0d11095fb9 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -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 @@ -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 diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index 6e5560912c8..6e936951b76 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -53,7 +53,6 @@ jobs: run: make quality env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GRYPE_BY_CVE: "true" Integration-Test: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline diff --git a/go.mod b/go.mod index c2c54cc05de..bdb115b5f9a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a2b72ce4e7d..48d2f9198e7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/grype/version/apk_constraint.go b/grype/version/apk_constraint.go index 9a70ae84260..37e69cad077 100644 --- a/grype/version/apk_constraint.go +++ b/grype/version/apk_constraint.go @@ -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 } diff --git a/grype/version/constraint.go b/grype/version/constraint.go index 773a472511a..97470a22bac 100644 --- a/grype/version/constraint.go +++ b/grype/version/constraint.go @@ -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: diff --git a/grype/version/format.go b/grype/version/format.go index fb9821f76fa..34506a55b36 100644 --- a/grype/version/format.go +++ b/grype/version/format.go @@ -11,6 +11,7 @@ const ( SemanticFormat ApkFormat DebFormat + MavenFormat RpmFormat PythonFormat KBFormat @@ -25,6 +26,7 @@ var formatStr = []string{ "Semantic", "Apk", "Deb", + "Maven", "RPM", "Python", "KB", @@ -36,6 +38,7 @@ var Formats = []Format{ SemanticFormat, ApkFormat, DebFormat, + MavenFormat, RpmFormat, PythonFormat, KBFormat, @@ -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": @@ -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: diff --git a/grype/version/format_test.go b/grype/version/format_test.go index a05e6027e46..ecc5647ba64 100644 --- a/grype/version/format_test.go +++ b/grype/version/format_test.go @@ -16,6 +16,10 @@ func TestParseFormat(t *testing.T) { input: "dpkg", format: DebFormat, }, + { + input: "maven", + format: MavenFormat, + }, { input: "gem", format: GemFormat, @@ -54,6 +58,10 @@ func TestFormatFromPkgType(t *testing.T) { pkgType: pkg.DebPkg, format: DebFormat, }, + { + pkgType: pkg.JavaPkg, + format: MavenFormat, + }, { pkgType: pkg.GemPkg, format: GemFormat, diff --git a/grype/version/fuzzy_constraint.go b/grype/version/fuzzy_constraint.go index 77d788332cf..ca333b7bf30 100644 --- a/grype/version/fuzzy_constraint.go +++ b/grype/version/fuzzy_constraint.go @@ -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)) { diff --git a/grype/version/maven_constraint.go b/grype/version/maven_constraint.go new file mode 100644 index 00000000000..8a191d673e5 --- /dev/null +++ b/grype/version/maven_constraint.go @@ -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) +} diff --git a/grype/version/maven_constraint_test.go b/grype/version/maven_constraint_test.go new file mode 100644 index 00000000000..0bb359e1f15 --- /dev/null +++ b/grype/version/maven_constraint_test.go @@ -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) + }) + } +} diff --git a/grype/version/maven_version.go b/grype/version/maven_version.go new file mode 100644 index 00000000000..29f1eb0759f --- /dev/null +++ b/grype/version/maven_version.go @@ -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()) +} diff --git a/grype/version/maven_version_test.go b/grype/version/maven_version_test.go new file mode 100644 index 00000000000..4b5143d7498 --- /dev/null +++ b/grype/version/maven_version_test.go @@ -0,0 +1,49 @@ +package version + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_javaVersion_Compare(t *testing.T) { + tests := []struct { + name string + compare string + want int + }{ + { + name: "1", + compare: "2", + want: -1, + }, + { + name: "1.8.0_282", + compare: "1.8.0_282", + want: 0, + }, + { + name: "2.5", + compare: "2.0", + want: 1, + }, + { + name: "2.414.2-cb-5", + compare: "2.414.2", + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + j, err := NewVersion(tt.name, MavenFormat) + assert.NoError(t, err) + + j2, err := NewVersion(tt.compare, MavenFormat) + assert.NoError(t, err) + + if got, _ := j2.rich.mavenVer.Compare(j); got != tt.want { + t.Errorf("Compare() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/grype/version/version.go b/grype/version/version.go index f2404bef69d..4f9f7cf394c 100644 --- a/grype/version/version.go +++ b/grype/version/version.go @@ -18,6 +18,7 @@ type rich struct { semVer *semanticVersion apkVer *apkVersion debVer *debVersion + mavenVer *mavenVersion rpmVer *rpmVersion kbVer *kbVersion portVer *portageVersion @@ -62,6 +63,10 @@ func (v *Version) populate() error { ver, err := newDebVersion(v.Raw) v.rich.debVer = ver return err + case MavenFormat: + ver, err := newMavenVersion(v.Raw) + v.rich.mavenVer = ver + return err case RpmFormat: ver, err := newRpmVersion(v.Raw) v.rich.rpmVer = &ver diff --git a/test/quality/.grype.yaml b/test/quality/.grype.yaml new file mode 100644 index 00000000000..3fda70abb9c --- /dev/null +++ b/test/quality/.grype.yaml @@ -0,0 +1 @@ +by-cve: true diff --git a/test/quality/.yardstick.yaml b/test/quality/.yardstick.yaml index dfae62e6f4d..65674ff78f0 100644 --- a/test/quality/.yardstick.yaml +++ b/test/quality/.yardstick.yaml @@ -103,9 +103,9 @@ result-sets: # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. - version: git:current-commit+import-db=db.tar.gz + # version: git:current-commit+import-db=db.tar.gz # for local build of grype, use for example: - # version: path:../../ + version: path:../../+import-db=db.tar.gz takes: SBOM - name: grype diff --git a/test/quality/Makefile b/test/quality/Makefile index fb81557bbe4..50826872da7 100644 --- a/test/quality/Makefile +++ b/test/quality/Makefile @@ -8,7 +8,7 @@ VULNERABILITY_LABELS = ./vulnerability-labels RESULT_SET = pr_vs_latest_via_sbom # update periodically with values from "grype db list" -TEST_DB_URL = https://toolbox-data.anchore.io/grype/databases/vulnerability-db_v5_2023-08-24T01:23:40Z_fd07204627d474f68f90.tar.gz +TEST_DB_URL = https://toolbox-data.anchore.io/grype/databases/vulnerability-db_v5_2023-10-25T01:27:28Z_fd5a911f9285633c57e3.tar.gz TEST_DB = db.tar.gz LISTING_FILE = https://toolbox-data.anchore.io/grype/databases/listing.json @@ -65,7 +65,7 @@ $(YARDSTICK_RESULT_DIR): mkdir -p $(YARDSTICK_RESULT_DIR) $(VULNERABILITY_LABELS)/Makefile: - git submodule update --init + git submodule update vulnerability-match-labels .PHONY: clear-results clear-results: venv ## Clear all existing yardstick results diff --git a/test/quality/requirements.txt b/test/quality/requirements.txt index 63d706f57b5..8f8a9a2d7fe 100644 --- a/test/quality/requirements.txt +++ b/test/quality/requirements.txt @@ -1,3 +1,3 @@ -git+https://github.com/anchore/yardstick@v0.8.0 +git+https://github.com/anchore/yardstick@v0.9.1 # ../../../yardstick tabulate==0.9.0 diff --git a/test/quality/vulnerability-match-labels b/test/quality/vulnerability-match-labels index 3e6c878d144..78ee717268d 160000 --- a/test/quality/vulnerability-match-labels +++ b/test/quality/vulnerability-match-labels @@ -1 +1 @@ -Subproject commit 3e6c878d144f95aab8bbb398ad0e7c717d6c3c31 +Subproject commit 78ee717268d80fa4f59772213af434da3dceefcd