From a07fe7d3b715a4f2f08a0bdffa694d33f39b9627 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 13 Sep 2024 16:59:26 -0400 Subject: [PATCH 1/5] add jvm version comparison Signed-off-by: Alex Goodman --- .gitignore | 4 +- cmd/grype/cli/commands/root.go | 4 + cmd/grype/cli/options/match.go | 2 + grype/internal/packagemetadata/names.go | 11 +- grype/match/matcher_type.go | 2 + grype/matcher/jvm/matcher.go | 51 ++++++++ grype/matcher/matchers.go | 3 + grype/matcher/stock/matcher.go | 8 ++ grype/pkg/java_metadata.go | 31 +++++ grype/pkg/package.go | 17 +++ grype/search/cpe.go | 17 ++- grype/version/constraint.go | 2 + grype/version/format.go | 59 +++++---- grype/version/format_test.go | 74 ++++++++++-- grype/version/jvm_constraint.go | 43 +++++++ grype/version/jvm_constraint_test.go | 70 +++++++++++ grype/version/jvm_version.go | 151 +++++++++++++++++++++++ grype/version/jvm_version_test.go | 152 ++++++++++++++++++++++++ grype/version/version.go | 9 +- internal/regex_helpers.go | 45 +++++++ internal/regex_helpers_test.go | 70 +++++++++++ 21 files changed, 779 insertions(+), 46 deletions(-) create mode 100644 grype/matcher/jvm/matcher.go create mode 100644 grype/version/jvm_constraint.go create mode 100644 grype/version/jvm_constraint_test.go create mode 100644 grype/version/jvm_version.go create mode 100644 grype/version/jvm_version_test.go create mode 100644 internal/regex_helpers.go create mode 100644 internal/regex_helpers_test.go diff --git a/.gitignore b/.gitignore index af1ef36b559..f1e1f81bd2c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,8 @@ bin/ /.task # changelog generation -CHANGELOG.md -VERSION +/CHANGELOG.md +/VERSION # IDE configuration .vscode/ diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index f23382db24a..1fe2f6db1c8 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -22,6 +22,7 @@ import ( "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/java" "github.com/anchore/grype/grype/matcher/javascript" + "github.com/anchore/grype/grype/matcher/jvm" "github.com/anchore/grype/grype/matcher/python" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/stock" @@ -285,6 +286,9 @@ func getMatchers(opts *options.Grype) []matcher.Matcher { ExternalSearchConfig: opts.ExternalSources.ToJavaMatcherConfig(), UseCPEs: opts.Match.Java.UseCPEs, }, + JVM: jvm.MatcherConfig{ + UseCPEs: opts.Match.JVM.UseCPEs, + }, Ruby: ruby.MatcherConfig(opts.Match.Ruby), Python: python.MatcherConfig(opts.Match.Python), Dotnet: dotnet.MatcherConfig(opts.Match.Dotnet), diff --git a/cmd/grype/cli/options/match.go b/cmd/grype/cli/options/match.go index f789a5b4689..b24df165b29 100644 --- a/cmd/grype/cli/options/match.go +++ b/cmd/grype/cli/options/match.go @@ -5,6 +5,7 @@ import "github.com/anchore/clio" // matchConfig contains all matching-related configuration options available to the user via the application config. type matchConfig struct { Java matcherConfig `yaml:"java" json:"java" mapstructure:"java"` // settings for the java matcher + JVM matcherConfig `yaml:"jvm" json:"jvm" mapstructure:"jvm"` // settings for the jvm matcher Dotnet matcherConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"` // settings for the dotnet matcher Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"` // settings for the golang matcher Javascript matcherConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"` // settings for the javascript matcher @@ -43,6 +44,7 @@ func defaultMatchConfig() matchConfig { dontUseCpe := matcherConfig{UseCPEs: false} return matchConfig{ Java: dontUseCpe, + JVM: useCpe, Dotnet: dontUseCpe, Golang: defaultGolangConfig(), Javascript: dontUseCpe, diff --git a/grype/internal/packagemetadata/names.go b/grype/internal/packagemetadata/names.go index caf79c0465b..75e01a0da10 100644 --- a/grype/internal/packagemetadata/names.go +++ b/grype/internal/packagemetadata/names.go @@ -13,11 +13,12 @@ import ( // not the same it may be important to select different names. This design decision has been deferred, for now // the same metadata types that have been used in the past should be used here. var jsonNameFromType = map[reflect.Type][]string{ - reflect.TypeOf(pkg.ApkMetadata{}): nameList("ApkMetadata"), - reflect.TypeOf(pkg.GolangBinMetadata{}): nameList("GolangBinMetadata"), - reflect.TypeOf(pkg.GolangModMetadata{}): nameList("GolangModMetadata"), - reflect.TypeOf(pkg.JavaMetadata{}): nameList("JavaMetadata"), - reflect.TypeOf(pkg.RpmMetadata{}): nameList("RpmMetadata"), + reflect.TypeOf(pkg.ApkMetadata{}): nameList("ApkMetadata"), + reflect.TypeOf(pkg.GolangBinMetadata{}): nameList("GolangBinMetadata"), + reflect.TypeOf(pkg.GolangModMetadata{}): nameList("GolangModMetadata"), + reflect.TypeOf(pkg.JavaMetadata{}): nameList("JavaMetadata"), + reflect.TypeOf(pkg.RpmMetadata{}): nameList("RpmMetadata"), + reflect.TypeOf(pkg.JavaVMInstallationMetadata{}): nameList("JavaVMInstallationMetadata"), } //nolint:unparam diff --git a/grype/match/matcher_type.go b/grype/match/matcher_type.go index ad547c6d94c..29f6f9255de 100644 --- a/grype/match/matcher_type.go +++ b/grype/match/matcher_type.go @@ -16,6 +16,7 @@ const ( GoModuleMatcher MatcherType = "go-module-matcher" OpenVexMatcher MatcherType = "openvex-matcher" RustMatcher MatcherType = "rust-matcher" + JVMMatcher MatcherType = "jvm-matcher" ) var AllMatcherTypes = []MatcherType{ @@ -32,6 +33,7 @@ var AllMatcherTypes = []MatcherType{ GoModuleMatcher, OpenVexMatcher, RustMatcher, + JVMMatcher, } type MatcherType string diff --git a/grype/matcher/jvm/matcher.go b/grype/matcher/jvm/matcher.go new file mode 100644 index 00000000000..e26b1b39752 --- /dev/null +++ b/grype/matcher/jvm/matcher.go @@ -0,0 +1,51 @@ +package jvm + +import ( + "fmt" + + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" + "github.com/anchore/grype/grype/vulnerability" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +type MatcherConfig struct { + UseCPEs bool +} + +type Matcher struct { + cfg MatcherConfig +} + +func NewJVMMatcher(cfg MatcherConfig) *Matcher { + return &Matcher{ + cfg: cfg, + } +} + +func (m *Matcher) PackageTypes() []syftPkg.Type { + return []syftPkg.Type{syftPkg.BinaryPkg} +} + +func (m *Matcher) Type() match.MatcherType { + return match.JVMMatcher +} + +func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { + if !pkg.IsJvmPackage(p) { + return nil, nil + } + + criteria := search.CommonCriteria + if m.cfg.UseCPEs { + criteria = append(criteria, search.ByCPE) + } + matches, err := search.ByCriteria(store, d, p, m.Type(), criteria...) + if err != nil { + return nil, fmt.Errorf("failed to match by exact package: %w", err) + } + + return matches, nil +} diff --git a/grype/matcher/matchers.go b/grype/matcher/matchers.go index 72778eb6292..8c538433b5b 100644 --- a/grype/matcher/matchers.go +++ b/grype/matcher/matchers.go @@ -7,6 +7,7 @@ import ( "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/java" "github.com/anchore/grype/grype/matcher/javascript" + "github.com/anchore/grype/grype/matcher/jvm" "github.com/anchore/grype/grype/matcher/msrc" "github.com/anchore/grype/grype/matcher/portage" "github.com/anchore/grype/grype/matcher/python" @@ -19,6 +20,7 @@ import ( // Config contains values used by individual matcher structs for advanced configuration type Config struct { Java java.MatcherConfig + JVM jvm.MatcherConfig Ruby ruby.MatcherConfig Python python.MatcherConfig Dotnet dotnet.MatcherConfig @@ -36,6 +38,7 @@ func NewDefaultMatchers(mc Config) []Matcher { dotnet.NewDotnetMatcher(mc.Dotnet), &rpm.Matcher{}, java.NewJavaMatcher(mc.Java), + jvm.NewJVMMatcher(mc.JVM), javascript.NewJavascriptMatcher(mc.Javascript), &apk.Matcher{}, golang.NewGolangMatcher(mc.Golang), diff --git a/grype/matcher/stock/matcher.go b/grype/matcher/stock/matcher.go index 7f30a52df9a..661f4b67604 100644 --- a/grype/matcher/stock/matcher.go +++ b/grype/matcher/stock/matcher.go @@ -32,9 +32,17 @@ func (m *Matcher) Type() match.MatcherType { } func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { + if !inboundsForMatcher(p) { + return nil, nil + } + criteria := search.CommonCriteria if m.cfg.UseCPEs { criteria = append(criteria, search.ByCPE) } return search.ByCriteria(store, d, p, m.Type(), criteria...) } + +func inboundsForMatcher(p pkg.Package) bool { + return !pkg.IsJvmPackage(p) +} diff --git a/grype/pkg/java_metadata.go b/grype/pkg/java_metadata.go index 24ba9371787..8b2b2ac08b8 100644 --- a/grype/pkg/java_metadata.go +++ b/grype/pkg/java_metadata.go @@ -1,5 +1,11 @@ package pkg +import ( + "strings" + + "github.com/anchore/syft/syft/pkg" +) + type JavaMetadata struct { VirtualPath string `json:"virtualPath"` PomArtifactID string `json:"pomArtifactID"` @@ -12,3 +18,28 @@ type Digest struct { Algorithm string `json:"algorithm"` Value string `json:"value"` } + +type JavaVMInstallationMetadata struct { + Release JavaVMReleaseMetadata `json:"release,omitempty"` +} + +type JavaVMReleaseMetadata struct { + JavaRuntimeVersion string `json:"javaRuntimeVersion,omitempty"` + JavaVersion string `json:"javaVersion,omitempty"` + FullVersion string `json:"fullVersion,omitempty"` + SemanticVersion string `json:"semanticVersion,omitempty"` +} + +func IsJvmPackage(p Package) bool { + if p.Type == pkg.BinaryPkg { + if strings.Contains(p.Name, "jdk") || strings.Contains(p.Name, "jre") || strings.Contains(p.Name, "java") { + return true + } + } + + if _, ok := p.Metadata.(JavaVMInstallationMetadata); ok { + return true + } + + return false +} diff --git a/grype/pkg/package.go b/grype/pkg/package.go index e5b6399ac88..89cbd9208d1 100644 --- a/grype/pkg/package.go +++ b/grype/pkg/package.go @@ -214,10 +214,27 @@ func dataFromPkg(p pkg.Package) (interface{}, []UpstreamPackage) { case pkg.ApkDBEntry: metadata = apkMetadataFromPkg(p) upstreams = apkDataFromPkg(p) + case pkg.JavaVMInstallation: + metadata = javaVMDataFromPkg(p) } return metadata, upstreams } +func javaVMDataFromPkg(p pkg.Package) any { + if value, ok := p.Metadata.(pkg.JavaVMInstallation); ok { + return JavaVMInstallationMetadata{ + Release: JavaVMReleaseMetadata{ + JavaRuntimeVersion: value.Release.JavaRuntimeVersion, + JavaVersion: value.Release.JavaVersion, + FullVersion: value.Release.FullVersion, + SemanticVersion: value.Release.SemanticVersion, + }, + } + } + + return nil +} + func apkMetadataFromPkg(p pkg.Package) interface{} { if m, ok := p.Metadata.(pkg.ApkDBEntry); ok { metadata := ApkMetadata{} diff --git a/grype/search/cpe.go b/grype/search/cpe.go index c6aaa2ea5b3..83ef8266639 100644 --- a/grype/search/cpe.go +++ b/grype/search/cpe.go @@ -105,7 +105,14 @@ func ByPackageCPE(store vulnerability.ProviderByCPE, d *distro.Distro, p pkg.Pac if searchVersion == wfn.NA || searchVersion == wfn.Any { searchVersion = p.Version } - verObj, err := version.NewVersion(searchVersion, version.FormatFromPkgType(p.Type)) + + format := version.FormatFromPkg(p) + + if format == version.JVMFormat { + searchVersion = specificLegacyJvmVersion(searchVersion, c.Attributes.Update) + } + + verObj, err := version.NewVersion(searchVersion, format) if err != nil { return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) } @@ -140,6 +147,14 @@ func ByPackageCPE(store vulnerability.ProviderByCPE, d *distro.Distro, p pkg.Pac return toMatches(matchesByFingerprint), nil } +func specificLegacyJvmVersion(searchVersion, updateCpeField string) string { + // we should take into consideration the CPE update field for JVM packages + if strings.HasPrefix(searchVersion, "1.") && !strings.Contains(searchVersion, "_") && updateCpeField != wfn.NA && updateCpeField != wfn.Any { + searchVersion = fmt.Sprintf("%s_%s", searchVersion, strings.TrimPrefix(updateCpeField, "update")) + } + return searchVersion +} + func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vulnerability.Vulnerability, p pkg.Package, searchVersion version.Version, upstreamMatcher match.MatcherType, searchedByCPE cpe.CPE) { candidateMatch := match.Match{ diff --git a/grype/version/constraint.go b/grype/version/constraint.go index ec463ae53ef..00a83ce56e0 100644 --- a/grype/version/constraint.go +++ b/grype/version/constraint.go @@ -29,6 +29,8 @@ func GetConstraint(constStr string, format Format) (Constraint, error) { return newKBConstraint(constStr) case PortageFormat: return newPortageConstraint(constStr) + case JVMFormat: + return newJvmConstraint(constStr) case UnknownFormat: return newFuzzyConstraint(constStr, "unknown") } diff --git a/grype/version/format.go b/grype/version/format.go index 9164456e75a..ab7f7de8c93 100644 --- a/grype/version/format.go +++ b/grype/version/format.go @@ -3,7 +3,8 @@ package version import ( "strings" - "github.com/anchore/syft/syft/pkg" + "github.com/anchore/grype/grype/pkg" + syftPkg "github.com/anchore/syft/syft/pkg" ) const ( @@ -18,6 +19,7 @@ const ( GemFormat PortageFormat GolangFormat + JVMFormat ) type Format int @@ -34,6 +36,7 @@ var formatStr = []string{ "Gem", "Portage", "Go", + "JVM", } var Formats = []Format{ @@ -46,6 +49,8 @@ var Formats = []Format{ KBFormat, GemFormat, PortageFormat, + GolangFormat, + JVMFormat, } func ParseFormat(userStr string) Format { @@ -70,35 +75,39 @@ func ParseFormat(userStr string) Format { return GemFormat case strings.ToLower(PortageFormat.String()), "portage": return PortageFormat + case strings.ToLower(JVMFormat.String()), "jvm", "jre", "jdk", "openjdk", "jep223": + return JVMFormat } return UnknownFormat } -func FormatFromPkgType(t pkg.Type) Format { - var format Format - switch t { - case pkg.ApkPkg: - format = ApkFormat - case pkg.DebPkg: - format = DebFormat - case pkg.JavaPkg: - format = MavenFormat - case pkg.RpmPkg: - format = RpmFormat - case pkg.GemPkg: - format = GemFormat - case pkg.PythonPkg: - format = PythonFormat - case pkg.KbPkg: - format = KBFormat - case pkg.PortagePkg: - format = PortageFormat - case pkg.GoModulePkg: - format = GolangFormat - default: - format = UnknownFormat +func FormatFromPkg(p pkg.Package) Format { + switch p.Type { + case syftPkg.ApkPkg: + return ApkFormat + case syftPkg.DebPkg: + return DebFormat + case syftPkg.JavaPkg: + return MavenFormat + case syftPkg.RpmPkg: + return RpmFormat + case syftPkg.GemPkg: + return GemFormat + case syftPkg.PythonPkg: + return PythonFormat + case syftPkg.KbPkg: + return KBFormat + case syftPkg.PortagePkg: + return PortageFormat + case syftPkg.GoModulePkg: + return GolangFormat + } + + if pkg.IsJvmPackage(p) { + return JVMFormat } - return format + + return UnknownFormat } func (f Format) String() string { diff --git a/grype/version/format_test.go b/grype/version/format_test.go index ecc5647ba64..5471ebaa159 100644 --- a/grype/version/format_test.go +++ b/grype/version/format_test.go @@ -4,7 +4,8 @@ import ( "fmt" "testing" - "github.com/anchore/syft/syft/pkg" + "github.com/anchore/grype/grype/pkg" + syftPkg "github.com/anchore/syft/syft/pkg" ) func TestParseFormat(t *testing.T) { @@ -51,29 +52,78 @@ func TestParseFormat(t *testing.T) { func TestFormatFromPkgType(t *testing.T) { tests := []struct { - pkgType pkg.Type - format Format + name string + p pkg.Package + format Format }{ { - pkgType: pkg.DebPkg, - format: DebFormat, + name: "deb", + p: pkg.Package{ + Type: syftPkg.DebPkg, + }, + format: DebFormat, + }, + { + name: "java jar", + p: pkg.Package{ + Type: syftPkg.JavaPkg, + }, + format: MavenFormat, + }, + { + name: "gem", + p: pkg.Package{ + Type: syftPkg.GemPkg, + }, + format: GemFormat, + }, + { + name: "jvm by metadata", + p: pkg.Package{ + Metadata: pkg.JavaVMInstallationMetadata{}, + }, + format: JVMFormat, + }, + { + name: "jvm by type and name (jdk)", + p: pkg.Package{ + Type: syftPkg.BinaryPkg, + Name: "jdk", + }, + format: JVMFormat, + }, + { + name: "jvm by type and name (openjdk)", + p: pkg.Package{ + Type: syftPkg.BinaryPkg, + Name: "openjdk", + }, + format: JVMFormat, }, { - pkgType: pkg.JavaPkg, - format: MavenFormat, + name: "jvm by type and name (jre)", + p: pkg.Package{ + Type: syftPkg.BinaryPkg, + Name: "jre", + }, + format: JVMFormat, }, { - pkgType: pkg.GemPkg, - format: GemFormat, + name: "jvm by type and name (java_se)", + p: pkg.Package{ + Type: syftPkg.BinaryPkg, + Name: "java_se", + }, + format: JVMFormat, }, } for _, test := range tests { - name := fmt.Sprintf("pkgType[%s]->format[%s]", test.pkgType, test.format) + name := fmt.Sprintf("pkgType[%s]->format[%s]", test.p.Type, test.format) t.Run(name, func(t *testing.T) { - actual := FormatFromPkgType(test.pkgType) + actual := FormatFromPkg(test.p) if actual != test.format { - t.Errorf("mismatched pkgType->format mapping, pkgType='%s': '%s'!='%s'", test.pkgType, test.format, actual) + t.Errorf("mismatched pkgType->format mapping, pkgType='%s': '%s'!='%s'", test.p.Type, test.format, actual) } }) } diff --git a/grype/version/jvm_constraint.go b/grype/version/jvm_constraint.go new file mode 100644 index 00000000000..bae6e06436e --- /dev/null +++ b/grype/version/jvm_constraint.go @@ -0,0 +1,43 @@ +package version + +import "fmt" + +var _ Constraint = (*jvmConstraint)(nil) + +type jvmConstraint struct { + raw string + expression constraintExpression +} + +func newJvmConstraint(raw string) (jvmConstraint, error) { + constraints, err := newConstraintExpression(raw, newJvmComparator) + if err != nil { + return jvmConstraint{}, err + } + return jvmConstraint{ + expression: constraints, + raw: raw, + }, nil +} + +func (g jvmConstraint) String() string { + if g.raw == "" { + return "none (jvm)" + } + return fmt.Sprintf("%s (jvm)", g.raw) +} + +func (g jvmConstraint) Satisfied(version *Version) (bool, error) { + if g.raw == "" { + return true, nil // the empty constraint is always satisfied + } + return g.expression.satisfied(version) +} + +func newJvmComparator(unit constraintUnit) (Comparator, error) { + ver, err := newJvmVersion(unit.version) + if err != nil { + return nil, fmt.Errorf("unable to parse constraint version (%s): %w", unit.version, err) + } + return ver, nil +} diff --git a/grype/version/jvm_constraint_test.go b/grype/version/jvm_constraint_test.go new file mode 100644 index 00000000000..b587662826c --- /dev/null +++ b/grype/version/jvm_constraint_test.go @@ -0,0 +1,70 @@ +package version + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVersionConstraintJVM(t *testing.T) { + tests := []testCase{ + // pre jep 223 versions + {version: "1.7.0_80", constraint: "< 1.8.0", satisfied: true}, + {version: "1.8.0_131", constraint: "> 1.8.0", satisfied: true}, + {version: "1.8.0_131", constraint: "< 1.8.0_132", satisfied: true}, + {version: "1.8.0_131-b11", constraint: "< 1.8.0_132", satisfied: true}, + + {version: "1.7.0_80", constraint: "> 1.8.0", satisfied: false}, + {version: "1.8.0_131", constraint: "< 1.8.0", satisfied: false}, + {version: "1.8.0_131", constraint: "> 1.8.0_132", satisfied: false}, + {version: "1.8.0_131-b11", constraint: "> 1.8.0_132", satisfied: false}, + + {version: "1.7.0_80", constraint: "= 1.8.0", satisfied: false}, + {version: "1.8.0_131", constraint: "= 1.8.0", satisfied: false}, + {version: "1.8.0_131", constraint: "= 1.8.0_132", satisfied: false}, + {version: "1.8.0_131-b11", constraint: "= 1.8.0_132", satisfied: false}, + + {version: "1.8.0_80", constraint: "= 1.8.0_80", satisfied: true}, + {version: "1.8.0_131", constraint: ">= 1.8.0_131", satisfied: true}, + {version: "1.8.0_131", constraint: "= 1.8.0_131-b001", satisfied: true}, // builds should not matter + {version: "1.8.0_131-ea-b11", constraint: "= 1.8.0_131-ea", satisfied: true}, + + // jep 223 versions + {version: "8.0.4", constraint: "> 8.0.3", satisfied: true}, + {version: "8.0.4", constraint: "< 8.0.5", satisfied: true}, + {version: "9.0.0", constraint: "> 8.0.5", satisfied: true}, + {version: "9.0.0", constraint: "< 9.1.0", satisfied: true}, + {version: "11.0.4", constraint: "<= 11.0.4", satisfied: true}, + {version: "11.0.5", constraint: "> 11.0.4", satisfied: true}, + + {version: "8.0.4", constraint: "< 8.0.3", satisfied: false}, + {version: "8.0.4", constraint: "> 8.0.5", satisfied: false}, + {version: "9.0.0", constraint: "< 8.0.5", satisfied: false}, + {version: "9.0.0", constraint: "> 9.1.0", satisfied: false}, + {version: "11.0.4", constraint: "> 11.0.4", satisfied: false}, + {version: "11.0.5", constraint: "< 11.0.4", satisfied: false}, + + // mixed versions + {version: "1.8.0_131", constraint: "< 9.0.0", satisfied: true}, // 1.8.0_131 -> 8.0.131 + {version: "9.0.0", constraint: "> 1.8.0_131", satisfied: true}, // 1.8.0_131 -> 8.0.131 + {version: "1.8.0_131", constraint: "<= 8.0.131", satisfied: true}, + {version: "1.8.0_131", constraint: "> 7.0.79", satisfied: true}, + {version: "1.8.0_131", constraint: "= 8.0.131", satisfied: true}, + {version: "1.8.0_131", constraint: ">= 9.0.0", satisfied: false}, + {version: "9.0.1", constraint: "< 8.0.131", satisfied: false}, + + // pre-release versions + {version: "1.8.0_131-ea", constraint: "< 1.8.0_131", satisfied: true}, + {version: "1.8.0_131", constraint: "> 1.8.0_131-ea", satisfied: true}, + {version: "9.0.0-ea", constraint: "< 9.0.0", satisfied: true}, + {version: "9.0.0-ea", constraint: "> 1.8.0_131", satisfied: true}, + } + + for _, test := range tests { + t.Run(test.version+"_constraint_"+test.constraint, func(t *testing.T) { + constraint, err := newJvmConstraint(test.constraint) + require.NoError(t, err) + test.assertVersionConstraint(t, JVMFormat, constraint) + }) + } +} diff --git a/grype/version/jvm_version.go b/grype/version/jvm_version.go new file mode 100644 index 00000000000..f07a15d13e7 --- /dev/null +++ b/grype/version/jvm_version.go @@ -0,0 +1,151 @@ +package version + +import ( + "fmt" + "regexp" + "strings" + + hashiVer "github.com/anchore/go-version" + "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/log" +) + +var _ Comparator = (*jvmVersion)(nil) + +var ( + preJep223VersionPattern = regexp.MustCompile(`^1\.(?P\d+)(\.(?P\d+)([_-](update)?(_)?(?P\d+))?(-(?P[^b][^-]+))?(-b(?P\d+))?)?`) + nonCompliantSemverIsh = regexp.MustCompile(`^(?P\d+)(\.(?P\d+)(\.(?P\d+))?([_-](update)?(_)?(?P\d+))?(-(?P[^b][^-]+))?(-b(?P\d+))?)?`) +) + +type jvmVersion struct { + isPreJep223 bool + semVer *hashiVer.Version +} + +func newJvmVersion(raw string) (*jvmVersion, error) { + isPreJep233 := strings.HasPrefix(raw, "1.") + + if isPreJep233 { + // convert the pre-JEP 223 version to semver + raw = convertPreJep223Version(raw) + } else { + raw = convertNonCompliantSemver(raw) + } + verObj, err := hashiVer.NewVersion(raw) + if err != nil { + return nil, fmt.Errorf("unable to create semver obj for JVM version: %w", err) + } + + return &jvmVersion{ + isPreJep223: isPreJep233, + semVer: verObj, + }, nil +} + +func (v *jvmVersion) Compare(other *Version) (int, error) { + if other.Format == JVMFormat { + if other.rich.jvmVersion == nil { + return -1, fmt.Errorf("given empty jvmVersion object") + } + return other.rich.jvmVersion.compare(*v), nil + } + + if other.Format == SemanticFormat { + if other.rich.semVer == nil { + return -1, fmt.Errorf("given empty semVer object") + } + return other.rich.semVer.verObj.Compare(v.semVer), nil + } + + return -1, fmt.Errorf("unable to compare JVM to given format: %s", other.Format) +} + +func (v jvmVersion) compare(other jvmVersion) int { + return v.semVer.Compare(other.semVer) +} + +func convertNonCompliantSemver(version string) string { + // if there is -update as a prerelease, and the patch version is missing or 0, then we should parse the prerelease + // info that has the update value and extract the version. This should be used as the patch version. + + // 8.0-update302 --> 8.0.302 + // 8.0-update302-b08 --> 8.0.302+8 + // 8.0-update_302-b08 --> 8.0.302+8 + + matches := internal.MatchNamedCaptureGroups(nonCompliantSemverIsh, version) + if len(matches) == 0 { + log.WithFields("version", version).Trace("unable to convert pre-JEP 223 JVM version") + return version + } + + // extract relevant parts from the matches + majorVersion := trim0sFromLeft(matches["major"]) + minorVersion := trim0sFromLeft(matches["minor"]) + patchVersion := trim0sFromLeft(matches["patch"]) + update := trim0sFromLeft(matches["update"]) + preRelease := trim0sFromLeft(matches["prerelease"]) + build := trim0sFromLeft(matches["build"]) + + if (patchVersion == "" || patchVersion == "0") && update != "" { + patchVersion = update + } + + return buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build) +} + +func convertPreJep223Version(version string) string { + // convert the following pre JEP 223 version strings to semvers + // 1.8.0_302-b08 --> 8.0.302+8 + // 1.9.0-ea-b19 --> 9.0.0-ea+19 + // NOTE: this makes an assumption that the old update field is the patch version in semver... + // this is NOT strictly in the spec, but for 1.8 this tends to be true (especially for temurin-based builds) + version = strings.TrimSpace(version) + + matches := internal.MatchNamedCaptureGroups(preJep223VersionPattern, version) + if len(matches) == 0 { + log.WithFields("version", version).Trace("unable to convert pre-JEP 223 JVM version") + return version + } + + // extract relevant parts from the matches + majorVersion := trim0sFromLeft(matches["major"]) + minorVersion := trim0sFromLeft(matches["minor"]) + patchVersion := trim0sFromLeft(matches["patch"]) + preRelease := trim0sFromLeft(matches["prerelease"]) + build := trim0sFromLeft(matches["build"]) + + if patchVersion == "" { + patchVersion = "0" + } + + return buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build) +} +func buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build string) string { + if minorVersion == "" { + minorVersion = "0" + } + + segs := []string{majorVersion, minorVersion} + if patchVersion != "" { + segs = append(segs, patchVersion) + } + + var semver strings.Builder + semver.WriteString(strings.Join(segs, ".")) + + if preRelease != "" { + semver.WriteString(fmt.Sprintf("-%s", preRelease)) + } + if build != "" { + semver.WriteString(fmt.Sprintf("+%s", build)) + } + + return semver.String() +} + +func trim0sFromLeft(v string) string { + if v == "0" { + return v + } + return strings.TrimLeft(v, "0") +} diff --git a/grype/version/jvm_version_test.go b/grype/version/jvm_version_test.go new file mode 100644 index 00000000000..26b3a94c885 --- /dev/null +++ b/grype/version/jvm_version_test.go @@ -0,0 +1,152 @@ +package version + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVersionJVM(t *testing.T) { + tests := []struct { + v1 string + v2 string + expected int + }{ + // pre jep223 versions + {"1.8", "1.8.0", 0}, + {"1.8.0", "1.8.0_0", 0}, + {"1.8.0", "1.8.0", 0}, + {"1.7.0", "1.8.0", -1}, + {"1.8.0_131", "1.8.0_131", 0}, + {"1.8.0_131", "1.8.0_132", -1}, + + // builds should not matter + {"1.8.0_131", "1.8.0_130", 1}, + {"1.8.0_131", "1.8.0_132-b11", -1}, + {"1.8.0_131-b11", "1.8.0_132-b11", -1}, + {"1.8.0_131-b11", "1.8.0_131-b12", 0}, + {"1.8.0_131-b11", "1.8.0_131-b10", 0}, + {"1.8.0_131-b11", "1.8.0_131", 0}, + {"1.8.0_131-b11", "1.8.0_131-b11", 0}, + + // jep223 versions (semver) + {"8.0.4", "8.0.4", 0}, + {"8.0.4", "8.0.5", -1}, + {"8.0.4", "8.0.3", 1}, + {"8.0.4", "8.0.4+b1", 0}, + + // mix comparison + {"1.8.0_131", "8.0.4", 1}, // 1.8.0_131 --> 8.0.131 + {"8.0.4", "1.8.0_131", -1}, // doesn't matter which side the comparison is on + {"1.8.0_131-b002", "8.0.131+b2", 0}, // builds should not matter + {"1.8.0_131-b002", "8.0.131+b1", 0}, // builds should not matter + {"1.6.0", "8.0.1", -1}, // 1.6.0 --> 6.0.0 + + // prerelease + {"1.8.0_13-ea-b002", "1.8.0_13-ea-b001", 0}, + {"1.8.0_13-ea", "1.8.0_13-ea-b001", 0}, + {"1.8.0_13-ea-b002", "8.0.13-ea+b2", 0}, + {"1.8.0_13-ea-b002", "8.0.13+b2", -1}, + {"1.8.0_13-b002", "8.0.13-ea+b2", 1}, + + // pre 1.8 (when the jep 223 was introduced) + {"1.7.0", "7.0.0", 0}, // there is no v7 of the JVM, but we want to honor this comparison since it may be someone mistakenly using the wrong version format + + // invalid but we should work with these + {"1.8.0_131", "1.8.0-update131-b02", 0}, + {"1.8.0_131", "1.8.0-update_131-b02", 0}, + } + + for _, test := range tests { + name := test.v1 + "_vs_" + test.v2 + t.Run(name, func(t *testing.T) { + v1, err := newJvmVersion(test.v1) + require.NotNil(t, v1) + require.NoError(t, err) + + v2, err := newJvmVersion(test.v2) + require.NotNil(t, v2) + require.NoError(t, err) + + actual := v1.compare(*v2) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestConvertNonCompliantSemver(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "simple update", + input: "8.0-update302", + expected: "8.0.302", + }, + { + name: "update with build", + input: "8.0-update302-b08", + expected: "8.0.302+8", + }, + { + name: "update with underscore and build", + input: "8.0-update_302-b08", + expected: "8.0.302+8", + }, + { + name: "version without patch and prerelease", + input: "8.0.0", + expected: "8.0.0", + }, + { + name: "version with patch, no update", + input: "8.0.100", + expected: "8.0.100", + }, + { + name: "version with patch and prerelease", + input: "8.0.0-rc1", + expected: "8.0.0-rc1", + }, + { + name: "invalid update format, no update keyword", + input: "8.0-foo302", + expected: "8.0-foo302", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertNonCompliantSemver(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestVersionJVM_invalid(t *testing.T) { + tests := []struct { + name string + version string + wantErr require.ErrorAssertionFunc + }{ + { + name: "invalid version", + version: "1.a", + wantErr: require.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + v, err := newJvmVersion(tt.version) + assert.Nil(t, v) + tt.wantErr(t, err) + }) + } +} diff --git a/grype/version/version.go b/grype/version/version.go index 3819ecb22d8..6173ccf678d 100644 --- a/grype/version/version.go +++ b/grype/version/version.go @@ -28,6 +28,7 @@ type rich struct { kbVer *kbVersion portVer *portageVersion pep440version *pep440Version + jvmVersion *jvmVersion } func NewVersion(raw string, format Format) (*Version, error) { @@ -45,7 +46,9 @@ func NewVersion(raw string, format Format) (*Version, error) { } func NewVersionFromPkg(p pkg.Package) (*Version, error) { - ver, err := NewVersion(p.Version, FormatFromPkgType(p.Type)) + format := FormatFromPkg(p) + + ver, err := NewVersion(p.Version, format) if err != nil { return nil, err } @@ -96,6 +99,10 @@ func (v *Version) populate() error { ver := newPortageVersion(v.Raw) v.rich.portVer = &ver return nil + case JVMFormat: + ver, err := newJvmVersion(v.Raw) + v.rich.jvmVersion = ver + return err case UnknownFormat: // use the raw string + fuzzy constraint return nil diff --git a/internal/regex_helpers.go b/internal/regex_helpers.go new file mode 100644 index 00000000000..7130f21a89c --- /dev/null +++ b/internal/regex_helpers.go @@ -0,0 +1,45 @@ +package internal + +import "regexp" + +// MatchNamedCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map. +// This is only for the first match in the regex. Callers shouldn't be providing regexes with multiple capture groups with the same name. +func MatchNamedCaptureGroups(regEx *regexp.Regexp, content string) map[string]string { + // note: we are looking across all matches and stopping on the first non-empty match. Why? Take the following example: + // input: "cool something to match against" pattern: `((?Pmatch) (?Pagainst))?`. Since the pattern is + // encapsulated in an optional capture group, there will be results for each character, but the results will match + // on nothing. The only "true" match will be at the end ("match against"). + allMatches := regEx.FindAllStringSubmatch(content, -1) + var results map[string]string + for _, match := range allMatches { + // fill a candidate results map with named capture group results, accepting empty values, but not groups with + // no names + for nameIdx, name := range regEx.SubexpNames() { + if nameIdx > len(match) || len(name) == 0 { + continue + } + if results == nil { + results = make(map[string]string) + } + results[name] = match[nameIdx] + } + // note: since we are looking for the first best potential match we should stop when we find the first one + // with non-empty results. + if !isEmptyMap(results) { + break + } + } + return results +} + +func isEmptyMap(m map[string]string) bool { + if len(m) == 0 { + return true + } + for _, value := range m { + if value != "" { + return false + } + } + return true +} diff --git a/internal/regex_helpers_test.go b/internal/regex_helpers_test.go new file mode 100644 index 00000000000..1c483775309 --- /dev/null +++ b/internal/regex_helpers_test.go @@ -0,0 +1,70 @@ +package internal + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMatchCaptureGroups(t *testing.T) { + tests := []struct { + name string + input string + pattern string + expected map[string]string + }{ + { + name: "go-case", + input: "match this thing", + pattern: `(?Pmatch).*(?Pthing)`, + expected: map[string]string{ + "name": "match", + "version": "thing", + }, + }, + { + name: "only matches the first instance", + input: "match this thing batch another think", + pattern: `(?P[mb]atch).*?(?Pthin[gk])`, + expected: map[string]string{ + "name": "match", + "version": "thing", + }, + }, + { + name: "nested capture groups", + input: "cool something to match against", + pattern: `((?Pmatch) (?Pagainst))`, + expected: map[string]string{ + "name": "match", + "version": "against", + }, + }, + { + name: "nested optional capture groups", + input: "cool something to match against", + pattern: `((?Pmatch) (?Pagainst))?`, + expected: map[string]string{ + "name": "match", + "version": "against", + }, + }, + { + name: "nested optional capture groups with larger match", + input: "cool something to match against match never", + pattern: `.*?((?Pmatch) (?P(against|never)))?`, + expected: map[string]string{ + "name": "match", + "version": "against", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := MatchNamedCaptureGroups(regexp.MustCompile(test.pattern), test.input) + assert.Equal(t, test.expected, actual) + }) + } +} From 8f38ac0528d244359f14cb7e9062b7950179e59b Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 16 Sep 2024 17:34:33 -0400 Subject: [PATCH 2/5] add integration tests Signed-off-by: Alex Goodman --- go.mod | 2 +- go.sum | 4 +- grype/cpe/cpe.go | 2 + grype/internal/packagemetadata/generated.go | 2 +- grype/internal/packagemetadata/names_test.go | 5 + grype/matcher/jvm/matcher_test.go | 152 ++++++++++++++++++ grype/pkg/java_metadata.go | 18 ++- grype/pkg/java_metadata_test.go | 99 ++++++++++++ grype/pkg/package_test.go | 39 +++++ grype/search/cpe.go | 14 +- grype/version/format.go | 2 +- grype/version/generic_constraint.go | 38 +++++ grype/version/golang_constraint.go | 34 +--- grype/version/jvm_constraint.go | 34 +--- test/integration/db_mock_test.go | 9 ++ test/integration/match_by_image_test.go | 115 +++++++++++-- .../image-jvm-match-coverage/Dockerfile | 2 + .../opt/java/openjdk/release | 5 + 18 files changed, 488 insertions(+), 88 deletions(-) create mode 100644 grype/matcher/jvm/matcher_test.go create mode 100644 grype/pkg/java_metadata_test.go create mode 100644 grype/version/generic_constraint.go create mode 100644 test/integration/test-fixtures/image-jvm-match-coverage/Dockerfile create mode 100644 test/integration/test-fixtures/image-jvm-match-coverage/opt/java/openjdk/release diff --git a/go.mod b/go.mod index 50fee004d68..920307e0a48 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 github.com/anchore/packageurl-go v0.1.1-0.20240507183024-848e011fc24f github.com/anchore/stereoscope v0.0.3 - github.com/anchore/syft v1.12.2 + github.com/anchore/syft v1.12.3-0.20240916182519-7c617fd14e11 github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 github.com/bmatcuk/doublestar/v2 v2.0.4 github.com/charmbracelet/bubbletea v1.1.0 diff --git a/go.sum b/go.sum index 0b509373609..76b51d9c5b3 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/anchore/packageurl-go v0.1.1-0.20240507183024-848e011fc24f h1:B/E9ixK github.com/anchore/packageurl-go v0.1.1-0.20240507183024-848e011fc24f/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= github.com/anchore/stereoscope v0.0.3 h1:JRPHySy8S6P+Ff3IDiQ29ap1i8/laUQxDk9K1eFh/2U= github.com/anchore/stereoscope v0.0.3/go.mod h1:5DJheGPjVRsSqegTB24Zi6SCHnYQnA519yeIG+RG+I4= -github.com/anchore/syft v1.12.2 h1:K5YXJ2Ox4C3+Q+rA4jDpsLAoYNd27RMfinvY2JmbEiM= -github.com/anchore/syft v1.12.2/go.mod h1:xFMGMFmhWTK0CJvaKwz6OPVgRdcyCkl7QO/3O/JAXI0= +github.com/anchore/syft v1.12.3-0.20240916182519-7c617fd14e11 h1:M7xJv6jPxyS2GaPmbS+l02YGnO77SGxwcprDhEUupVg= +github.com/anchore/syft v1.12.3-0.20240916182519-7c617fd14e11/go.mod h1:xFMGMFmhWTK0CJvaKwz6OPVgRdcyCkl7QO/3O/JAXI0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= diff --git a/grype/cpe/cpe.go b/grype/cpe/cpe.go index 99d288bb4b5..6f660e10337 100644 --- a/grype/cpe/cpe.go +++ b/grype/cpe/cpe.go @@ -24,8 +24,10 @@ func NewSlice(cpeStrs ...string) ([]cpe.CPE, error) { func MatchWithoutVersion(c cpe.CPE, candidates []cpe.CPE) []cpe.CPE { matches := make([]cpe.CPE, 0) a := wfn.Attributes(c.Attributes) + a.Update = wfn.Any for _, candidate := range candidates { canCopy := wfn.Attributes(candidate.Attributes) + canCopy.Update = wfn.Any if a.MatchWithoutVersion(&canCopy) { matches = append(matches, candidate) } diff --git a/grype/internal/packagemetadata/generated.go b/grype/internal/packagemetadata/generated.go index 4248a11c762..c70c2be3ef0 100644 --- a/grype/internal/packagemetadata/generated.go +++ b/grype/internal/packagemetadata/generated.go @@ -6,5 +6,5 @@ import "github.com/anchore/grype/grype/pkg" // AllTypes returns a list of all pkg metadata types that grype supports (that are represented in the pkg.Package.Metadata field). func AllTypes() []any { - return []any{pkg.ApkMetadata{}, pkg.GolangBinMetadata{}, pkg.GolangModMetadata{}, pkg.JavaMetadata{}, pkg.RpmMetadata{}} + return []any{pkg.ApkMetadata{}, pkg.GolangBinMetadata{}, pkg.GolangModMetadata{}, pkg.JavaMetadata{}, pkg.JavaVMInstallationMetadata{}, pkg.RpmMetadata{}} } diff --git a/grype/internal/packagemetadata/names_test.go b/grype/internal/packagemetadata/names_test.go index 8f3d64e45f7..f1d4e9333b7 100644 --- a/grype/internal/packagemetadata/names_test.go +++ b/grype/internal/packagemetadata/names_test.go @@ -58,6 +58,11 @@ func TestReflectTypeFromJSONName(t *testing.T) { lookup: "RpmMetadata", wantRecord: reflect.TypeOf(pkg.RpmMetadata{}), }, + { + name: "JavaVMInstallationMetadata lookup", + lookup: "JavaVMInstallationMetadata", + wantRecord: reflect.TypeOf(pkg.JavaVMInstallationMetadata{}), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/grype/matcher/jvm/matcher_test.go b/grype/matcher/jvm/matcher_test.go new file mode 100644 index 00000000000..d14230f765a --- /dev/null +++ b/grype/matcher/jvm/matcher_test.go @@ -0,0 +1,152 @@ +package jvm + +import ( + "testing" + + "github.com/google/uuid" + "github.com/scylladb/go-set/strset" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/syft/syft/cpe" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +func TestMatcher(t *testing.T) { + p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "java_se", + Version: "1.8.0_400", + Type: syftPkg.BinaryPkg, + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:oracle:java_se:1.8.0:update400:*:*:*:*:*:*", cpe.DeclaredSource), + }, + } + matcher := Matcher{ + cfg: MatcherConfig{ + UseCPEs: true, + }, + } + store := newMockProvider() + actual, _ := matcher.Match(store, nil, p) + + foundCVEs := strset.New() + for _, v := range actual { + foundCVEs.Add(v.Vulnerability.ID) + + require.NotEmpty(t, v.Details) + for _, d := range v.Details { + assert.Equal(t, match.CPEMatch, d.Type, "indirect match not indicated") + assert.Equal(t, matcher.Type(), d.Matcher, "failed to capture matcher type") + } + assert.Equal(t, p.Name, v.Package.Name, "failed to capture original package name") + } + + expected := strset.New( + "CVE-2024-20919-real", + "CVE-2024-20919-underscore", + "CVE-2024-20919-bonkers-format", + "CVE-2024-20919-post-jep223", + ) + + for _, id := range expected.List() { + if !foundCVEs.Has(id) { + t.Errorf("missing CVE: %s", id) + } + } + + extra := strset.Difference(foundCVEs, expected) + + for _, id := range extra.List() { + t.Errorf("unexpected CVE: %s", id) + } + + if t.Failed() { + t.Logf("discovered CVES: %d", foundCVEs.Size()) + for _, id := range foundCVEs.List() { + t.Logf(" - %s", id) + } + } +} + +func newMockProvider() *mockProvider { + mp := mockProvider{ + data: make(map[syftPkg.Language]map[string][]vulnerability.Vulnerability), + } + + mp.populateData() + + return &mp +} + +type mockProvider struct { + data map[syftPkg.Language]map[string][]vulnerability.Vulnerability +} + +func (mp *mockProvider) Get(_, _ string) ([]vulnerability.Vulnerability, error) { + // TODO implement me + panic("not implemented") +} + +func (mp *mockProvider) populateData() { + + // derived from vuln data found on CVE-2024-20919 + hit := "< 1.8.0_401 || >= 1.9-ea, < 8.0.401 || >= 9-ea, < 11.0.22 || >= 12-ea, < 17.0.10 || >= 18-ea, < 21.0.2" + + mp.data["nvd:cpe"] = map[string][]vulnerability.Vulnerability{ + "java_se": { + { + // positive cases + Constraint: version.MustGetConstraint(hit, version.JVMFormat), + ID: "CVE-2024-20919-real", + }, + { + // positive cases + Constraint: version.MustGetConstraint("< 22.22.22", version.UnknownFormat), + ID: "CVE-2024-20919-bonkers-format", + }, + { + // positive cases + Constraint: version.MustGetConstraint(hit, version.JVMFormat), + ID: "CVE-2024-20919-underscore", + }, + { + // negative case + Constraint: version.MustGetConstraint("< 1.8.0_399 || >= 1.9-ea, < 8.0.399 || >= 9-ea, < 11.0.22 || >= 12-ea, < 17.0.10 || >= 18-ea, < 21.0.2", version.JVMFormat), + ID: "CVE-FAKE-bad-update", + }, + { + // positive case + Constraint: version.MustGetConstraint("< 8.0.401", version.JVMFormat), + ID: "CVE-2024-20919-post-jep223", + }, + { + // negative case + Constraint: version.MustGetConstraint("< 8.0.399", version.JVMFormat), + ID: "CVE-FAKE-bad-range-post-jep223", + }, + { + // negative case + Constraint: version.MustGetConstraint("< 7.0.0", version.JVMFormat), + ID: "CVE-FAKE-bad-range-post-jep223", + }, + }, + } +} + +func (mp *mockProvider) GetByCPE(p cpe.CPE) ([]vulnerability.Vulnerability, error) { + return mp.data["nvd:cpe"][p.Attributes.Product], nil +} + +func (mp *mockProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) { + return []vulnerability.Vulnerability{}, nil +} + +func (mp *mockProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) { + return mp.data[l][p.Name], nil +} diff --git a/grype/pkg/java_metadata.go b/grype/pkg/java_metadata.go index 8b2b2ac08b8..529f1450017 100644 --- a/grype/pkg/java_metadata.go +++ b/grype/pkg/java_metadata.go @@ -1,7 +1,7 @@ package pkg import ( - "strings" + "github.com/scylladb/go-set/strset" "github.com/anchore/syft/syft/pkg" ) @@ -31,15 +31,21 @@ type JavaVMReleaseMetadata struct { } func IsJvmPackage(p Package) bool { + if _, ok := p.Metadata.(JavaVMInstallationMetadata); ok { + return true + } + if p.Type == pkg.BinaryPkg { - if strings.Contains(p.Name, "jdk") || strings.Contains(p.Name, "jre") || strings.Contains(p.Name, "java") { + if HasJvmPackageName(p.Name) { return true } } - if _, ok := p.Metadata.(JavaVMInstallationMetadata); ok { - return true - } - return false } + +var jvmIndications = strset.New("java_se", "jre", "jdk", "zulu", "openjdk", "java") + +func HasJvmPackageName(name string) bool { + return jvmIndications.Has(name) +} diff --git a/grype/pkg/java_metadata_test.go b/grype/pkg/java_metadata_test.go new file mode 100644 index 00000000000..271ff885de0 --- /dev/null +++ b/grype/pkg/java_metadata_test.go @@ -0,0 +1,99 @@ +package pkg + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + syftPkg "github.com/anchore/syft/syft/pkg" +) + +func TestIsJvmPackage(t *testing.T) { + tests := []struct { + name string + pkg Package + expected bool + }{ + { + name: "binary package with jdk in name set", + pkg: Package{ + Type: syftPkg.BinaryPkg, + Name: "jdk", + }, + expected: true, + }, + { + name: "binary package with jre in name set", + pkg: Package{ + Type: syftPkg.BinaryPkg, + Name: "jre", + }, + expected: true, + }, + { + name: "binary package with java_se in name set", + pkg: Package{ + Type: syftPkg.BinaryPkg, + Name: "java_se", + }, + expected: true, + }, + { + name: "binary package with zulu in name set", + pkg: Package{ + Type: syftPkg.BinaryPkg, + Name: "zulu", + }, + expected: true, + }, + { + name: "binary package with openjdk in name set", + pkg: Package{ + Type: syftPkg.BinaryPkg, + Name: "openjdk", + }, + expected: true, + }, + { + name: "binary package without jvm-related name", + pkg: Package{ + Type: syftPkg.BinaryPkg, + Name: "nodejs", + }, + expected: false, + }, + { + name: "non-binary package with jvm-related name", + pkg: Package{ + Type: syftPkg.NpmPkg, // we know this could not be a JVM package installation + Name: "jdk", + }, + expected: false, + }, + { + name: "package with JavaVMInstallationMetadata", + pkg: Package{ + Type: syftPkg.RpmPkg, + Name: "random-package", + Metadata: JavaVMInstallationMetadata{}, + }, + expected: true, + }, + { + name: "package without JavaVMInstallationMetadata", + pkg: Package{ + Type: syftPkg.RpmPkg, + Name: "non-jvm-package", + Metadata: nil, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsJvmPackage(tt.pkg) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go index a0de6471eb4..0e0dcdaba90 100644 --- a/grype/pkg/package_test.go +++ b/grype/pkg/package_test.go @@ -692,6 +692,45 @@ func TestNew(t *testing.T) { }, }, }, + { + name: "jvm-installation-entry", + syftPkg: syftPkg.Package{ + Metadata: syftPkg.JavaVMInstallation{ + Release: syftPkg.JavaVMRelease{ + Implementor: "a", + ImplementorVersion: "a", + JavaRuntimeVersion: "b", + JavaVersion: "c", + JavaVersionDate: "a", + Libc: "a", + Modules: []string{"a"}, + OsArch: "a", + OsName: "a", + OsVersion: "a", + Source: "a", + BuildSource: "a", + BuildSourceRepo: "a", + SourceRepo: "a", + FullVersion: "d", + SemanticVersion: "e", + BuildInfo: "a", + JvmVariant: "a", + JvmVersion: "a", + ImageType: "a", + BuildType: "a", + }, + Files: []string{"a"}, + }, + }, + metadata: JavaVMInstallationMetadata{ + Release: JavaVMReleaseMetadata{ + JavaRuntimeVersion: "b", + JavaVersion: "c", + FullVersion: "d", + SemanticVersion: "e", + }, + }, + }, } // capture each observed metadata type, we should see all of them relate to what syft provides by the end of testing diff --git a/grype/search/cpe.go b/grype/search/cpe.go index 83ef8266639..d876b0e4ce2 100644 --- a/grype/search/cpe.go +++ b/grype/search/cpe.go @@ -109,7 +109,7 @@ func ByPackageCPE(store vulnerability.ProviderByCPE, d *distro.Distro, p pkg.Pac format := version.FormatFromPkg(p) if format == version.JVMFormat { - searchVersion = specificLegacyJvmVersion(searchVersion, c.Attributes.Update) + searchVersion = transformJvmVersion(searchVersion, c.Attributes.Update) } verObj, err := version.NewVersion(searchVersion, format) @@ -147,7 +147,7 @@ func ByPackageCPE(store vulnerability.ProviderByCPE, d *distro.Distro, p pkg.Pac return toMatches(matchesByFingerprint), nil } -func specificLegacyJvmVersion(searchVersion, updateCpeField string) string { +func transformJvmVersion(searchVersion, updateCpeField string) string { // we should take into consideration the CPE update field for JVM packages if strings.HasPrefix(searchVersion, "1.") && !strings.Contains(searchVersion, "_") && updateCpeField != wfn.NA && updateCpeField != wfn.Any { searchVersion = fmt.Sprintf("%s_%s", searchVersion, strings.TrimPrefix(updateCpeField, "update")) @@ -238,7 +238,15 @@ func filterCPEsByVersion(pkgVersion version.Version, allCPEs []cpe.CPE) (matched continue } - constraint, err := version.GetConstraint(c.Attributes.Version, version.UnknownFormat) + ver := c.Attributes.Version + + if pkgVersion.Format == version.JVMFormat { + if c.Attributes.Update != wfn.Any && c.Attributes.Update != wfn.NA { + ver = transformJvmVersion(ver, c.Attributes.Update) + } + } + + constraint, err := version.GetConstraint(ver, pkgVersion.Format) if err != nil { // if we can't get a version constraint, don't filter out the CPE matchedCPEs = append(matchedCPEs, c) diff --git a/grype/version/format.go b/grype/version/format.go index ab7f7de8c93..c4a47c89570 100644 --- a/grype/version/format.go +++ b/grype/version/format.go @@ -25,7 +25,7 @@ const ( type Format int var formatStr = []string{ - "UnknownFormat", + "Unknown", "Semantic", "Apk", "Deb", diff --git a/grype/version/generic_constraint.go b/grype/version/generic_constraint.go new file mode 100644 index 00000000000..ebd039e290d --- /dev/null +++ b/grype/version/generic_constraint.go @@ -0,0 +1,38 @@ +package version + +import "fmt" + +var _ Constraint = (*genericConstraint)(nil) + +type genericConstraint struct { + raw string + expression constraintExpression + name string +} + +func newGenericConstraint(raw string, genFn comparatorGenerator, name string) (genericConstraint, error) { + constraints, err := newConstraintExpression(raw, genFn) + if err != nil { + return genericConstraint{}, err + } + return genericConstraint{ + expression: constraints, + raw: raw, + name: name, + }, nil +} + +func (g genericConstraint) String() string { + value := "none" + if g.raw != "" { + value = g.raw + } + return fmt.Sprintf("%s (%s)", value, g.name) +} + +func (g genericConstraint) Satisfied(version *Version) (bool, error) { + if g.raw == "" { + return true, nil // the empty constraint is always satisfied + } + return g.expression.satisfied(version) +} diff --git a/grype/version/golang_constraint.go b/grype/version/golang_constraint.go index b0fdda9d3d8..f9443311a3d 100644 --- a/grype/version/golang_constraint.go +++ b/grype/version/golang_constraint.go @@ -2,42 +2,14 @@ package version import "fmt" -var _ Constraint = (*golangConstraint)(nil) - -type golangConstraint struct { - raw string - expression constraintExpression -} - -func newGolangConstraint(raw string) (golangConstraint, error) { - constraints, err := newConstraintExpression(raw, newGolangComparator) - if err != nil { - return golangConstraint{}, err - } - return golangConstraint{ - expression: constraints, - raw: raw, - }, nil -} - -func (g golangConstraint) String() string { - if g.raw == "" { - return "none (go)" - } - return fmt.Sprintf("%s (go)", g.raw) -} - -func (g golangConstraint) Satisfied(version *Version) (bool, error) { - if g.raw == "" { - return true, nil // the empty constraint is always satisfied - } - return g.expression.satisfied(version) +func newGolangConstraint(raw string) (Constraint, error) { + return newGenericConstraint(raw, newGolangComparator, "go") } func newGolangComparator(unit constraintUnit) (Comparator, error) { ver, err := newGolangVersion(unit.version) if err != nil { - return nil, fmt.Errorf("unable to parse constraint version (%s): %w", unit.version, err) + return nil, fmt.Errorf("unable to parse Golang constraint version (%s): %w", unit.version, err) } return ver, nil } diff --git a/grype/version/jvm_constraint.go b/grype/version/jvm_constraint.go index bae6e06436e..1f7930f4d5e 100644 --- a/grype/version/jvm_constraint.go +++ b/grype/version/jvm_constraint.go @@ -2,42 +2,14 @@ package version import "fmt" -var _ Constraint = (*jvmConstraint)(nil) - -type jvmConstraint struct { - raw string - expression constraintExpression -} - -func newJvmConstraint(raw string) (jvmConstraint, error) { - constraints, err := newConstraintExpression(raw, newJvmComparator) - if err != nil { - return jvmConstraint{}, err - } - return jvmConstraint{ - expression: constraints, - raw: raw, - }, nil -} - -func (g jvmConstraint) String() string { - if g.raw == "" { - return "none (jvm)" - } - return fmt.Sprintf("%s (jvm)", g.raw) -} - -func (g jvmConstraint) Satisfied(version *Version) (bool, error) { - if g.raw == "" { - return true, nil // the empty constraint is always satisfied - } - return g.expression.satisfied(version) +func newJvmConstraint(raw string) (Constraint, error) { + return newGenericConstraint(raw, newJvmComparator, "jvm") } func newJvmComparator(unit constraintUnit) (Comparator, error) { ver, err := newJvmVersion(unit.version) if err != nil { - return nil, fmt.Errorf("unable to parse constraint version (%s): %w", unit.version, err) + return nil, fmt.Errorf("unable to parse JVM constraint version (%s): %w", unit.version, err) } return ver, nil } diff --git a/test/integration/db_mock_test.go b/test/integration/db_mock_test.go index e71a1ae525b..704eefec8e8 100644 --- a/test/integration/db_mock_test.go +++ b/test/integration/db_mock_test.go @@ -43,6 +43,15 @@ func newMockDbStore() *mockStore { }, backend: map[string]map[string][]grypeDB.Vulnerability{ "nvd:cpe": { + "jdk": []grypeDB.Vulnerability{ + { + ID: "CVE-jdk", + PackageName: "jdk", + VersionConstraint: "< 1.8.0_401", + VersionFormat: "jvm", + CPEs: []string{"cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*"}, + }, + }, "libvncserver": []grypeDB.Vulnerability{ { ID: "CVE-alpine-libvncserver", diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 8f1cdf35b62..1a6341e0842 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -14,7 +14,17 @@ import ( "github.com/anchore/grype/grype/db" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" + "github.com/anchore/grype/grype/matcher/dotnet" + "github.com/anchore/grype/grype/matcher/golang" + "github.com/anchore/grype/grype/matcher/java" + "github.com/anchore/grype/grype/matcher/javascript" + "github.com/anchore/grype/grype/matcher/jvm" + "github.com/anchore/grype/grype/matcher/python" + "github.com/anchore/grype/grype/matcher/ruby" + "github.com/anchore/grype/grype/matcher/rust" + "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/store" "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/grype/vulnerability" @@ -541,6 +551,50 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C }) } +func addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { + packages := catalog.PackagesByPath("/opt/java/openjdk/release") + if len(packages) < 1 { + t.Logf("JVM Packages: %+v", packages) + t.Fatalf("problem with upstream syft cataloger (java-jvm-cataloger)") + } + + for _, p := range packages { + thePkg := pkg.New(p) + theVuln := theStore.backend["nvd:cpe"][strings.ToLower(thePkg.Name)][0] + vulnObj, err := vulnerability.NewVulnerability(theVuln) + vulnObj.CPEs = []cpe.CPE{ + cpe.Must("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", ""), + } + require.NoError(t, err) + + theResult.Add(match.Match{ + Vulnerability: *vulnObj, + Package: thePkg, + Details: []match.Detail{ + { + Type: match.CPEMatch, + Confidence: 0.9, + SearchedBy: search.CPEParameters{ + Namespace: "nvd:cpe", + CPEs: []string{ + "cpe:2.3:a:oracle:jdk:1.8.0:update400:*:*:*:*:*:*", + }, + Package: search.CPEPackageParameter{Name: "jdk", Version: "1.8.0_400-b07"}, + }, + Found: search.CPEResult{ + VulnerabilityID: "CVE-jdk", + VersionConstraint: "< 1.8.0_401 (jvm)", + CPEs: []string{ + "cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", + }, + }, + Matcher: match.JVMMatcher, + }, + }, + }) + } +} + func addRustMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) { packages := catalog.PackagesByPath("/hello-auditable") if len(packages) < 1 { @@ -588,11 +642,11 @@ func TestMatchByImage(t *testing.T) { } tests := []struct { - fixtureImage string - expectedFn func(source.Source, *syftPkg.Collection, *mockStore) match.Matches + name string + expectedFn func(source.Source, *syftPkg.Collection, *mockStore) match.Matches }{ { - fixtureImage: "image-debian-match-coverage", + name: "image-debian-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { expectedMatches := match.NewMatches() addPythonMatches(t, theSource, catalog, theStore, &expectedMatches) @@ -607,7 +661,7 @@ func TestMatchByImage(t *testing.T) { }, }, { - fixtureImage: "image-centos-match-coverage", + name: "image-centos-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { expectedMatches := match.NewMatches() addRhelMatches(t, theSource, catalog, theStore, &expectedMatches) @@ -615,7 +669,7 @@ func TestMatchByImage(t *testing.T) { }, }, { - fixtureImage: "image-alpine-match-coverage", + name: "image-alpine-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { expectedMatches := match.NewMatches() addAlpineMatches(t, theSource, catalog, theStore, &expectedMatches) @@ -623,7 +677,7 @@ func TestMatchByImage(t *testing.T) { }, }, { - fixtureImage: "image-sles-match-coverage", + name: "image-sles-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { expectedMatches := match.NewMatches() addSlesMatches(t, theSource, catalog, theStore, &expectedMatches) @@ -631,7 +685,7 @@ func TestMatchByImage(t *testing.T) { }, }, { - fixtureImage: "image-portage-match-coverage", + name: "image-portage-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { expectedMatches := match.NewMatches() addPortageMatches(t, theSource, catalog, theStore, &expectedMatches) @@ -639,21 +693,29 @@ func TestMatchByImage(t *testing.T) { }, }, { - fixtureImage: "image-rust-auditable-match-coverage", + name: "image-rust-auditable-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { expectedMatches := match.NewMatches() addRustMatches(t, theSource, catalog, theStore, &expectedMatches) return expectedMatches }, }, + { + name: "image-jvm-match-coverage", + expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches { + expectedMatches := match.NewMatches() + addJvmMatches(t, theSource, catalog, theStore, &expectedMatches) + return expectedMatches + }, + }, } for _, test := range tests { - t.Run(test.fixtureImage, func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { theStore := newMockDbStore() - imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage) - tarPath := imagetest.GetFixtureImageTarPath(t, test.fixtureImage) + imagetest.GetFixtureImage(t, "docker-archive", test.name) + tarPath := imagetest.GetFixtureImageTarPath(t, test.name) // this is purely done to help setup mocks theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive")) @@ -671,7 +733,36 @@ func TestMatchByImage(t *testing.T) { require.NoError(t, err) require.NotNil(t, s) - matchers := matcher.NewDefaultMatchers(matcher.Config{}) + // TODO: we need to use the API default configuration, not something hard coded here + matchers := matcher.NewDefaultMatchers(matcher.Config{ + Java: java.MatcherConfig{ + UseCPEs: true, + }, + JVM: jvm.MatcherConfig{ + UseCPEs: true, + }, + Ruby: ruby.MatcherConfig{ + UseCPEs: true, + }, + Python: python.MatcherConfig{ + UseCPEs: true, + }, + Dotnet: dotnet.MatcherConfig{ + UseCPEs: true, + }, + Javascript: javascript.MatcherConfig{ + UseCPEs: true, + }, + Golang: golang.MatcherConfig{ + UseCPEs: true, + }, + Rust: rust.MatcherConfig{ + UseCPEs: true, + }, + Stock: stock.MatcherConfig{ + UseCPEs: true, + }, + }) vp, err := db.NewVulnerabilityProvider(theStore) require.NoError(t, err) diff --git a/test/integration/test-fixtures/image-jvm-match-coverage/Dockerfile b/test/integration/test-fixtures/image-jvm-match-coverage/Dockerfile new file mode 100644 index 00000000000..04fdc270146 --- /dev/null +++ b/test/integration/test-fixtures/image-jvm-match-coverage/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +COPY . . diff --git a/test/integration/test-fixtures/image-jvm-match-coverage/opt/java/openjdk/release b/test/integration/test-fixtures/image-jvm-match-coverage/opt/java/openjdk/release new file mode 100644 index 00000000000..532486dc639 --- /dev/null +++ b/test/integration/test-fixtures/image-jvm-match-coverage/opt/java/openjdk/release @@ -0,0 +1,5 @@ +JAVA_VERSION="1.8.0_400" +FULL_VERSION="1.8.0_400-b07" +NOPE_SEMANTIC_VERSION="8.0.400+7" +IMPLEMENTOR="Oracle" +IMAGE_TYPE="JDK" From d997b31f7737d967ea0a6636ca9b160c39aa3b00 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 18 Sep 2024 09:48:13 -0400 Subject: [PATCH 3/5] remove jvm matcher Signed-off-by: Alex Goodman --- cmd/grype/cli/commands/root.go | 4 -- grype/match/matcher_type.go | 2 - grype/matcher/jvm/matcher.go | 51 -------------------- grype/matcher/matchers.go | 3 -- grype/matcher/stock/matcher.go | 8 --- grype/matcher/{jvm => stock}/matcher_test.go | 7 +-- test/integration/match_by_image_test.go | 6 +-- 7 files changed, 5 insertions(+), 76 deletions(-) delete mode 100644 grype/matcher/jvm/matcher.go rename grype/matcher/{jvm => stock}/matcher_test.go (96%) diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index 1fe2f6db1c8..f23382db24a 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -22,7 +22,6 @@ import ( "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/java" "github.com/anchore/grype/grype/matcher/javascript" - "github.com/anchore/grype/grype/matcher/jvm" "github.com/anchore/grype/grype/matcher/python" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/stock" @@ -286,9 +285,6 @@ func getMatchers(opts *options.Grype) []matcher.Matcher { ExternalSearchConfig: opts.ExternalSources.ToJavaMatcherConfig(), UseCPEs: opts.Match.Java.UseCPEs, }, - JVM: jvm.MatcherConfig{ - UseCPEs: opts.Match.JVM.UseCPEs, - }, Ruby: ruby.MatcherConfig(opts.Match.Ruby), Python: python.MatcherConfig(opts.Match.Python), Dotnet: dotnet.MatcherConfig(opts.Match.Dotnet), diff --git a/grype/match/matcher_type.go b/grype/match/matcher_type.go index 29f6f9255de..ad547c6d94c 100644 --- a/grype/match/matcher_type.go +++ b/grype/match/matcher_type.go @@ -16,7 +16,6 @@ const ( GoModuleMatcher MatcherType = "go-module-matcher" OpenVexMatcher MatcherType = "openvex-matcher" RustMatcher MatcherType = "rust-matcher" - JVMMatcher MatcherType = "jvm-matcher" ) var AllMatcherTypes = []MatcherType{ @@ -33,7 +32,6 @@ var AllMatcherTypes = []MatcherType{ GoModuleMatcher, OpenVexMatcher, RustMatcher, - JVMMatcher, } type MatcherType string diff --git a/grype/matcher/jvm/matcher.go b/grype/matcher/jvm/matcher.go deleted file mode 100644 index e26b1b39752..00000000000 --- a/grype/matcher/jvm/matcher.go +++ /dev/null @@ -1,51 +0,0 @@ -package jvm - -import ( - "fmt" - - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/search" - "github.com/anchore/grype/grype/vulnerability" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -type MatcherConfig struct { - UseCPEs bool -} - -type Matcher struct { - cfg MatcherConfig -} - -func NewJVMMatcher(cfg MatcherConfig) *Matcher { - return &Matcher{ - cfg: cfg, - } -} - -func (m *Matcher) PackageTypes() []syftPkg.Type { - return []syftPkg.Type{syftPkg.BinaryPkg} -} - -func (m *Matcher) Type() match.MatcherType { - return match.JVMMatcher -} - -func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - if !pkg.IsJvmPackage(p) { - return nil, nil - } - - criteria := search.CommonCriteria - if m.cfg.UseCPEs { - criteria = append(criteria, search.ByCPE) - } - matches, err := search.ByCriteria(store, d, p, m.Type(), criteria...) - if err != nil { - return nil, fmt.Errorf("failed to match by exact package: %w", err) - } - - return matches, nil -} diff --git a/grype/matcher/matchers.go b/grype/matcher/matchers.go index 8c538433b5b..72778eb6292 100644 --- a/grype/matcher/matchers.go +++ b/grype/matcher/matchers.go @@ -7,7 +7,6 @@ import ( "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/java" "github.com/anchore/grype/grype/matcher/javascript" - "github.com/anchore/grype/grype/matcher/jvm" "github.com/anchore/grype/grype/matcher/msrc" "github.com/anchore/grype/grype/matcher/portage" "github.com/anchore/grype/grype/matcher/python" @@ -20,7 +19,6 @@ import ( // Config contains values used by individual matcher structs for advanced configuration type Config struct { Java java.MatcherConfig - JVM jvm.MatcherConfig Ruby ruby.MatcherConfig Python python.MatcherConfig Dotnet dotnet.MatcherConfig @@ -38,7 +36,6 @@ func NewDefaultMatchers(mc Config) []Matcher { dotnet.NewDotnetMatcher(mc.Dotnet), &rpm.Matcher{}, java.NewJavaMatcher(mc.Java), - jvm.NewJVMMatcher(mc.JVM), javascript.NewJavascriptMatcher(mc.Javascript), &apk.Matcher{}, golang.NewGolangMatcher(mc.Golang), diff --git a/grype/matcher/stock/matcher.go b/grype/matcher/stock/matcher.go index 661f4b67604..7f30a52df9a 100644 --- a/grype/matcher/stock/matcher.go +++ b/grype/matcher/stock/matcher.go @@ -32,17 +32,9 @@ func (m *Matcher) Type() match.MatcherType { } func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - if !inboundsForMatcher(p) { - return nil, nil - } - criteria := search.CommonCriteria if m.cfg.UseCPEs { criteria = append(criteria, search.ByCPE) } return search.ByCriteria(store, d, p, m.Type(), criteria...) } - -func inboundsForMatcher(p pkg.Package) bool { - return !pkg.IsJvmPackage(p) -} diff --git a/grype/matcher/jvm/matcher_test.go b/grype/matcher/stock/matcher_test.go similarity index 96% rename from grype/matcher/jvm/matcher_test.go rename to grype/matcher/stock/matcher_test.go index d14230f765a..f50335462c8 100644 --- a/grype/matcher/jvm/matcher_test.go +++ b/grype/matcher/stock/matcher_test.go @@ -1,4 +1,4 @@ -package jvm +package stock import ( "testing" @@ -17,7 +17,7 @@ import ( syftPkg "github.com/anchore/syft/syft/pkg" ) -func TestMatcher(t *testing.T) { +func TestMatcher_JVMPackage(t *testing.T) { p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "java_se", @@ -33,7 +33,8 @@ func TestMatcher(t *testing.T) { }, } store := newMockProvider() - actual, _ := matcher.Match(store, nil, p) + actual, err := matcher.Match(store, nil, p) + require.NoError(t, err) foundCVEs := strset.New() for _, v := range actual { diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 1a6341e0842..25da2b0fc0f 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -18,7 +18,6 @@ import ( "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/java" "github.com/anchore/grype/grype/matcher/javascript" - "github.com/anchore/grype/grype/matcher/jvm" "github.com/anchore/grype/grype/matcher/python" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/rust" @@ -588,7 +587,7 @@ func addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Colle "cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", }, }, - Matcher: match.JVMMatcher, + Matcher: match.StockMatcher, }, }, }) @@ -738,9 +737,6 @@ func TestMatchByImage(t *testing.T) { Java: java.MatcherConfig{ UseCPEs: true, }, - JVM: jvm.MatcherConfig{ - UseCPEs: true, - }, Ruby: ruby.MatcherConfig{ UseCPEs: true, }, From eeb97555c81a125f7a51ac485528d03a093c885b Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 18 Sep 2024 12:43:11 -0400 Subject: [PATCH 4/5] add syft binary jre/jdk packages Signed-off-by: Alex Goodman --- grype/pkg/java_metadata.go | 2 +- grype/pkg/java_metadata_test.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/grype/pkg/java_metadata.go b/grype/pkg/java_metadata.go index 529f1450017..88d82014588 100644 --- a/grype/pkg/java_metadata.go +++ b/grype/pkg/java_metadata.go @@ -44,7 +44,7 @@ func IsJvmPackage(p Package) bool { return false } -var jvmIndications = strset.New("java_se", "jre", "jdk", "zulu", "openjdk", "java") +var jvmIndications = strset.New("java_se", "jre", "jdk", "zulu", "openjdk", "java", "java/jre", "java/jdk") func HasJvmPackageName(name string) bool { return jvmIndications.Has(name) diff --git a/grype/pkg/java_metadata_test.go b/grype/pkg/java_metadata_test.go index 271ff885de0..1f93622e15b 100644 --- a/grype/pkg/java_metadata_test.go +++ b/grype/pkg/java_metadata_test.go @@ -54,6 +54,22 @@ func TestIsJvmPackage(t *testing.T) { }, expected: true, }, + { + name: "binary package from syft (java/jdk", + pkg: Package{ + Type: syftPkg.BinaryPkg, + Name: "java/jre", + }, + expected: true, + }, + { + name: "binary package from syft (java/jre)", + pkg: Package{ + Type: syftPkg.BinaryPkg, + Name: "java/jdk", + }, + expected: true, + }, { name: "binary package without jvm-related name", pkg: Package{ From ebeb8f800ace9ae629ba376c0541043a3328ee52 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 18 Sep 2024 13:15:37 -0400 Subject: [PATCH 5/5] bump syft rev to get permissive glob change Signed-off-by: Alex Goodman --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 920307e0a48..1e97a2c3d6f 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 github.com/anchore/packageurl-go v0.1.1-0.20240507183024-848e011fc24f github.com/anchore/stereoscope v0.0.3 - github.com/anchore/syft v1.12.3-0.20240916182519-7c617fd14e11 + github.com/anchore/syft v1.12.3-0.20240918171143-1629043d7a0d github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 github.com/bmatcuk/doublestar/v2 v2.0.4 github.com/charmbracelet/bubbletea v1.1.0 diff --git a/go.sum b/go.sum index 76b51d9c5b3..891111da530 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/anchore/packageurl-go v0.1.1-0.20240507183024-848e011fc24f h1:B/E9ixK github.com/anchore/packageurl-go v0.1.1-0.20240507183024-848e011fc24f/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= github.com/anchore/stereoscope v0.0.3 h1:JRPHySy8S6P+Ff3IDiQ29ap1i8/laUQxDk9K1eFh/2U= github.com/anchore/stereoscope v0.0.3/go.mod h1:5DJheGPjVRsSqegTB24Zi6SCHnYQnA519yeIG+RG+I4= -github.com/anchore/syft v1.12.3-0.20240916182519-7c617fd14e11 h1:M7xJv6jPxyS2GaPmbS+l02YGnO77SGxwcprDhEUupVg= -github.com/anchore/syft v1.12.3-0.20240916182519-7c617fd14e11/go.mod h1:xFMGMFmhWTK0CJvaKwz6OPVgRdcyCkl7QO/3O/JAXI0= +github.com/anchore/syft v1.12.3-0.20240918171143-1629043d7a0d h1:Pl1fOL2fNaT6kXpDXOhGSYT+hG9Mgur/PSxMnd1Z+yc= +github.com/anchore/syft v1.12.3-0.20240918171143-1629043d7a0d/go.mod h1:xFMGMFmhWTK0CJvaKwz6OPVgRdcyCkl7QO/3O/JAXI0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=