diff --git a/pkg/purl/purl.go b/pkg/purl/purl.go index a10686e6e369..94db9ab9f104 100644 --- a/pkg/purl/purl.go +++ b/pkg/purl/purl.go @@ -289,6 +289,36 @@ func (p *PackageURL) Package() *ftypes.Package { return pkg } +// Match returns true if the given PURL "target" satisfies the constraint PURL "p". +// - If the constraint does not have a version, it will match any version in the target. +// - If the constraint has qualifiers, the target must have the same set of qualifiers to match. +func (p *PackageURL) Match(target *packageurl.PackageURL) bool { + if target == nil { + return false + } + switch { + case p.Type != target.Type: + return false + case p.Namespace != target.Namespace: + return false + case p.Name != target.Name: + return false + case p.Version != "" && p.Version != target.Version: + return false + case p.Subpath != "" && p.Subpath != target.Subpath: + return false + } + + // All qualifiers in the constraint must be in the target to match + q := target.Qualifiers.Map() + for k, v1 := range p.Qualifiers.Map() { + if v2, ok := q[k]; !ok || v1 != v2 { + return false + } + } + return true +} + // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#oci func parseOCI(metadata types.Metadata) (packageurl.PackageURL, error) { if len(metadata.RepoDigests) == 0 { diff --git a/pkg/purl/purl_test.go b/pkg/purl/purl_test.go index fa9ca25dbcd3..876911ca521a 100644 --- a/pkg/purl/purl_test.go +++ b/pkg/purl/purl_test.go @@ -585,7 +585,7 @@ func TestFromString(t *testing.T) { } } -func TestToPackage(t *testing.T) { +func TestPackageURL_Package(t *testing.T) { tests := []struct { name string pkgURL *purl.PackageURL @@ -769,7 +769,7 @@ func TestToPackage(t *testing.T) { } } -func TestLangType(t *testing.T) { +func TestPackageURL_LangType(t *testing.T) { tests := []struct { name string purl packageurl.PackageURL @@ -812,3 +812,72 @@ func TestLangType(t *testing.T) { }) } } + +func TestPackageURL_Match(t *testing.T) { + tests := []struct { + name string + constraint string + target string + want bool + }{ + { + name: "same purl", + constraint: "pkg:golang/github.com/aquasecurity/trivy@0.49.0", + target: "pkg:golang/github.com/aquasecurity/trivy@0.49.0", + want: true, + }, + { + name: "different type", + constraint: "pkg:golang/github.com/aquasecurity/trivy@0.49.0", + target: "pkg:maven/github.com/aquasecurity/trivy@0.49.0", + want: false, + }, + { + name: "different namespace", + constraint: "pkg:golang/github.com/aquasecurity/trivy@0.49.0", + target: "pkg:golang/github.com/aquasecurity2/trivy@0.49.0", + want: false, + }, + { + name: "different name", + constraint: "pkg:golang/github.com/aquasecurity/trivy@0.49.0", + target: "pkg:golang/github.com/aquasecurity/tracee@0.49.0", + want: false, + }, + { + name: "different version", + constraint: "pkg:golang/github.com/aquasecurity/trivy@0.49.0", + target: "pkg:golang/github.com/aquasecurity/trivy@0.49.1", + want: false, + }, + { + name: "version wildcard", + constraint: "pkg:golang/github.com/aquasecurity/trivy", + target: "pkg:golang/github.com/aquasecurity/trivy@0.50.0", + want: true, + }, + { + name: "different qualifier", + constraint: "pkg:bitnami/wordpress@6.2.0?arch=arm64&distro=debian-12", + target: "pkg:bitnami/wordpress@6.2.0?arch=arm64&distro=debian-13", + want: false, + }, + { + name: "target more qualifiers", + constraint: "pkg:bitnami/wordpress@6.2.0?arch=arm64", + target: "pkg:bitnami/wordpress@6.2.0?arch=arm64&distro=debian-13", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := purl.FromString(tt.constraint) + require.NoError(t, err) + + p, err := purl.FromString(tt.target) + require.NoError(t, err) + + assert.Equalf(t, tt.want, c.Match(p.Unwrap()), "Match()") + }) + } +} diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go index 86c88c712e7a..9239b5aa99ce 100644 --- a/pkg/vex/csaf.go +++ b/pkg/vex/csaf.go @@ -1,14 +1,13 @@ package vex import ( - csaf "github.com/csaf-poc/csaf_distribution/v3/csaf" - "github.com/samber/lo" - "go.uber.org/zap" - "golang.org/x/exp/slices" - "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/purl" "github.com/aquasecurity/trivy/pkg/types" + csaf "github.com/csaf-poc/csaf_distribution/v3/csaf" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "go.uber.org/zap" ) type CSAF struct { @@ -32,11 +31,11 @@ func (v *CSAF) Filter(vulns []types.DetectedVulnerability) []types.DetectedVulne return true } - return v.affected(found, purl.WithPath(vuln.PkgIdentifier.PURL, vuln.PkgPath)) + return v.affected(found, vuln.PkgIdentifier.PURL) }) } -func (v *CSAF) affected(vuln *csaf.Vulnerability, pkgURL *purl.PackageURL) bool { +func (v *CSAF) affected(vuln *csaf.Vulnerability, pkgURL *packageurl.PackageURL) bool { if pkgURL == nil || vuln.ProductStatus == nil { return true } @@ -60,17 +59,24 @@ func (v *CSAF) affected(vuln *csaf.Vulnerability, pkgURL *purl.PackageURL) bool } // matchPURL returns true if the given PackageURL is found in the ProductTree. -func (v *CSAF) matchPURL(products *csaf.Products, pkgURL *purl.PackageURL) bool { +func (v *CSAF) matchPURL(products *csaf.Products, pkgURL *packageurl.PackageURL) bool { for _, product := range lo.FromPtr(products) { helpers := v.advisory.ProductTree.CollectProductIdentificationHelpers(lo.FromPtr(product)) - purls := lo.FilterMap(helpers, func(helper *csaf.ProductIdentificationHelper, _ int) (string, bool) { + purls := lo.FilterMap(helpers, func(helper *csaf.ProductIdentificationHelper, _ int) (*purl.PackageURL, bool) { if helper == nil || helper.PURL == nil { - return "", false + return nil, false + } + p, err := purl.FromString(string(*helper.PURL)) + if err != nil { + log.Logger.Errorw("Invalid PURL", zap.String("purl", string(*helper.PURL)), zap.Error(err)) + return nil, false } - return string(*helper.PURL), true + return p, true }) - if slices.Contains(purls, pkgURL.String()) { - return true + for _, p := range purls { + if p.Match(pkgURL) { + return true + } } }