Canonicalize Ghostscript CPE/PURL for ghostscript packages from PE Binaries (#4308)

* binary(pe): canonicalize Ghostscript CPE to artifex:ghostscript and add generic purl for PE (#4275)\n\n- Detect Ghostscript via PE version resources and set purl pkg:generic/ghostscript@<version>\n- Add PE-specific CPE candidates: vendor 'artifex', product 'ghostscript'\n- Add focused unit tests for purl and CPE generation

Signed-off-by: kdt523 <krushna.datir231@vit.edu>

* fix: gofmt formatting for static analysis pass (pe-ghostscript-cpe-purl-4275)

Signed-off-by: kdt523 <krushna.datir231@vit.edu>

---------

Signed-off-by: kdt523 <krushna.datir231@vit.edu>
This commit is contained in:
kdt523 2025-11-03 20:24:48 +05:30 committed by GitHub
parent 793b0a346f
commit 3e4e82f03e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 146 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import (
"sort" "sort"
"strings" "strings"
packageurl "github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
@ -32,6 +33,19 @@ func newPEPackage(versionResources map[string]string, f file.Location) pkg.Packa
Metadata: newPEBinaryVersionResourcesFromMap(versionResources), Metadata: newPEBinaryVersionResourcesFromMap(versionResources),
} }
// If this appears to be Ghostscript, emit a canonical generic purl
// Example expected: pkg:generic/ghostscript@<version>
prod := strings.ToLower(spaceNormalize(versionResources["ProductName"]))
if prod == "" {
// fall back to FileDescription if ProductName is missing
prod = strings.ToLower(spaceNormalize(versionResources["FileDescription"]))
}
if p.Version != "" && strings.Contains(prod, "ghostscript") {
// build a generic PURL for ghostscript
purl := packageurl.NewPackageURL(packageurl.TypeGeneric, "", "ghostscript", p.Version, nil, "").ToString()
p.PURL = purl
}
p.SetID() p.SetID()
return p return p

View File

@ -0,0 +1,24 @@
package binary
import (
"testing"
"github.com/anchore/syft/syft/file"
)
func TestGhostscriptPEGeneratesGenericPURL(t *testing.T) {
vr := map[string]string{
"CompanyName": "Artifex Software, Inc.",
"ProductName": "GPL Ghostscript",
"FileDescription": "Ghostscript Interpreter",
"ProductVersion": "9.54.0",
}
loc := file.NewLocation("/usr/bin/gswin64c.exe")
p := newPEPackage(vr, loc)
expected := "pkg:generic/ghostscript@9.54.0"
if p.PURL != expected {
t.Fatalf("expected purl %q, got %q", expected, p.PURL)
}
}

View File

@ -0,0 +1,63 @@
package cpegenerate
import (
"strings"
"github.com/anchore/syft/syft/pkg"
)
// candidateVendorsForPE returns vendor candidates for PE (BinaryPkg) packages based on common metadata hints.
// Specifically, normalize Ghostscript binaries to vendor "artifex" when detected.
func candidateVendorsForPE(p pkg.Package) fieldCandidateSet {
candidates := newFieldCandidateSet()
meta, ok := p.Metadata.(pkg.PEBinary)
if !ok {
return candidates
}
var company, product, fileDesc string
for _, kv := range meta.VersionResources {
switch strings.ToLower(kv.Key) {
case "companyname":
company = strings.ToLower(kv.Value)
case "productname":
product = strings.ToLower(kv.Value)
case "filedescription":
fileDesc = strings.ToLower(kv.Value)
}
}
if strings.Contains(product, "ghostscript") || strings.Contains(fileDesc, "ghostscript") || strings.Contains(company, "artifex") {
candidates.addValue("artifex")
}
return candidates
}
// candidateProductsForPE returns product candidates for PE (BinaryPkg) packages based on common metadata hints.
// Specifically, normalize Ghostscript binaries to product "ghostscript" when detected.
func candidateProductsForPE(p pkg.Package) fieldCandidateSet {
candidates := newFieldCandidateSet()
meta, ok := p.Metadata.(pkg.PEBinary)
if !ok {
return candidates
}
var product, fileDesc string
for _, kv := range meta.VersionResources {
switch strings.ToLower(kv.Key) {
case "productname":
product = strings.ToLower(kv.Value)
case "filedescription":
fileDesc = strings.ToLower(kv.Value)
}
}
if strings.Contains(product, "ghostscript") || strings.Contains(fileDesc, "ghostscript") {
candidates.addValue("ghostscript")
}
return candidates
}

View File

@ -225,6 +225,9 @@ func candidateVendors(p pkg.Package) []string {
vendors.union(candidateVendorsForAPK(p)) vendors.union(candidateVendorsForAPK(p))
case pkg.NpmPackage: case pkg.NpmPackage:
vendors.union(candidateVendorsForJavascript(p)) vendors.union(candidateVendorsForJavascript(p))
case pkg.PEBinary:
// Add PE-specific vendor hints (e.g. ghostscript -> artifex)
vendors.union(candidateVendorsForPE(p))
case pkg.WordpressPluginEntry: case pkg.WordpressPluginEntry:
vendors.clear() vendors.clear()
vendors.union(candidateVendorsForWordpressPlugin(p)) vendors.union(candidateVendorsForWordpressPlugin(p))
@ -301,6 +304,9 @@ func candidateProductSet(p pkg.Package) fieldCandidateSet {
switch p.Metadata.(type) { switch p.Metadata.(type) {
case pkg.ApkDBEntry: case pkg.ApkDBEntry:
products.union(candidateProductsForAPK(p)) products.union(candidateProductsForAPK(p))
case pkg.PEBinary:
// Add PE-specific product hints (e.g. ghostscript)
products.union(candidateProductsForPE(p))
case pkg.WordpressPluginEntry: case pkg.WordpressPluginEntry:
products.clear() products.clear()
products.union(candidateProductsForWordpressPlugin(p)) products.union(candidateProductsForWordpressPlugin(p))

View File

@ -0,0 +1,39 @@
package cpegenerate
import (
"testing"
"github.com/anchore/syft/syft/pkg"
)
func TestGhostscriptPEGeneratesArtifexCPE(t *testing.T) {
// construct a BinaryPkg with PE metadata resembling Ghostscript
p := pkg.Package{
Name: "GPL Ghostscript",
Version: "9.54.0",
Type: pkg.BinaryPkg,
Metadata: pkg.PEBinary{
VersionResources: pkg.KeyValues{
{Key: "CompanyName", Value: "Artifex Software, Inc."},
{Key: "ProductName", Value: "GPL Ghostscript"},
{Key: "FileDescription", Value: "Ghostscript Interpreter"},
},
},
}
cpes := FromPackageAttributes(p)
if len(cpes) == 0 {
t.Fatalf("expected at least one CPE, got none")
}
found := false
for _, c := range cpes {
if c.Attributes.Vendor == "artifex" && c.Attributes.Product == "ghostscript" && c.Attributes.Version == p.Version {
found = true
break
}
}
if !found {
t.Fatalf("expected to find CPE with vendor 'artifex' and product 'ghostscript' for Ghostscript PE binary; got: %+v", cpes)
}
}