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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 669 additions and 116 deletions

View file

@ -3,4 +3,7 @@ permit:
- MIT.*
- Apache.*
- MPL.*
- ISC
- ISC
ignore-packages:
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
- github.com/package-url/packageurl-go

View file

@ -68,7 +68,7 @@ func startWorker(userInput string) <-chan error {
}
}
catalog, scope, _, err := syft.Catalog(userInput, appConfig.ScopeOpt)
catalog, scope, distro, err := syft.Catalog(userInput, appConfig.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %+v", err)
return
@ -76,7 +76,7 @@ func startWorker(userInput string) <-chan error {
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: presenter.GetPresenter(appConfig.PresenterOpt, *scope, catalog),
Value: presenter.GetPresenter(appConfig.PresenterOpt, *scope, catalog, distro),
})
}()
return errs

1
go.mod
View file

@ -21,6 +21,7 @@ require (
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1
github.com/olekukonko/tablewriter v0.0.4
github.com/package-url/packageurl-go v0.1.0
github.com/pelletier/go-toml v1.8.0
github.com/rogpeppe/go-internal v1.5.2
github.com/sergi/go-diff v1.1.0

2
go.sum
View file

@ -668,6 +668,8 @@ github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5X
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/package-url/packageurl-go v0.1.0 h1:efWBc98O/dBZRg1pw2xiDzovnlMjCa9NPnfaiBduh8I=
github.com/package-url/packageurl-go v0.1.0/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=

View file

@ -24,9 +24,10 @@ func TestSinglePackage(t *testing.T) {
{
name: "Test Single Package",
expected: pkg.DpkgMetadata{
Package: "apt",
Source: "apt-dev",
Version: "1.8.2",
Package: "apt",
Source: "apt-dev",
Version: "1.8.2",
Architecture: "amd64",
},
},
}
@ -65,13 +66,15 @@ func TestMultiplePackages(t *testing.T) {
name: "Test Multiple Package",
expected: []pkg.DpkgMetadata{
{
Package: "tzdata",
Version: "2020a-0+deb10u1",
Source: "tzdata-dev",
Package: "tzdata",
Version: "2020a-0+deb10u1",
Source: "tzdata-dev",
Architecture: "all",
},
{
Package: "util-linux",
Version: "2.33.1-0.1",
Package: "util-linux",
Version: "2.33.1-0.1",
Architecture: "amd64",
},
},
},

View file

@ -54,6 +54,7 @@ func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
//Version: fmt.Sprintf("%d:%s-%s.%s", entry.Epoch, entry.Version, entry.Release, entry.Arch),
Type: pkg.RpmPkg,
Metadata: pkg.RpmMetadata{
Name: entry.Name,
Version: entry.Version,
Epoch: entry.Epoch,
Arch: entry.Arch,

View file

@ -14,6 +14,7 @@ func TestParseRpmDB(t *testing.T) {
Version: "0.9.2-1",
Type: pkg.RpmPkg,
Metadata: pkg.RpmMetadata{
Name: "dive",
Epoch: 0,
Arch: "x86_64",
Release: "1",

50
syft/pkg/apk_metadata.go Normal file
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()
}

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
syft/pkg/dpkg_metadata.go Normal file
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()
}

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
syft/pkg/java_metadata.go Normal file
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 ""
}

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))
}
})
}
}

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"`
}

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
syft/pkg/package_test.go Normal file
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
syft/pkg/rpm_metadata.go Normal file
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()
}

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))
}
})
}
}

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 ""
}
}

View file

