Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve CPE and upstream generation logic for Alpine packages #1567

Merged
merged 9 commits into from
Feb 13, 2023
20 changes: 19 additions & 1 deletion syft/pkg/cataloger/apkdb/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
"github.com/anchore/syft/syft/source"
)

var (
prefixes = []string{"py-", "py2-", "py3-", "ruby-"}
)

func newPackage(d pkg.ApkMetadata, release *linux.Release, locations ...source.Location) pkg.Package {
p := pkg.Package{
Name: d.Package,
Expand All @@ -26,6 +30,20 @@ func newPackage(d pkg.ApkMetadata, release *linux.Release, locations ...source.L
return p
}

func generateUpstream(m pkg.ApkMetadata) string {
if m.OriginPackage != "" && m.OriginPackage != m.Package {
return m.OriginPackage
}

for _, p := range prefixes {
if strings.HasPrefix(m.Package, p) {
return strings.TrimPrefix(m.Package, p)
}
}

return m.Package
}

// packageURL returns the PURL for the specific Alpine package (see https://github.com/package-url/purl-spec)
func packageURL(m pkg.ApkMetadata, distro *linux.Release) string {
if distro == nil || distro.ID != "alpine" {
Expand All @@ -38,7 +56,7 @@ func packageURL(m pkg.ApkMetadata, distro *linux.Release) string {
}

if m.OriginPackage != "" {
qualifiers[pkg.PURLQualifierUpstream] = m.OriginPackage
qualifiers[pkg.PURLQualifierUpstream] = generateUpstream(m)
}

return packageurl.NewPackageURL(
Expand Down
28 changes: 28 additions & 0 deletions syft/pkg/cataloger/apkdb/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,34 @@ func Test_PackageURL(t *testing.T) {
},
expected: "pkg:apk/alpine/p@v?arch=a&upstream=origin&distro=alpine-3.4.6",
},
{
name: "upstream python package information as qualifier",
metadata: pkg.ApkMetadata{
Package: "py3-potatoes",
Version: "v",
Architecture: "a",
OriginPackage: "py3-potatoes",
},
distro: linux.Release{
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:apk/alpine/py3-potatoes@v?arch=a&upstream=potatoes&distro=alpine-3.4.6",
},
{
name: "python package with origin package as upstream",
metadata: pkg.ApkMetadata{
Package: "py3-non-existant",
Version: "v",
Architecture: "a",
OriginPackage: "abcdefg",
},
distro: linux.Release{
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:apk/alpine/py3-non-existant@v?arch=a&upstream=abcdefg&distro=alpine-3.4.6",
},
}

for _, test := range tests {
Expand Down
173 changes: 173 additions & 0 deletions syft/pkg/cataloger/common/cpe/apk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package cpe

import (
"strings"

"github.com/anchore/syft/syft/pkg"
)

var (
pythonPrefixes = []string{"py-", "py2-", "py3-"}
rubyPrefixes = []string{"ruby-"}
)

func pythonCandidateVendorsFromName(v string) fieldCandidateSet {
vendors := newFieldCandidateSet()
vendors.add(fieldCandidate{
value: v,
disallowSubSelections: true,
disallowDelimiterVariations: true,
})

vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.PythonPkg, v, v)...)
vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.PythonPkg, v)...)

for _, av := range additionalVendorsForPython(v) {
vendors.add(fieldCandidate{
value: av,
disallowSubSelections: true,
disallowDelimiterVariations: true,
})
vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.PythonPkg, av, av)...)
vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.PythonPkg, av)...)
}

return vendors
}

func pythonCandidateVendorsFromAPK(m pkg.ApkMetadata) fieldCandidateSet {
vendors := newFieldCandidateSet()

for _, p := range pythonPrefixes {
if strings.HasPrefix(m.Package, p) {
t := strings.ToLower(strings.TrimPrefix(m.Package, p))
vendors.union(pythonCandidateVendorsFromName(t))
}

if m.OriginPackage != m.Package && strings.HasPrefix(m.OriginPackage, p) {
t := strings.ToLower(strings.TrimPrefix(m.OriginPackage, p))
vendors.union(pythonCandidateVendorsFromName(t))
}
}

return vendors
}

func pythonCandidateProductsFromName(p string) fieldCandidateSet {
products := newFieldCandidateSet()
products.add(fieldCandidate{
value: p,
disallowSubSelections: true,
disallowDelimiterVariations: true,
})

products.addValue(findAdditionalProducts(defaultCandidateAdditions, pkg.PythonPkg, p)...)
products.removeByValue(findProductsToRemove(defaultCandidateRemovals, pkg.PythonPkg, p)...)
return products
}

