From 2f8682b3db6c993fa9dc852775e7345eecb0e51d Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 25 Jan 2022 10:29:16 -0500 Subject: [PATCH] Add ability to merge matches (#602) * enable merging of matches Signed-off-by: Alex Goodman * add ability for matches constructor to take initial matches Signed-off-by: Alex Goodman * update tests to include IDs on package objects Signed-off-by: Alex Goodman * rename common matcher helper package to search package Signed-off-by: Alex Goodman * rename search functions and add SearchByCriteria Signed-off-by: Alex Goodman * cleanup imports Signed-off-by: Alex Goodman --- cmd/root_test.go | 11 +- go.mod | 6 +- go.sum | 22 +-- grype/db/v3/namespace_test.go | 12 ++ grype/db/vulnerability_provider_test.go | 3 + grype/match/details.go | 54 ++++++ grype/match/explicit_ignores_test.go | 24 ++- grype/match/fingerprint.go | 31 +++ grype/match/ignore.go | 4 +- grype/match/ignore_test.go | 30 +-- grype/match/match.go | 46 +++-- grype/match/matcher_type.go | 43 ++--- grype/match/matches.go | 63 +++--- grype/match/matches_test.go | 103 +++++++--- grype/match/type.go | 45 ++--- grype/matcher/apk/matcher.go | 14 +- grype/matcher/apk/matcher_test.go | 48 +++-- grype/matcher/common/language_matchers.go | 57 ------ grype/matcher/controller.go | 2 +- grype/matcher/dpkg/matcher.go | 14 +- grype/matcher/dpkg/matcher_test.go | 11 +- grype/matcher/java/matcher.go | 18 +- grype/matcher/javascript/matcher.go | 18 +- grype/matcher/msrc/matcher.go | 13 +- grype/matcher/msrc/matcher_test.go | 4 + grype/matcher/python/matcher.go | 18 +- grype/matcher/rpmdb/matcher.go | 11 +- grype/matcher/rpmdb/matcher_test.go | 18 +- grype/matcher/ruby/matcher.go | 18 +- grype/matcher/stock/matcher.go | 21 +- grype/pkg/package_test.go | 3 +- grype/presenter/cyclonedx/presenter_test.go | 53 +++-- .../snapshot/TestCycloneDxPresenterDir.golden | 34 ++-- .../TestCycloneDxPresenterImage.golden | 34 ++-- .../presenter/cyclonedx/vulnerability_test.go | 6 +- grype/presenter/json/presenter_test.go | 39 ++-- .../snapshot/TestJsonDirsPresenter.golden | 3 + .../snapshot/TestJsonImgsPresenter.golden | 3 + grype/presenter/models/document_test.go | 22 ++- grype/presenter/models/match.go | 8 +- grype/presenter/models/models_helpers.go | 17 +- grype/presenter/table/presenter_test.go | 17 +- .../common/cpe_matchers.go => search/cpe.go} | 61 +++--- .../cpe_test.go} | 182 +++++++++--------- grype/search/criteria.go | 47 +++++ .../distro_matchers.go => search/distro.go} | 41 ++-- .../distro_test.go} | 19 +- grype/search/language.go | 52 +++++ .../language_test.go} | 11 +- grype/search/only_vulnerable_versions.go | 34 ++++ .../{matcher/common => search}/utils_test.go | 2 +- grype/vulnerability/vulnerability.go | 2 +- internal/config/registry_test.go | 3 +- test/integration/match_by_image_test.go | 68 ++++--- .../match_by_sbom_document_test.go | 20 +- 55 files changed, 874 insertions(+), 689 deletions(-) create mode 100644 grype/match/details.go create mode 100644 grype/match/fingerprint.go delete mode 100644 grype/matcher/common/language_matchers.go rename grype/{matcher/common/cpe_matchers.go => search/cpe.go} (73%) rename grype/{matcher/common/cpe_matchers_test.go => search/cpe_test.go} (87%) create mode 100644 grype/search/criteria.go rename grype/{matcher/common/distro_matchers.go => search/distro.go} (52%) rename grype/{matcher/common/distro_matchers_test.go => search/distro_test.go} (89%) create mode 100644 grype/search/language.go rename grype/{matcher/common/language_matchers_test.go => search/language_test.go} (90%) create mode 100644 grype/search/only_vulnerable_versions.go rename grype/{matcher/common => search}/utils_test.go (97%) diff --git a/cmd/root_test.go b/cmd/root_test.go index 62a471ec0df..a6b5fecee03 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -3,6 +3,8 @@ package cmd import ( "testing" + "github.com/google/uuid" + "github.com/anchore/grype/grype/db" grypeDB "github.com/anchore/grype/grype/db/v3" "github.com/anchore/grype/grype/match" @@ -37,19 +39,24 @@ func (d *mockMetadataStore) GetVulnerabilityMetadata(id, recordSource string) (* func TestAboveAllowableSeverity(t *testing.T) { thePkg := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "the-package", Version: "v0.1", Type: syftPkg.RpmPkg, } matches := match.NewMatches() - matches.Add(thePkg, match.Match{ - Type: match.ExactDirectMatch, + matches.Add(match.Match{ Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2014-fake-1", Namespace: "source-1", }, Package: thePkg, + Details: match.Details{ + { + Type: match.ExactDirectMatch, + }, + }, }) tests := []struct { diff --git a/go.mod b/go.mod index 81bff5c0d98..7de7f8c0124 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 github.com/anchore/stereoscope v0.0.0-20220110181730-c91cf94a3718 github.com/anchore/syft v0.36.0 - github.com/aws/aws-sdk-go v1.31.6 // indirect github.com/bmatcuk/doublestar/v2 v2.0.4 github.com/docker/docker v20.10.11+incompatible github.com/dustin/go-humanize v1.0.0 @@ -20,7 +19,6 @@ require ( github.com/google/go-cmp v0.5.6 github.com/google/uuid v1.2.0 github.com/gookit/color v1.4.2 - github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-getter v1.5.9 github.com/hashicorp/go-multierror v1.1.0 @@ -29,15 +27,13 @@ require ( github.com/jinzhu/gorm v1.9.14 github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d - github.com/lib/pq v1.2.0 // indirect - github.com/mattn/go-colorable v0.1.6 // indirect github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/olekukonko/tablewriter v0.0.4 github.com/pkg/profile v1.6.0 github.com/scylladb/go-set v1.0.2 github.com/sergi/go-diff v1.1.0 github.com/sirupsen/logrus v1.8.1 - github.com/smartystreets/assertions v1.0.0 // indirect github.com/spf13/afero v1.6.0 github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index 3569743ea69..42c5b25c521 100644 --- a/go.sum +++ b/go.sum @@ -132,9 +132,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.15.78 h1:LaXy6lWR0YK7LKyuU0QWy2ws/LWTPfYV/UgfiBu4tvY= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/aws/aws-sdk-go v1.31.6 h1:nKjQbpXhdImctBh1e0iLg9iQW/X297LPPuY/9f92R2k= -github.com/aws/aws-sdk-go v1.31.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -498,9 +497,8 @@ github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1K github.com/gookit/color v1.2.7/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw= -github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -571,9 +569,8 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7 h1:SMvOWPJCES2GdFracYbBQh93GXac8fq7HeN6JnpduB8= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -617,9 +614,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -629,14 +625,12 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= @@ -810,9 +804,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= -github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -1122,7 +1115,6 @@ golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/grype/db/v3/namespace_test.go b/grype/db/v3/namespace_test.go index dd373148a74..3eaa9d4d7d5 100644 --- a/grype/db/v3/namespace_test.go +++ b/grype/db/v3/namespace_test.go @@ -7,6 +7,7 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" syftPkg "github.com/anchore/syft/syft/pkg" + "github.com/google/uuid" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" ) @@ -214,6 +215,7 @@ func Test_NamespacesForLanguage(t *testing.T) { { language: syftPkg.Rust, namerInput: &pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", }, expectedNamespaces: []string{ @@ -226,6 +228,7 @@ func Test_NamespacesForLanguage(t *testing.T) { { language: syftPkg.Go, namerInput: &pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", }, expectedNamespaces: []string{ @@ -239,6 +242,7 @@ func Test_NamespacesForLanguage(t *testing.T) { { language: syftPkg.Ruby, namerInput: &pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", }, expectedNamespaces: []string{ @@ -251,6 +255,7 @@ func Test_NamespacesForLanguage(t *testing.T) { { language: syftPkg.JavaScript, namerInput: &pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", }, expectedNamespaces: []string{ @@ -263,6 +268,7 @@ func Test_NamespacesForLanguage(t *testing.T) { { language: syftPkg.Python, namerInput: &pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", }, expectedNamespaces: []string{ @@ -275,6 +281,7 @@ func Test_NamespacesForLanguage(t *testing.T) { { language: syftPkg.Java, namerInput: &pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", Metadata: pkg.JavaMetadata{ VirtualPath: "v-path", @@ -329,6 +336,7 @@ func Test_githubJavaPackageNamer(t *testing.T) { { name: "both artifact and manifest", namerInput: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", Metadata: pkg.JavaMetadata{ VirtualPath: "v-path", @@ -345,6 +353,7 @@ func Test_githubJavaPackageNamer(t *testing.T) { { name: "no group id", namerInput: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", Metadata: pkg.JavaMetadata{ VirtualPath: "v-path", @@ -357,6 +366,7 @@ func Test_githubJavaPackageNamer(t *testing.T) { { name: "only manifest", namerInput: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", Metadata: pkg.JavaMetadata{ VirtualPath: "v-path", @@ -371,6 +381,7 @@ func Test_githubJavaPackageNamer(t *testing.T) { { name: "only artifact", namerInput: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", Metadata: pkg.JavaMetadata{ VirtualPath: "v-path", @@ -385,6 +396,7 @@ func Test_githubJavaPackageNamer(t *testing.T) { { name: "no artifact or manifest", namerInput: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-name", Metadata: pkg.JavaMetadata{ VirtualPath: "v-path", diff --git a/grype/db/vulnerability_provider_test.go b/grype/db/vulnerability_provider_test.go index d383dbedded..864627a61c7 100644 --- a/grype/db/vulnerability_provider_test.go +++ b/grype/db/vulnerability_provider_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/anchore/grype/grype/vulnerability" + "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -24,6 +25,7 @@ func TestGetByDistro(t *testing.T) { } p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "neutron", } @@ -62,6 +64,7 @@ func TestGetByDistro_nilDistro(t *testing.T) { provider := NewVulnerabilityProvider(newMockStore()) p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "neutron", } diff --git a/grype/match/details.go b/grype/match/details.go new file mode 100644 index 00000000000..1b6527658d3 --- /dev/null +++ b/grype/match/details.go @@ -0,0 +1,54 @@ +package match + +import ( + "fmt" + + "github.com/mitchellh/hashstructure/v2" +) + +type Details []Detail + +type Detail struct { + Type Type // The kind of match made (an exact match, fuzzy match, indirect vs direct, etc). + SearchedBy interface{} // The specific attributes that were used to search (other than package name and version) --this indicates "how" the match was made. + Found interface{} // The specific attributes on the vulnerability object that were matched with --this indicates "what" was matched on / within. + Matcher MatcherType // The matcher object that discovered the match. + Confidence float64 // The certainty of the match as a ratio (currently unused, reserved for future use). +} + +// String is the string representation of select match fields. +func (m Detail) String() string { + return fmt.Sprintf("Detail(searchedBy=%q found=%q matcher=%q)", m.SearchedBy, m.Found, m.Matcher) +} + +func (m Details) Matchers() (tys []MatcherType) { + if len(m) == 0 { + return nil + } + for _, d := range m { + tys = append(tys, d.Matcher) + } + return tys +} + +func (m Details) Types() (tys []Type) { + if len(m) == 0 { + return nil + } + for _, d := range m { + tys = append(tys, d.Type) + } + return tys +} + +func (m Detail) ID() string { + f, err := hashstructure.Hash(&m, hashstructure.FormatV2, &hashstructure.HashOptions{ + ZeroNil: true, + SlicesAsSets: true, + }) + if err != nil { + return "" + } + + return fmt.Sprintf("%x", f) +} diff --git a/grype/match/explicit_ignores_test.go b/grype/match/explicit_ignores_test.go index f9d13bb9cbe..b3a0accbbf3 100644 --- a/grype/match/explicit_ignores_test.go +++ b/grype/match/explicit_ignores_test.go @@ -63,22 +63,20 @@ func Test_ApplyExplicitIgnoreRules(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - matches := Matches{ - byPackage: make(map[pkg.ID][]Match), - } + matches := NewMatches() for _, cp := range test.matches { - matches.byPackage[pkg.ID(cp.pkg)] = []Match{ - { - Package: pkg.Package{ - Name: cp.pkg, - Type: test.typ, - }, - Vulnerability: vulnerability.Vulnerability{ - ID: cp.cve, - }, + matches.Add(Match{ + + Package: pkg.Package{ + ID: pkg.ID(cp.pkg), + Name: cp.pkg, + Type: test.typ, + }, + Vulnerability: vulnerability.Vulnerability{ + ID: cp.cve, }, - } + }) } filtered := ApplyExplicitIgnoreRules(matches) diff --git a/grype/match/fingerprint.go b/grype/match/fingerprint.go new file mode 100644 index 00000000000..0363fec2e19 --- /dev/null +++ b/grype/match/fingerprint.go @@ -0,0 +1,31 @@ +package match + +import ( + "fmt" + + "github.com/anchore/grype/grype/pkg" + "github.com/mitchellh/hashstructure/v2" +) + +type Fingerprint struct { + vulnerabilityID string + vulnerabilityNamespace string + vulnerabilityFixes string + packageID pkg.ID // note: this encodes package name, version, type, location +} + +func (m Fingerprint) String() string { + return fmt.Sprintf("Fingerprint(vuln=%q namespace=%q fixes=%q package=%q)", m.vulnerabilityID, m.vulnerabilityNamespace, m.vulnerabilityFixes, m.packageID) +} + +func (m Fingerprint) ID() string { + f, err := hashstructure.Hash(&m, hashstructure.FormatV2, &hashstructure.HashOptions{ + ZeroNil: true, + SlicesAsSets: true, + }) + if err != nil { + return "" + } + + return fmt.Sprintf("%x", f) +} diff --git a/grype/match/ignore.go b/grype/match/ignore.go index 70dd56f2975..5564243a680 100644 --- a/grype/match/ignore.go +++ b/grype/match/ignore.go @@ -40,7 +40,7 @@ func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMa var ignoredMatches []IgnoredMatch remainingMatches := NewMatches() - for match := range matches.Enumerate() { + for _, match := range matches.Sorted() { var applicableRules []IgnoreRule for _, rule := range rules { @@ -58,7 +58,7 @@ func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMa continue } - remainingMatches.add(match.Package.ID, match) + remainingMatches.Add(match) } return remainingMatches, ignoredMatches diff --git a/grype/match/ignore_test.go b/grype/match/ignore_test.go index e6aa561d2ba..da2dfc8a6fa 100644 --- a/grype/match/ignore_test.go +++ b/grype/match/ignore_test.go @@ -3,7 +3,7 @@ package match import ( "testing" - "github.com/google/go-cmp/cmp" + "github.com/google/uuid" "github.com/anchore/syft/syft/source" @@ -25,6 +25,7 @@ var ( }, }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "dive", Version: "0.5.2", Type: "deb", @@ -41,6 +42,7 @@ var ( }, }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "reach", Version: "100.0.50", Type: "gem", @@ -57,6 +59,7 @@ var ( }, }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "beach", Version: "100.0.51", Type: "gem", @@ -73,6 +76,7 @@ var ( }, }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "speach", Version: "100.0.52", Type: "gem", @@ -227,44 +231,28 @@ func TestApplyIgnoreRules(t *testing.T) { for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { - locationComparerOption := cmp.Comparer(func(x, y source.Location) bool { - return x.RealPath == y.RealPath && x.VirtualPath == y.VirtualPath - }) - actualRemainingMatches, actualIgnoredMatches := ApplyIgnoreRules(sliceToMatches(testCase.allMatches), testCase.ignoreRules) - if diff := cmp.Diff(testCase.expectedRemainingMatches, matchesToSlice(actualRemainingMatches), locationComparerOption); diff != "" { - t.Errorf("unexpected diff in remaining matches (-expected +actual):\n%s", diff) - } + assertMatchOrder(t, testCase.expectedRemainingMatches, actualRemainingMatches.Sorted()) + assertIgnoredMatchOrder(t, testCase.expectedIgnoredMatches, actualIgnoredMatches) - if diff := cmp.Diff(testCase.expectedIgnoredMatches, actualIgnoredMatches, locationComparerOption); diff != "" { - t.Errorf("unexpected diff in ignored matches (-expected +actual):\n%s", diff) - } }) } } func sliceToMatches(s []Match) Matches { matches := NewMatches() - matches.add("123", s...) + matches.Add(s...) return matches } -func matchesToSlice(m Matches) []Match { - slice := m.Sorted() - if len(slice) == 0 { - return nil - } - - return slice -} - var ( exampleMatch = Match{ Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2000-1234", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "a-pkg", Version: "1.0", Locations: []source.Location{ diff --git a/grype/match/match.go b/grype/match/match.go index 493dac1b8a3..bb4308ee74f 100644 --- a/grype/match/match.go +++ b/grype/match/match.go @@ -6,38 +6,25 @@ import ( "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" + "github.com/scylladb/go-set/strset" ) +var ErrCannotMerge = fmt.Errorf("unable to merge vulnerability matches") + // Match represents a finding in the vulnerability matching process, pairing a single package and a single vulnerability object. type Match struct { - Type Type // The kind of match made (an exact match, fuzzy match, indirect vs direct, etc). Vulnerability vulnerability.Vulnerability // The vulnerability details of the match. Package pkg.Package // The package used to search for a match. - MatchDetails []Details // all ways in which how this particular match was made. -} - -type Details struct { - SearchedBy interface{} // The specific attributes that were used to search (other than package name and version) --this indicates "how" the match was made. - Found interface{} // The specific attributes on the vulnerability object that were matched with --this indicates "what" was matched on / within. - Matcher MatcherType // The matcher object that discovered the match. - Confidence float64 // The certainty of the match as a ratio (currently unused, reserved for future use). -} - -type Fingerprint struct { - vulnerabilityID string - vulnerabilityNamespace string - vulnerabilityFixes string - packageID pkg.ID // this encodes package name, version, type, location - matchType Type + Details Details // all ways in which how this particular match was made. } // String is the string representation of select match fields. func (m Match) String() string { - return fmt.Sprintf("Match(pkg=%s vuln=%q type=%q)", m.Package, m.Vulnerability.String(), m.Type) + return fmt.Sprintf("Match(pkg=%s vuln=%q types=%q)", m.Package, m.Vulnerability.String(), m.Details.Types()) } func (m Match) Summary() string { - return fmt.Sprintf("vuln=%q type=%q searchedBy=%q foundBy=%q", m.Vulnerability.ID, m.Type, m.MatchDetails[0].SearchedBy, m.MatchDetails[0].Matcher) + return fmt.Sprintf("vuln=%q matchers=%s", m.Vulnerability.ID, m.Details.Matchers()) } func (m Match) Fingerprint() Fingerprint { @@ -46,6 +33,25 @@ func (m Match) Fingerprint() Fingerprint { vulnerabilityNamespace: m.Vulnerability.Namespace, vulnerabilityFixes: strings.Join(m.Vulnerability.Fix.Versions, ","), packageID: m.Package.ID, - matchType: m.Type, } } + +func (m *Match) Merge(other Match) error { + if other.Fingerprint() != m.Fingerprint() { + return ErrCannotMerge + } + + detailIDs := strset.New() + for _, d := range m.Details { + detailIDs.Add(d.ID()) + } + + // keep details from the other match that are unique + for _, d := range other.Details { + if detailIDs.Has(d.ID()) { + continue + } + m.Details = append(m.Details, d) + } + return nil +} diff --git a/grype/match/matcher_type.go b/grype/match/matcher_type.go index b58f32681a0..80302749604 100644 --- a/grype/match/matcher_type.go +++ b/grype/match/matcher_type.go @@ -1,31 +1,18 @@ package match const ( - UnknownMatcherType MatcherType = iota - StockMatcher - ApkMatcher - RubyGemMatcher - DpkgMatcher - RpmDBMatcher - JavaMatcher - PythonMatcher - JavascriptMatcher - MsrcMatcher + UnknownMatcherType MatcherType = "UnknownMatcherType" + StockMatcher MatcherType = "stock-matcher" + ApkMatcher MatcherType = "apk-matcher" + RubyGemMatcher MatcherType = "ruby-gem-matcher" + DpkgMatcher MatcherType = "dpkg-matcher" + RpmDBMatcher MatcherType = "rpmdb-matcher" + JavaMatcher MatcherType = "java-matcher" + PythonMatcher MatcherType = "python-matcher" + JavascriptMatcher MatcherType = "javascript-matcher" + MsrcMatcher MatcherType = "msrc-matcher" ) -var matcherTypeStr = []string{ - "UnknownMatcherType", - "stock-matcher", - "apk-matcher", - "ruby-gem-matcher", - "dpkg-matcher", - "rpmdb-matcher", - "java-matcher", - "python-matcher", - "javascript-matcher", - "msrc-matcher", -} - var AllMatcherTypes = []MatcherType{ ApkMatcher, RubyGemMatcher, @@ -37,12 +24,4 @@ var AllMatcherTypes = []MatcherType{ MsrcMatcher, } -type MatcherType int - -func (f MatcherType) String() string { - if int(f) >= len(matcherTypeStr) || f < 0 { - return matcherTypeStr[0] - } - - return matcherTypeStr[f] -} +type MatcherType string diff --git a/grype/match/matches.go b/grype/match/matches.go index 251696e9da9..7397057867f 100644 --- a/grype/match/matches.go +++ b/grype/match/matches.go @@ -3,58 +3,73 @@ package match import ( "sort" + "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/grype/pkg" ) type Matches struct { - byPackage map[pkg.ID][]Match + byFingerprint map[Fingerprint]Match + byPackage map[pkg.ID][]Fingerprint +} + +func NewMatches(matches ...Match) Matches { + m := newMatches() + m.Add(matches...) + return m } -func NewMatches() Matches { +func newMatches() Matches { return Matches{ - byPackage: make(map[pkg.ID][]Match), + byFingerprint: make(map[Fingerprint]Match), + byPackage: make(map[pkg.ID][]Fingerprint), } } // GetByPkgID returns a slice of potential matches from an ID -func (r *Matches) GetByPkgID(id pkg.ID) []Match { - matches, ok := r.byPackage[id] - if !ok { - return nil +func (r *Matches) GetByPkgID(id pkg.ID) (matches []Match) { + for _, fingerprint := range r.byPackage[id] { + matches = append(matches, r.byFingerprint[fingerprint]) } return matches } func (r *Matches) Merge(other Matches) { - // note: de-duplication of matches is an upstream concern (not here) - for pkgID, matches := range other.byPackage { - r.add(pkgID, matches...) + for _, fingerprints := range other.byPackage { + for _, fingerprint := range fingerprints { + r.Add(other.byFingerprint[fingerprint]) + } } } -func (r *Matches) add(id pkg.ID, matches ...Match) { +func (r *Matches) Add(matches ...Match) { if len(matches) == 0 { - // only packages with matches should be added return } - if _, ok := r.byPackage[id]; !ok { - r.byPackage[id] = make([]Match, 0) - } - r.byPackage[id] = append(r.byPackage[id], matches...) -} + for _, newMatch := range matches { + fingerprint := newMatch.Fingerprint() -func (r *Matches) Add(p pkg.Package, matches ...Match) { - r.add(p.ID, matches...) + // add or merge the new match with an existing match + if existingMatch, exists := r.byFingerprint[fingerprint]; exists { + if err := existingMatch.Merge(newMatch); err != nil { + log.Warnf("unable to merge matches: original=%q new=%q : %w", existingMatch.String(), newMatch.String(), err) + // TODO: dropped match in this case, we should figure a way to handle this + } + } else { + r.byFingerprint[fingerprint] = newMatch + } + + // keep track of which matches correspond to which packages + r.byPackage[newMatch.Package.ID] = append(r.byPackage[newMatch.Package.ID], fingerprint) + } } func (r *Matches) Enumerate() <-chan Match { channel := make(chan Match) go func() { defer close(channel) - for _, matches := range r.byPackage { - for _, m := range matches { - channel <- m - } + for _, match := range r.byFingerprint { + channel <- match } }() return channel @@ -73,5 +88,5 @@ func (r *Matches) Sorted() []Match { // Count returns the total number of matches in a result func (r *Matches) Count() int { - return len(r.byPackage) + return len(r.byFingerprint) } diff --git a/grype/match/matches_test.go b/grype/match/matches_test.go index f2d6ced2177..905efcc133a 100644 --- a/grype/match/matches_test.go +++ b/grype/match/matches_test.go @@ -3,6 +3,9 @@ package match import ( "testing" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" @@ -15,6 +18,7 @@ func TestMatchesSortMixedDimensions(t *testing.T) { ID: "CVE-2020-0010", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, @@ -25,6 +29,7 @@ func TestMatchesSortMixedDimensions(t *testing.T) { ID: "CVE-2020-0020", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-a", Version: "1.0.0", Type: syftPkg.NpmPkg, @@ -35,6 +40,7 @@ func TestMatchesSortMixedDimensions(t *testing.T) { ID: "CVE-2020-0020", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-a", Version: "2.0.0", Type: syftPkg.RpmPkg, @@ -45,6 +51,7 @@ func TestMatchesSortMixedDimensions(t *testing.T) { ID: "CVE-2020-0020", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-c", Version: "3.0.0", Type: syftPkg.ApkPkg, @@ -55,26 +62,20 @@ func TestMatchesSortMixedDimensions(t *testing.T) { ID: "CVE-2020-0020", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-d", Version: "2.0.0", Type: syftPkg.RpmPkg, }, } - matches := NewMatches() input := []Match{ // shuffle vulnerability id, package name, package version, and package type fifth, third, first, second, fourth, } - for _, i := range input { - matches.Add(i.Package, i) - } - - expected := []Match{ - first, second, third, fourth, fifth, - } + matches := NewMatches(input...) - assert.Equal(t, expected, matches.Sorted()) + assertMatchOrder(t, []Match{first, second, third, fourth, fifth}, matches.Sorted()) } @@ -84,6 +85,7 @@ func TestMatchesSortByVulnerability(t *testing.T) { ID: "CVE-2020-0010", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, @@ -94,20 +96,19 @@ func TestMatchesSortByVulnerability(t *testing.T) { ID: "CVE-2020-0020", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } - input := []Match{second, first} - - matches := NewMatches() - for _, i := range input { - matches.Add(i.Package, i) + input := []Match{ + second, first, } + matches := NewMatches(input...) - assert.Equal(t, []Match{first, second}, matches.Sorted()) + assertMatchOrder(t, []Match{first, second}, matches.Sorted()) } @@ -117,6 +118,7 @@ func TestMatchesSortByPackage(t *testing.T) { ID: "CVE-2020-0010", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, @@ -127,20 +129,19 @@ func TestMatchesSortByPackage(t *testing.T) { ID: "CVE-2020-0010", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-c", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } - input := []Match{second, first} - - matches := NewMatches() - for _, i := range input { - matches.Add(i.Package, i) + input := []Match{ + second, first, } + matches := NewMatches(input...) - assert.Equal(t, []Match{first, second}, matches.Sorted()) + assertMatchOrder(t, []Match{first, second}, matches.Sorted()) } @@ -150,6 +151,7 @@ func TestMatchesSortByPackageVersion(t *testing.T) { ID: "CVE-2020-0010", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, @@ -160,20 +162,19 @@ func TestMatchesSortByPackageVersion(t *testing.T) { ID: "CVE-2020-0010", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "2.0.0", Type: syftPkg.RpmPkg, }, } - input := []Match{second, first} - - matches := NewMatches() - for _, i := range input { - matches.Add(i.Package, i) + input := []Match{ + second, first, } + matches := NewMatches(input...) - assert.Equal(t, []Match{first, second}, matches.Sorted()) + assertMatchOrder(t, []Match{first, second}, matches.Sorted()) } @@ -183,6 +184,7 @@ func TestMatchesSortByPackageType(t *testing.T) { ID: "CVE-2020-0010", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.ApkPkg, @@ -193,19 +195,56 @@ func TestMatchesSortByPackageType(t *testing.T) { ID: "CVE-2020-0010", }, Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } - input := []Match{second, first} + input := []Match{ + second, first, + } + matches := NewMatches(input...) + + assertMatchOrder(t, []Match{first, second}, matches.Sorted()) + +} + +func assertMatchOrder(t *testing.T, expected, actual []Match) { + + var expectedStr []string + for _, e := range expected { + expectedStr = append(expectedStr, e.Package.Name) + } + + var actualStr []string + for _, a := range actual { + actualStr = append(actualStr, a.Package.Name) + } + + // makes this easier on the eyes to sanity check... + require.Equal(t, expectedStr, actualStr) + + // make certain the fields are what you'd expect + assert.Equal(t, expected, actual) +} + +func assertIgnoredMatchOrder(t *testing.T, expected, actual []IgnoredMatch) { + + var expectedStr []string + for _, e := range expected { + expectedStr = append(expectedStr, e.Package.Name) + } - matches := NewMatches() - for _, i := range input { - matches.Add(i.Package, i) + var actualStr []string + for _, a := range actual { + actualStr = append(actualStr, a.Package.Name) } - assert.Equal(t, []Match{first, second}, matches.Sorted()) + // makes this easier on the eyes to sanity check... + require.Equal(t, expectedStr, actualStr) + // make certain the fields are what you'd expect + assert.Equal(t, expected, actual) } diff --git a/grype/match/type.go b/grype/match/type.go index 4e5786db444..7f4573667a3 100644 --- a/grype/match/type.go +++ b/grype/match/type.go @@ -1,37 +1,26 @@ package match -import "strings" +import ( + "github.com/anchore/grype/grype/pkg" +) const ( - UnknownMatchType Type = iota - ExactDirectMatch - ExactIndirectMatch - FuzzyMatch + ExactDirectMatch Type = "exact-direct-match" + ExactIndirectMatch Type = "exact-indirect-match" + CPEMatch Type = "cpe-match" ) -var typeStr = []string{ - "UnknownMatchType", - "Exact-Direct Match", - "Exact-Indirect Match", - "Fuzzy Match", -} - -type Type int +type Type string -func ParseType(userStr string) Type { - switch strings.ToLower(userStr) { - case strings.ToLower(ExactDirectMatch.String()): - return ExactDirectMatch - case strings.ToLower(ExactIndirectMatch.String()): - return ExactIndirectMatch +func ConvertToIndirectMatches(matches []Match, p pkg.Package) { + for idx := range matches { + for dIdx := range matches[idx].Details { + // only override the match details to "indirect" if the match details are explicitly indicate a "direct" match + if matches[idx].Details[dIdx].Type == ExactDirectMatch { + matches[idx].Details[dIdx].Type = ExactIndirectMatch + } + } + // we always override the package to the direct package + matches[idx].Package = p } - return UnknownMatchType -} - -func (f Type) String() string { - if int(f) >= len(typeStr) || f < 0 { - return typeStr[0] - } - - return typeStr[f] } diff --git a/grype/matcher/apk/matcher.go b/grype/matcher/apk/matcher.go index 87c35c42789..16beea7c99a 100644 --- a/grype/matcher/apk/matcher.go +++ b/grype/matcher/apk/matcher.go @@ -7,8 +7,8 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" @@ -49,7 +49,7 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa func (m *Matcher) cpeMatchesWithoutSecDBFixes(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { // find CPE-indexed vulnerability matches specific to the given package name and version - cpeMatches, err := common.FindMatchesByPackageCPE(store, p, m.Type()) + cpeMatches, err := search.ByPackageCPE(store, p, m.Type()) if err != nil { return nil, err } @@ -139,7 +139,7 @@ func vulnerabilitiesByID(vulns []vulnerability.Vulnerability) map[string][]vulne func (m *Matcher) findApkPackage(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { // find Alpine SecDB matches for the given package name and version - secDBMatches, err := common.FindMatchesByPackageDistro(store, d, p, m.Type()) + secDBMatches, err := search.ByPackageDistro(store, d, p, m.Type()) if err != nil { return nil, err } @@ -178,13 +178,7 @@ func (m *Matcher) matchBySourceIndirection(store vulnerability.Provider, d *dist // we want to make certain that we are tracking the match based on the package from the SBOM (not the indirect package) // however, we also want to keep the indirect package around for future reference - for idx := range matches { - matches[idx].Package = p - - if matches[idx].Type == match.ExactDirectMatch { - matches[idx].Type = match.ExactIndirectMatch - } - } + match.ConvertToIndirectMatches(matches, p) return matches, nil } diff --git a/grype/matcher/apk/matcher_test.go b/grype/matcher/apk/matcher_test.go index 348226d300b..1d3e4bf7f27 100644 --- a/grype/matcher/apk/matcher_test.go +++ b/grype/matcher/apk/matcher_test.go @@ -1,17 +1,18 @@ package apk import ( + "github.com/anchore/grype/grype/search" "testing" "github.com/anchore/grype/grype/db" grypeDB "github.com/anchore/grype/grype/db/v3" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/go-test/deep" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -60,6 +61,7 @@ func TestSecDBOnlyMatch(t *testing.T) { } p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, @@ -73,11 +75,12 @@ func TestSecDBOnlyMatch(t *testing.T) { expected := []match.Match{ { - Type: match.ExactDirectMatch, + Vulnerability: *vulnFound, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -145,6 +148,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) { } p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, @@ -159,11 +163,12 @@ func TestBothSecdbAndNvdMatches(t *testing.T) { expected := []match.Match{ { - Type: match.ExactDirectMatch, + Vulnerability: *vulnFound, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -230,6 +235,7 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) { t.Fatalf("failed to create a new distro: %+v", err) } p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, @@ -245,11 +251,12 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) { expected := []match.Match{ { - Type: match.ExactDirectMatch, + Vulnerability: *vulnFound, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -303,6 +310,7 @@ func TestNvdOnlyMatches(t *testing.T) { t.Fatalf("failed to create a new distro: %+v", err) } p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, @@ -317,17 +325,18 @@ func TestNvdOnlyMatches(t *testing.T) { expected := []match.Match{ { - Type: match.FuzzyMatch, + Vulnerability: *vulnFound, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: common.SearchedByCPEs{ + SearchedBy: search.CPEParameters{ CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"}, Namespace: "nvd", }, - Found: common.FoundCPEs{ + Found: search.CPEResult{ CPEs: []string{vulnFound.CPEs[0].BindToFmtString()}, VersionConstraint: vulnFound.Constraint.String(), }, @@ -380,6 +389,7 @@ func TestNvdMatchesWithSecDBFix(t *testing.T) { t.Fatalf("failed to create a new distro: %+v", err) } p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.11", Type: syftPkg.ApkPkg, @@ -433,6 +443,7 @@ func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) { t.Fatalf("failed to create a new distro: %+v", err) } p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.11", Type: syftPkg.ApkPkg, @@ -476,6 +487,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) { t.Fatalf("failed to create a new distro: %+v", err) } p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "musl-utils", Version: "1.3.2-r0", Type: syftPkg.ApkPkg, @@ -487,11 +499,12 @@ func TestDistroMatchBySourceIndirection(t *testing.T) { expected := []match.Match{ { - Type: match.ExactIndirectMatch, + Vulnerability: *vulnFound, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactIndirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -546,6 +559,7 @@ func TestNVDMatchBySourceIndirection(t *testing.T) { t.Fatalf("failed to create a new distro: %+v", err) } p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "musl-utils", Version: "1.3.2-r0", Type: syftPkg.ApkPkg, @@ -562,17 +576,17 @@ func TestNVDMatchBySourceIndirection(t *testing.T) { expected := []match.Match{ { - Type: match.FuzzyMatch, Vulnerability: *vulnFound, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: common.SearchedByCPEs{ + SearchedBy: search.CPEParameters{ CPEs: []string{"cpe:2.3:a:musl:musl:*:*:*:*:*:*:*:*"}, Namespace: "nvd", }, - Found: common.FoundCPEs{ + Found: search.CPEResult{ CPEs: []string{vulnFound.CPEs[0].BindToFmtString()}, VersionConstraint: vulnFound.Constraint.String(), }, diff --git a/grype/matcher/common/language_matchers.go b/grype/matcher/common/language_matchers.go deleted file mode 100644 index 0832a0ecde1..00000000000 --- a/grype/matcher/common/language_matchers.go +++ /dev/null @@ -1,57 +0,0 @@ -package common - -import ( - "fmt" - - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/version" - "github.com/anchore/grype/grype/vulnerability" - syftPkg "github.com/anchore/syft/syft/pkg" -) - -func FindMatchesByPackageLanguage(store vulnerability.ProviderByLanguage, l syftPkg.Language, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { - verObj, err := version.NewVersionFromPkg(p) - if err != nil { - return nil, fmt.Errorf("matcher failed to parse version pkg='%s' ver='%s': %w", p.Name, p.Version, err) - } - - allPkgVulns, err := store.GetByLanguage(l, p) - if err != nil { - return nil, fmt.Errorf("matcher failed to fetch language='%s' pkg='%s': %w", l, p.Name, err) - } - - matches := make([]match.Match, 0) - for _, vuln := range allPkgVulns { - // if the constraint it met, then the given package has the vulnerability - isPackageVulnerable, err := vuln.Constraint.Satisfied(verObj) - if err != nil { - return nil, fmt.Errorf("language matcher failed to check constraint='%s' version='%s': %w", vuln.Constraint, verObj, err) - } - - if !isPackageVulnerable { - continue - } - - matches = append(matches, match.Match{ - Type: match.ExactDirectMatch, - Vulnerability: vuln, - Package: p, - MatchDetails: []match.Details{ - { - Confidence: 1.0, // TODO: this is hard coded for now - Matcher: upstreamMatcher, - SearchedBy: map[string]interface{}{ - "language": l.String(), - "namespace": vuln.Namespace, - }, - Found: map[string]interface{}{ - "versionConstraint": vuln.Constraint.String(), - }, - }, - }, - }) - } - - return matches, err -} diff --git a/grype/matcher/controller.go b/grype/matcher/controller.go index 31bde2ec5ce..ce5fffb0c9d 100644 --- a/grype/matcher/controller.go +++ b/grype/matcher/controller.go @@ -109,7 +109,7 @@ func (c *controller) findMatches(provider vulnerability.Provider, release *linux log.Warnf("matcher failed for pkg=%s: %+v", p, err) } else { logMatches(p, matches) - res.Add(p, matches...) + res.Add(matches...) vulnerabilitiesDiscovered.N += int64(len(matches)) } } diff --git a/grype/matcher/dpkg/matcher.go b/grype/matcher/dpkg/matcher.go index 3ff56c0f673..e652cc520a9 100644 --- a/grype/matcher/dpkg/matcher.go +++ b/grype/matcher/dpkg/matcher.go @@ -3,13 +3,12 @@ package dpkg import ( "fmt" - syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "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" "github.com/jinzhu/copier" ) @@ -33,7 +32,7 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa } matches = append(matches, sourceMatches...) - exactMatches, err := common.FindMatchesByPackageDistro(store, d, p, m.Type()) + exactMatches, err := search.ByPackageDistro(store, d, p, m.Type()) if err != nil { return nil, fmt.Errorf("failed to match by exact package name: %w", err) } @@ -64,17 +63,14 @@ func (m *Matcher) matchBySourceIndirection(store vulnerability.ProviderByDistro, // use the source package name indirectPackage.Name = metadata.Source - matches, err := common.FindMatchesByPackageDistro(store, d, indirectPackage, m.Type()) + matches, err := search.ByPackageDistro(store, d, indirectPackage, m.Type()) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities by dpkg source indirection: %w", err) } // we want to make certain that we are tracking the match based on the package from the SBOM (not the indirect package) // however, we also want to keep the indirect package around for future reference - for idx := range matches { - matches[idx].Type = match.ExactIndirectMatch - matches[idx].Package = p - } + match.ConvertToIndirectMatches(matches, p) return matches, nil } diff --git a/grype/matcher/dpkg/matcher_test.go b/grype/matcher/dpkg/matcher_test.go index 6b42e7f78f5..6e7cf19c8b7 100644 --- a/grype/matcher/dpkg/matcher_test.go +++ b/grype/matcher/dpkg/matcher_test.go @@ -3,6 +3,9 @@ package dpkg import ( "testing" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/distro" @@ -15,6 +18,7 @@ import ( func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) { matcher := Matcher{} p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "neutron", Version: "2014.1.3-6", Type: syftPkg.DebPkg, @@ -37,9 +41,12 @@ func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) { for _, a := range actual { foundCVEs.Add(a.Vulnerability.ID) - assert.Equal(t, match.ExactIndirectMatch, a.Type, "indirect match not indicated") + require.NotEmpty(t, a.Details) + for _, d := range a.Details { + assert.Equal(t, match.ExactIndirectMatch, d.Type, "indirect match not indicated") + } assert.Equal(t, p.Name, a.Package.Name, "failed to capture original package name") - for _, detail := range a.MatchDetails { + for _, detail := range a.Details { assert.Equal(t, matcher.Type(), detail.Matcher, "failed to capture matcher type") } } diff --git a/grype/matcher/java/matcher.go b/grype/matcher/java/matcher.go index fff34c5c698..92b1219d934 100644 --- a/grype/matcher/java/matcher.go +++ b/grype/matcher/java/matcher.go @@ -3,8 +3,8 @@ package java import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "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" ) @@ -20,18 +20,6 @@ func (m *Matcher) Type() match.MatcherType { return match.JavaMatcher } -func (m *Matcher) Match(store vulnerability.Provider, _ *distro.Distro, p pkg.Package) ([]match.Match, error) { - var matches = make([]match.Match, 0) - langMatches, err := common.FindMatchesByPackageLanguage(store, p.Language, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, langMatches...) - - cpeMatches, err := common.FindMatchesByPackageCPE(store, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, cpeMatches...) - return matches, nil +func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { + return search.ByCriteria(store, d, p, m.Type(), search.CommonCriteria...) } diff --git a/grype/matcher/javascript/matcher.go b/grype/matcher/javascript/matcher.go index f6df204047a..3d713234098 100644 --- a/grype/matcher/javascript/matcher.go +++ b/grype/matcher/javascript/matcher.go @@ -3,8 +3,8 @@ package javascript import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "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" ) @@ -20,18 +20,6 @@ func (m *Matcher) Type() match.MatcherType { return match.JavascriptMatcher } -func (m *Matcher) Match(store vulnerability.Provider, _ *distro.Distro, p pkg.Package) ([]match.Match, error) { - var matches = make([]match.Match, 0) - langMatches, err := common.FindMatchesByPackageLanguage(store, p.Language, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, langMatches...) - - cpeMatches, err := common.FindMatchesByPackageCPE(store, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, cpeMatches...) - return matches, nil +func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { + return search.ByCriteria(store, d, p, m.Type(), search.CommonCriteria...) } diff --git a/grype/matcher/msrc/matcher.go b/grype/matcher/msrc/matcher.go index 68e4dc0c042..1fd95baa570 100644 --- a/grype/matcher/msrc/matcher.go +++ b/grype/matcher/msrc/matcher.go @@ -3,8 +3,8 @@ package msrc import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "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" ) @@ -24,17 +24,8 @@ func (m *Matcher) Type() match.MatcherType { } func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { - var matches []match.Match - // find KB matches for the MSFT version given in the package and version. // The "distro" holds the information about the Windows version, and its // patch (KB) - kbMatches, err := common.FindMatchesByPackageDistro(store, d, p, m.Type()) - if err != nil { - return nil, err - } - - matches = append(matches, kbMatches...) - - return matches, nil + return search.ByCriteria(store, d, p, m.Type(), search.ByDistro) } diff --git a/grype/matcher/msrc/matcher_test.go b/grype/matcher/msrc/matcher_test.go index 935822a90fe..0c195f1b74d 100644 --- a/grype/matcher/msrc/matcher_test.go +++ b/grype/matcher/msrc/matcher_test.go @@ -9,6 +9,7 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" syftPkg "github.com/anchore/syft/syft/pkg" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -69,6 +70,7 @@ func TestMatches(t *testing.T) { { name: "direct KB match", pkg: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: d.RawVersion, Version: "3200970", Type: syftPkg.KbPkg, @@ -80,6 +82,7 @@ func TestMatches(t *testing.T) { { name: "multiple direct KB match", pkg: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: d.RawVersion, Version: "878787", Type: syftPkg.KbPkg, @@ -92,6 +95,7 @@ func TestMatches(t *testing.T) { { name: "no KBs found", pkg: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: d.RawVersion, // this is the assumed version if no KBs are found Version: "base", diff --git a/grype/matcher/python/matcher.go b/grype/matcher/python/matcher.go index e9d74141e38..e49fead2f49 100644 --- a/grype/matcher/python/matcher.go +++ b/grype/matcher/python/matcher.go @@ -3,8 +3,8 @@ package python import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "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" ) @@ -20,18 +20,6 @@ func (m *Matcher) Type() match.MatcherType { return match.PythonMatcher } -func (m *Matcher) Match(store vulnerability.Provider, _ *distro.Distro, p pkg.Package) ([]match.Match, error) { - var matches = make([]match.Match, 0) - langMatches, err := common.FindMatchesByPackageLanguage(store, p.Language, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, langMatches...) - - cpeMatches, err := common.FindMatchesByPackageCPE(store, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, cpeMatches...) - return matches, nil +func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { + return search.ByCriteria(store, d, p, m.Type(), search.CommonCriteria...) } diff --git a/grype/matcher/rpmdb/matcher.go b/grype/matcher/rpmdb/matcher.go index 4e64c3afcb0..4ecfdff4bee 100644 --- a/grype/matcher/rpmdb/matcher.go +++ b/grype/matcher/rpmdb/matcher.go @@ -7,8 +7,8 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" @@ -150,17 +150,14 @@ func (m *Matcher) matchBySourceIndirection(store vulnerability.ProviderByDistro, indirectPackage.Name = sourceName indirectPackage.Version = sourceVersion - matches, err := common.FindMatchesByPackageDistro(store, d, indirectPackage, m.Type()) + matches, err := search.ByPackageDistro(store, d, indirectPackage, m.Type()) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities by dpkg source indirection: %w", err) } // we want to make certain that we are tracking the match based on the package from the SBOM (not the indirect package). // The match details already contains the specific indirect package information used to make the match. - for idx := range matches { - matches[idx].Type = match.ExactIndirectMatch - matches[idx].Package = p - } + match.ConvertToIndirectMatches(matches, p) return matches, nil } @@ -176,7 +173,7 @@ func (m *Matcher) matchOnPackage(store vulnerability.ProviderByDistro, d *distro modifiedPackage.Version = addZeroEpicIfApplicable(p.Version) - matches, err := common.FindMatchesByPackageDistro(store, d, modifiedPackage, m.Type()) + matches, err := search.ByPackageDistro(store, d, modifiedPackage, m.Type()) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities by dpkg source indirection: %w", err) } diff --git a/grype/matcher/rpmdb/matcher_test.go b/grype/matcher/rpmdb/matcher_test.go index 9ce3fff257a..0fe04b6a24c 100644 --- a/grype/matcher/rpmdb/matcher_test.go +++ b/grype/matcher/rpmdb/matcher_test.go @@ -3,6 +3,9 @@ package rpmdb import ( "testing" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/distro" @@ -27,6 +30,7 @@ func TestMatcherRpmdb(t *testing.T) { { name: "Rpmdb Match matches by direct and by source indirection", p: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "neutron-libs", Version: "7.1.3-6", Type: syftPkg.RpmPkg, @@ -54,6 +58,7 @@ func TestMatcherRpmdb(t *testing.T) { { name: "Rpmdb Match matches by direct and ignores the source rpm when the package names are the same", p: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "neutron", Version: "7.1.3-6", Type: syftPkg.RpmPkg, @@ -80,6 +85,7 @@ func TestMatcherRpmdb(t *testing.T) { // Regression against https://github.com/anchore/grype/issues/376 name: "Rpmdb Match matches by direct and by source indirection when the SourceRpm version is desynced from package version", p: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "neutron-libs", Version: "7.1.3-6", Type: syftPkg.RpmPkg, @@ -107,6 +113,7 @@ func TestMatcherRpmdb(t *testing.T) { // Regression: https://github.com/anchore/grype/issues/437 name: "Rpmdb Match should not occur due to source match even though source has no epoch", p: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "0:1.28-419.el8_4.1", Type: syftPkg.RpmPkg, @@ -135,6 +142,7 @@ func TestMatcherRpmdb(t *testing.T) { { name: "package without epoch is assumed to be 0 - compared against vuln with NO epoch (direct match only)", p: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "1.28-419.el8_4.1", Type: syftPkg.RpmPkg, @@ -158,6 +166,7 @@ func TestMatcherRpmdb(t *testing.T) { { name: "package without epoch is assumed to be 0 - compared against vuln WITH epoch (direct match only)", p: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "1.28-419.el8_4.1", Type: syftPkg.RpmPkg, @@ -181,6 +190,7 @@ func TestMatcherRpmdb(t *testing.T) { { name: "package WITH epoch - compared against vuln with NO epoch (direct match only)", p: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "2:1.28-419.el8_4.1", Type: syftPkg.RpmPkg, @@ -204,6 +214,7 @@ func TestMatcherRpmdb(t *testing.T) { { name: "package WITH epoch - compared against vuln WITH epoch (direct match only)", p: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "2:1.28-419.el8_4.1", Type: syftPkg.RpmPkg, @@ -239,11 +250,14 @@ func TestMatcherRpmdb(t *testing.T) { t.Errorf("return unkown match CVE: %s", a.Vulnerability.ID) continue } else { - assert.Equal(t, val, a.Type) + require.NotEmpty(t, a.Details) + for _, de := range a.Details { + assert.Equal(t, val, de.Type) + } } assert.Equal(t, test.p.Name, a.Package.Name, "failed to capture original package name") - for _, detail := range a.MatchDetails { + for _, detail := range a.Details { assert.Equal(t, matcher.Type(), detail.Matcher, "failed to capture matcher type") } } diff --git a/grype/matcher/ruby/matcher.go b/grype/matcher/ruby/matcher.go index 1e9788294df..0008948d9ee 100644 --- a/grype/matcher/ruby/matcher.go +++ b/grype/matcher/ruby/matcher.go @@ -3,8 +3,8 @@ package ruby import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "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" ) @@ -20,18 +20,6 @@ func (m *Matcher) Type() match.MatcherType { return match.RubyGemMatcher } -func (m *Matcher) Match(store vulnerability.Provider, _ *distro.Distro, p pkg.Package) ([]match.Match, error) { - var matches = make([]match.Match, 0) - langMatches, err := common.FindMatchesByPackageLanguage(store, p.Language, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, langMatches...) - - cpeMatches, err := common.FindMatchesByPackageCPE(store, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, cpeMatches...) - return matches, nil +func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { + return search.ByCriteria(store, d, p, m.Type(), search.CommonCriteria...) } diff --git a/grype/matcher/stock/matcher.go b/grype/matcher/stock/matcher.go index a9fa15a6547..fa1da61da86 100644 --- a/grype/matcher/stock/matcher.go +++ b/grype/matcher/stock/matcher.go @@ -3,8 +3,8 @@ package stock import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/matcher/common" "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" ) @@ -20,21 +20,6 @@ func (m *Matcher) Type() match.MatcherType { return match.StockMatcher } -func (m *Matcher) Match(store vulnerability.Provider, _ *distro.Distro, p pkg.Package) ([]match.Match, error) { - var matches = make([]match.Match, 0) - - if p.Language != "" { - langMatches, err := common.FindMatchesByPackageLanguage(store, p.Language, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, langMatches...) - } - - cpeMatches, err := common.FindMatchesByPackageCPE(store, p, m.Type()) - if err != nil { - return nil, err - } - matches = append(matches, cpeMatches...) - return matches, nil +func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) { + return search.ByCriteria(store, d, p, m.Type(), search.CommonCriteria...) } diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go index 5394b3785c6..15544519c0e 100644 --- a/grype/pkg/package_test.go +++ b/grype/pkg/package_test.go @@ -3,10 +3,9 @@ package pkg import ( "testing" - "github.com/anchore/syft/syft/source" - "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" "github.com/scylladb/go-set" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" diff --git a/grype/presenter/cyclonedx/presenter_test.go b/grype/presenter/cyclonedx/presenter_test.go index f82f637f5bc..62d0ee597db 100644 --- a/grype/presenter/cyclonedx/presenter_test.go +++ b/grype/presenter/cyclonedx/presenter_test.go @@ -6,11 +6,10 @@ import ( "regexp" "testing" - "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/go-testutils" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/stereoscope/pkg/imagetest" syftPkg "github.com/anchore/syft/syft/pkg" @@ -21,51 +20,49 @@ import ( var update = flag.Bool("update", false, "update the *.golden files for json presenters") func createResults() (match.Matches, []pkg.Package) { - // the catalog is needed to assign the package IDs - catalog := syftPkg.NewCatalog( - syftPkg.Package{ - Name: "package-1", - Version: "1.0.1", - Type: syftPkg.DebPkg, - }, - syftPkg.Package{ - Name: "package-2", - Version: "2.0.1", - Type: syftPkg.DebPkg, - Licenses: []string{ - "MIT", - "Apache-v2", - }, - }) - - packages := pkg.FromCatalog(catalog) - var pkg1 = packages[0] - var pkg2 = packages[1] + pkg1 := pkg.Package{ + ID: "package-1-id", + Name: "package-1", + Version: "1.0.1", + Type: syftPkg.DebPkg, + } + pkg2 := pkg.Package{ + ID: "package-2-id", + Name: "package-2", + Version: "2.0.1", + Type: syftPkg.DebPkg, + Licenses: []string{ + "MIT", + "Apache-v2", + }, + } var match1 = match.Match{ - Type: match.ExactDirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0001", Namespace: "source-1", }, Package: pkg1, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Matcher: match.DpkgMatcher, }, }, } var match2 = match.Match{ - Type: match.ExactIndirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0002", Namespace: "source-2", }, Package: pkg2, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactIndirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "some": "key", @@ -76,9 +73,9 @@ func createResults() (match.Matches, []pkg.Package) { matches := match.NewMatches() - matches.Add(pkg1, match1, match2) + matches.Add(match1, match2) - return matches, packages + return matches, []pkg.Package{pkg1, pkg2} } func TestCycloneDxPresenterImage(t *testing.T) { diff --git a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden index 9de6191d6e9..b1017cd7f51 100644 --- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden +++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden @@ -1,7 +1,7 @@ - + - 2021-03-29T10:37:58-04:00 + 2022-01-14T16:06:26-05:00 anchore @@ -19,7 +19,7 @@ package-1 1.0.1 - + CVE-1999-0001 http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0001 @@ -41,7 +41,21 @@ 1999-01 description - + + + + package-2 + 2.0.1 + + + MIT + + + Apache-v2 + + + + CVE-1999-0002 http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0002 @@ -65,17 +79,5 @@ - - package-2 - 2.0.1 - - - MIT - - - Apache-v2 - - - \ No newline at end of file diff --git a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden index 74f5c003adc..99d2c14811e 100644 --- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden +++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden @@ -1,7 +1,7 @@ - + - 2021-03-29T10:37:58-04:00 + 2022-01-14T16:06:26-05:00 anchore @@ -19,7 +19,7 @@ package-1 1.0.1 - + CVE-1999-0001 http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0001 @@ -41,7 +41,21 @@ 1999-01 description - + + + + package-2 + 2.0.1 + + + MIT + + + Apache-v2 + + + + CVE-1999-0002 http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0002 @@ -65,17 +79,5 @@ - - package-2 - 2.0.1 - - - MIT - - - Apache-v2 - - - \ No newline at end of file diff --git a/grype/presenter/cyclonedx/vulnerability_test.go b/grype/presenter/cyclonedx/vulnerability_test.go index f4bd1b129e8..552376cc9c3 100644 --- a/grype/presenter/cyclonedx/vulnerability_test.go +++ b/grype/presenter/cyclonedx/vulnerability_test.go @@ -87,10 +87,9 @@ func TestNewVulnerability_AlwaysIncludesSeverity(t *testing.T) { { name: "populates severity with missing CVSS records", match: match.Match{ - Type: 0, Vulnerability: vulnerability.Vulnerability{}, Package: pkg.Package{}, - MatchDetails: nil, + Details: nil, }, metadataProvider: &metadataProvider{ severity: "High", @@ -99,10 +98,9 @@ func TestNewVulnerability_AlwaysIncludesSeverity(t *testing.T) { { name: "populates severity with all CVSS records", match: match.Match{ - Type: 0, Vulnerability: vulnerability.Vulnerability{}, Package: pkg.Package{}, - MatchDetails: nil, + Details: nil, }, metadataProvider: &metadataProvider{ severity: "High", diff --git a/grype/presenter/json/presenter_test.go b/grype/presenter/json/presenter_test.go index a53517524c1..a9c718a32f8 100644 --- a/grype/presenter/json/presenter_test.go +++ b/grype/presenter/json/presenter_test.go @@ -5,9 +5,8 @@ import ( "flag" "testing" - "github.com/anchore/syft/syft/linux" - "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/linux" "github.com/anchore/go-testutils" "github.com/anchore/grype/grype/match" @@ -38,6 +37,7 @@ func TestJsonImgsPresenter(t *testing.T) { } var pkg1 = pkg.Package{ + ID: pkg.ID("package-1-id"), Name: "package-1", Version: "1.1.1", Type: syftPkg.DebPkg, @@ -57,6 +57,7 @@ func TestJsonImgsPresenter(t *testing.T) { } var pkg2 = pkg.Package{ + ID: pkg.ID("package-2-id"), Name: "package-2", Version: "2.2.2", Type: syftPkg.DebPkg, @@ -66,7 +67,7 @@ func TestJsonImgsPresenter(t *testing.T) { } var match1 = match.Match{ - Type: match.ExactDirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0001", Namespace: "source-1", @@ -77,8 +78,9 @@ func TestJsonImgsPresenter(t *testing.T) { }, }, Package: pkg1, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -94,14 +96,15 @@ func TestJsonImgsPresenter(t *testing.T) { } var match2 = match.Match{ - Type: match.ExactIndirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0002", Namespace: "source-2", }, Package: pkg1, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactIndirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "cpe": "somecpe", @@ -114,7 +117,7 @@ func TestJsonImgsPresenter(t *testing.T) { } var match3 = match.Match{ - Type: match.ExactIndirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0003", Namespace: "source-1", @@ -125,8 +128,9 @@ func TestJsonImgsPresenter(t *testing.T) { }, }, Package: pkg1, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactIndirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "language": "java", @@ -139,7 +143,7 @@ func TestJsonImgsPresenter(t *testing.T) { } matches := match.NewMatches() - matches.Add(pkg1, match1, match2, match3) + matches.Add(match1, match2, match3) packages := []pkg.Package{pkg1, pkg2} @@ -211,7 +215,7 @@ func TestJsonDirsPresenter(t *testing.T) { } var match1 = match.Match{ - Type: match.ExactDirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0001", Namespace: "source-1", @@ -222,8 +226,9 @@ func TestJsonDirsPresenter(t *testing.T) { }, }, Package: pkg1, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -239,14 +244,15 @@ func TestJsonDirsPresenter(t *testing.T) { } var match2 = match.Match{ - Type: match.ExactIndirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0002", Namespace: "source-2", }, Package: pkg1, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactIndirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "cpe": "somecpe", @@ -259,7 +265,7 @@ func TestJsonDirsPresenter(t *testing.T) { } var match3 = match.Match{ - Type: match.ExactIndirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0003", Namespace: "source-1", @@ -270,8 +276,9 @@ func TestJsonDirsPresenter(t *testing.T) { }, }, Package: pkg1, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactIndirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "language": "java", @@ -284,7 +291,7 @@ func TestJsonDirsPresenter(t *testing.T) { } matches := match.NewMatches() - matches.Add(pkg1, match1, match2, match3) + matches.Add(match1, match2, match3) s, err := syftSource.NewFromDirectory("/some/path") if err != nil { diff --git a/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden b/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden index 83a3087cce5..f5611639ced 100644 --- a/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden +++ b/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden @@ -28,6 +28,7 @@ "relatedVulnerabilities": [], "matchDetails": [ { + "type": "exact-direct-match", "matcher": "dpkg-matcher", "searchedBy": { "distro": { @@ -89,6 +90,7 @@ "relatedVulnerabilities": [], "matchDetails": [ { + "type": "exact-indirect-match", "matcher": "dpkg-matcher", "searchedBy": { "cpe": "somecpe" @@ -135,6 +137,7 @@ "relatedVulnerabilities": [], "matchDetails": [ { + "type": "exact-indirect-match", "matcher": "dpkg-matcher", "searchedBy": { "language": "java" diff --git a/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden b/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden index 5453491156a..060df2ca7f6 100644 --- a/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden +++ b/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden @@ -28,6 +28,7 @@ "relatedVulnerabilities": [], "matchDetails": [ { + "type": "exact-direct-match", "matcher": "dpkg-matcher", "searchedBy": { "distro": { @@ -92,6 +93,7 @@ "relatedVulnerabilities": [], "matchDetails": [ { + "type": "exact-indirect-match", "matcher": "dpkg-matcher", "searchedBy": { "cpe": "somecpe" @@ -141,6 +143,7 @@ "relatedVulnerabilities": [], "matchDetails": [ { + "type": "exact-indirect-match", "matcher": "dpkg-matcher", "searchedBy": { "language": "java" diff --git a/grype/presenter/models/document_test.go b/grype/presenter/models/document_test.go index 98c5648ea29..8642c5d8c11 100644 --- a/grype/presenter/models/document_test.go +++ b/grype/presenter/models/document_test.go @@ -15,43 +15,57 @@ import ( func TestPackagesAreSorted(t *testing.T) { var pkg1 = pkg.Package{ + ID: "package-1-id", Name: "package-1", Version: "1.1.1", Type: syftPkg.DebPkg, } var pkg2 = pkg.Package{ + ID: "package-2-id", Name: "package-2", Version: "2.2.2", Type: syftPkg.DebPkg, } var match1 = match.Match{ - Type: match.ExactDirectMatch, Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0003", }, Package: pkg1, + Details: match.Details{ + { + Type: match.ExactDirectMatch, + }, + }, } var match2 = match.Match{ - Type: match.ExactIndirectMatch, Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0002", }, Package: pkg1, + Details: match.Details{ + { + Type: match.ExactIndirectMatch, + }, + }, } var match3 = match.Match{ - Type: match.ExactIndirectMatch, Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0001", }, Package: pkg1, + Details: match.Details{ + { + Type: match.ExactIndirectMatch, + }, + }, } matches := match.NewMatches() - matches.Add(pkg1, match1, match2, match3) + matches.Add(match1, match2, match3) packages := []pkg.Package{pkg1, pkg2} diff --git a/grype/presenter/models/match.go b/grype/presenter/models/match.go index 43d6c87e477..a475d2e87e2 100644 --- a/grype/presenter/models/match.go +++ b/grype/presenter/models/match.go @@ -18,6 +18,7 @@ type Match struct { // MatchDetails contains all data that indicates how the result match was found type MatchDetails struct { + Type string `json:"type"` Matcher string `json:"matcher"` SearchedBy interface{} `json:"searchedBy"` Found interface{} `json:"found"` @@ -40,10 +41,11 @@ func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.Metad return nil, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err) } - details := make([]MatchDetails, len(m.MatchDetails)) - for idx, d := range m.MatchDetails { + details := make([]MatchDetails, len(m.Details)) + for idx, d := range m.Details { details[idx] = MatchDetails{ - Matcher: d.Matcher.String(), + Type: string(d.Type), + Matcher: string(d.Matcher), SearchedBy: d.SearchedBy, Found: d.Found, } diff --git a/grype/presenter/models/models_helpers.go b/grype/presenter/models/models_helpers.go index a8b32498f12..78f49ed4abc 100644 --- a/grype/presenter/models/models_helpers.go +++ b/grype/presenter/models/models_helpers.go @@ -3,6 +3,8 @@ package models import ( "testing" + "github.com/google/uuid" + grypeDb "github.com/anchore/grype/grype/db/v3" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" @@ -28,7 +30,7 @@ func generateMatches(t *testing.T, p pkg.Package) match.Matches { matches := []match.Match{ { - Type: match.ExactDirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0001", Namespace: "source-1", @@ -38,8 +40,9 @@ func generateMatches(t *testing.T, p pkg.Package) match.Matches { }, }, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -54,14 +57,15 @@ func generateMatches(t *testing.T, p pkg.Package) match.Matches { }, }, { - Type: match.ExactIndirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0002", Namespace: "source-2", }, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactIndirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "cpe": "somecpe", @@ -74,8 +78,7 @@ func generateMatches(t *testing.T, p pkg.Package) match.Matches { }, } - collection := match.NewMatches() - collection.Add(p, matches...) + collection := match.NewMatches(matches...) return collection } @@ -85,6 +88,7 @@ func generatePackages(t *testing.T) []pkg.Package { return []pkg.Package{ { + ID: pkg.ID(uuid.NewString()), Name: "package-1", Version: "1.1.1", Type: syftPkg.DebPkg, @@ -102,6 +106,7 @@ func generatePackages(t *testing.T) []pkg.Package { }, }, { + ID: pkg.ID(uuid.NewString()), Name: "package-2", Version: "2.2.2", Type: syftPkg.DebPkg, diff --git a/grype/presenter/table/presenter_test.go b/grype/presenter/table/presenter_test.go index 5f93d848b72..253b9cfb7b7 100644 --- a/grype/presenter/table/presenter_test.go +++ b/grype/presenter/table/presenter_test.go @@ -5,11 +5,10 @@ import ( "flag" "testing" - "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/go-testutils" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/go-test/deep" @@ -23,33 +22,36 @@ func TestTablePresenter(t *testing.T) { var buffer bytes.Buffer var pkg1 = pkg.Package{ + ID: "package-1-id", Name: "package-1", Version: "1.0.1", Type: syftPkg.DebPkg, } var pkg2 = pkg.Package{ + ID: "package-2-id", Name: "package-2", Version: "2.0.1", Type: syftPkg.DebPkg, } var match1 = match.Match{ - Type: match.ExactDirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0001", Namespace: "source-1", }, Package: pkg1, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Matcher: match.DpkgMatcher, }, }, } var match2 = match.Match{ - Type: match.ExactIndirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-1999-0002", Namespace: "source-2", @@ -60,8 +62,9 @@ func TestTablePresenter(t *testing.T) { }, }, Package: pkg2, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactIndirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "some": "key", @@ -72,7 +75,7 @@ func TestTablePresenter(t *testing.T) { matches := match.NewMatches() - matches.Add(pkg1, match1, match2) + matches.Add(match1, match2) packages := []pkg.Package{pkg1, pkg2} diff --git a/grype/matcher/common/cpe_matchers.go b/grype/search/cpe.go similarity index 73% rename from grype/matcher/common/cpe_matchers.go rename to grype/search/cpe.go index 66dde4e2f57..57a994d955a 100644 --- a/grype/matcher/common/cpe_matchers.go +++ b/grype/search/cpe.go @@ -1,4 +1,4 @@ -package common +package search import ( "fmt" @@ -13,12 +13,12 @@ import ( "github.com/scylladb/go-set/strset" ) -type SearchedByCPEs struct { +type CPEParameters struct { Namespace string `json:"namespace"` CPEs []string `json:"cpes"` } -func (i *SearchedByCPEs) Merge(other SearchedByCPEs) error { +func (i *CPEParameters) Merge(other CPEParameters) error { if i.Namespace != other.Namespace { return fmt.Errorf("namespaces do not match") } @@ -31,12 +31,12 @@ func (i *SearchedByCPEs) Merge(other SearchedByCPEs) error { return nil } -type FoundCPEs struct { +type CPEResult struct { VersionConstraint string `json:"versionConstraint"` CPEs []string `json:"cpes"` } -func (h FoundCPEs) Equals(other FoundCPEs) bool { +func (h CPEResult) Equals(other CPEResult) bool { if h.VersionConstraint != other.VersionConstraint { return false } @@ -54,8 +54,10 @@ func (h FoundCPEs) Equals(other FoundCPEs) bool { return true } -// FindMatchesByPackageCPE retrieves all vulnerabilities that match the generated CPE -func FindMatchesByPackageCPE(store vulnerability.ProviderByCPE, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { +// ByPackageCPE retrieves all vulnerabilities that match the generated CPE +func ByPackageCPE(store vulnerability.ProviderByCPE, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { + // we attempt to merge match details within the same matcher when searching by CPEs, in this way there are fewer duplicated match + // objects (and fewer duplicated match details). matchesByFingerprint := make(map[match.Fingerprint]match.Match) for _, cpe := range p.CPEs { // prefer the CPE version, but if npt specified use the package version @@ -63,31 +65,27 @@ func FindMatchesByPackageCPE(store vulnerability.ProviderByCPE, p pkg.Package, u if searchVersion == wfn.NA || searchVersion == wfn.Any { searchVersion = p.Version } - searchVersionObj, err := version.NewVersion(searchVersion, version.FormatFromPkgType(p.Type)) + verObj, err := version.NewVersion(searchVersion, version.FormatFromPkgType(p.Type)) if err != nil { - return nil, fmt.Errorf("matcher failed to parse version pkg='%s' ver='%s': %w", p.Name, p.Version, err) + return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) } // find all vulnerability records in the DB for the given CPE (not including version comparisons) allPkgVulns, err := store.GetByCPE(cpe) if err != nil { - return nil, fmt.Errorf("matcher failed to fetch by CPE pkg='%s': %w", p.Name, err) + return nil, fmt.Errorf("matcher failed to fetch by CPE pkg=%q: %w", p.Name, err) + } + + applicableVulns, err := onlyVulnerableVersions(verObj, allPkgVulns) + if err != nil { + return nil, fmt.Errorf("unable to filter cpe-related vulnerabilities: %w", err) } // for each vulnerability record found, check the version constraint. If the constraint is satisfied // relative to the current version information from the CPE (or the package) then the given package // is vulnerable. - for _, vuln := range allPkgVulns { - isPackageVulnerable, err := vuln.Constraint.Satisfied(searchVersionObj) - if err != nil { - return nil, fmt.Errorf("cpe matcher failed to check constraint='%s' version='%s': %w", vuln.Constraint, searchVersionObj, err) - } - - if !isPackageVulnerable { - continue - } - - addNewMatch(matchesByFingerprint, vuln, p, *searchVersionObj, upstreamMatcher, cpe) + for _, vuln := range applicableVulns { + addNewMatch(matchesByFingerprint, vuln, p, *verObj, upstreamMatcher, cpe) } } @@ -96,7 +94,7 @@ func FindMatchesByPackageCPE(store vulnerability.ProviderByCPE, p pkg.Package, u func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vulnerability.Vulnerability, p pkg.Package, searchVersion version.Version, upstreamMatcher match.MatcherType, searchedByCPE syftPkg.CPE) { candidateMatch := match.Match{ - Type: match.FuzzyMatch, + Vulnerability: vuln, Package: p, } @@ -105,17 +103,18 @@ func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vu candidateMatch = existingMatch } - candidateMatch.MatchDetails = addMatchDetails(candidateMatch.MatchDetails, - match.Details{ + candidateMatch.Details = addMatchDetails(candidateMatch.Details, + match.Detail{ + Type: match.CPEMatch, Confidence: 0.9, // TODO: this is hard coded for now Matcher: upstreamMatcher, - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: vuln.Namespace, CPEs: []string{ searchedByCPE.BindToFmtString(), }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: vuln.Constraint.String(), CPEs: cpesToString(filterCPEsByVersion(searchVersion, vuln.CPEs)), }, @@ -125,23 +124,23 @@ func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vu matchesByFingerprint[candidateMatch.Fingerprint()] = candidateMatch } -func addMatchDetails(existingDetails []match.Details, newDetails match.Details) []match.Details { - newFound, ok := newDetails.Found.(FoundCPEs) +func addMatchDetails(existingDetails []match.Detail, newDetails match.Detail) []match.Detail { + newFound, ok := newDetails.Found.(CPEResult) if !ok { return existingDetails } - newSearchedBy, ok := newDetails.SearchedBy.(SearchedByCPEs) + newSearchedBy, ok := newDetails.SearchedBy.(CPEParameters) if !ok { return existingDetails } for idx, detail := range existingDetails { - found, ok := detail.Found.(FoundCPEs) + found, ok := detail.Found.(CPEResult) if !ok { continue } - searchedBy, ok := detail.SearchedBy.(SearchedByCPEs) + searchedBy, ok := detail.SearchedBy.(CPEParameters) if !ok { continue } diff --git a/grype/matcher/common/cpe_matchers_test.go b/grype/search/cpe_test.go similarity index 87% rename from grype/matcher/common/cpe_matchers_test.go rename to grype/search/cpe_test.go index 1bd05b94f46..6e102513f1d 100644 --- a/grype/matcher/common/cpe_matchers_test.go +++ b/grype/search/cpe_test.go @@ -1,9 +1,10 @@ -package common +package search import ( "testing" "github.com/anchore/grype/grype/db" + "github.com/google/uuid" grypeDB "github.com/anchore/grype/grype/db/v3" "github.com/anchore/grype/grype/match" @@ -124,7 +125,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) { }, expected: []match.Match{ { - Type: match.FuzzyMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2017-fake-1", }, @@ -138,14 +139,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"}, }, - Found: FoundCPEs{ + Found: CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, VersionConstraint: "< 3.7.6 (semver)", }, @@ -169,7 +171,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) { }, expected: []match.Match{ { - Type: match.FuzzyMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2017-fake-1", }, @@ -184,16 +186,17 @@ func TestFindMatchesByPackageCPE(t *testing.T) { Type: syftPkg.GemPkg, }, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*", }, Namespace: "nvd", }, - Found: FoundCPEs{ + Found: CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, VersionConstraint: "< 3.7.6 (semver)", }, @@ -202,7 +205,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) { }, }, { - Type: match.FuzzyMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2017-fake-2", }, @@ -217,14 +220,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { Type: syftPkg.GemPkg, }, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*"}, Namespace: "nvd", }, - Found: FoundCPEs{ + Found: CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"}, VersionConstraint: "< 3.7.4 (semver)", }, @@ -247,7 +251,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) { }, expected: []match.Match{ { - Type: match.FuzzyMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2017-fake-3", }, @@ -260,14 +264,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ CPEs: []string{"cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*"}, Namespace: "nvd", }, - Found: FoundCPEs{ + Found: CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*"}, VersionConstraint: "= 4.0.1 (semver)", }, @@ -280,6 +285,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) { { name: "no match", p: pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "couldntgetthisrightcouldyou", Version: "4.0.1", Language: syftPkg.Ruby, @@ -298,7 +304,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) { }, expected: []match.Match{ { - Type: match.FuzzyMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2017-fake-4", }, @@ -310,14 +316,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { Version: "98SE1", }, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ CPEs: []string{"cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*"}, Namespace: "nvd", }, - Found: FoundCPEs{ + Found: CPEResult{ CPEs: []string{"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*"}, VersionConstraint: "< 98SP3 (unknown)", }, @@ -340,7 +347,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) { }, expected: []match.Match{ { - Type: match.FuzzyMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2017-fake-5", }, @@ -354,14 +361,15 @@ func TestFindMatchesByPackageCPE(t *testing.T) { Type: syftPkg.GemPkg, }, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.CPEMatch, Confidence: 0.9, - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ CPEs: []string{"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*"}, Namespace: "nvd", }, - Found: FoundCPEs{ + Found: CPEResult{ CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", @@ -378,11 +386,11 @@ func TestFindMatchesByPackageCPE(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual, err := FindMatchesByPackageCPE(db.NewVulnerabilityProvider(newMockStore()), test.p, matcher) + actual, err := ByPackageCPE(db.NewVulnerabilityProvider(newMockStore()), test.p, matcher) assert.NoError(t, err) assertMatchesUsingIDsForVulnerabilities(t, test.expected, actual) for idx, e := range test.expected { - assert.Equal(t, e.MatchDetails, actual[idx].MatchDetails) + assert.Equal(t, e.Details, actual[idx].Details) } }) } @@ -440,21 +448,21 @@ func TestFilterCPEsByVersion(t *testing.T) { func TestAddMatchDetails(t *testing.T) { tests := []struct { name string - existing []match.Details - new match.Details - expected []match.Details + existing []match.Detail + new match.Detail + expected []match.Detail }{ { name: "append new entry -- found not equal", - existing: []match.Details{ + existing: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -462,29 +470,29 @@ func TestAddMatchDetails(t *testing.T) { }, }, }, - new: match.Details{ - SearchedBy: SearchedByCPEs{ + new: match.Detail{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "totally-different-search", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "totally-different-match", }, }, }, - expected: []match.Details{ + expected: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -492,13 +500,13 @@ func TestAddMatchDetails(t *testing.T) { }, }, { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "totally-different-search", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "totally-different-match", @@ -509,15 +517,15 @@ func TestAddMatchDetails(t *testing.T) { }, { name: "append new entry -- searchedBy merge fails", - existing: []match.Details{ + existing: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -525,29 +533,29 @@ func TestAddMatchDetails(t *testing.T) { }, }, }, - new: match.Details{ - SearchedBy: SearchedByCPEs{ + new: match.Detail{ + SearchedBy: CPEParameters{ Namespace: "totally-different", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, - expected: []match.Details{ + expected: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -555,13 +563,13 @@ func TestAddMatchDetails(t *testing.T) { }, }, { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "totally-different", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -572,15 +580,15 @@ func TestAddMatchDetails(t *testing.T) { }, { name: "merge with exiting entry", - existing: []match.Details{ + existing: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -588,30 +596,30 @@ func TestAddMatchDetails(t *testing.T) { }, }, }, - new: match.Details{ - SearchedBy: SearchedByCPEs{ + new: match.Detail{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "totally-different-search", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, - expected: []match.Details{ + expected: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", "totally-different-search", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -622,15 +630,15 @@ func TestAddMatchDetails(t *testing.T) { }, { name: "no addition - bad new searchedBy type", - existing: []match.Details{ + existing: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -638,24 +646,24 @@ func TestAddMatchDetails(t *testing.T) { }, }, }, - new: match.Details{ + new: match.Detail{ SearchedBy: "something else!", - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, - expected: []match.Details{ + expected: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -666,15 +674,15 @@ func TestAddMatchDetails(t *testing.T) { }, { name: "no addition - bad new found type", - existing: []match.Details{ + existing: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -682,8 +690,8 @@ func TestAddMatchDetails(t *testing.T) { }, }, }, - new: match.Details{ - SearchedBy: SearchedByCPEs{ + new: match.Detail{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", @@ -691,15 +699,15 @@ func TestAddMatchDetails(t *testing.T) { }, Found: "something-else!", }, - expected: []match.Details{ + expected: []match.Detail{ { - SearchedBy: SearchedByCPEs{ + SearchedBy: CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, - Found: FoundCPEs{ + Found: CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", @@ -720,19 +728,19 @@ func TestAddMatchDetails(t *testing.T) { func TestCPESearchHit_Equals(t *testing.T) { tests := []struct { name string - current FoundCPEs - other FoundCPEs + current CPEResult + other CPEResult expected bool }{ { name: "different version constraint", - current: FoundCPEs{ + current: CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, - other: FoundCPEs{ + other: CPEResult{ VersionConstraint: "different-constraint", CPEs: []string{ "a-cpe", @@ -742,13 +750,13 @@ func TestCPESearchHit_Equals(t *testing.T) { }, { name: "different number of CPEs", - current: FoundCPEs{ + current: CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, - other: FoundCPEs{ + other: CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", @@ -759,13 +767,13 @@ func TestCPESearchHit_Equals(t *testing.T) { }, { name: "different CPE value", - current: FoundCPEs{ + current: CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, - other: FoundCPEs{ + other: CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "b-cpe", @@ -775,13 +783,13 @@ func TestCPESearchHit_Equals(t *testing.T) { }, { name: "matches", - current: FoundCPEs{ + current: CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, - other: FoundCPEs{ + other: CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", diff --git a/grype/search/criteria.go b/grype/search/criteria.go new file mode 100644 index 00000000000..2b931702347 --- /dev/null +++ b/grype/search/criteria.go @@ -0,0 +1,47 @@ +package search + +import ( + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/vulnerability" +) + +var ( + ByCPE Criteria = "by-cpe" + ByLanguage Criteria = "by-language" + ByDistro Criteria = "by-distro" + CommonCriteria = []Criteria{ + ByLanguage, + ByCPE, + } +) + +type Criteria string + +func ByCriteria(store vulnerability.Provider, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType, criteria ...Criteria) ([]match.Match, error) { + var matches []match.Match + for _, c := range criteria { + switch c { + case ByCPE: + m, err := ByPackageCPE(store, p, upstreamMatcher) + if err != nil { + return nil, err + } + matches = append(matches, m...) + case ByLanguage: + m, err := ByPackageLanguage(store, p, upstreamMatcher) + if err != nil { + return nil, err + } + matches = append(matches, m...) + case ByDistro: + m, err := ByPackageDistro(store, d, p, upstreamMatcher) + if err != nil { + return nil, err + } + matches = append(matches, m...) + } + } + return matches, nil +} diff --git a/grype/matcher/common/distro_matchers.go b/grype/search/distro.go similarity index 52% rename from grype/matcher/common/distro_matchers.go rename to grype/search/distro.go index 77ef27b608e..b07a58f7d27 100644 --- a/grype/matcher/common/distro_matchers.go +++ b/grype/search/distro.go @@ -1,11 +1,8 @@ -package common +package search import ( - "errors" "fmt" - "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" @@ -13,46 +10,34 @@ import ( "github.com/anchore/grype/grype/vulnerability" ) -func FindMatchesByPackageDistro(store vulnerability.ProviderByDistro, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { +func ByPackageDistro(store vulnerability.ProviderByDistro, d *distro.Distro, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { if d == nil { return nil, nil } verObj, err := version.NewVersionFromPkg(p) if err != nil { - return nil, fmt.Errorf("matcher failed to parse version pkg='%s' ver='%s': %w", p.Name, p.Version, err) + return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) } - var allPkgVulns []vulnerability.Vulnerability - - allPkgVulns, err = store.GetByDistro(d, p) + allPkgVulns, err := store.GetByDistro(d, p) if err != nil { - return nil, fmt.Errorf("matcher failed to fetch distro='%s' pkg='%s': %w", d, p.Name, err) + return nil, fmt.Errorf("matcher failed to fetch distro=%q pkg=%q: %w", d, p.Name, err) } - matches := make([]match.Match, 0) - for _, vuln := range allPkgVulns { - // if the constraint it met, then the given package has the vulnerability - isPackageVulnerable, err := vuln.Constraint.Satisfied(verObj) - if err != nil { - var e *version.NonFatalConstraintError - if errors.As(err, &e) { - log.Warn(e) - } else { - return nil, fmt.Errorf("distro matcher failed to check constraint='%s' version='%s': %w", vuln.Constraint, verObj, err) - } - } - - if !isPackageVulnerable { - continue - } + applicableVulns, err := onlyVulnerableVersions(verObj, allPkgVulns) + if err != nil { + return nil, fmt.Errorf("unable to filter distro-related vulnerabilities: %w", err) + } + var matches []match.Match + for _, vuln := range applicableVulns { matches = append(matches, match.Match{ - Type: match.ExactDirectMatch, Vulnerability: vuln, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Matcher: upstreamMatcher, SearchedBy: map[string]interface{}{ "distro": map[string]string{ diff --git a/grype/matcher/common/distro_matchers_test.go b/grype/search/distro_test.go similarity index 89% rename from grype/matcher/common/distro_matchers_test.go rename to grype/search/distro_test.go index 538847d8006..4ab2c21730e 100644 --- a/grype/matcher/common/distro_matchers_test.go +++ b/grype/search/distro_test.go @@ -1,4 +1,4 @@ -package common +package search import ( "strings" @@ -10,6 +10,7 @@ import ( "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -54,6 +55,7 @@ func (pr *mockDistroProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vu func TestFindMatchesByPackageDistro(t *testing.T) { p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "neutron", Version: "2014.1.3-6", Type: syftPkg.DebPkg, @@ -69,13 +71,14 @@ func TestFindMatchesByPackageDistro(t *testing.T) { expected := []match.Match{ { - Type: match.ExactDirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2014-fake-1", }, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -98,13 +101,14 @@ func TestFindMatchesByPackageDistro(t *testing.T) { } store := newMockProviderByDistro() - actual, err := FindMatchesByPackageDistro(store, d, p, match.PythonMatcher) + actual, err := ByPackageDistro(store, d, p, match.PythonMatcher) assert.NoError(t, err) assertMatchesUsingIDsForVulnerabilities(t, expected, actual) } func TestFindMatchesByPackageDistroSles(t *testing.T) { p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "sles_test_package", Version: "2014.1.3-6", Type: syftPkg.RpmPkg, @@ -120,13 +124,14 @@ func TestFindMatchesByPackageDistroSles(t *testing.T) { expected := []match.Match{ { - Type: match.ExactDirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2014-fake-4", }, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -149,7 +154,7 @@ func TestFindMatchesByPackageDistroSles(t *testing.T) { } store := newMockProviderByDistro() - actual, err := FindMatchesByPackageDistro(store, d, p, match.PythonMatcher) + actual, err := ByPackageDistro(store, d, p, match.PythonMatcher) assert.NoError(t, err) assertMatchesUsingIDsForVulnerabilities(t, expected, actual) } diff --git a/grype/search/language.go b/grype/search/language.go new file mode 100644 index 00000000000..b3fe3544414 --- /dev/null +++ b/grype/search/language.go @@ -0,0 +1,52 @@ +package search + +import ( + "fmt" + + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" +) + +func ByPackageLanguage(store vulnerability.ProviderByLanguage, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { + verObj, err := version.NewVersionFromPkg(p) + if err != nil { + return nil, fmt.Errorf("matcher failed to parse version pkg=%q ver=%q: %w", p.Name, p.Version, err) + } + + allPkgVulns, err := store.GetByLanguage(p.Language, p) + if err != nil { + return nil, fmt.Errorf("matcher failed to fetch language=%q pkg=%q: %w", p.Language, p.Name, err) + } + + applicableVulns, err := onlyVulnerableVersions(verObj, allPkgVulns) + if err != nil { + return nil, fmt.Errorf("unable to filter language-related vulnerabilities: %w", err) + } + + var matches []match.Match + for _, vuln := range applicableVulns { + matches = append(matches, match.Match{ + + Vulnerability: vuln, + Package: p, + Details: []match.Detail{ + { + Type: match.ExactDirectMatch, + Confidence: 1.0, // TODO: this is hard coded for now + Matcher: upstreamMatcher, + SearchedBy: map[string]interface{}{ + "language": string(p.Language), + "namespace": vuln.Namespace, + }, + Found: map[string]interface{}{ + "versionConstraint": vuln.Constraint.String(), + }, + }, + }, + }) + } + + return matches, err +} diff --git a/grype/matcher/common/language_matchers_test.go b/grype/search/language_test.go similarity index 90% rename from grype/matcher/common/language_matchers_test.go rename to grype/search/language_test.go index 87f29638023..94c993f68fb 100644 --- a/grype/matcher/common/language_matchers_test.go +++ b/grype/search/language_test.go @@ -1,10 +1,11 @@ -package common +package search import ( "fmt" "testing" "github.com/anchore/grype/grype/match" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/pkg" @@ -52,6 +53,7 @@ func (pr *mockLanguageProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) func TestFindMatchesByPackageLanguage(t *testing.T) { p := pkg.Package{ + ID: pkg.ID(uuid.NewString()), Name: "activerecord", Version: "3.7.5", Language: syftPkg.Ruby, @@ -60,13 +62,14 @@ func TestFindMatchesByPackageLanguage(t *testing.T) { expected := []match.Match{ { - Type: match.ExactDirectMatch, + Vulnerability: vulnerability.Vulnerability{ ID: "CVE-2017-fake-1", }, Package: p, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1, SearchedBy: map[string]interface{}{ "language": "ruby", @@ -82,7 +85,7 @@ func TestFindMatchesByPackageLanguage(t *testing.T) { } store := newMockProviderByLanguage() - actual, err := FindMatchesByPackageLanguage(store, p.Language, p, match.RubyGemMatcher) + actual, err := ByPackageLanguage(store, p, match.RubyGemMatcher) assert.NoError(t, err) assertMatchesUsingIDsForVulnerabilities(t, expected, actual) } diff --git a/grype/search/only_vulnerable_versions.go b/grype/search/only_vulnerable_versions.go new file mode 100644 index 00000000000..482429d4f2a --- /dev/null +++ b/grype/search/only_vulnerable_versions.go @@ -0,0 +1,34 @@ +package search + +import ( + "errors" + "fmt" + + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" +) + +func onlyVulnerableVersions(verObj *version.Version, allVulns []vulnerability.Vulnerability) ([]vulnerability.Vulnerability, error) { + var vulns []vulnerability.Vulnerability + + for _, vuln := range allVulns { + isPackageVulnerable, err := vuln.Constraint.Satisfied(verObj) + if err != nil { + var e *version.NonFatalConstraintError + if errors.As(err, &e) { + log.Warn(e) + } else { + return nil, fmt.Errorf("failed to check constraint=%q version=%q: %w", vuln.Constraint, verObj, err) + } + } + + if !isPackageVulnerable { + continue + } + + vulns = append(vulns, vuln) + } + + return vulns, nil +} diff --git a/grype/matcher/common/utils_test.go b/grype/search/utils_test.go similarity index 97% rename from grype/matcher/common/utils_test.go rename to grype/search/utils_test.go index 1ff853999e9..2e9b044bad6 100644 --- a/grype/matcher/common/utils_test.go +++ b/grype/search/utils_test.go @@ -1,4 +1,4 @@ -package common +package search import ( "testing" diff --git a/grype/vulnerability/vulnerability.go b/grype/vulnerability/vulnerability.go index 7831329fe9f..8224a1f010a 100644 --- a/grype/vulnerability/vulnerability.go +++ b/grype/vulnerability/vulnerability.go @@ -62,7 +62,7 @@ func NewVulnerability(vuln grypeDB.Vulnerability) (*Vulnerability, error) { } func (v Vulnerability) String() string { - return fmt.Sprintf("Vuln(id=%s constraint='%s')", v.ID, v.Constraint) + return fmt.Sprintf("Vuln(id=%s constraint=%q)", v.ID, v.Constraint.String()) } func (v *Vulnerability) hash() string { diff --git a/internal/config/registry_test.go b/internal/config/registry_test.go index c9199f5b10d..9b2faf6d13c 100644 --- a/internal/config/registry_test.go +++ b/internal/config/registry_test.go @@ -2,9 +2,10 @@ package config import ( "fmt" - "github.com/anchore/stereoscope/pkg/image" "testing" + "github.com/anchore/stereoscope/pkg/image" + "github.com/stretchr/testify/assert" ) diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 59f41cf54cf..8bdabc085c1 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -30,13 +30,14 @@ func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca if err != nil { t.Fatalf("failed to create vuln obj: %+v", err) } - theResult.Add(thePkg, match.Match{ + theResult.Add(match.Match{ // note: we are matching on the secdb record, not NVD primarily - Type: match.ExactDirectMatch, + Vulnerability: *vulnObj, Package: thePkg, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "cpe": "cpe:2.3:*:*:libvncserver:0.9.9:*:*:*:*:*:*:*", @@ -63,12 +64,13 @@ func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPk if err != nil { t.Fatalf("failed to create vuln obj: %+v", err) } - theResult.Add(thePkg, match.Match{ - Type: match.ExactDirectMatch, + theResult.Add(match.Match{ + Vulnerability: *vulnObj, Package: thePkg, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "language": "javascript", @@ -97,12 +99,13 @@ func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Ca if err != nil { t.Fatalf("failed to create vuln obj: %+v", err) } - theResult.Add(thePkg, match.Match{ - Type: match.ExactDirectMatch, + theResult.Add(match.Match{ + Vulnerability: *vulnObj, Package: thePkg, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "language": "python", @@ -128,12 +131,13 @@ func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata if err != nil { t.Fatalf("failed to create vuln obj: %+v", err) } - theResult.Add(thePkg, match.Match{ - Type: match.ExactDirectMatch, + theResult.Add(match.Match{ + Vulnerability: *vulnObj, Package: thePkg, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "language": "ruby", @@ -168,12 +172,13 @@ func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata if err != nil { t.Fatalf("failed to create vuln obj: %+v", err) } - theResult.Add(thePkg, match.Match{ - Type: match.ExactDirectMatch, + theResult.Add(match.Match{ + Vulnerability: *vulnObj, Package: thePkg, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "language": "java", @@ -200,12 +205,13 @@ func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata if err != nil { t.Fatalf("failed to create vuln obj: %+v", err) } - theResult.Add(thePkg, match.Match{ - Type: match.ExactIndirectMatch, + theResult.Add(match.Match{ + Vulnerability: *vulnObj, Package: thePkg, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactIndirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -234,12 +240,13 @@ func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata if err != nil { t.Fatalf("failed to create vuln obj: %+v", err) } - theResult.Add(thePkg, match.Match{ - Type: match.ExactDirectMatch, + theResult.Add(match.Match{ + Vulnerability: *vulnObj, Package: thePkg, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -268,12 +275,13 @@ func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Cata if err != nil { t.Fatalf("failed to create vuln obj: %+v", err) } - theResult.Add(thePkg, match.Match{ - Type: match.ExactDirectMatch, + theResult.Add(match.Match{ + Vulnerability: *vulnObj, Package: thePkg, - MatchDetails: []match.Details{ + Details: []match.Detail{ { + Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: map[string]interface{}{ "distro": map[string]string{ @@ -295,7 +303,7 @@ func TestMatchByImage(t *testing.T) { observedMatchers := internal.NewStringSet() definedMatchers := internal.NewStringSet() for _, l := range match.AllMatcherTypes { - definedMatchers.Add(l.String()) + definedMatchers.Add(string(l)) } tests := []struct { @@ -387,8 +395,8 @@ func TestMatchByImage(t *testing.T) { actualCount := 0 for aMatch := range actualResults.Enumerate() { actualCount++ - for _, details := range aMatch.MatchDetails { - observedMatchers.Add(details.Matcher.String()) + for _, details := range aMatch.Details { + observedMatchers.Add(string(details.Matcher)) } value, ok := expectedMatchSet[aMatch.Package.Name] if !ok { @@ -410,9 +418,9 @@ func TestMatchByImage(t *testing.T) { } // ensure that integration test cases stay in sync with the implemented matchers - observedMatchers.Remove(match.UnknownMatcherType.String()) - definedMatchers.Remove(match.UnknownMatcherType.String()) - definedMatchers.Remove(match.MsrcMatcher.String()) + observedMatchers.Remove(string(match.UnknownMatcherType)) + definedMatchers.Remove(string(match.UnknownMatcherType)) + definedMatchers.Remove(string(match.MsrcMatcher)) if len(observedMatchers) != len(definedMatchers) { t.Errorf("matcher coverage incomplete (matchers=%d, coverage=%d)", len(definedMatchers), len(observedMatchers)) diff --git a/test/integration/match_by_sbom_document_test.go b/test/integration/match_by_sbom_document_test.go index 1909b1ee8e5..31e0b64fa73 100644 --- a/test/integration/match_by_sbom_document_test.go +++ b/test/integration/match_by_sbom_document_test.go @@ -2,14 +2,13 @@ package integration import ( "fmt" + "github.com/anchore/grype/grype/search" "testing" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db" - "github.com/anchore/grype/grype/matcher/common" - "github.com/anchore/grype/grype" "github.com/anchore/grype/grype/match" "github.com/anchore/syft/syft/source" @@ -23,14 +22,15 @@ func TestMatchBySBOMDocument(t *testing.T) { name string fixture string expectedIDs []string - expectedDetails []match.Details + expectedDetails []match.Detail }{ { name: "single KB package", fixture: "test-fixtures/sbom/syft-sbom-with-kb-packages.json", expectedIDs: []string{"CVE-2016-3333"}, - expectedDetails: []match.Details{ + expectedDetails: []match.Detail{ { + Type: match.ExactDirectMatch, SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": "windows", @@ -54,15 +54,16 @@ func TestMatchBySBOMDocument(t *testing.T) { name: "unknown package type", fixture: "test-fixtures/sbom/syft-sbom-with-unknown-packages.json", expectedIDs: []string{"CVE-bogus-my-package-1", "CVE-bogus-my-package-2-python"}, - expectedDetails: []match.Details{ + expectedDetails: []match.Detail{ { - SearchedBy: common.SearchedByCPEs{ + Type: match.CPEMatch, + SearchedBy: search.CPEParameters{ Namespace: "nvd", CPEs: []string{ "cpe:2.3:a:bogus:my-package:1.0.5:*:*:*:*:*:*:*", }, }, - Found: common.FoundCPEs{ + Found: search.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:a:bogus:my-package:*:*:*:*:*:*:something:*", @@ -72,6 +73,7 @@ func TestMatchBySBOMDocument(t *testing.T) { Confidence: 0.9, }, { + Type: match.ExactDirectMatch, SearchedBy: map[string]interface{}{ "language": "python", "namespace": "github:python", @@ -91,10 +93,10 @@ func TestMatchBySBOMDocument(t *testing.T) { provider := db.NewVulnerabilityProvider(newMockDbStore()) matches, _, _, err := grype.FindVulnerabilities(provider, fmt.Sprintf("sbom:%s", test.fixture), source.SquashedScope, nil) assert.NoError(t, err) - details := make([]match.Details, 0) + details := make([]match.Detail, 0) ids := strset.New() for _, m := range matches.Sorted() { - details = append(details, m.MatchDetails...) + details = append(details, m.Details...) ids.Add(m.Vulnerability.ID) }