diff --git a/cmd/grype/cli/options/database_search_packages.go b/cmd/grype/cli/options/database_search_packages.go index 68e7a6679b6..7af59695d49 100644 --- a/cmd/grype/cli/options/database_search_packages.go +++ b/cmd/grype/cli/options/database_search_packages.go @@ -62,8 +62,9 @@ func (o *DBSearchPackages) PostLoad() error { log.Warnf("ignoring version and qualifiers for package URL %q", purl) } - o.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Name: purl.Name, Ecosystem: purl.Type}) - o.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: "a", Product: purl.Name, TargetSW: purl.Type}}) + name := packageNameFromPURL(&purl) + o.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Name: name, Ecosystem: purl.Type}) + o.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: "a", Product: name, TargetSW: purl.Type}}) default: o.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Name: p, Ecosystem: o.Ecosystem}) @@ -82,3 +83,32 @@ func (o *DBSearchPackages) PostLoad() error { return nil } + +// packageNameFromPURL reconstructs the package name as it is stored in the DB +// for the PURL's ecosystem. Most ecosystems are flat-namespaced and use +// purl.Name directly, but some encode part of the name in the PURL namespace: +// +// - golang modules carry the module path across namespace + name, e.g. +// pkg:golang/github.com/gin-gonic/gin parses to Namespace="github.com/gin-gonic" +// and Name="gin", while the DB keys the record under the full module path +// "github.com/gin-gonic/gin". +// - npm scoped packages parse to Namespace="@scope" and Name="name", and are +// stored as "@scope/name". +// - Maven packages parse to Namespace="groupId" and Name="artifactId", and are +// stored as "groupId:artifactId". +// +// Without this, a search for a namespaced PURL only used purl.Name and silently +// failed to match. This mirrors the same reconstruction the openvex build +// transformer performs (grype/db/v6/build/transformers/openvex). +func packageNameFromPURL(purl *packageurl.PackageURL) string { + if purl.Namespace == "" { + return purl.Name + } + switch purl.Type { + case packageurl.TypeMaven: + return purl.Namespace + ":" + purl.Name + case packageurl.TypeGolang, packageurl.TypeNPM: + return purl.Namespace + "/" + purl.Name + } + return purl.Name +} diff --git a/cmd/grype/cli/options/database_search_packages_test.go b/cmd/grype/cli/options/database_search_packages_test.go index a806dc4213c..44551ad6954 100644 --- a/cmd/grype/cli/options/database_search_packages_test.go +++ b/cmd/grype/cli/options/database_search_packages_test.go @@ -42,6 +42,42 @@ func TestDBSearchPackagesPostLoad(t *testing.T) { {CPE: &cpe.Attributes{Part: "a", Product: "package-name", TargetSW: "npm"}}, }, }, + { + name: "golang PURL keeps the module path", + input: DBSearchPackages{ + Packages: []string{"pkg:golang/github.com/gin-gonic/gin@v1.9.0"}, + }, + expectedPkg: v6.PackageSpecifiers{ + {Name: "github.com/gin-gonic/gin", Ecosystem: "golang"}, + }, + expectedCPE: v6.PackageSpecifiers{ + {CPE: &cpe.Attributes{Part: "a", Product: "github.com/gin-gonic/gin", TargetSW: "golang"}}, + }, + }, + { + name: "npm scoped PURL keeps the scope", + input: DBSearchPackages{ + Packages: []string{"pkg:npm/%40babel/core@7.0.0"}, + }, + expectedPkg: v6.PackageSpecifiers{ + {Name: "@babel/core", Ecosystem: "npm"}, + }, + expectedCPE: v6.PackageSpecifiers{ + {CPE: &cpe.Attributes{Part: "a", Product: "@babel/core", TargetSW: "npm"}}, + }, + }, + { + name: "maven PURL joins group and artifact", + input: DBSearchPackages{ + Packages: []string{"pkg:maven/org.apache.commons/commons-lang3@3.12.0"}, + }, + expectedPkg: v6.PackageSpecifiers{ + {Name: "org.apache.commons:commons-lang3", Ecosystem: "maven"}, + }, + expectedCPE: v6.PackageSpecifiers{ + {CPE: &cpe.Attributes{Part: "a", Product: "org.apache.commons:commons-lang3", TargetSW: "maven"}}, + }, + }, { name: "plain package name", input: DBSearchPackages{