func pythonCandidateProductsFromAPK(m pkg.ApkMetadata) fieldCandidateSet {
products := newFieldCandidateSet()

for _, p := range pythonPrefixes {
if strings.HasPrefix(m.Package, p) {
t := strings.ToLower(strings.TrimPrefix(m.Package, p))
products.union(pythonCandidateProductsFromName(t))
}

if m.OriginPackage != m.Package && strings.HasPrefix(m.OriginPackage, p) {
t := strings.ToLower(strings.TrimPrefix(m.OriginPackage, p))
products.union(pythonCandidateProductsFromName(t))
}
}

return products
}

func rubyCandidateVendorsFromName(v string) fieldCandidateSet {
vendors := newFieldCandidateSet()
vendors.add(fieldCandidate{
value: v,
disallowSubSelections: true,
disallowDelimiterVariations: true,
})

vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.GemPkg, v, v)...)
vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.GemPkg, v)...)
return vendors
}

func rubyCandidateVendorsFromAPK(m pkg.ApkMetadata) fieldCandidateSet {
vendors := newFieldCandidateSet()

for _, p := range rubyPrefixes {
if strings.HasPrefix(m.Package, p) {
t := strings.ToLower(strings.TrimPrefix(m.Package, p))
vendors.union(rubyCandidateVendorsFromName(t))
}

if m.OriginPackage != m.Package && strings.HasPrefix(m.OriginPackage, p) {
t := strings.ToLower(strings.TrimPrefix(m.OriginPackage, p))
vendors.union(rubyCandidateVendorsFromName(t))
}
}

return vendors
}

func rubyCandidateProductsFromName(p string) fieldCandidateSet {
products := newFieldCandidateSet()
products.add(fieldCandidate{
value: p,
disallowSubSelections: true,
disallowDelimiterVariations: true,
})

products.addValue(findAdditionalProducts(defaultCandidateAdditions, pkg.GemPkg, p)...)
products.removeByValue(findProductsToRemove(defaultCandidateRemovals, pkg.GemPkg, p)...)
return products
}

func rubyCandidateProductsFromAPK(m pkg.ApkMetadata) fieldCandidateSet {
products := newFieldCandidateSet()

for _, p := range rubyPrefixes {
if strings.HasPrefix(m.Package, p) {
t := strings.ToLower(strings.TrimPrefix(m.Package, p))
products.union(rubyCandidateProductsFromName(t))
}

if m.OriginPackage != m.Package && strings.HasPrefix(m.OriginPackage, p) {
t := strings.ToLower(strings.TrimPrefix(m.OriginPackage, p))
products.union(rubyCandidateProductsFromName(t))
}
}

return products
}

func candidateVendorsForAPK(p pkg.Package) fieldCandidateSet {
metadata, ok := p.Metadata.(pkg.ApkMetadata)
if !ok {
return nil
}

vendors := newFieldCandidateSet()
vendors.union(pythonCandidateVendorsFromAPK(metadata))
vendors.union(rubyCandidateVendorsFromAPK(metadata))

return vendors
}

func candidateProductsForAPK(p pkg.Package) fieldCandidateSet {
metadata, ok := p.Metadata.(pkg.ApkMetadata)
if !ok {
return nil
}

products := newFieldCandidateSet()
products.union(pythonCandidateProductsFromAPK(metadata))
products.union(rubyCandidateProductsFromAPK(metadata))

return products
}
91 changes: 91 additions & 0 deletions syft/pkg/cataloger/common/cpe/apk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cpe

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/anchore/syft/syft/pkg"
)

func Test_candidateVendorsForAPK(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
expected []string
}{
{
name: "py3-cryptography Package",
pkg: pkg.Package{
Metadata: pkg.ApkMetadata{
Package: "py3-cryptography",
},
},
expected: []string{"python-cryptography_project", "cryptography", "cryptographyproject", "cryptography_project"},
},
{
name: "py2-pypdf OriginPackage",
pkg: pkg.Package{
Metadata: pkg.ApkMetadata{
OriginPackage: "py2-pypdf",
},
},
expected: []string{"pypdf", "pypdfproject", "pypdf_project"},
},
{
name: "ruby-armadillo Package",
pkg: pkg.Package{
Metadata: pkg.ApkMetadata{
Package: "ruby-armadillo",
},
},
expected: []string{"armadillo"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, candidateVendorsForAPK(test.pkg).values(), "different vendors")
})
}
}

func Test_candidateProductsForAPK(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
expected []string
}{
{
name: "py3-cryptography Package",
pkg: pkg.Package{
Metadata: pkg.ApkMetadata{
Package: "py3-cryptography",
},
},
expected: []string{"cryptography", "python-cryptography"},
},
{
name: "py2-pypdf OriginPackage",
pkg: pkg.Package{
Metadata: pkg.ApkMetadata{
OriginPackage: "py2-pypdf",
},
},
expected: []string{"pypdf"},
},
{
name: "ruby-armadillo Package",
pkg: pkg.Package{
Metadata: pkg.ApkMetadata{
Package: "ruby-armadillo",
},
},
expected: []string{"armadillo"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, candidateProductsForAPK(test.pkg).values(), "different products")
})
}
}
Loading