diff --git a/internal/regex_helpers.go b/internal/regex_helpers.go index 78a024c4b..dce1d3496 100644 --- a/internal/regex_helpers.go +++ b/internal/regex_helpers.go @@ -41,11 +41,11 @@ func MatchNamedCaptureGroups(regEx *regexp.Regexp, content string) map[string]st // 1.5x the reader chunk size (1MB * 1.5). func MatchNamedCaptureGroupsFromReader(re *regexp.Regexp, r io.Reader) (map[string]string, error) { results := make(map[string]string) - _, err := processReaderInChunks(r, readerChunkSize, matchNamedCaptureGroupsHandler(re, results)) + matches, err := processReaderInChunks(r, readerChunkSize, matchNamedCaptureGroupsHandler(re, results)) if err != nil { return nil, err } - if len(results) == 0 { + if !matches { return nil, nil } return results, nil diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index 79b2c295c..3d79fc349 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -697,21 +697,21 @@ func Test_Cataloger_PositiveCases(t *testing.T) { { logicalFixture: "java-jre-openjdk/1.8.0_352-b08/linux-amd64", expected: pkg.Package{ - Name: "java/jre", + Name: "openjdk", Version: "1.8.0_352-b08", Type: "binary", - PURL: "pkg:generic/java/jre@1.8.0_352-b08", + PURL: "pkg:generic/oracle/openjdk@1.8.0_352-b08", Locations: locations("java"), - Metadata: metadata("java-binary-openjdk", "java"), + Metadata: metadata("java-binary-openjdk-with-update", "java"), }, }, { logicalFixture: "java-jre-openjdk/11.0.17/linux-amd64", expected: pkg.Package{ - Name: "java/jre", + Name: "openjdk", Version: "11.0.17+8-LTS", Type: "binary", - PURL: "pkg:generic/java/jre@11.0.17%2B8-LTS", + PURL: "pkg:generic/oracle/openjdk@11.0.17%2B8-LTS", Locations: locations("java"), Metadata: metadata("java-binary-openjdk", "java"), }, @@ -719,10 +719,10 @@ func Test_Cataloger_PositiveCases(t *testing.T) { { logicalFixture: "java-jre-openjdk-eclipse/11.0.22/linux-amd64", expected: pkg.Package{ - Name: "java/jre", + Name: "openjdk", Version: "11.0.22+7", Type: "binary", - PURL: "pkg:generic/java/jre@11.0.22%2B7", + PURL: "pkg:generic/oracle/openjdk@11.0.22%2B7", Locations: locations("java"), Metadata: metadata("java-binary-openjdk", "java"), }, @@ -730,10 +730,10 @@ func Test_Cataloger_PositiveCases(t *testing.T) { { logicalFixture: "java-jre-openjdk-arm64-eclipse/11.0.22/linux-arm64", expected: pkg.Package{ - Name: "java/jre", + Name: "openjdk", Version: "11.0.22+7", Type: "binary", - PURL: "pkg:generic/java/jre@11.0.22%2B7", + PURL: "pkg:generic/oracle/openjdk@11.0.22%2B7", Locations: locations("java"), Metadata: metadata("java-binary-openjdk", "java"), }, @@ -741,10 +741,10 @@ func Test_Cataloger_PositiveCases(t *testing.T) { { logicalFixture: "java-graal-openjdk/17.0.3+7-jvmci-22.1-b06/linux-amd64", expected: pkg.Package{ - Name: "java/graalvm", + Name: "graalvm", Version: "17.0.3+7-jvmci-22.1-b06", Type: "binary", - PURL: "pkg:generic/java/graalvm@17.0.3%2B7-jvmci-22.1-b06", + PURL: "pkg:generic/oracle/graalvm@17.0.3%2B7-jvmci-22.1-b06", Locations: locations("java"), Metadata: metadata("java-binary-graalvm", "java"), }, @@ -754,10 +754,10 @@ func Test_Cataloger_PositiveCases(t *testing.T) { // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo logicalFixture: "java-jre-oracle/19.0.1/linux-amd64", expected: pkg.Package{ - Name: "java/jre", + Name: "jre", Version: "19.0.1+10-21", Type: "binary", - PURL: "pkg:generic/java/jre@19.0.1%2B10-21", + PURL: "pkg:generic/oracle/jre@19.0.1%2B10-21", Locations: locations("java"), Metadata: metadata("java-binary-oracle", "java"), }, @@ -767,10 +767,10 @@ func Test_Cataloger_PositiveCases(t *testing.T) { // note: cannot find the original binary, using a custom snippet based on the original snippet in the repo logicalFixture: "java-jre-oracle/19.0.1/darwin", expected: pkg.Package{ - Name: "java/jre", + Name: "jre", Version: "19.0.1+10-21", Type: "binary", - PURL: "pkg:generic/java/jre@19.0.1%2B10-21", + PURL: "pkg:generic/oracle/jre@19.0.1%2B10-21", Locations: locations("java"), Metadata: metadata("java-binary-oracle", "java"), }, @@ -778,23 +778,45 @@ func Test_Cataloger_PositiveCases(t *testing.T) { { logicalFixture: "java-jre-ibm/1.8.0_391/linux-amd64", expected: pkg.Package{ - Name: "java/jre", + Name: "java", Version: "1.8.0-foreman_2023_10_12_13_27-b00", Type: "binary", - PURL: "pkg:generic/java/jre@1.8.0-foreman_2023_10_12_13_27-b00", + PURL: "pkg:generic/ibm/java@1.8.0-foreman_2023_10_12_13_27-b00", Locations: locations("java"), Metadata: metadata("java-binary-ibm", "java"), }, }, + { + logicalFixture: "java-ibm-8-jre/1.8.0_451/linux-amd64", + expected: pkg.Package{ + Name: "java", + Version: "1.8.0-_2025_04_14_02_37-b00", + Type: "binary", + PURL: "pkg:generic/ibm/java@1.8.0-_2025_04_14_02_37-b00", + Locations: locations("java"), + Metadata: metadata("java-binary-ibm", "java"), + }, + }, + { + logicalFixture: "java-ibm-8-sdk-alpine/1.8.0_321/linux-amd64", + expected: pkg.Package{ + Name: "java_sdk", + Version: "1.8.0-foreman_2022_01_20_09_33-b00", + Type: "binary", + PURL: "pkg:generic/ibm/java_sdk@1.8.0-foreman_2022_01_20_09_33-b00", + Locations: locations("jdb"), + Metadata: metadata("java-sdk-binary-ibm", "jdb"), + }, + }, { logicalFixture: "java-jdk-openjdk/21.0.2+13-LTS/linux-amd64", expected: pkg.Package{ - Name: "java/jdk", + Name: "openjdk", Version: "21.0.2+13-LTS", Type: "binary", - PURL: "pkg:generic/java/jdk@21.0.2%2B13-LTS", + PURL: "pkg:generic/oracle/openjdk@21.0.2%2B13-LTS", Locations: locations("jdb"), - Metadata: metadata("java-binary-jdk", "java"), + Metadata: metadata("java-binary-openjdk-fallthrough", "jdb"), }, }, { @@ -1463,8 +1485,8 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) { Class: "foo-binary", FileGlob: "**/foo", EvidenceMatcher: binutils.FileContentsVersionMatcher( - `(?m)foobar\s(?P[0-9]+\.[0-9]+\.[0-9]+)`, catalogerName, + `(?m)foobar\s(?P[0-9]+\.[0-9]+\.[0-9]+)`, ), Package: "foo", PURL: mustPURL("pkg:generic/foo@version"), @@ -1521,7 +1543,7 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) { binutils.Classifier{ Class: "foo-binary", FileGlob: "**/foo", - EvidenceMatcher: binutils.FileContentsVersionMatcher(`(?m)not there`, catalogerName), + EvidenceMatcher: binutils.FileContentsVersionMatcher(catalogerName, `(?m)not there`), Package: "foo", PURL: mustPURL("pkg:generic/foo@version"), CPEs: singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"), @@ -1739,7 +1761,7 @@ func TestCatalogerConfig_MarshalJSON(t *testing.T) { { Class: "class", FileGlob: "glob", - EvidenceMatcher: binutils.FileContentsVersionMatcher(".thing", catalogerName), + EvidenceMatcher: binutils.FileContentsVersionMatcher(catalogerName, ".thing"), Package: "pkg", PURL: packageurl.PackageURL{ Type: "type", diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go index 90b403e6c..08bfd2a23 100644 --- a/syft/pkg/cataloger/binary/classifiers.go +++ b/syft/pkg/cataloger/binary/classifiers.go @@ -28,11 +28,11 @@ func DefaultClassifiers() []binutils.Classifier { // ruby 2.7.7p221 (2022-11-24 revision 168ec2b1e5) [x86_64-linux] `(?m)ruby (?P[0-9]+\.[0-9]+\.[0-9]+((p|preview|rc|dev)[0-9]*)?) `) - return []binutils.Classifier{ + classifiers := []binutils.Classifier{ { Class: "python-binary", FileGlob: "**/python*", - EvidenceMatcher: binutils.EvidenceMatchers( + EvidenceMatcher: binutils.MatchAny( // try to find version information from libpython shared libraries binutils.SharedLibraryLookup( `^libpython[0-9]+(?:\.[0-9]+)+[a-z]?\.so.*$`, @@ -98,7 +98,7 @@ func DefaultClassifiers() []binutils.Classifier { { Class: "redis-binary", FileGlob: "**/redis-server", - EvidenceMatcher: binutils.EvidenceMatchers( + EvidenceMatcher: binutils.MatchAny( // matches most recent versions of redis (~v7), e.g. "7.0.14buildkitsandbox-1702957741000000000" m.FileContentsVersionMatcher(`[^\d](?P\d+.\d+\.\d+)buildkitsandbox-\d+`), // matches against older versions of redis (~v3 - v6), e.g. "4.0.11841ce7054bd9-1542359302000000000" @@ -113,73 +113,10 @@ func DefaultClassifiers() []binutils.Classifier { cpe.Must("cpe:2.3:a:redis:redis:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), }, }, - { - Class: "java-binary-openjdk", - FileGlob: "**/java", - EvidenceMatcher: binutils.MatchExcluding( - binutils.EvidenceMatchers( - m.FileContentsVersionMatcher( - // [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL] - // [NUL]openjdk[NUL]java[NUL]1.8[NUL]1.8.0_352-b08[NUL] - `(?m)\x00openjdk\x00java\x00(?P[0-9]+[.0-9]*)\x00(?P[0-9]+[^\x00]+)\x00`), - m.FileContentsVersionMatcher( - // arm64 versions: [NUL]0.0[NUL][NUL][NUL][NUL][NUL]11.0.22+7[NUL][NUL][NUL][NUL][NUL][NUL][NUL]openjdk[NUL]java[NUL] - `(?m)\x00(?P[0-9]+[.0-9]*)\x00+(?P[0-9]+[^\x00]+)\x00+openjdk\x00java`), - ), - // don't match graalvm - "-jvmci-", - ), - Package: "java/jre", - PURL: mustPURL("pkg:generic/java/jre@version"), - // TODO the updates might need to be part of the CPE Attributes, like: 1.8.0:update152 - CPEs: singleCPE("cpe:2.3:a:oracle:openjdk:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), - }, - { - Class: "java-binary-ibm", - FileGlob: "**/java", - EvidenceMatcher: m.FileContentsVersionMatcher( - // [NUL]java[NUL]1.8[NUL][NUL][NUL][NUL]1.8.0-foreman_2022_09_22_15_30-b00[NUL] - `(?m)\x00java\x00(?P[0-9]+[.0-9]+)\x00{4}(?P[0-9]+[-._a-zA-Z0-9]+)\x00`), - Package: "java/jre", - PURL: mustPURL("pkg:generic/java/jre@version"), - CPEs: singleCPE("cpe:2.3:a:ibm:java:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), - }, - { - Class: "java-binary-oracle", - FileGlob: "**/java", - EvidenceMatcher: binutils.MatchExcluding( - m.FileContentsVersionMatcher( - // [NUL]19.0.1+10-21[NUL] - `(?m)\x00(?P[0-9]+[.0-9]+[+][-0-9]+)\x00`), - // don't match openjdk - `\x00openjdk\x00`, - ), - Package: "java/jre", - PURL: mustPURL("pkg:generic/java/jre@version"), - CPEs: singleCPE("cpe:2.3:a:oracle:jre:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), - }, - { - Class: "java-binary-graalvm", - FileGlob: "**/java", - EvidenceMatcher: m.FileContentsVersionMatcher( - `(?m)\x00(?P[0-9]+[.0-9]+[.0-9]+\+[0-9]+-jvmci-[0-9]+[.0-9]+-b[0-9]+)\x00`), - Package: "java/graalvm", - PURL: mustPURL("pkg:generic/java/graalvm@version"), - CPEs: singleCPE("cpe:2.3:a:oracle:graalvm:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), - }, - { - Class: "java-binary-jdk", - FileGlob: "**/jdb", - EvidenceMatcher: m.FileContentsVersionMatcher( - `(?m)\x00(?P[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?([-._a-zA-Z0-9]+)?)\x00`), - Package: "java/jdk", - PURL: mustPURL("pkg:generic/java/jdk@version"), - CPEs: singleCPE("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), - }, { Class: "nodejs-binary", FileGlob: "**/node", - EvidenceMatcher: binutils.EvidenceMatchers( + EvidenceMatcher: binutils.MatchAny( // [NUL]node v0.10.48[NUL] // [NUL]v0.12.18[NUL] // [NUL]v4.9.1[NUL] @@ -221,7 +158,7 @@ func DefaultClassifiers() []binutils.Classifier { { Class: "haproxy-binary", FileGlob: "**/haproxy", - EvidenceMatcher: binutils.EvidenceMatchers( + EvidenceMatcher: binutils.MatchAny( m.FileContentsVersionMatcher(`(?m)version (?P[0-9]+\.[0-9]+(\.|-dev|-rc)[0-9]+)(-[a-z0-9]{7})?, released 20`), m.FileContentsVersionMatcher(`(?m)HA-Proxy version (?P[0-9]+\.[0-9]+(\.|-dev)[0-9]+)`), m.FileContentsVersionMatcher(`(?m)(?P[0-9]+\.[0-9]+(\.|-dev)[0-9]+)-[0-9a-zA-Z]{7}.+HAProxy version`), @@ -303,7 +240,7 @@ func DefaultClassifiers() []binutils.Classifier { { Class: "mysql-binary", FileGlob: "**/mysql", - EvidenceMatcher: binutils.EvidenceMatchers( + EvidenceMatcher: binutils.MatchAny( // shutdown[NUL]8.0.37[NUL][NUL][NUL][NUL][NUL]mysql_real_esc m.FileContentsVersionMatcher(`\x00(?P[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)\x00+mysql`), // /export/home/pb2/build/sb_0-26781090-1516292385.58/release/mysql-8.0.4-rc/mysys_ssl/my_default.cc @@ -380,7 +317,7 @@ func DefaultClassifiers() []binutils.Classifier { { Class: "ruby-binary", FileGlob: "**/ruby", - EvidenceMatcher: binutils.EvidenceMatchers( + EvidenceMatcher: binutils.MatchAny( rubyMatcher, binutils.SharedLibraryLookup( // try to find version information from libruby shared libraries @@ -394,7 +331,7 @@ func DefaultClassifiers() []binutils.Classifier { { Class: "erlang-binary", FileGlob: "**/erlexec", - EvidenceMatcher: binutils.EvidenceMatchers( + EvidenceMatcher: binutils.MatchAny( m.FileContentsVersionMatcher( // [NUL]/usr/src/otp_src_25.3.2.6/erts/ `(?m)/src/otp_src_(?P[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)/erts/`, @@ -411,7 +348,7 @@ func DefaultClassifiers() []binutils.Classifier { { Class: "erlang-alpine-binary", FileGlob: "**/beam.smp", - EvidenceMatcher: binutils.EvidenceMatchers( + EvidenceMatcher: binutils.MatchAny( m.FileContentsVersionMatcher( // [NUL]/usr/src/otp_src_25.3.2.6/erts/ `(?m)/src/otp_src_(?P[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)/erts/`, @@ -432,7 +369,7 @@ func DefaultClassifiers() []binutils.Classifier { { Class: "erlang-library", FileGlob: "**/liberts_internal.a", - EvidenceMatcher: binutils.EvidenceMatchers( + EvidenceMatcher: binutils.MatchAny( m.FileContentsVersionMatcher( // [NUL]/usr/src/otp_src_25.3.2.6/erts/ `(?m)/src/otp_src_(?P[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)/erts/`, @@ -681,6 +618,8 @@ func DefaultClassifiers() []binutils.Classifier { CPEs: singleCPE("cpe:2.3:a:google:chrome:*:*:*:*:*:*:*:*"), }, } + + return append(classifiers, defaultJavaClassifiers()...) } // singleCPE returns a []cpe.CPE with Source: Generated based on the cpe string or panics if the diff --git a/syft/pkg/cataloger/binary/classifiers_java.go b/syft/pkg/cataloger/binary/classifiers_java.go new file mode 100644 index 000000000..af87dedd4 --- /dev/null +++ b/syft/pkg/cataloger/binary/classifiers_java.go @@ -0,0 +1,196 @@ +package binary + +import ( + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/pkg/cataloger/internal/binutils" +) + +//nolint:funlen +func defaultJavaClassifiers() []binutils.Classifier { + m := binutils.ContextualEvidenceMatchers{CatalogerName: catalogerName} + + return []binutils.Classifier{ + { + Class: "java-binary", + FileGlob: "**/java", + EvidenceMatcher: binutils.BranchingEvidenceMatcher([]binutils.Classifier{ + { + Class: "java-binary-graalvm", + EvidenceMatcher: m.FileContentsVersionMatcher( + `(?m)\x00(?P[0-9]+[.0-9]+[.0-9]+\+[0-9]+-jvmci-[0-9]+[.0-9]+-b[0-9]+)\x00`), + Package: "graalvm", + PURL: mustPURL("pkg:generic/oracle/graalvm@version"), + CPEs: singleCPE("cpe:2.3:a:oracle:graalvm:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-binary-openjdk-zulu", + EvidenceMatcher: binutils.MatchAll( + binutils.MatchPath(`**/*zulu*/**`), + binutils.MatchAny( + m.FileContentsVersionMatcher( + // [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL] + `(?m)\x00java\x00(?P[0-9]+[.0-9]*)\x00(?P[0-9]+[^\x00]+)\x00`), + m.FileContentsVersionMatcher( + // arm64 versions: [NUL]0.0[NUL][NUL][NUL][NUL][NUL]11.0.22+7[NUL][NUL][NUL][NUL][NUL][NUL][NUL]openjdk[NUL]java[NUL] + `(?m)\x00(?P[0-9]+[.0-9]*)\x00+(?P[0-9]+[^\x00]+)\x00+(openjdk|java)`), + ), + ), + Package: "zulu", + PURL: mustPURL("pkg:generic/azul/zulu@version"), + CPEs: singleCPE("cpe:2.3:a:azul:zulu:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-binary-openjdk-with-update", + EvidenceMatcher: binutils.MatchAny( + m.FileContentsVersionMatcher( + `openjdk`, + // [NUL]openjdk[NUL]java[NUL]1.8[NUL]1.8.0_352-b08[NUL] + `(?m)java\x00(?P[0-9]+[.0-9]*)\x00(?P(?[0-9]+[^\x00]+)_(?[^\x00]+)-[^\x00]+)\x00`), + m.FileContentsVersionMatcher( + `openjdk`, + // arm64 versions: [NUL]0.0[NUL][NUL][NUL][NUL][NUL]1.8.0_352-b08[NUL][NUL][NUL][NUL][NUL][NUL][NUL]openjdk[NUL]java[NUL] + `(?m)\x00(?P[0-9]+[.0-9]*)\x00+(?P(?[0-9]+[^\x00]+)_(?[^\x00]+)-[^\x00]+)\x00+openjdk\x00java`), + ), + Package: "openjdk", + PURL: mustPURL("pkg:generic/oracle/openjdk@version"), + CPEs: singleCPE("cpe:2.3:a:oracle:openjdk:{{.primary}}:update{{.update}}:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-binary-openjdk", + EvidenceMatcher: binutils.MatchAny( + m.FileContentsVersionMatcher( + // [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL] + `(?m)\x00openjdk\x00java\x00(?P[0-9]+[.0-9]*)\x00(?P[0-9]+[^\x00]+)\x00`), + m.FileContentsVersionMatcher( + // arm64 versions: [NUL]0.0[NUL][NUL][NUL][NUL][NUL]11.0.22+7[NUL][NUL][NUL][NUL][NUL][NUL][NUL]openjdk[NUL]java[NUL] + `(?m)\x00(?P[0-9]+[.0-9]*)\x00+(?P[0-9]+[^\x00]+)\x00+openjdk\x00java`), + ), + Package: "openjdk", + PURL: mustPURL("pkg:generic/oracle/openjdk@version"), + CPEs: singleCPE("cpe:2.3:a:oracle:openjdk:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-binary-ibm", + EvidenceMatcher: binutils.MatchAll( + binutils.MatchAny( + binutils.MatchPath("**/ibm/**"), + binutils.SharedLibraryLookup( + `^libjli\.so$`, + m.FileContentsVersionMatcher("IBM_JAVA")), + ), + m.FileContentsVersionMatcher( + // [NUL]java[NUL]1.8[NUL][NUL][NUL]1.8.0-foreman_2022_01_20_09_33-b00[NUL] + `(?m)\x00java\x00+(?P[0-9]+[.0-9]+)\x00+(?P[0-9]+[-._a-zA-Z0-9]+)\x00`), + ), + Package: "java", + PURL: mustPURL("pkg:generic/ibm/java@version"), + CPEs: singleCPE("cpe:2.3:a:ibm:java:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-binary-openjdk-fallthrough", + EvidenceMatcher: m.FileContentsVersionMatcher( + "openjdk", + // [NUL]19.0.1+10-21[NUL] + `(?m)\x00(?P[0-9]+[.0-9]+[+][-0-9]+)\x00`, + ), + Package: "jre", + PURL: mustPURL("pkg:generic/oracle/jre@version"), + CPEs: singleCPE("cpe:2.3:a:oracle:jre:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-binary-oracle", + EvidenceMatcher: m.FileContentsVersionMatcher( + // [NUL]19.0.1+10-21[NUL] + // java[NUL]1.8[NUL]1.8.0_451-b10 + `(?m)\x00(?P[0-9]+\.[0-9]+\.[-._+a-zA-Z0-9]+)\x00`, + ), + Package: "jre", + PURL: mustPURL("pkg:generic/oracle/jre@version"), + CPEs: singleCPE("cpe:2.3:a:oracle:jre:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + }...), + }, + { + Class: "java-jdb-binary", + FileGlob: "**/jdb", + EvidenceMatcher: binutils.BranchingEvidenceMatcher([]binutils.Classifier{ + { + Class: "java-binary-graalvm", + EvidenceMatcher: m.FileContentsVersionMatcher( + `(?m)\x00(?P[0-9]+[.0-9]+[.0-9]+\+[0-9]+-jvmci-[0-9]+[.0-9]+-b[0-9]+)\x00`), + Package: "graalvm", + PURL: mustPURL("pkg:generic/oracle/graalvm@version"), + CPEs: singleCPE("cpe:2.3:a:oracle:graalvm_for_jdk:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-jdb-binary-openjdk", + EvidenceMatcher: binutils.MatchAll( + m.FileContentsVersionMatcher( + // [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL] + // [NUL]openjdk[NUL]java[NUL]1.8[NUL]1.8.0_352-b08[NUL] + `(?m)\x00openjdk\x00java\x00(?P[0-9]+[.0-9]*)\x00(?P[0-9]+[^\x00]+)\x00`), + m.FileContentsVersionMatcher( + // arm64 versions: [NUL]0.0[NUL][NUL][NUL][NUL][NUL]11.0.22+7[NUL][NUL][NUL][NUL][NUL][NUL][NUL]openjdk[NUL]java[NUL] + `(?m)\x00(?P[0-9]+[.0-9]*)\x00+(?P[0-9]+[^\x00]+)\x00+openjdk\x00java`), + ), + Package: "openjdk", + PURL: mustPURL("pkg:generic/oracle/openjdk@version"), + CPEs: singleCPE("cpe:2.3:a:oracle:openjdk:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "jdb-binary-openjdk-zulu", + EvidenceMatcher: binutils.MatchAll( + binutils.MatchPath("**/*zulu*/**"), + binutils.MatchAny( + m.FileContentsVersionMatcher( + // [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL] + `(?m)\x00openjdk\x00java\x00(?P[0-9]+[.0-9]*)\x00(?P[0-9]+[^\x00]+)\x00`), + m.FileContentsVersionMatcher( + // arm64 versions: [NUL]0.0[NUL][NUL][NUL][NUL][NUL]11.0.22+7[NUL][NUL][NUL][NUL][NUL][NUL][NUL]openjdk[NUL]java[NUL] + `(?m)\x00(?P[0-9]+[.0-9]*)\x00+(?P[0-9]+[^\x00]+)\x00+openjdk\x00java`), + ), + ), + Package: "zulu", + PURL: mustPURL("pkg:generic/azul/zulu@version"), + CPEs: singleCPE("cpe:2.3:a:azul:zulu:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-sdk-binary-ibm", + EvidenceMatcher: binutils.MatchAll( + binutils.MatchAny( + binutils.MatchPath("**/ibm/**"), + binutils.SharedLibraryLookup( + `^libjli\.so$`, + m.FileContentsVersionMatcher("IBM_JAVA")), + ), + m.FileContentsVersionMatcher( + // [NUL]java[NUL]./lib/tools.jar./lib/sa-jdi.jar./classes.-J-ms8m[NUL][NUL]1.8[NUL][NUL][NUL]1.8.0-foreman_2022_01_20_09_33-b00[NUL] + `(?m)\x00java\x00.+?\x00(?P[0-9]+[-._a-zA-Z0-9]+)\x00`), + ), + Package: "java_sdk", + PURL: mustPURL("pkg:generic/ibm/java_sdk@version"), + CPEs: singleCPE("cpe:2.3:a:ibm:java_sdk:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-binary-openjdk-fallthrough", + EvidenceMatcher: binutils.MatchAll( + m.FileContentsVersionMatcher( + "openjdk", + `(?m)\x00(?P[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?([-._a-zA-Z0-9]+)?)\x00`), + ), + Package: "openjdk", + PURL: mustPURL("pkg:generic/oracle/openjdk@version"), + CPEs: singleCPE("cpe:2.3:a:oracle:openjdk:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + { + Class: "java-binary-jdk", + EvidenceMatcher: m.FileContentsVersionMatcher( + `(?m)\x00(?P[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?([-._a-zA-Z0-9]+)?)\x00`), + Package: "jdk", + PURL: mustPURL("pkg:generic/oracle/jdk@version"), + CPEs: singleCPE("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), + }, + }...), + }, + } +} diff --git a/syft/pkg/cataloger/binary/deprecated.go b/syft/pkg/cataloger/binary/deprecated.go index effccbc73..a8ecbea79 100644 --- a/syft/pkg/cataloger/binary/deprecated.go +++ b/syft/pkg/cataloger/binary/deprecated.go @@ -14,5 +14,5 @@ type EvidenceMatcher = binutils.EvidenceMatcher func FileContentsVersionMatcher( pattern string, ) EvidenceMatcher { - return binutils.FileContentsVersionMatcher(pattern, catalogerName) + return binutils.FileContentsVersionMatcher(catalogerName, pattern) } diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jdk-oracle/1.8.0_451-b10/linux-amd64/java b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jdk-oracle/1.8.0_451-b10/linux-amd64/java new file mode 100644 index 000000000..3a2382f62 Binary files /dev/null and b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jdk-oracle/1.8.0_451-b10/linux-amd64/java differ diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jdk-oracle/1.8.0_451-b10/linux-amd64/jdb b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jdk-oracle/1.8.0_451-b10/linux-amd64/jdb new file mode 100644 index 000000000..52db5e05b Binary files /dev/null and b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jdk-oracle/1.8.0_451-b10/linux-amd64/jdb differ diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jre-ibm/1.8.0_391/linux-amd64/java b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jre-ibm/1.8.0_391/linux-amd64/java deleted file mode 100644 index e555bc208..000000000 Binary files a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jre-ibm/1.8.0_391/linux-amd64/java and /dev/null differ diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jre-oracle-sdk/1.8.0_451-b10/linux-amd64/java b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jre-oracle-sdk/1.8.0_451-b10/linux-amd64/java new file mode 100644 index 000000000..3a2382f62 Binary files /dev/null and b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jre-oracle-sdk/1.8.0_451-b10/linux-amd64/java differ diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jre-oracle/1.8.0_451-b10/linux-amd64/java b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jre-oracle/1.8.0_451-b10/linux-amd64/java new file mode 100644 index 000000000..3a2382f62 Binary files /dev/null and b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/java-jre-oracle/1.8.0_451-b10/linux-amd64/java differ diff --git a/syft/pkg/cataloger/binary/test-fixtures/config.yaml b/syft/pkg/cataloger/binary/test-fixtures/config.yaml index b55527b63..a2b23c81a 100644 --- a/syft/pkg/cataloger/binary/test-fixtures/config.yaml +++ b/syft/pkg/cataloger/binary/test-fixtures/config.yaml @@ -194,6 +194,25 @@ from-images: platform: linux/amd64 paths: - /opt/ibm/java/jre/bin/java + - /opt/ibm/java/jre/lib/amd64/jli/libjli.so + + - name: java-ibm-8-jre + version: 1.8.0_451 + images: + - ref: ibmjava:8-jre@sha256:3588cd1cc9b8646fe03b3b15210e69b1b520f1321f8518b69c0e7013d702fd23 + platform: linux/amd64 + paths: + - /opt/ibm/java/jre/bin/java + - /opt/ibm/java/jre/lib/amd64/jli/libjli.so + + - name: java-ibm-8-sdk-alpine + version: 1.8.0_321 + images: + - ref: ibmjava:8-sdk-alpine@sha256:4f8ad2029e78f7b91721745a77fc6011a7c0e09b9edeffb6b20b6ec34a6e63cd + platform: linux/amd64 + paths: + - /opt/ibm/java/bin/jdb + - /opt/ibm/java/lib/amd64/jli/libjli.so - version: 10.6.15 images: @@ -320,6 +339,23 @@ from-images: paths: - /usr/local/bin/node + - name: java-jdk-oracle + version: 1.8.0_451-b10 + images: + - ref: container-registry.oracle.com/java/jdk:8u451-oraclelinux8@sha256:f9c980e52853bd0a8815ad908c9a4025ed68cb52f31f3b8cd0cd442ee367bfd1 + platform: linux/amd64 + paths: + - /usr/java/jdk-8/bin/java + - /usr/java/jdk-8/bin/jdb + + - name: java-jre-oracle-sdk + version: 1.8.0_451-b10 + images: + - ref: container-registry.oracle.com/java/serverjre:1.8.0_451@sha256:fe78fe3efd292dceb5685882ce1b13eb78492f383ccfd52a1b88a4f755b9d996 + platform: linux/amd64 + paths: + - /usr/java/jdk-8/bin/java + - name: java-jre-openjdk version: 1.8.0_352-b08 images: diff --git a/syft/pkg/cataloger/internal/binutils/branching_matcher.go b/syft/pkg/cataloger/internal/binutils/branching_matcher.go new file mode 100644 index 000000000..54f4484ed --- /dev/null +++ b/syft/pkg/cataloger/internal/binutils/branching_matcher.go @@ -0,0 +1,43 @@ +package binutils + +import ( + "io" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/internal/unionreader" + "github.com/anchore/syft/syft/pkg" +) + +// BranchingEvidenceMatcher loads the contents of the reader into memory, assuming that all +// classifier matchers are going to be reading the same file, e.g. a "java" binary, continuing to +// try classifier EvidenceMatcher until the first set of packages is found +func BranchingEvidenceMatcher(classifiers ...Classifier) EvidenceMatcher { + return func(_ Classifier, context MatcherContext) ([]pkg.Package, error) { + // we are scanning the same contents multiple times, so read it into memory for a reader that can reset + rdr, err := getReader(context) + if err != nil { + return nil, err + } + defer internal.CloseAndLogError(rdr, context.Location.RealPath) + if err != nil { + return nil, err + } + for _, c := range classifiers { + pkgs, err := c.EvidenceMatcher(c, MatcherContext{ + Resolver: context.Resolver, + Location: context.Location, + GetReader: func(_ MatcherContext) (unionreader.UnionReader, error) { + _, err := rdr.Seek(0, io.SeekStart) + if err != nil { + return nil, err + } + return rdr, nil + }, + }) + if len(pkgs) > 0 || err != nil { + return pkgs, err + } + } + return nil, nil + } +} diff --git a/syft/pkg/cataloger/internal/binutils/branching_matcher_test.go b/syft/pkg/cataloger/internal/binutils/branching_matcher_test.go new file mode 100644 index 000000000..faf6a7633 --- /dev/null +++ b/syft/pkg/cataloger/internal/binutils/branching_matcher_test.go @@ -0,0 +1,95 @@ +package binutils + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/unionreader" +) + +func Test_BranchingMatcher(t *testing.T) { + matchingTest := FileContentsVersionMatcher("", `my-verison:(?\d+\.\d+)`) + notMatchingTest := MatchPath("**/not-version*") + + tests := []struct { + name string + matcher EvidenceMatcher + expectedPackageNames []string + }{ + { + name: "not matching", + matcher: BranchingEvidenceMatcher( + Classifier{ + EvidenceMatcher: MatchAll( + notMatchingTest, + matchingTest, + ), + Package: "a-pkg", + }, + ), + expectedPackageNames: nil, + }, + { + name: "both match", + matcher: BranchingEvidenceMatcher( + Classifier{ + EvidenceMatcher: matchingTest, + Package: "a-pkg", + }, + Classifier{ + EvidenceMatcher: matchingTest, + Package: "b-pkg", + }, + ), + expectedPackageNames: []string{"a-pkg"}, + }, + { + name: "first-does-not-match", + matcher: BranchingEvidenceMatcher( + Classifier{ + EvidenceMatcher: MatchAll( + notMatchingTest, + matchingTest, + ), + Package: "b-pkg", + }, + Classifier{ + EvidenceMatcher: matchingTest, + Package: "c-pkg", + }, + ), + expectedPackageNames: []string{"c-pkg"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + resolver := file.NewMockResolverForPaths("test-fixtures/version.txt", "test-fixtures/version-parts.txt") + locs, err := resolver.FilesByGlob("**/version.txt") + require.NoError(t, err) + require.Len(t, locs, 1) + loc := locs[0] + rdr, err := resolver.FileContentsByLocation(loc) + require.NoError(t, err) + urdr, err := unionreader.GetUnionReader(rdr) + require.NoError(t, err) + pkgs, err := test.matcher(Classifier{ + Package: "a-pkg", + }, MatcherContext{ + Resolver: resolver, + Location: loc, + GetReader: func(resolver MatcherContext) (unionreader.UnionReader, error) { + return urdr, nil + }, + }) + require.NoError(t, err) + var got []string + for i := range pkgs { + got = append(got, pkgs[i].Name) + } + require.EqualValues(t, test.expectedPackageNames, got) + }) + } +} diff --git a/syft/pkg/cataloger/internal/binutils/classifier.go b/syft/pkg/cataloger/internal/binutils/classifier.go index c48a657c6..e01ea9666 100644 --- a/syft/pkg/cataloger/internal/binutils/classifier.go +++ b/syft/pkg/cataloger/internal/binutils/classifier.go @@ -8,11 +8,14 @@ import ( "encoding/json" "fmt" "io" + "maps" "regexp" "strconv" "strings" "text/template" + "github.com/bmatcuk/doublestar/v4" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" @@ -71,7 +74,8 @@ func (cfg Classifier) MarshalJSON() ([]byte, error) { return json.Marshal(m) } -// EvidenceMatcher is a function called to catalog Packages that match some sort of evidence +// EvidenceMatcher is a function called to identify based on some sort of evidence in the filesystem contents. +// A non-nil return value indicates a successful match, regardless of packages being returned. type EvidenceMatcher func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) type MatcherContext struct { @@ -80,13 +84,16 @@ type MatcherContext struct { GetReader func(resolver MatcherContext) (unionreader.UnionReader, error) } -func EvidenceMatchers(matchers ...EvidenceMatcher) EvidenceMatcher { +// MatchAny returns a combined evidence matcher that returns results from the first +// matcher that returns results +func MatchAny(matchers ...EvidenceMatcher) EvidenceMatcher { return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) { for _, matcher := range matchers { match, err := matcher(classifier, context) if err != nil { return nil, err } + // only return when results if match != nil { return match, nil } @@ -95,6 +102,23 @@ func EvidenceMatchers(matchers ...EvidenceMatcher) EvidenceMatcher { } } +// MatchAll executes all matchers until one returns nil results, only returning the final results +func MatchAll(matchers ...EvidenceMatcher) EvidenceMatcher { + return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) { + var out []pkg.Package + for _, matcher := range matchers { + match, err := matcher(classifier, context) + if match == nil || err != nil { + return nil, err + } + if len(match) > 0 { + out = match + } + } + return out, nil + } +} + type ContextualEvidenceMatchers struct { CatalogerName string } @@ -103,8 +127,8 @@ func (c ContextualEvidenceMatchers) FileNameTemplateVersionMatcher(fileNamePatte return FileNameTemplateVersionMatcher(fileNamePattern, contentTemplate, c.CatalogerName) } -func (c ContextualEvidenceMatchers) FileContentsVersionMatcher(pattern string) EvidenceMatcher { - return FileContentsVersionMatcher(pattern, c.CatalogerName) +func (c ContextualEvidenceMatchers) FileContentsVersionMatcher(patterns ...string) EvidenceMatcher { + return FileContentsVersionMatcher(c.CatalogerName, patterns...) } func FileNameTemplateVersionMatcher(fileNamePattern, contentTemplate, catalogerName string) EvidenceMatcher { @@ -156,17 +180,36 @@ func FileNameTemplateVersionMatcher(fileNamePattern, contentTemplate, catalogerN } } -func FileContentsVersionMatcher(pattern, catalogerName string) EvidenceMatcher { - pat := regexp.MustCompile(pattern) +// FileContentsVersionMatcher will match all provided patterns, extracting named capture groups from each pattern, overwriting earlier results +func FileContentsVersionMatcher(catalogerName string, patterns ...string) EvidenceMatcher { + if len(patterns) == 0 { + panic("must specify at least one pattern") + } + var pats []*regexp.Regexp + for _, pattern := range patterns { + pats = append(pats, regexp.MustCompile(pattern)) + } return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) { - contents, err := getReader(context) - if err != nil { - return nil, fmt.Errorf("unable to get read contents for file: %w", err) - } + var matchMetadata map[string]string - matchMetadata, err := internal.MatchNamedCaptureGroupsFromReader(pat, contents) - if err != nil { - return nil, fmt.Errorf("unable to match version: %w", err) + for _, pat := range pats { + contents, err := getReader(context) + if err != nil { + return nil, fmt.Errorf("unable to get read contents for file: %w", err) + } + + match, err := internal.MatchNamedCaptureGroupsFromReader(pat, contents) + if err != nil { + return nil, fmt.Errorf("unable to match version: %w", err) + } + if match == nil { + return nil, nil + } + if matchMetadata == nil { + matchMetadata = match + } else { + maps.Copy(matchMetadata, match) + } } // Convert {major: 1, minor: 2, patch: 3} to "1.2.3" @@ -187,6 +230,10 @@ func FileContentsVersionMatcher(pattern, catalogerName string) EvidenceMatcher { p := NewClassifierPackage(classifier, context.Location, matchMetadata, catalogerName) if p == nil { + if matchMetadata != nil { + // if we had a successful metadata match, but no packages, return a successful match result + return []pkg.Package{}, nil + } return nil, nil } @@ -194,29 +241,6 @@ func FileContentsVersionMatcher(pattern, catalogerName string) EvidenceMatcher { } } -// MatchExcluding tests the provided regular expressions against the file, and if matched, DOES NOT return -// anything that the matcher would otherwise return -func MatchExcluding(matcher EvidenceMatcher, contentPatternsToExclude ...string) EvidenceMatcher { - var nonMatchPatterns []*regexp.Regexp - for _, p := range contentPatternsToExclude { - nonMatchPatterns = append(nonMatchPatterns, regexp.MustCompile(p)) - } - return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) { - contents, err := getReader(context) - if err != nil { - return nil, fmt.Errorf("unable to get read contents for file: %w", err) - } - matches, err := internal.MatchAnyFromReader(contents, nonMatchPatterns...) - if err != nil { - return nil, fmt.Errorf("unable to match content: %w", err) - } - if matches { - return nil, nil - } - return matcher(classifier, context) - } -} - func SharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher EvidenceMatcher) EvidenceMatcher { pat := regexp.MustCompile(sharedLibraryPattern) return func(classifier Classifier, context MatcherContext) (packages []pkg.Package, _ error) { @@ -234,16 +258,19 @@ func SharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher Evide return nil, err } for _, libraryLocation := range locations { + // create a new resolver without the cached context lookup -- this is decidedly a different file newResolver := MatcherContext{ - Resolver: context.Resolver, - Location: libraryLocation, - GetReader: context.GetReader, + Resolver: context.Resolver, + Location: libraryLocation, } - newResolver.Location = libraryLocation pkgs, err := sharedLibraryMatcher(classifier, newResolver) if err != nil { return nil, err } + // not a successful match + if pkgs == nil { + continue + } for _, p := range pkgs { // set the source binary as the first location locationSet := file.NewLocationSet(context.Location) @@ -260,12 +287,28 @@ func SharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher Evide } packages = append(packages, p) } + // return non-nil package results as a successful match indication if the evidence matcher returned a successful match indication + if packages == nil { + packages = pkgs + } } } return packages, nil } } +func MatchPath(path string) EvidenceMatcher { + if !doublestar.ValidatePattern(path) { + panic("invalid pattern") + } + return func(_ Classifier, context MatcherContext) ([]pkg.Package, error) { + if doublestar.MatchUnvalidated(path, context.Location.RealPath) { + return []pkg.Package{}, nil // return non-nil + } + return nil, nil + } +} + func getReader(context MatcherContext) (unionreader.UnionReader, error) { if context.GetReader != nil { return context.GetReader(context) diff --git a/syft/pkg/cataloger/internal/binutils/classifier_package.go b/syft/pkg/cataloger/internal/binutils/classifier_package.go index 897a88e97..42831ddae 100644 --- a/syft/pkg/cataloger/internal/binutils/classifier_package.go +++ b/syft/pkg/cataloger/internal/binutils/classifier_package.go @@ -1,9 +1,12 @@ package binutils import ( + "bytes" "reflect" + "text/template" "github.com/anchore/packageurl-go" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" @@ -21,8 +24,8 @@ func NewClassifierPackage(classifier Classifier, location file.Location, matchMe var cpes []cpe.CPE for _, c := range classifier.CPEs { - c.Attributes.Version = version - c.Attributes.Update = update + c.Attributes.Version = templatedUpdate(c.Attributes.Version, matchMetadata, version) + c.Attributes.Update = templatedUpdate(c.Attributes.Update, matchMetadata, update) cpes = append(cpes, c) } @@ -55,3 +58,28 @@ func NewClassifierPackage(classifier Classifier, location file.Location, matchMe return &p } + +func templatedUpdate(providedValue string, matchMetadata map[string]string, defaultValue string) string { + // if no template provided, just use the value directly + if providedValue == "" { + return defaultValue + } + // support templated updates + t, err := template.New("").Option("missingkey=zero").Parse(providedValue) + if err != nil { + log.Debugf("unable to parse classifier template=%q : %w", providedValue, err) + } else { + update := bytes.Buffer{} + err = t.Execute(&update, matchMetadata) + if err != nil { + log.Debugf("unable to render template: %w", err) + } else { + // only use the template result if it's non-empty + providedValue = update.String() + if providedValue != "" { + return providedValue + } + } + } + return defaultValue +} diff --git a/syft/pkg/cataloger/internal/binutils/classifier_test.go b/syft/pkg/cataloger/internal/binutils/classifier_test.go index 0cfb0f9a4..e0497957d 100644 --- a/syft/pkg/cataloger/internal/binutils/classifier_test.go +++ b/syft/pkg/cataloger/internal/binutils/classifier_test.go @@ -27,7 +27,7 @@ func Test_ClassifierCPEs(t *testing.T) { classifier: Classifier{ Package: "some-app", FileGlob: "**/version.txt", - EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P[0-9.]+)`, "cataloger-name"), + EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-verison:(?P[0-9.]+)`), CPEs: []cpe.CPE{}, }, cpes: nil, @@ -38,7 +38,7 @@ func Test_ClassifierCPEs(t *testing.T) { classifier: Classifier{ Package: "some-app", FileGlob: "**/version.txt", - EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P[0-9.]+)`, "cataloger-name"), + EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-verison:(?P[0-9.]+)`), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource), }, @@ -53,7 +53,7 @@ func Test_ClassifierCPEs(t *testing.T) { classifier: Classifier{ Package: "some-app", FileGlob: "**/version.txt", - EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P[0-9.]+)`, "cataloger-name"), + EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-verison:(?P[0-9.]+)`), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource), cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*", cpe.GeneratedSource), @@ -70,7 +70,7 @@ func Test_ClassifierCPEs(t *testing.T) { classifier: Classifier{ Package: "some-app", FileGlob: "**/version-parts.txt", - EvidenceMatcher: FileContentsVersionMatcher(`(?m)\x00(?P[0-9.]+)\x00(?P[0-9.]+)\x00(?P[0-9.]+)\x00`, "cataloger-name"), + EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)\x00(?P[0-9.]+)\x00(?P[0-9.]+)\x00(?P[0-9.]+)\x00`), CPEs: []cpe.CPE{}, }, cpes: nil, @@ -113,7 +113,7 @@ func TestClassifier_MarshalJSON(t *testing.T) { classifier: Classifier{ Class: "class", FileGlob: "glob", - EvidenceMatcher: FileContentsVersionMatcher(".thing", "cataloger-name"), + EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", ".thing"), Package: "pkg", PURL: packageurl.PackageURL{ Type: "type", @@ -168,7 +168,7 @@ func TestFileContentsVersionMatcher(t *testing.T) { mockGetContent := func(context MatcherContext) (unionreader.UnionReader, error) { return unionreader.GetUnionReader(io.NopCloser(bytes.NewBufferString(tt.data))) } - fn := FileContentsVersionMatcher(tt.pattern, "cataloger-name") + fn := FileContentsVersionMatcher("cataloger-name", tt.pattern) p, err := fn(Classifier{}, MatcherContext{ GetReader: mockGetContent, }) diff --git a/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go b/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go index fb2ec7d91..fd2db0aad 100644 --- a/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go +++ b/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go @@ -11,7 +11,7 @@ import ( "path" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" @@ -36,16 +36,16 @@ func TestParseNativeImage(t *testing.T) { for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { f, err := os.Open("test-fixtures/java-builds/packages/" + test.fixture) - assert.NoError(t, err) + require.NoError(t, err) readerCloser := io.NopCloser(f) reader, err := unionreader.GetUnionReader(readerCloser) - assert.NoError(t, err) + require.NoError(t, err) parsed := false readers, err := unionreader.GetReaders(reader) - assert.NoError(t, err) + require.NoError(t, err) for _, r := range readers { ni, err := test.newFn(test.fixture, r) - assert.NoError(t, err) + require.NoError(t, err) _, _, err = ni.fetchPkgs() if err == nil { t.Fatalf("should have failed to extract SBOM.") @@ -228,11 +228,11 @@ func buildPackageMap(packages []pkg.Package) map[string]pkg.Package { } func verifyPackageFields(t *testing.T, expected, actual pkg.Package) { - assert.Equal(t, expected.Name, actual.Name) - assert.Equal(t, expected.Version, actual.Version) - assert.Equal(t, expected.FoundBy, actual.FoundBy) - assert.Equal(t, expected.PURL, actual.PURL) - assert.ElementsMatch(t, expected.CPEs, actual.CPEs) + require.Equal(t, expected.Name, actual.Name) + require.Equal(t, expected.Version, actual.Version) + require.Equal(t, expected.FoundBy, actual.FoundBy) + require.Equal(t, expected.PURL, actual.PURL) + require.ElementsMatch(t, expected.CPEs, actual.CPEs) } func verifyRelationships(t *testing.T, expected, actual []artifact.Relationship) { @@ -260,8 +260,8 @@ func buildRelationshipMap(relationships []artifact.Relationship) map[string][]ar } func verifyRelationshipFields(t *testing.T, expected, actual []artifact.Relationship) { - assert.Equal(t, len(expected), len(actual)) + require.Equal(t, len(expected), len(actual)) for i := range expected { - assert.Equal(t, expected[i].Type, actual[i].Type) + require.Equal(t, expected[i].Type, actual[i].Type) } } diff --git a/syft/pkg/cataloger/java/parse_jvm_release.go b/syft/pkg/cataloger/java/parse_jvm_release.go index df1a5cf5e..3ef5233ee 100644 --- a/syft/pkg/cataloger/java/parse_jvm_release.go +++ b/syft/pkg/cataloger/java/parse_jvm_release.go @@ -91,7 +91,7 @@ func parseJVMRelease(_ context.Context, resolver file.Resolver, _ *generic.Envir // the reason we use the reference to get the real path is in cases where the file cataloger is involved // (thus the real path is not available except for in the original file reference). This is important // since the path is critical for distinguishing between different JVM vendors. - vendor, product := jvmPrimaryVendorProduct(ri.Implementor, string(reader.Reference().RealPath), ri.ImageType, hasJdk) + vendor, product := jvmPrimaryVendorProduct(ri, string(reader.Reference().RealPath), hasJdk) p := pkg.Package{ Name: product, @@ -163,11 +163,11 @@ func jvmPurl(ri pkg.JavaVMRelease, version, vendor, product string) string { return pURL.ToString() } -func jvmPrimaryVendorProduct(implementor, path, imageType string, hasJdk bool) (string, string) { - implementor = strings.ReplaceAll(strings.ToLower(implementor), " ", "") +func jvmPrimaryVendorProduct(ri *pkg.JavaVMRelease, path string, hasJdk bool) (string, string) { + implementor := strings.ReplaceAll(strings.ToLower(ri.Implementor), " ", "") pickProduct := func() string { - if hasJdk || jvmProjectByType(imageType) == jdk { + if hasJdk || jvmProjectByType(ri.ImageType) == jdk { return jdk } return jre @@ -180,7 +180,13 @@ func jvmPrimaryVendorProduct(implementor, path, imageType string, hasJdk bool) ( case strings.Contains(implementor, "sun"): return "sun", pickProduct() - case strings.Contains(implementor, "oracle") || strings.Contains(path, "oracle"): + case strings.Contains(implementor, "ibm") || strings.Contains(path, "/ibm"): + if hasJdk { + return "ibm", "java_sdk" + } + return "ibm", "java" + + case strings.Contains(implementor, "oracle") || strings.Contains(path, "oracle") || strings.Contains(ri.BuildType, "commercial"): return oracleVendor, pickProduct() } return oracleVendor, openJdkProduct diff --git a/syft/pkg/cataloger/java/parse_jvm_release_test.go b/syft/pkg/cataloger/java/parse_jvm_release_test.go index 08a8cb4d5..0e7c32da7 100644 --- a/syft/pkg/cataloger/java/parse_jvm_release_test.go +++ b/syft/pkg/cataloger/java/parse_jvm_release_test.go @@ -307,6 +307,7 @@ func TestJvmPrimaryVendorProduct(t *testing.T) { tests := []struct { name string implementor string + buildType string path string imageType string hasJdk bool @@ -340,6 +341,25 @@ func TestJvmPrimaryVendorProduct(t *testing.T) { expectedVendor: "oracle", expectedProduct: "jre", }, + { + name: "Oracle commercial build type with JRE", + implementor: "", + buildType: "commercial", + path: "/usr/lib/jvm/jdk8/release", + imageType: "JRE", + hasJdk: false, + expectedVendor: "oracle", + expectedProduct: "jre", + }, + { + name: "Oracle commercial build type with JDK", + implementor: "", + buildType: "commercial", + path: "/usr/lib/jvm/jdk8/release", + hasJdk: true, + expectedVendor: "oracle", + expectedProduct: "jdk", + }, { name: "Oracle vendor with JDK in path", implementor: "", @@ -367,11 +387,29 @@ func TestJvmPrimaryVendorProduct(t *testing.T) { expectedVendor: "oracle", // corretto upstream is oracle openjdk expectedProduct: "openjdk", }, + { + name: "IBM JRE", + path: "/opt/ibm/java/release", + expectedVendor: "ibm", + expectedProduct: "java", + }, + { + name: "IBM JDK", + path: "/opt/ibm/java/release", + hasJdk: true, + expectedVendor: "ibm", + expectedProduct: "java_sdk", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - vendor, product := jvmPrimaryVendorProduct(tt.implementor, tt.path, tt.imageType, tt.hasJdk) + ri := pkg.JavaVMRelease{ + Implementor: tt.implementor, + BuildType: tt.buildType, + ImageType: tt.imageType, + } + vendor, product := jvmPrimaryVendorProduct(&ri, tt.path, tt.hasJdk) assert.Equal(t, tt.expectedVendor, vendor) assert.Equal(t, tt.expectedProduct, product) }) diff --git a/syft/pkg/cataloger/php/interpreter_cataloger.go b/syft/pkg/cataloger/php/interpreter_cataloger.go index a4abe2433..4c1619eb3 100644 --- a/syft/pkg/cataloger/php/interpreter_cataloger.go +++ b/syft/pkg/cataloger/php/interpreter_cataloger.go @@ -219,7 +219,7 @@ func (p interpreterCataloger) getClassifier(realPath string) (string, *binutils. return name, &binutils.Classifier{ Class: fmt.Sprintf("php-ext-%s-binary", name), - EvidenceMatcher: binutils.FileContentsVersionMatcher(match, p.name), + EvidenceMatcher: binutils.FileContentsVersionMatcher(p.name, match), Package: name, PURL: packageurl.PackageURL{ Type: packageurl.TypeGeneric,