mirror of
https://github.com/anchore/syft
synced 2024-11-13 23:57:07 +00:00
Improve package URL support (#754)
* rename npm metadata struct Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * improve os package URLs Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * improve language package URLs Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * wire up composer pURL method Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
c350bd55f6
commit
1350d6c5bf
18 changed files with 501 additions and 296 deletions
|
@ -3,15 +3,18 @@ package pkg
|
|||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
const ApkDBGlob = "**/lib/apk/db/installed"
|
||||
|
||||
var _ FileOwner = (*ApkMetadata)(nil)
|
||||
var (
|
||||
_ FileOwner = (*ApkMetadata)(nil)
|
||||
_ urlIdentifier = (*ApkMetadata)(nil)
|
||||
)
|
||||
|
||||
// ApkMetadata represents all captured data for a Alpine DB package entry.
|
||||
// See the following sources for more information:
|
||||
|
@ -45,22 +48,22 @@ type ApkFileRecord struct {
|
|||
}
|
||||
|
||||
// PackageURL returns the PURL for the specific Alpine package (see https://github.com/package-url/purl-spec)
|
||||
func (m ApkMetadata) PackageURL() string {
|
||||
pURL := packageurl.NewPackageURL(
|
||||
func (m ApkMetadata) PackageURL(distro *linux.Release) string {
|
||||
return packageurl.NewPackageURL(
|
||||
// note: this is currently a candidate and not technically within spec
|
||||
// see https://github.com/package-url/purl-spec#other-candidate-types-to-define
|
||||
"alpine",
|
||||
"",
|
||||
m.Package,
|
||||
m.Version,
|
||||
packageurl.Qualifiers{
|
||||
{
|
||||
Key: "arch",
|
||||
Value: m.Architecture,
|
||||
purlQualifiers(
|
||||
map[string]string{
|
||||
purlArchQualifier: m.Architecture,
|
||||
},
|
||||
},
|
||||
"")
|
||||
return pURL.ToString()
|
||||
distro,
|
||||
),
|
||||
"",
|
||||
).ToString()
|
||||
}
|
||||
|
||||
func (m ApkMetadata) OwnedFiles() (result []string) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
@ -11,16 +12,35 @@ import (
|
|||
|
||||
func TestApkMetadata_pURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
metadata ApkMetadata
|
||||
distro linux.Release
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "gocase",
|
||||
metadata: ApkMetadata{
|
||||
Package: "p",
|
||||
Version: "v",
|
||||
Architecture: "a",
|
||||
},
|
||||
expected: "pkg:alpine/p@v?arch=a",
|
||||
distro: linux.Release{
|
||||
ID: "alpine",
|
||||
VersionID: "3.4.6",
|
||||
},
|
||||
expected: "pkg:alpine/p@v?arch=a&distro=alpine-3.4.6",
|
||||
},
|
||||
{
|
||||
name: "missing architecure",
|
||||
metadata: ApkMetadata{
|
||||
Package: "p",
|
||||
Version: "v",
|
||||
},
|
||||
distro: linux.Release{
|
||||
ID: "alpine",
|
||||
VersionID: "3.4.6",
|
||||
},
|
||||
expected: "pkg:alpine/p@v?distro=alpine-3.4.6",
|
||||
},
|
||||
// verify #351
|
||||
{
|
||||
|
@ -29,7 +49,11 @@ func TestApkMetadata_pURL(t *testing.T) {
|
|||
Version: "v84",
|
||||
Architecture: "am86",
|
||||
},
|
||||
expected: "pkg:alpine/g++@v84?arch=am86",
|
||||
distro: linux.Release{
|
||||
ID: "alpine",
|
||||
VersionID: "3.4.6",
|
||||
},
|
||||
expected: "pkg:alpine/g++@v84?arch=am86&distro=alpine-3.4.6",
|
||||
},
|
||||
{
|
||||
metadata: ApkMetadata{
|
||||
|
@ -37,13 +61,17 @@ func TestApkMetadata_pURL(t *testing.T) {
|
|||
Version: "v84",
|
||||
Architecture: "am86",
|
||||
},
|
||||
expected: "pkg:alpine/g%20plus%20plus@v84?arch=am86",
|
||||
distro: linux.Release{
|
||||
ID: "alpine",
|
||||
VersionID: "3.15.0",
|
||||
},
|
||||
expected: "pkg:alpine/g%20plus%20plus@v84?arch=am86&distro=alpine-3.15.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.expected, func(t *testing.T) {
|
||||
actual := test.metadata.PackageURL()
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := test.metadata.PackageURL(&test.distro)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
|
|
|
@ -68,7 +68,7 @@ func Catalog(resolver source.FileResolver, release *linux.Release, catalogers ..
|
|||
p.CPEs = cpe.Generate(p)
|
||||
|
||||
// generate PURL (note: this is excluded from package ID, so is safe to mutate)
|
||||
p.PURL = generatePackageURL(p, release)
|
||||
p.PURL = pkg.URL(p, release)
|
||||
|
||||
// create file-to-package relationships for files owned by the package
|
||||
owningRelationships, err := packageFileOwnershipRelationships(p, resolver)
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
package cataloger
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// generatePackageURL returns a package-URL representation of the given package (see https://github.com/package-url/purl-spec)
|
||||
func generatePackageURL(p pkg.Package, release *linux.Release) string {
|
||||
// default to pURLs on the metadata
|
||||
if p.Metadata != nil {
|
||||
if i, ok := p.Metadata.(interface{ PackageURL() string }); ok {
|
||||
return i.PackageURL()
|
||||
} else if i, ok := p.Metadata.(interface{ PackageURL(*linux.Release) string }); ok {
|
||||
return i.PackageURL(release)
|
||||
}
|
||||
}
|
||||
|
||||
var purlType = p.Type.PackageURLType()
|
||||
var name = p.Name
|
||||
var namespace = ""
|
||||
|
||||
switch {
|
||||
case purlType == "":
|
||||
// there is no purl type, don't attempt to craft a purl
|
||||
// TODO: should this be a "generic" purl type instead?
|
||||
return ""
|
||||
case p.Type == pkg.GoModulePkg:
|
||||
re := regexp.MustCompile(`(/)[^/]*$`)
|
||||
fields := re.Split(p.Name, -1)
|
||||
namespace = fields[0]
|
||||
name = strings.TrimPrefix(p.Name, namespace+"/")
|
||||
}
|
||||
|
||||
// generate a purl from the package data
|
||||
pURL := packageurl.NewPackageURL(
|
||||
purlType,
|
||||
namespace,
|
||||
name,
|
||||
p.Version,
|
||||
nil,
|
||||
"")
|
||||
|
||||
return pURL.ToString()
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
package cataloger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
func TestPackageURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
distro *linux.Release
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "golang",
|
||||
pkg: pkg.Package{
|
||||
Name: "github.com/anchore/syft",
|
||||
Version: "v0.1.0",
|
||||
Type: pkg.GoModulePkg,
|
||||
},
|
||||
expected: "pkg:golang/github.com/anchore/syft@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "pip with vcs url",
|
||||
pkg: pkg.Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: pkg.PythonPkg,
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{
|
||||
VCS: "git",
|
||||
URL: "https://github.com/test/test.git",
|
||||
CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "pkg:pypi/name@v0.1.0?vcs_url=git+https:%2F%2Fgithub.com%2Ftest%2Ftest.git@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
{
|
||||
name: "pip without vcs url",
|
||||
pkg: pkg.Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: pkg.PythonPkg,
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
},
|
||||
},
|
||||
expected: "pkg:pypi/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "gem",
|
||||
pkg: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
Type: pkg.GemPkg,
|
||||
},
|
||||
expected: "pkg:gem/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "npm",
|
||||
pkg: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
Type: pkg.NpmPkg,
|
||||
},
|
||||
expected: "pkg:npm/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "deb with arch",
|
||||
distro: &linux.Release{
|
||||
ID: "ubuntu",
|
||||
},
|
||||
pkg: pkg.Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: pkg.DebPkg,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "name",
|
||||
Version: "v0.1.0",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
},
|
||||
expected: "pkg:deb/ubuntu/name@v0.1.0?arch=amd64",
|
||||
},
|
||||
{
|
||||
name: "deb with epoch",
|
||||
distro: &linux.Release{
|
||||
ID: "centos",
|
||||
},
|
||||
pkg: pkg.Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: pkg.RpmPkg,
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
Name: "name",
|
||||
Version: "0.1.0",
|
||||
Epoch: intRef(2),
|
||||
Arch: "amd64",
|
||||
Release: "3",
|
||||
},
|
||||
},
|
||||
expected: "pkg:rpm/centos/name@0.1.0-3?arch=amd64&epoch=2",
|
||||
},
|
||||
{
|
||||
name: "deb with nil epoch",
|
||||
distro: &linux.Release{
|
||||
ID: "centos",
|
||||
},
|
||||
pkg: pkg.Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: pkg.RpmPkg,
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
Name: "name",
|
||||
Version: "0.1.0",
|
||||
Epoch: nil,
|
||||
Arch: "amd64",
|
||||
Release: "3",
|
||||
},
|
||||
},
|
||||
expected: "pkg:rpm/centos/name@0.1.0-3?arch=amd64",
|
||||
},
|
||||
{
|
||||
name: "deb with unknown distro",
|
||||
distro: nil,
|
||||
pkg: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
Type: pkg.DebPkg,
|
||||
},
|
||||
expected: "pkg:deb/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "cargo",
|
||||
pkg: pkg.Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
Type: pkg.RustPkg,
|
||||
},
|
||||
expected: "pkg:cargo/name@v0.1.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := generatePackageURL(test.pkg, test.distro)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func intRef(i int) *int {
|
||||
return &i
|
||||
}
|
|
@ -12,7 +12,10 @@ import (
|
|||
|
||||
const DpkgDBGlob = "**/var/lib/dpkg/{status,status.d/**}"
|
||||
|
||||
var _ FileOwner = (*DpkgMetadata)(nil)
|
||||
var (
|
||||
_ FileOwner = (*DpkgMetadata)(nil)
|
||||
_ urlIdentifier = (*DpkgMetadata)(nil)
|
||||
)
|
||||
|
||||
// DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described
|
||||
// at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section.
|
||||
|
@ -36,24 +39,25 @@ type DpkgFileRecord struct {
|
|||
|
||||
// PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec)
|
||||
func (m DpkgMetadata) PackageURL(distro *linux.Release) string {
|
||||
if distro == nil {
|
||||
return ""
|
||||
var namespace string
|
||||
if distro != nil {
|
||||
namespace = distro.ID
|
||||
}
|
||||
pURL := packageurl.NewPackageURL(
|
||||
return packageurl.NewPackageURL(
|
||||
// TODO: replace with `packageurl.TypeDebian` upon merge of https://github.com/package-url/packageurl-go/pull/21
|
||||
// TODO: or, since we're now using an Anchore fork of this module, we could do this sooner.
|
||||
"deb",
|
||||
distro.ID,
|
||||
namespace,
|
||||
m.Package,
|
||||
m.Version,
|
||||
packageurl.Qualifiers{
|
||||
{
|
||||
Key: "arch",
|
||||
Value: m.Architecture,
|
||||
purlQualifiers(
|
||||
map[string]string{
|
||||
purlArchQualifier: m.Architecture,
|
||||
},
|
||||
},
|
||||
"")
|
||||
return pURL.ToString()
|
||||
distro,
|
||||
),
|
||||
"",
|
||||
).ToString()
|
||||
}
|
||||
|
||||
func (m DpkgMetadata) OwnedFiles() (result []string) {
|
||||
|
|
|
@ -12,25 +12,29 @@ import (
|
|||
|
||||
func TestDpkgMetadata_pURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
distro linux.Release
|
||||
name string
|
||||
distro *linux.Release
|
||||
metadata DpkgMetadata
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
distro: linux.Release{
|
||||
name: "go case",
|
||||
distro: &linux.Release{
|
||||
ID: "debian",
|
||||
VersionID: "11",
|
||||
},
|
||||
metadata: DpkgMetadata{
|
||||
Package: "p",
|
||||
Source: "s",
|
||||
Version: "v",
|
||||
Architecture: "a",
|
||||
},
|
||||
expected: "pkg:deb/debian/p@v?arch=a",
|
||||
expected: "pkg:deb/debian/p@v?distro=debian-11",
|
||||
},
|
||||
{
|
||||
distro: linux.Release{
|
||||
name: "with arch info",
|
||||
distro: &linux.Release{
|
||||
ID: "ubuntu",
|
||||
VersionID: "16.04",
|
||||
},
|
||||
metadata: DpkgMetadata{
|
||||
Package: "p",
|
||||
|
@ -38,13 +42,22 @@ func TestDpkgMetadata_pURL(t *testing.T) {
|
|||
Version: "v",
|
||||
Architecture: "a",
|
||||
},
|
||||
expected: "pkg:deb/ubuntu/p@v?arch=a",
|
||||
expected: "pkg:deb/ubuntu/p@v?arch=a&distro=ubuntu-16.04",
|
||||
},
|
||||
{
|
||||
name: "missing distro",
|
||||
metadata: DpkgMetadata{
|
||||
Package: "p",
|
||||
Source: "s",
|
||||
Version: "v",
|
||||
},
|
||||
expected: "pkg:deb/p@v",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.expected, func(t *testing.T) {
|
||||
actual := test.metadata.PackageURL(&test.distro)
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := test.metadata.PackageURL(test.distro)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
|
|
|
@ -3,10 +3,14 @@ package pkg
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
var _ urlIdentifier = (*JavaMetadata)(nil)
|
||||
|
||||
var JenkinsPluginPomPropertiesGroupIDs = []string{
|
||||
"io.jenkins.plugins",
|
||||
"org.jenkins.plugins",
|
||||
|
@ -69,7 +73,7 @@ type JavaManifest struct {
|
|||
}
|
||||
|
||||
// PackageURL returns the PURL for the specific Alpine package (see https://github.com/package-url/purl-spec)
|
||||
func (m JavaMetadata) PackageURL() string {
|
||||
func (m JavaMetadata) PackageURL(_ *linux.Release) string {
|
||||
if m.PomProperties != nil {
|
||||
pURL := packageurl.NewPackageURL(
|
||||
packageurl.TypeMaven,
|
||||
|
|
|
@ -136,7 +136,7 @@ func TestJavaMetadata_pURL(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
t.Run(test.expected, func(t *testing.T) {
|
||||
actual := test.metadata.PackageURL()
|
||||
actual := test.metadata.PackageURL(nil)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
|
|
|
@ -4,8 +4,11 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
)
|
||||
|
||||
var _ urlIdentifier = (*PhpComposerJSONMetadata)(nil)
|
||||
|
||||
// PhpComposerJSONMetadata represents information found from composer v1/v2 "installed.json" files as well as composer.lock files
|
||||
type PhpComposerJSONMetadata struct {
|
||||
Name string `json:"name"`
|
||||
|
@ -40,7 +43,7 @@ type PhpComposerAuthors struct {
|
|||
Homepage string `json:"homepage,omitempty"`
|
||||
}
|
||||
|
||||
func (m PhpComposerJSONMetadata) PackageURL() string {
|
||||
func (m PhpComposerJSONMetadata) PackageURL(_ *linux.Release) string {
|
||||
var name, vendor string
|
||||
fields := strings.Split(m.Name, "/")
|
||||
switch len(fields) {
|
||||
|
|
|
@ -20,7 +20,8 @@ func TestPhpComposerJsonMetadata_pURL(t *testing.T) {
|
|||
Version: "1.0.1",
|
||||
},
|
||||
expected: "pkg:composer/ven/name@1.0.1",
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: "name with slashes (invalid)",
|
||||
metadata: PhpComposerJSONMetadata{
|
||||
Name: "ven/name/component",
|
||||
|
@ -36,11 +37,23 @@ func TestPhpComposerJsonMetadata_pURL(t *testing.T) {
|
|||
},
|
||||
expected: "pkg:composer/name@1.0.1",
|
||||
},
|
||||
{
|
||||
name: "ignores distro",
|
||||
distro: &linux.Release{
|
||||
ID: "rhel",
|
||||
VersionID: "8.4",
|
||||
},
|
||||
metadata: PhpComposerJSONMetadata{
|
||||
Name: "ven/name",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
expected: "pkg:composer/ven/name@1.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := test.metadata.PackageURL()
|
||||
actual := test.metadata.PackageURL(test.distro)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
|
|
|
@ -4,11 +4,16 @@ import (
|
|||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
var _ FileOwner = (*PythonPackageMetadata)(nil)
|
||||
var (
|
||||
_ FileOwner = (*PythonPackageMetadata)(nil)
|
||||
_ urlIdentifier = (*PythonPackageMetadata)(nil)
|
||||
)
|
||||
|
||||
// PythonFileDigest represents the file metadata for a single file attributed to a python package.
|
||||
type PythonFileDigest struct {
|
||||
|
@ -76,7 +81,7 @@ func (m PythonPackageMetadata) OwnedFiles() (result []string) {
|
|||
return result
|
||||
}
|
||||
|
||||
func (m PythonPackageMetadata) PackageURL() string {
|
||||
func (m PythonPackageMetadata) PackageURL(_ *linux.Release) string {
|
||||
// generate a purl from the package data
|
||||
pURL := packageurl.NewPackageURL(
|
||||
packageurl.TypePyPi,
|
||||
|
@ -101,7 +106,7 @@ func (p PythonDirectURLOriginInfo) vcsURLQualifier() packageurl.Qualifiers {
|
|||
if p.VCS != "" {
|
||||
// Taken from https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#known-qualifiers-keyvalue-pairs
|
||||
// packageurl-go still doesn't support all qualifier names
|
||||
return packageurl.Qualifiers{{Key: "vcs_url", Value: fmt.Sprintf("%s+%s@%s", p.VCS, p.URL, p.CommitID)}}
|
||||
return packageurl.Qualifiers{{Key: purlVCSURL, Value: fmt.Sprintf("%s+%s@%s", p.VCS, p.URL, p.CommitID)}}
|
||||
}
|
||||
return packageurl.Qualifiers{}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,12 +1,60 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func TestPythonPackageMetadata_pURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
distro *linux.Release
|
||||
metadata PythonPackageMetadata
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "with vcs info",
|
||||
metadata: PythonPackageMetadata{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
DirectURLOrigin: &PythonDirectURLOriginInfo{
|
||||
VCS: "git",
|
||||
URL: "https://github.com/test/test.git",
|
||||
CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
},
|
||||
expected: "pkg:pypi/name@v0.1.0?vcs_url=git+https:%2F%2Fgithub.com%2Ftest%2Ftest.git@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
{
|
||||
name: "should not respond to release info",
|
||||
distro: &linux.Release{
|
||||
ID: "rhel",
|
||||
VersionID: "8.4",
|
||||
},
|
||||
metadata: PythonPackageMetadata{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
},
|
||||
expected: "pkg:pypi/name@v0.1.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := test.metadata.PackageURL(test.distro)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPythonMetadata_FileOwner(t *testing.T) {
|
||||
tests := []struct {
|
||||
metadata PythonPackageMetadata
|
||||
|
|
|
@ -15,7 +15,10 @@ import (
|
|||
|
||||
const RpmDBGlob = "**/var/lib/rpm/Packages"
|
||||
|
||||
var _ FileOwner = (*RpmdbMetadata)(nil)
|
||||
var (
|
||||
_ FileOwner = (*RpmdbMetadata)(nil)
|
||||
_ urlIdentifier = (*RpmdbMetadata)(nil)
|
||||
)
|
||||
|
||||
// RpmdbMetadata represents all captured data for a RPM DB package entry.
|
||||
type RpmdbMetadata struct {
|
||||
|
@ -47,36 +50,32 @@ type RpmdbFileMode uint16
|
|||
|
||||
// PackageURL returns the PURL for the specific RHEL package (see https://github.com/package-url/purl-spec)
|
||||
func (m RpmdbMetadata) PackageURL(distro *linux.Release) string {
|
||||
if distro == nil {
|
||||
return ""
|
||||
var namespace string
|
||||
if distro != nil {
|
||||
namespace = distro.ID
|
||||
}
|
||||
|
||||
qualifiers := packageurl.Qualifiers{
|
||||
{
|
||||
Key: "arch",
|
||||
Value: m.Arch,
|
||||
},
|
||||
qualifiers := map[string]string{
|
||||
purlArchQualifier: m.Arch,
|
||||
}
|
||||
|
||||
if m.Epoch != nil {
|
||||
qualifiers = append(qualifiers,
|
||||
packageurl.Qualifier{
|
||||
Key: "epoch",
|
||||
Value: strconv.Itoa(*m.Epoch),
|
||||
},
|
||||
)
|
||||
qualifiers[purlEpochQualifier] = strconv.Itoa(*m.Epoch)
|
||||
}
|
||||
|
||||
pURL := packageurl.NewPackageURL(
|
||||
return packageurl.NewPackageURL(
|
||||
packageurl.TypeRPM,
|
||||
distro.ID,
|
||||
namespace,
|
||||
m.Name,
|
||||
// for purl the epoch is a qualifier, not part of the version
|
||||
// see https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst under the RPM section
|
||||
fmt.Sprintf("%s-%s", m.Version, m.Release),
|
||||
purlQualifiers(
|
||||
qualifiers,
|
||||
"")
|
||||
return pURL.ToString()
|
||||
distro,
|
||||
),
|
||||
"",
|
||||
).ToString()
|
||||
}
|
||||
|
||||
func (m RpmdbMetadata) OwnedFiles() (result []string) {
|
||||
|
|
|
@ -12,13 +12,16 @@ import (
|
|||
|
||||
func TestRpmMetadata_pURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
distro linux.Release
|
||||
name string
|
||||
distro *linux.Release
|
||||
metadata RpmdbMetadata
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
distro: linux.Release{
|
||||
name: "with arch and epoch",
|
||||
distro: &linux.Release{
|
||||
ID: "centos",
|
||||
VersionID: "7",
|
||||
},
|
||||
metadata: RpmdbMetadata{
|
||||
Name: "p",
|
||||
|
@ -27,26 +30,37 @@ func TestRpmMetadata_pURL(t *testing.T) {
|
|||
Release: "r",
|
||||
Epoch: intRef(1),
|
||||
},
|
||||
expected: "pkg:rpm/centos/p@v-r?arch=a&epoch=1",
|
||||
expected: "pkg:rpm/centos/p@v-r?arch=a&epoch=1&distro=centos-7",
|
||||
},
|
||||
{
|
||||
distro: linux.Release{
|
||||
name: "go case",
|
||||
distro: &linux.Release{
|
||||
ID: "rhel",
|
||||
VersionID: "8.4",
|
||||
},
|
||||
metadata: RpmdbMetadata{
|
||||
Name: "p",
|
||||
Version: "v",
|
||||
Arch: "a",
|
||||
Release: "r",
|
||||
Epoch: nil,
|
||||
},
|
||||
expected: "pkg:rpm/rhel/p@v-r?arch=a",
|
||||
expected: "pkg:rpm/rhel/p@v-r?distro=rhel-8.4",
|
||||
},
|
||||
{
|
||||
name: "missing distro",
|
||||
metadata: RpmdbMetadata{
|
||||
Name: "p",
|
||||
Version: "v",
|
||||
Release: "r",
|
||||
Epoch: nil,
|
||||
},
|
||||
expected: "pkg:rpm/p@v-r",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.expected, func(t *testing.T) {
|
||||
actual := test.metadata.PackageURL(&test.distro)
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := test.metadata.PackageURL(test.distro)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
|
|
86
syft/pkg/url.go
Normal file
86
syft/pkg/url.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
)
|
||||
|
||||
const (
|
||||
purlArchQualifier = "arch"
|
||||
purlDistroQualifier = "distro"
|
||||
purlEpochQualifier = "epoch"
|
||||
purlVCSURL = "vcs_url"
|
||||
)
|
||||
|
||||
type urlIdentifier interface {
|
||||
PackageURL(*linux.Release) string
|
||||
}
|
||||
|
||||
func URL(p Package, release *linux.Release) string {
|
||||
if p.Metadata != nil {
|
||||
if i, ok := p.Metadata.(urlIdentifier); ok {
|
||||
return i.PackageURL(release)
|
||||
}
|
||||
}
|
||||
|
||||
// the remaining cases are primarily reserved for packages without metadata struct instances
|
||||
|
||||
var purlType = p.Type.PackageURLType()
|
||||
var name = p.Name
|
||||
var namespace = ""
|
||||
|
||||
switch {
|
||||
case purlType == "":
|
||||
// there is no purl type, don't attempt to craft a purl
|
||||
// TODO: should this be a "generic" purl type instead?
|
||||
return ""
|
||||
case p.Type == GoModulePkg:
|
||||
re := regexp.MustCompile(`(/)[^/]*$`)
|
||||
fields := re.Split(p.Name, -1)
|
||||
namespace = fields[0]
|
||||
name = strings.TrimPrefix(p.Name, namespace+"/")
|
||||
}
|
||||
|
||||
// generate a purl from the package data
|
||||
return packageurl.NewPackageURL(
|
||||
purlType,
|
||||
namespace,
|
||||
name,
|
||||
p.Version,
|
||||
nil,
|
||||
"",
|
||||
).ToString()
|
||||
}
|
||||
|
||||
func purlQualifiers(vars map[string]string, release *linux.Release) (q packageurl.Qualifiers) {
|
||||
keys := make([]string, 0, len(vars))
|
||||
for k := range vars {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
val := vars[k]
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
q = append(q, packageurl.Qualifier{
|
||||
Key: k,
|
||||
Value: vars[k],
|
||||
})
|
||||
}
|
||||
|
||||
if release != nil && release.ID != "" && release.VersionID != "" {
|
||||
q = append(q, packageurl.Qualifier{
|
||||
Key: purlDistroQualifier,
|
||||
Value: fmt.Sprintf("%s-%s", release.ID, release.VersionID),
|
||||
})
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
200
syft/pkg/url_test.go
Normal file
200
syft/pkg/url_test.go
Normal file
|
@ -0,0 +1,200 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPackageURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg Package
|
||||
distro *linux.Release
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "golang",
|
||||
pkg: Package{
|
||||
Name: "github.com/anchore/syft",
|
||||
Version: "v0.1.0",
|
||||
Type: GoModulePkg,
|
||||
},
|
||||
expected: "pkg:golang/github.com/anchore/syft@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "python",
|
||||
pkg: Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: PythonPkg,
|
||||
Metadata: PythonPackageMetadata{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
},
|
||||
},
|
||||
expected: "pkg:pypi/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "gem",
|
||||
pkg: Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
Type: GemPkg,
|
||||
},
|
||||
expected: "pkg:gem/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "npm",
|
||||
pkg: Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
Type: NpmPkg,
|
||||
},
|
||||
expected: "pkg:npm/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "deb",
|
||||
distro: &linux.Release{
|
||||
ID: "ubuntu",
|
||||
VersionID: "20.04",
|
||||
},
|
||||
pkg: Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: DebPkg,
|
||||
Metadata: DpkgMetadata{
|
||||
Package: "name",
|
||||
Version: "v0.1.0",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
},
|
||||
expected: "pkg:deb/ubuntu/name@v0.1.0?arch=amd64&distro=ubuntu-20.04",
|
||||
},
|
||||
{
|
||||
name: "rpm",
|
||||
distro: &linux.Release{
|
||||
ID: "centos",
|
||||
VersionID: "7",
|
||||
},
|
||||
pkg: Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: RpmPkg,
|
||||
Metadata: RpmdbMetadata{
|
||||
Name: "name",
|
||||
Version: "0.1.0",
|
||||
Epoch: intRef(2),
|
||||
Arch: "amd64",
|
||||
Release: "3",
|
||||
},
|
||||
},
|
||||
expected: "pkg:rpm/centos/name@0.1.0-3?arch=amd64&epoch=2&distro=centos-7",
|
||||
},
|
||||
{
|
||||
name: "cargo",
|
||||
pkg: Package{
|
||||
Name: "name",
|
||||
Version: "v0.1.0",
|
||||
Type: RustPkg,
|
||||
},
|
||||
expected: "pkg:cargo/name@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "apk",
|
||||
distro: &linux.Release{
|
||||
ID: "alpine",
|
||||
VersionID: "3.4.6",
|
||||
},
|
||||
pkg: Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: ApkPkg,
|
||||
Metadata: ApkMetadata{
|
||||
Package: "name",
|
||||
Version: "v0.1.0",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
},
|
||||
expected: "pkg:alpine/name@v0.1.0?arch=amd64&distro=alpine-3.4.6",
|
||||
},
|
||||
{
|
||||
name: "php-composer",
|
||||
pkg: Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: PhpComposerPkg,
|
||||
Metadata: PhpComposerJSONMetadata{
|
||||
Name: "vendor/name",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
},
|
||||
expected: "pkg:composer/vendor/name@2.0.1",
|
||||
},
|
||||
{
|
||||
name: "java",
|
||||
pkg: Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: JavaPkg,
|
||||
Metadata: JavaMetadata{
|
||||
PomProperties: &PomProperties{
|
||||
Path: "p",
|
||||
Name: "n",
|
||||
GroupID: "g.id",
|
||||
ArtifactID: "a",
|
||||
Version: "v",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expected: "pkg:maven/g.id/a@v",
|
||||
},
|
||||
{
|
||||
name: "jenkins-plugin",
|
||||
pkg: Package{
|
||||
Name: "bad-name",
|
||||
Version: "bad-v0.1.0",
|
||||
Type: JenkinsPluginPkg,
|
||||
Metadata: JavaMetadata{
|
||||
PomProperties: &PomProperties{
|
||||
Path: "p",
|
||||
Name: "n",
|
||||
GroupID: "g.id",
|
||||
ArtifactID: "a",
|
||||
Version: "v",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expected: "pkg:maven/g.id/a@v",
|
||||
},
|
||||
}
|
||||
|
||||
var pkgTypes []string
|
||||
var expectedTypes = strset.New()
|
||||
for _, ty := range AllPkgs {
|
||||
expectedTypes.Add(string(ty))
|
||||
}
|
||||
|
||||
// testing microsoft packages is not valid for purl at this time
|
||||
expectedTypes.Remove(string(KbPkg))
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.pkg.Type != "" {
|
||||
pkgTypes = append(pkgTypes, string(test.pkg.Type))
|
||||
}
|
||||
actual := URL(test.pkg, test.distro)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
})
|
||||
}
|
||||
assert.ElementsMatch(t, expectedTypes.List(), pkgTypes, "missing one or more package types to test against (maybe a package type was added?)")
|
||||
}
|
Loading…
Reference in a new issue