Add package URL support to the CycloneDX presenter (#164)

* add package URL support to the CycloneDX presenter

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* wrap license tags with licenses

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman
2020-08-30 21:40:19 -04:00
committed by GitHub
parent eda0f8c774
commit 8a4886ec0e
27 changed files with 669 additions and 116 deletions
+50
View File
@@ -0,0 +1,50 @@
package pkg
import (
"github.com/package-url/packageurl-go"
)
// ApkMetadata represents all captured data for a Alpine DB package entry. See https://wiki.alpinelinux.org/wiki/Apk_spec for more information.
type ApkMetadata struct {
Package string `mapstructure:"P" json:"package"`
OriginPackage string `mapstructure:"o" json:"origin-package"`
Maintainer string `mapstructure:"m" json:"maintainer"`
Version string `mapstructure:"V" json:"version"`
License string `mapstructure:"L" json:"license"`
Architecture string `mapstructure:"A" json:"architecture"`
URL string `mapstructure:"U" json:"url"`
Description string `mapstructure:"T" json:"description"`
Size int `mapstructure:"S" json:"size"`
InstalledSize int `mapstructure:"I" json:"installed-size"`
PullDependencies string `mapstructure:"D" json:"pull-dependencies"`
PullChecksum string `mapstructure:"C" json:"pull-checksum"`
GitCommitOfAport string `mapstructure:"c" json:"git-commit-of-apk-port"`
Files []ApkFileRecord `json:"files"`
}
// ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records).
type ApkFileRecord struct {
Path string `json:"path"`
OwnerUID string `json:"owner-uid"`
OwnerGUI string `json:"owner-gid"`
Permissions string `json:"permissions"`
Checksum string `json:"checksum"`
}
func (m ApkMetadata) PackageURL() string {
pURL := 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,
},
},
"")
return pURL.ToString()
}
+33
View File
@@ -0,0 +1,33 @@
package pkg
import (
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestApkMetadata_pURL(t *testing.T) {
tests := []struct {
metadata ApkMetadata
expected string
}{
{
metadata: ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
},
expected: "pkg:alpine/p@v?arch=a",
},
}
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
actual := test.metadata.PackageURL()
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
}
})
}
}
+33
View File
@@ -0,0 +1,33 @@
package pkg
import (
"github.com/anchore/syft/syft/distro"
"github.com/package-url/packageurl-go"
)
// 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.
type DpkgMetadata struct {
Package string `mapstructure:"Package" json:"package"`
Source string `mapstructure:"Source" json:"source"`
Version string `mapstructure:"Version" json:"version"`
Architecture string `mapstructure:"Architecture" json:"architecture"`
// TODO: consider keeping the remaining values as an embedded map
}
func (m DpkgMetadata) PackageURL(d distro.Distro) string {
pURL := packageurl.NewPackageURL(
// TODO: replace with `packageurl.TypeDebian` upon merge of https://github.com/package-url/packageurl-go/pull/21
"deb",
d.Type.String(),
m.Package,
m.Version,
packageurl.Qualifiers{
{
Key: "arch",
Value: m.Architecture,
},
},
"")
return pURL.ToString()
}
+51
View File
@@ -0,0 +1,51 @@
package pkg
import (
"github.com/anchore/syft/syft/distro"
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestDpkgMetadata_pURL(t *testing.T) {
tests := []struct {
distro distro.Distro
metadata DpkgMetadata
expected string
}{
{
distro: distro.Distro{
Type: distro.Debian,
},
metadata: DpkgMetadata{
Package: "p",
Source: "s",
Version: "v",
Architecture: "a",
},
expected: "pkg:deb/debian/p@v?arch=a",
},
{
distro: distro.Distro{
Type: distro.Ubuntu,
},
metadata: DpkgMetadata{
Package: "p",
Source: "s",
Version: "v",
Architecture: "a",
},
expected: "pkg:deb/ubuntu/p@v?arch=a",
},
}
for _, test := range tests {
t.Run(test.expected, 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))
}
})
}
}
+50
View File
@@ -0,0 +1,50 @@
package pkg
import "github.com/package-url/packageurl-go"
// JavaMetadata encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship.
type JavaMetadata struct {
Manifest *JavaManifest `mapstructure:"Manifest" json:"manifest"`
PomProperties *PomProperties `mapstructure:"PomProperties" json:"pom-properties"`
Parent *Package `json:"parent-package"` // TODO: should this be included in the json output?
}
// PomProperties represents the fields of interest extracted from a Java archive's pom.xml file.
type PomProperties struct {
Path string
Name string `mapstructure:"name" json:"name"`
GroupID string `mapstructure:"groupId" json:"group-id"`
ArtifactID string `mapstructure:"artifactId" json:"artifact-id"`
Version string `mapstructure:"version" json:"version"`
Extra map[string]string `mapstructure:",remain" json:"extra-fields"`
}
// JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file.
type JavaManifest struct {
Name string `mapstructure:"Name" json:"name"`
ManifestVersion string `mapstructure:"Manifest-Version" json:"manifest-version"`
SpecTitle string `mapstructure:"Specification-Title" json:"specification-title"`
SpecVersion string `mapstructure:"Specification-Version" json:"specification-version"`
SpecVendor string `mapstructure:"Specification-Vendor" json:"specification-vendor"`
ImplTitle string `mapstructure:"Implementation-Title" json:"implementation-title"`
ImplVersion string `mapstructure:"Implementation-Version" json:"implementation-version"`
ImplVendor string `mapstructure:"Implementation-Vendor" json:"implementation-vendor"`
Extra map[string]string `mapstructure:",remain" json:"extra-fields"`
}
func (m JavaMetadata) PackageURL() string {
if m.PomProperties != nil {
pURL := packageurl.NewPackageURL(
packageurl.TypeMaven,
m.PomProperties.GroupID,
m.PomProperties.ArtifactID,
m.PomProperties.Version,
nil, // TODO: there are probably several qualifiers that can be specified here
"")
return pURL.ToString()
}
// TODO: support non-maven artifacts
return ""
}
+41
View File
@@ -0,0 +1,41 @@
package pkg
import (
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestJavaMetadata_pURL(t *testing.T) {
tests := []struct {
metadata JavaMetadata
expected string
}{
{
metadata: JavaMetadata{
PomProperties: &PomProperties{
Path: "p",
Name: "n",
GroupID: "g.id",
ArtifactID: "a",
Version: "v",
},
},
expected: "pkg:maven/g.id/a@v",
},
{
metadata: JavaMetadata{},
expected: "",
},
}
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
actual := test.metadata.PackageURL()
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
}
})
}
}
-79
View File
@@ -1,79 +0,0 @@
package pkg
// 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.
type DpkgMetadata struct {
Package string `mapstructure:"Package" json:"package"`
Source string `mapstructure:"Source" json:"source"`
Version string `mapstructure:"Version" json:"version"`
// TODO: consider keeping the remaining values as an embedded map
}
// RpmMetadata represents all captured data for a RPM DB package entry.
type RpmMetadata struct {
Version string `mapstructure:"Version" json:"version"`
Epoch int `mapstructure:"Epoch" json:"epoch"`
Arch string `mapstructure:"Arch" json:"architecture"`
Release string `mapstructure:"Release" json:"release"`
SourceRpm string `mapstructure:"SourceRpm" json:"source-rpm"`
Size int `mapstructure:"Size" json:"size"`
License string `mapstructure:"License" json:"license"`
Vendor string `mapstructure:"Vendor" json:"vendor"`
}
// JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file.
type JavaManifest struct {
Name string `mapstructure:"Name" json:"name"`
ManifestVersion string `mapstructure:"Manifest-Version" json:"manifest-version"`
SpecTitle string `mapstructure:"Specification-Title" json:"specification-title"`
SpecVersion string `mapstructure:"Specification-Version" json:"specification-version"`
SpecVendor string `mapstructure:"Specification-Vendor" json:"specification-vendor"`
ImplTitle string `mapstructure:"Implementation-Title" json:"implementation-title"`
ImplVersion string `mapstructure:"Implementation-Version" json:"implementation-version"`
ImplVendor string `mapstructure:"Implementation-Vendor" json:"implementation-vendor"`
Extra map[string]string `mapstructure:",remain" json:"extra-fields"`
}
// PomProperties represents the fields of interest extracted from a Java archive's pom.xml file.
type PomProperties struct {
Path string
Name string `mapstructure:"name" json:"name"`
GroupID string `mapstructure:"groupId" json:"group-id"`
ArtifactID string `mapstructure:"artifactId" json:"artifact-id"`
Version string `mapstructure:"version" json:"version"`
Extra map[string]string `mapstructure:",remain" json:"extra-fields"`
}
// JavaMetadata encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship.
type JavaMetadata struct {
Manifest *JavaManifest `mapstructure:"Manifest" json:"manifest"`
PomProperties *PomProperties `mapstructure:"PomProperties" json:"pom-properties"`
Parent *Package `json:"parent-package"`
}
// ApkMetadata represents all captured data for a Alpine DB package entry. See https://wiki.alpinelinux.org/wiki/Apk_spec for more information.
type ApkMetadata struct {
Package string `mapstructure:"P" json:"package"`
OriginPackage string `mapstructure:"o" json:"origin-package"`
Maintainer string `mapstructure:"m" json:"maintainer"`
Version string `mapstructure:"V" json:"version"`
License string `mapstructure:"L" json:"license"`
Architecture string `mapstructure:"A" json:"architecture"`
URL string `mapstructure:"U" json:"url"`
Description string `mapstructure:"T" json:"description"`
Size int `mapstructure:"S" json:"size"`
InstalledSize int `mapstructure:"I" json:"installed-size"`
PullDependencies string `mapstructure:"D" json:"pull-dependencies"`
PullChecksum string `mapstructure:"C" json:"pull-checksum"`
GitCommitOfAport string `mapstructure:"c" json:"git-commit-of-apk-port"`
Files []ApkFileRecord `json:"files"`
}
// ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records).
type ApkFileRecord struct {
Path string `json:"path"`
OwnerUID string `json:"owner-uid"`
OwnerGUI string `json:"owner-gid"`
Permissions string `json:"permissions"`
Checksum string `json:"checksum"`
}
+43
View File
@@ -5,8 +5,12 @@ package pkg
import (
"fmt"
"regexp"
"strings"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/distro"
"github.com/package-url/packageurl-go"
)
type ID int64
@@ -34,3 +38,42 @@ func (p Package) ID() ID {
func (p Package) String() string {
return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s)", p.Type, p.Name, p.Version)
}
// PackageURL returns a package-URL representation of the given package (see https://github.com/package-url/purl-spec)
func (p Package) PackageURL(d distro.Distro) 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(distro.Distro) string }); ok {
return i.PackageURL(d)
}
}
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
pURL := packageurl.NewPackageURL(
purlType,
namespace,
name,
p.Version,
nil,
"")
return pURL.ToString()
}
+136
View File
@@ -0,0 +1,136 @@
package pkg
import (
"github.com/anchore/syft/syft/distro"
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestPackage_pURL(t *testing.T) {
tests := []struct {
pkg Package
distro distro.Distro
expected string
}{
{
pkg: Package{
Name: "github.com/anchore/syft",
Version: "v0.1.0",
Type: GoModulePkg,
},
expected: "pkg:golang/github.com/anchore/syft@v0.1.0",
},
{
pkg: Package{
Name: "name",
Version: "v0.1.0",
Type: WheelPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
Name: "name",
Version: "v0.1.0",
Type: EggPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
Name: "name",
Version: "v0.1.0",
Type: PythonSetupPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
Name: "name",
Version: "v0.1.0",
Type: PythonRequirementsPkg,
},
expected: "pkg:pypi/name@v0.1.0",
},
{
pkg: Package{
Name: "name",
Version: "v0.1.0",
Type: BundlerPkg,
},
expected: "pkg:gem/name@v0.1.0",
},
{
pkg: Package{
Name: "name",
Version: "v0.1.0",
Type: NpmPkg,
},
expected: "pkg:npm/name@v0.1.0",
},
{
pkg: Package{
Name: "name",
Version: "v0.1.0",
Type: YarnPkg,
},
expected: "pkg:npm/name@v0.1.0",
},
{
distro: distro.Distro{
Type: distro.Ubuntu,
},
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: distro.Distro{
Type: distro.CentOS,
},
pkg: Package{
Name: "bad-name",
Version: "bad-v0.1.0",
Type: RpmPkg,
Metadata: RpmMetadata{
Name: "name",
Version: "v0.1.0",
Epoch: 2,
Arch: "amd64",
Release: "3",
},
},
expected: "pkg:rpm/centos/name@2:v0.1.0-3?arch=amd64",
},
{
distro: distro.Distro{
Type: distro.UnknownDistroType,
},
pkg: Package{
Name: "name",
Version: "v0.1.0",
Type: DebPkg,
},
expected: "pkg:deb/name@v0.1.0",
},
}
for _, test := range tests {
t.Run(string(test.pkg.Type)+"|"+test.expected, func(t *testing.T) {
actual := test.pkg.PackageURL(test.distro)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
}
})
}
}
+37
View File
@@ -0,0 +1,37 @@
package pkg
import (
"fmt"
"github.com/anchore/syft/syft/distro"
"github.com/package-url/packageurl-go"
)
// RpmMetadata represents all captured data for a RPM DB package entry.
type RpmMetadata struct {
Name string `json:"name"`
Version string `json:"version"`
Epoch int `json:"epoch"`
Arch string `json:"architecture"`
Release string `json:"release"`
SourceRpm string `json:"source-rpm"`
Size int `json:"size"`
License string `json:"license"`
Vendor string `json:"vendor"`
}
func (m RpmMetadata) PackageURL(d distro.Distro) string {
pURL := packageurl.NewPackageURL(
packageurl.TypeRPM,
d.Type.String(),
m.Name,
fmt.Sprintf("%d:%s-%s", m.Epoch, m.Version, m.Release),
packageurl.Qualifiers{
{
Key: "arch",
Value: m.Arch,
},
},
"")
return pURL.ToString()
}
+53
View File
@@ -0,0 +1,53 @@
package pkg
import (
"github.com/anchore/syft/syft/distro"
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestRpmMetadata_pURL(t *testing.T) {
tests := []struct {
distro distro.Distro
metadata RpmMetadata
expected string
}{
{
distro: distro.Distro{
Type: distro.CentOS,
},
metadata: RpmMetadata{
Name: "p",
Version: "v",
Arch: "a",
Release: "r",
Epoch: 1,
},
expected: "pkg:rpm/centos/p@1:v-r?arch=a",
},
{
distro: distro.Distro{
Type: distro.RedHat,
},
metadata: RpmMetadata{
Name: "p",
Version: "v",
Arch: "a",
Release: "r",
Epoch: 1,
},
expected: "pkg:rpm/redhat/p@1:v-r?arch=a",
},
}
for _, test := range tests {
t.Run(test.expected, 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))
}
})
}
}
+26
View File
@@ -1,5 +1,7 @@
package pkg
import "github.com/package-url/packageurl-go"
// Type represents a Package Type for or within a language ecosystem (there may be multiple package types within a language ecosystem)
type Type string
@@ -38,3 +40,27 @@ var AllPkgs = []Type{
JenkinsPluginPkg,
GoModulePkg,
}
func (t Type) PackageURLType() string {
switch t {
case ApkPkg:
return "alpine"
case BundlerPkg:
return packageurl.TypeGem
case DebPkg:
return "deb"
case EggPkg, WheelPkg, PythonRequirementsPkg, PythonSetupPkg:
return packageurl.TypePyPi
case NpmPkg, YarnPkg:
return packageurl.TypeNPM
case JavaPkg, JenkinsPluginPkg:
return packageurl.TypeMaven
case RpmPkg:
return packageurl.TypeRPM
case GoModulePkg:
return packageurl.TypeGolang
default:
// TODO: should this be a "generic" purl type instead?
return ""
}
}