@ -14,6 +14,7 @@ type Component struct {
Version string `xml:"version"` // Required; The version of the component as defined by the project
Description string `xml:"description,omitempty"` // A description of the component
Licenses *[]License `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions
PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec
// TODO: scope, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
// TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.)
}

View file

@ -3,6 +3,8 @@ package cyclonedx
import (
"encoding/xml"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/google/uuid"
)
@ -31,13 +33,14 @@ func NewDocument() Document {
}
// NewDocumentFromCatalog returns a CycloneDX Document object populated with the catalog contents.
func NewDocumentFromCatalog(catalog *pkg.Catalog) Document {
func NewDocumentFromCatalog(catalog *pkg.Catalog, d distro.Distro) Document {
bom := NewDocument()
for p := range catalog.Enumerate() {
component := Component{
Type: "library", // TODO: this is not accurate
Name: p.Name,
Version: p.Version,
Type: "library", // TODO: this is not accurate
Name: p.Name,
Version: p.Version,
PackageURL: p.PackageURL(d),
}
var licenses []License
for _, licenseName := range p.Licenses {

View file

@ -8,6 +8,8 @@ import (
"fmt"
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
)
@ -16,19 +18,21 @@ import (
type Presenter struct {
catalog *pkg.Catalog
scope scope.Scope
distro distro.Distro
}
// NewPresenter creates a CycloneDX presenter from the given Catalog and Scope objects.
func NewPresenter(catalog *pkg.Catalog, s scope.Scope) *Presenter {
func NewPresenter(catalog *pkg.Catalog, s scope.Scope, d distro.Distro) *Presenter {
return &Presenter{
catalog: catalog,
scope: s,
distro: d,
}
}
// Present writes the CycloneDX report to the given io.Writer.
func (pres *Presenter) Present(output io.Writer) error {
bom := NewDocumentFromCatalog(pres.catalog)
bom := NewDocumentFromCatalog(pres.catalog, pres.distro)
srcObj := pres.scope.Source()

View file

@ -3,6 +3,7 @@ package cyclonedx
import (
"bytes"
"flag"
"github.com/anchore/syft/syft/distro"
"regexp"
"testing"
@ -22,16 +23,21 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
Name: "package1",
Version: "1.0.1",
Type: pkg.DebPkg,
FoundBy: "the-cataloger-1",
Source: []file.Reference{
{Path: "/some/path/pkg1"},
},
Metadata: pkg.DpkgMetadata{
Package: "package1",
Version: "1.0.1",
Architecture: "amd64",
},
})
catalog.Add(pkg.Package{
Name: "package-2",
Name: "package2",
Version: "2.0.1",
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
@ -42,13 +48,24 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
"MIT",
"Apache-v2",
},
Metadata: pkg.DpkgMetadata{
Package: "package2",
Version: "1.0.2",
Architecture: "amd64",
},
})
s, err := scope.NewScopeFromDir("/some/path")
if err != nil {
t.Fatal(err)
}
pres := NewPresenter(catalog, s)
d, err := distro.NewDistro(distro.Ubuntu, "20.04")
if err != nil {
t.Fatal(err)
}
pres := NewPresenter(catalog, s, d)
// run presenter
err = pres.Present(&buffer)
@ -84,30 +101,61 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
Name: "package1",
Version: "1.0.1",
Source: []file.Reference{
*img.SquashedTree().File("/somefile-1.txt"),
},
Type: pkg.DebPkg,
Type: pkg.RpmPkg,
FoundBy: "the-cataloger-1",
Metadata: pkg.RpmMetadata{
Name: "package1",
Epoch: 0,
Arch: "x86_64",
Release: "1",
Version: "1.0.1",
SourceRpm: "package1-1.0.1-1.src.rpm",
Size: 12406784,
License: "MIT",
Vendor: "",
},
})
catalog.Add(pkg.Package{
Name: "package-2",
Name: "package2",
Version: "2.0.1",
Source: []file.Reference{
*img.SquashedTree().File("/somefile-2.txt"),
},
Type: pkg.DebPkg,
Type: pkg.RpmPkg,
FoundBy: "the-cataloger-2",
Licenses: []string{
"MIT",
"Apache-v2",
},
Metadata: pkg.RpmMetadata{
Name: "package2",
Epoch: 0,
Arch: "x86_64",
Release: "1",
Version: "1.0.2",
SourceRpm: "package2-1.0.2-1.src.rpm",
Size: 12406784,
License: "MIT",
Vendor: "",
},
})
s, err := scope.NewScopeFromImage(img, scope.AllLayersScope)
pres := NewPresenter(catalog, s)
if err != nil {
t.Fatal(err)
}
d, err := distro.NewDistro(distro.RedHat, "8")
if err != nil {
t.Fatal(err)
}
pres := NewPresenter(catalog, s, d)
// run presenter
err = pres.Present(&buffer)

View file

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" xmlns:bd="http://cyclonedx.org/schema/ext/bom-descriptor/1.0" version="1" serialNumber="urn:uuid:0667955f-34d0-49c1-8062-996dbc636974">
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" xmlns:bd="http://cyclonedx.org/schema/ext/bom-descriptor/1.0" version="1" serialNumber="urn:uuid:88c9a559-fb74-45a1-9dbb-a3d8bcbcacc8">
<components>
<component type="library">
<name>package-1</name>
<name>package1</name>
<version>1.0.1</version>
<purl>pkg:deb/ubuntu/package1@1.0.1?arch=amd64</purl>
</component>
<component type="library">
<name>package-2</name>
<name>package2</name>
<version>2.0.1</version>
<licenses>
<license>
@ -16,10 +17,11 @@
<name>Apache-v2</name>
</license>
</licenses>
<purl>pkg:deb/ubuntu/package2@1.0.2?arch=amd64</purl>
</component>
</components>
<bd:metadata>
<bd:timestamp>2020-08-26T15:31:39-04:00</bd:timestamp>
<bd:timestamp>2020-08-29T20:17:49-04:00</bd:timestamp>
<bd:tool>
<bd:vendor>anchore</bd:vendor>
<bd:name>syft</bd:name>

View file

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" xmlns:bd="http://cyclonedx.org/schema/ext/bom-descriptor/1.0" version="1" serialNumber="urn:uuid:2e7af7d0-83a4-4730-9175-66858406b285">
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" xmlns:bd="http://cyclonedx.org/schema/ext/bom-descriptor/1.0" version="1" serialNumber="urn:uuid:6da957f1-3337-4128-870c-fe271aa195d1">
<components>
<component type="library">
<name>package-1</name>
<name>package1</name>
<version>1.0.1</version>
<purl>pkg:rpm/redhat/package1@0:1.0.1-1?arch=x86_64</purl>
</component>
<component type="library">
<name>package-2</name>
<name>package2</name>
<version>2.0.1</version>
<licenses>
<license>
@ -16,10 +17,11 @@
<name>Apache-v2</name>
</license>
</licenses>
<purl>pkg:rpm/redhat/package2@0:1.0.2-1?arch=x86_64</purl>
</component>
</components>
<bd:metadata>
<bd:timestamp>2020-08-26T15:31:39-04:00</bd:timestamp>
<bd:timestamp>2020-08-29T20:17:49-04:00</bd:timestamp>
<bd:tool>
<bd:vendor>anchore</bd:vendor>
<bd:name>syft</bd:name>

View file

@ -7,6 +7,8 @@ package presenter
import (
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/presenter/cyclonedx"
"github.com/anchore/syft/syft/pkg"
@ -23,7 +25,7 @@ type Presenter interface {
}
// GetPresenter returns a presenter for images or directories
func GetPresenter(option Option, s scope.Scope, catalog *pkg.Catalog) Presenter {
func GetPresenter(option Option, s scope.Scope, catalog *pkg.Catalog, d *distro.Distro) Presenter {
switch option {
case JSONPresenter:
return json.NewPresenter(catalog, s)
@ -32,7 +34,7 @@ func GetPresenter(option Option, s scope.Scope, catalog *pkg.Catalog) Presenter
case TablePresenter:
return table.NewPresenter(catalog, s)
case CycloneDxPresenter:
return cyclonedx.NewPresenter(catalog, s)
return cyclonedx.NewPresenter(catalog, s, *d)
default:
return nil
}

View file

@ -14,6 +14,7 @@ import (
"github.com/anchore/go-testutils"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/scope"
@ -63,12 +64,17 @@ func testJsonSchema(t *testing.T, catalog *pkg.Catalog, theScope *scope.Scope, p
output := bytes.NewBufferString("")
p := presenter.GetPresenter(presenter.JSONPresenter, *theScope, catalog)
d, err := distro.NewDistro(distro.CentOS, "5")
if err != nil {
t.Fatalf("bad distro: %+v", err)
}
p := presenter.GetPresenter(presenter.JSONPresenter, *theScope, catalog, &d)
if p == nil {
t.Fatal("unable to get presenter")
}
err := p.Present(output)
err = p.Present(output)
if err != nil {
t.Fatalf("unable to present: %+v", err)
}