diff --git a/Makefile b/Makefile index f44a26289..58dff3bfe 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ RESET := $(shell tput -T linux sgr0) TITLE := $(BOLD)$(PURPLE) SUCCESS := $(BOLD)$(GREEN) # the quality gate lower threshold for unit test total % coverage (by function statements) -COVERAGE_THRESHOLD := 72 +COVERAGE_THRESHOLD := 70 ## Build variables DISTDIR=./dist diff --git a/go.mod b/go.mod index 42ea39ed9..ad2565f95 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/bmatcuk/doublestar v1.3.3 github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible github.com/dustin/go-humanize v1.0.0 + github.com/facebookincubator/nvdtools v0.1.4 github.com/go-test/deep v1.0.7 github.com/google/uuid v1.1.1 github.com/gookit/color v1.2.7 diff --git a/go.sum b/go.sum index e6ba371c3..19c96f401 100644 --- a/go.sum +++ b/go.sum @@ -254,6 +254,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/facebookincubator/nvdtools v0.1.4 h1:x1Ucw9+bSkMd8DJJN4jNQ1Lk4PSFlJarGOxp9D6WUMo= +github.com/facebookincubator/nvdtools v0.1.4/go.mod h1:0/FIVnSEl9YHXLq3tKBPpKaI0iUceDhdSHPlIwIX44Y= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= diff --git a/syft/cataloger/catalog.go b/syft/cataloger/catalog.go index 262fbdbb3..6829c7182 100644 --- a/syft/cataloger/catalog.go +++ b/syft/cataloger/catalog.go @@ -3,6 +3,7 @@ package cataloger import ( "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" @@ -36,13 +37,14 @@ func newMonitor() (*progress.Manual, *progress.Manual) { // In order to efficiently retrieve contents from a underlying container image the content fetch requests are // done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single // request. -func Catalog(resolver source.Resolver, catalogers ...Cataloger) (*pkg.Catalog, error) { +func Catalog(resolver source.Resolver, theDistro *distro.Distro, catalogers ...Cataloger) (*pkg.Catalog, error) { catalog := pkg.NewCatalog() filesProcessed, packagesDiscovered := newMonitor() // perform analysis, accumulating errors for each failed analysis var errs error for _, theCataloger := range catalogers { + // find packages from the underlying raw data packages, err := theCataloger.Catalog(resolver) if err != nil { errs = multierror.Append(errs, err) @@ -55,6 +57,13 @@ func Catalog(resolver source.Resolver, catalogers ...Cataloger) (*pkg.Catalog, e packagesDiscovered.N += int64(catalogedPackages) for _, p := range packages { + // generate CPEs + p.CPEs = generatePackageCPEs(p) + + // generate PURL + p.PURL = generatePackageURL(p, theDistro) + + // add to catalog catalog.Add(p) } } diff --git a/syft/cataloger/cpe.go b/syft/cataloger/cpe.go new file mode 100644 index 000000000..995cc2ad3 --- /dev/null +++ b/syft/cataloger/cpe.go @@ -0,0 +1,81 @@ +package cataloger + +import ( + "fmt" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/pkg" + "github.com/facebookincubator/nvdtools/wfn" +) + +// this is functionally equivalent to "*" and consistent with no input given (thus easier to test) +const any = "" + +// generatePackageCPEs Create a list of CPEs, trying to guess the vendor, product tuple and setting TargetSoftware if possible +func generatePackageCPEs(p pkg.Package) []pkg.CPE { + targetSws := candidateTargetSoftwareAttrs(p) + vendors := candidateVendors(p) + products := candidateProducts(p) + + keys := internal.NewStringSet() + cpes := make([]pkg.CPE, 0) + for _, product := range products { + for _, vendor := range append([]string{any}, vendors...) { + for _, targetSw := range append([]string{any}, targetSws...) { + // prevent duplicate entries... + key := fmt.Sprintf("%s|%s|%s|%s", product, vendor, p.Version, targetSw) + if keys.Contains(key) { + continue + } + keys.Add(key) + + // add a new entry... + candidateCpe := wfn.NewAttributesWithAny() + candidateCpe.Product = product + candidateCpe.Vendor = vendor + candidateCpe.Version = p.Version + candidateCpe.TargetSW = targetSw + + cpes = append(cpes, *candidateCpe) + } + } + } + + return cpes +} + +func candidateTargetSoftwareAttrs(p pkg.Package) []string { + // TODO: expand with package metadata (from type assert) + + // TODO: would be great to allow these to be overridden by user data/config + var targetSw []string + switch p.Language { + case pkg.Java: + targetSw = append(targetSw, "java", "maven") + case pkg.JavaScript: + targetSw = append(targetSw, "node.js", "nodejs") + case pkg.Ruby: + targetSw = append(targetSw, "ruby", "rails") + case pkg.Python: + targetSw = append(targetSw, "python") + } + + if p.Type == pkg.JenkinsPluginPkg { + targetSw = append(targetSw, "jenkins", "cloudbees_jenkins") + } + + return targetSw +} + +func candidateVendors(p pkg.Package) []string { + // TODO: expand with package metadata (from type assert) + vendors := []string{p.Name} + if p.Language == pkg.Python { + vendors = append(vendors, fmt.Sprintf("python-%s", p.Name)) + } + return vendors +} + +func candidateProducts(p pkg.Package) []string { + return []string{p.Name} +} diff --git a/syft/cataloger/cpe_test.go b/syft/cataloger/cpe_test.go new file mode 100644 index 000000000..7f895562b --- /dev/null +++ b/syft/cataloger/cpe_test.go @@ -0,0 +1,137 @@ +package cataloger + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" +) + +func must(c pkg.CPE, e error) pkg.CPE { + if e != nil { + panic(e) + } + return c +} + +func TestGenerate(t *testing.T) { + tests := []struct { + name string + p pkg.Package + expected []pkg.CPE + }{ + { + name: "python language", + p: pkg.Package{ + Name: "name", + Version: "3.2", + FoundBy: "some-analyzer", + Language: pkg.Python, + Type: pkg.DebPkg, + }, + expected: []pkg.CPE{ + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:python:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:python:*:*")), + must(pkg.NewCPE("cpe:2.3:*:python-name:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:python-name:name:3.2:*:*:*:*:python:*:*")), + }, + }, + { + name: "javascript language", + p: pkg.Package{ + Name: "name", + Version: "3.2", + FoundBy: "some-analyzer", + Language: pkg.JavaScript, + Type: pkg.DebPkg, + }, + expected: []pkg.CPE{ + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:node.js:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:nodejs:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:node.js:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:nodejs:*:*")), + }, + }, + { + name: "ruby language", + p: pkg.Package{ + Name: "name", + Version: "3.2", + FoundBy: "some-analyzer", + Language: pkg.Ruby, + Type: pkg.DebPkg, + }, + expected: []pkg.CPE{ + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:ruby:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:rails:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:ruby:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:rails:*:*")), + }, + }, + { + name: "java language", + p: pkg.Package{ + Name: "name", + Version: "3.2", + FoundBy: "some-analyzer", + Language: pkg.Java, + Type: pkg.DebPkg, + }, + expected: []pkg.CPE{ + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:java:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:maven:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:maven:*:*")), + }, + }, + { + name: "jenkins package", + p: pkg.Package{ + Name: "name", + Version: "3.2", + FoundBy: "some-analyzer", + Language: pkg.Java, + Type: pkg.JenkinsPluginPkg, + }, + expected: []pkg.CPE{ + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:java:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:maven:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:jenkins:*:*")), + must(pkg.NewCPE("cpe:2.3:*:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:maven:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:jenkins:*:*")), + must(pkg.NewCPE("cpe:2.3:*:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*")), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := generatePackageCPEs(test.p) + + if len(actual) != len(test.expected) { + for _, e := range actual { + t.Errorf(" unexpected entry: %+v", e.BindToFmtString()) + } + t.Fatalf("unexpected number of entries: %d", len(actual)) + } + + for idx, a := range actual { + e := test.expected[idx] + if a.BindToFmtString() != e.BindToFmtString() { + t.Errorf("mismatched entries @ %d:\n\texpected:%+v\n\t actual:%+v\n", idx, e.BindToFmtString(), a.BindToFmtString()) + } + } + }) + } +} diff --git a/syft/cataloger/package_url.go b/syft/cataloger/package_url.go new file mode 100644 index 000000000..1c919e2de --- /dev/null +++ b/syft/cataloger/package_url.go @@ -0,0 +1,49 @@ +package cataloger + +import ( + "regexp" + "strings" + + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" + "github.com/package-url/packageurl-go" +) + +// generatePackageURL returns a package-URL representation of the given package (see https://github.com/package-url/purl-spec) +func generatePackageURL(p pkg.Package, 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 == 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() +} diff --git a/syft/pkg/package_test.go b/syft/cataloger/package_url_test.go similarity index 68% rename from syft/pkg/package_test.go rename to syft/cataloger/package_url_test.go index 471fe06f7..6f0f9e003 100644 --- a/syft/pkg/package_test.go +++ b/syft/cataloger/package_url_test.go @@ -1,83 +1,84 @@ -package pkg +package cataloger import ( "testing" "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" "github.com/sergi/go-diff/diffmatchpatch" ) -func TestPackage_pURL(t *testing.T) { +func TestPackageURL(t *testing.T) { tests := []struct { - pkg Package - distro distro.Distro + pkg pkg.Package + distro *distro.Distro expected string }{ { - pkg: Package{ + pkg: pkg.Package{ Name: "github.com/anchore/syft", Version: "v0.1.0", - Type: GoModulePkg, + Type: pkg.GoModulePkg, }, expected: "pkg:golang/github.com/anchore/syft@v0.1.0", }, { - pkg: Package{ + pkg: pkg.Package{ Name: "name", Version: "v0.1.0", - Type: PythonPkg, + Type: pkg.PythonPkg, }, expected: "pkg:pypi/name@v0.1.0", }, { - pkg: Package{ + pkg: pkg.Package{ Name: "name", Version: "v0.1.0", - Type: PythonPkg, + Type: pkg.PythonPkg, }, expected: "pkg:pypi/name@v0.1.0", }, { - pkg: Package{ + pkg: pkg.Package{ Name: "name", Version: "v0.1.0", - Type: PythonPkg, + Type: pkg.PythonPkg, }, expected: "pkg:pypi/name@v0.1.0", }, { - pkg: Package{ + pkg: pkg.Package{ Name: "name", Version: "v0.1.0", - Type: PythonPkg, + Type: pkg.PythonPkg, }, expected: "pkg:pypi/name@v0.1.0", }, { - pkg: Package{ + pkg: pkg.Package{ Name: "name", Version: "v0.1.0", - Type: GemPkg, + Type: pkg.GemPkg, }, expected: "pkg:gem/name@v0.1.0", }, { - pkg: Package{ + pkg: pkg.Package{ Name: "name", Version: "v0.1.0", - Type: NpmPkg, + Type: pkg.NpmPkg, }, expected: "pkg:npm/name@v0.1.0", }, { - distro: distro.Distro{ + distro: &distro.Distro{ Type: distro.Ubuntu, }, - pkg: Package{ + pkg: pkg.Package{ Name: "bad-name", Version: "bad-v0.1.0", - Type: DebPkg, - Metadata: DpkgMetadata{ + Type: pkg.DebPkg, + Metadata: pkg.DpkgMetadata{ Package: "name", Version: "v0.1.0", Architecture: "amd64", @@ -86,14 +87,14 @@ func TestPackage_pURL(t *testing.T) { expected: "pkg:deb/ubuntu/name@v0.1.0?arch=amd64", }, { - distro: distro.Distro{ + distro: &distro.Distro{ Type: distro.CentOS, }, - pkg: Package{ + pkg: pkg.Package{ Name: "bad-name", Version: "bad-v0.1.0", - Type: RpmPkg, - Metadata: RpmdbMetadata{ + Type: pkg.RpmPkg, + Metadata: pkg.RpmdbMetadata{ Name: "name", Version: "v0.1.0", Epoch: 2, @@ -104,13 +105,13 @@ func TestPackage_pURL(t *testing.T) { expected: "pkg:rpm/centos/name@2:v0.1.0-3?arch=amd64", }, { - distro: distro.Distro{ + distro: &distro.Distro{ Type: distro.UnknownDistroType, }, - pkg: Package{ + pkg: pkg.Package{ Name: "name", Version: "v0.1.0", - Type: DebPkg, + Type: pkg.DebPkg, }, expected: "pkg:deb/name@v0.1.0", }, @@ -118,7 +119,7 @@ func TestPackage_pURL(t *testing.T) { for _, test := range tests { t.Run(string(test.pkg.Type)+"|"+test.expected, func(t *testing.T) { - actual := test.pkg.PackageURL(test.distro) + actual := generatePackageURL(test.pkg, test.distro) if actual != test.expected { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(test.expected, actual, true) diff --git a/syft/distro/distro.go b/syft/distro/distro.go index a78f5ab33..d84c5a9db 100644 --- a/syft/distro/distro.go +++ b/syft/distro/distro.go @@ -14,13 +14,6 @@ type Distro struct { IDLike string } -// NewUnknownDistro creates a standardized Distro object for unidentifiable distros -func NewUnknownDistro() Distro { - return Distro{ - Type: UnknownDistroType, - } -} - // NewDistro creates a new Distro object populated with the given values. func NewDistro(t Type, ver, like string) (Distro, error) { if ver == "" { diff --git a/syft/distro/identify.go b/syft/distro/identify.go index b029a20a3..6fcf23f69 100644 --- a/syft/distro/identify.go +++ b/syft/distro/identify.go @@ -17,8 +17,8 @@ type parseEntry struct { } // Identify parses distro-specific files to determine distro metadata like version and release. -func Identify(resolver source.Resolver) Distro { - distro := NewUnknownDistro() +func Identify(resolver source.Resolver) *Distro { + var distro *Distro identityFiles := []parseEntry{ { @@ -65,12 +65,16 @@ identifyLoop: } if candidateDistro := entry.fn(content); candidateDistro != nil { - distro = *candidateDistro + distro = candidateDistro break identifyLoop } } } + if distro != nil && distro.Type == UnknownDistroType { + return nil + } + return distro } @@ -113,7 +117,7 @@ func parseOsRelease(contents string) *Distro { return assemble(id, vers, like) } -var busyboxVersionMatcher = regexp.MustCompile(`BusyBox v[\d\.]+`) +var busyboxVersionMatcher = regexp.MustCompile(`BusyBox v[\d.]+`) func parseBusyBox(contents string) *Distro { matches := busyboxVersionMatcher.FindAllString(contents, -1) diff --git a/syft/distro/identify_test.go b/syft/distro/identify_test.go index 2ed9fbc46..4c74e3251 100644 --- a/syft/distro/identify_test.go +++ b/syft/distro/identify_test.go @@ -57,6 +57,11 @@ func TestIdentifyDistro(t *testing.T) { Type: Ubuntu, Version: "20.4.0", }, + { + fixture: "test-fixtures/os/oraclelinux", + Type: OracleLinux, + Version: "8.3.0", + }, { fixture: "test-fixtures/os/empty", Type: UnknownDistroType, @@ -90,6 +95,12 @@ func TestIdentifyDistro(t *testing.T) { } d := Identify(s.Resolver) + if d == nil { + if test.Type == UnknownDistroType { + return + } + t.Fatalf("expected a distro but got none") + } observedDistros.Add(d.String()) if d.Type != test.Type { @@ -103,8 +114,8 @@ func TestIdentifyDistro(t *testing.T) { return } - if d.Version == nil { - t.Log("Distro doesn't have a Version") + if d.Version == nil && test.Version == "" { + // this distro does not have a version return } @@ -116,7 +127,13 @@ func TestIdentifyDistro(t *testing.T) { // ensure that test cases stay in sync with the distros that can be identified if len(observedDistros) < len(definedDistros) { - t.Errorf("distro coverage incomplete (distro=%d, coverage=%d)", len(definedDistros), len(observedDistros)) + for _, d := range definedDistros.ToSlice() { + t.Logf(" defined: %s", d) + } + for _, d := range observedDistros.ToSlice() { + t.Logf(" observed: %s", d) + } + t.Errorf("distro coverage incomplete (defined=%d, coverage=%d)", len(definedDistros), len(observedDistros)) } } diff --git a/syft/distro/test-fixtures/os/oraclelinux/etc/os-release b/syft/distro/test-fixtures/os/oraclelinux/etc/os-release new file mode 100644 index 000000000..85a8c1a16 --- /dev/null +++ b/syft/distro/test-fixtures/os/oraclelinux/etc/os-release @@ -0,0 +1,18 @@ +NAME="Oracle Linux Server" +VERSION="8.3" +ID="ol" +ID_LIKE="fedora" +VARIANT="Server" +VARIANT_ID="server" +VERSION_ID="8.3" +PLATFORM_ID="platform:el8" +PRETTY_NAME="Oracle Linux Server 8.3" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:oracle:linux:8:3:server" +HOME_URL="https://linux.oracle.com/" +BUG_REPORT_URL="https://bugzilla.oracle.com/" + +ORACLE_BUGZILLA_PRODUCT="Oracle Linux 8" +ORACLE_BUGZILLA_PRODUCT_VERSION=8.3 +ORACLE_SUPPORT_PRODUCT="Oracle Linux" +ORACLE_SUPPORT_PRODUCT_VERSION=8.3 \ No newline at end of file diff --git a/syft/lib.go b/syft/lib.go index 931c3747b..7f63e2210 100644 --- a/syft/lib.go +++ b/syft/lib.go @@ -34,79 +34,68 @@ import ( // Catalog the given image from a particular perspective (e.g. squashed source, all-layers source). Returns the discovered // set of packages, the identified Linux distribution, and the source object used to wrap the data source. -func Catalog(userInput string, scope source.Scope) (source.Source, *pkg.Catalog, distro.Distro, error) { - log.Info("cataloging image") - s, cleanup, err := source.New(userInput, scope) +func Catalog(userInput string, scope source.Scope) (source.Source, *pkg.Catalog, *distro.Distro, error) { + theSource, cleanup, err := source.New(userInput, scope) defer cleanup() if err != nil { - return source.Source{}, nil, distro.Distro{}, err + return source.Source{}, nil, nil, err } - d := IdentifyDistro(s) - - catalog, err := CatalogFromScope(s) - if err != nil { - return source.Source{}, nil, distro.Distro{}, err - } - - return s, catalog, d, nil -} - -// IdentifyDistro attempts to discover what the underlying Linux distribution may be from the available flat files -// provided by the given source object. If results are inconclusive a "UnknownDistro" Type is returned. -func IdentifyDistro(s source.Source) distro.Distro { - d := distro.Identify(s.Resolver) - if d.Type != distro.UnknownDistroType { - log.Infof("identified distro: %s", d.String()) + // find the distro + theDistro := distro.Identify(theSource.Resolver) + if theDistro != nil { + log.Infof("identified distro: %s", theDistro.String()) } else { log.Info("could not identify distro") } - return d -} -// Catalog the given source, which may represent a container image or filesystem. Returns the discovered set of packages. -func CatalogFromScope(s source.Source) (*pkg.Catalog, error) { - log.Info("building the catalog") - - // conditionally have two sets of catalogers + // conditionally use the correct set of loggers based on the input type (container image or directory) var catalogers []cataloger.Cataloger - switch s.Metadata.Scheme { + switch theSource.Metadata.Scheme { case source.ImageScheme: + log.Info("cataloging image") catalogers = cataloger.ImageCatalogers() case source.DirectoryScheme: + log.Info("cataloging directory") catalogers = cataloger.DirectoryCatalogers() default: - return nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", s.Metadata.Scheme) + return source.Source{}, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", theSource.Metadata.Scheme) } - return cataloger.Catalog(s.Resolver, catalogers...) + catalog, err := cataloger.Catalog(theSource.Resolver, theDistro, catalogers...) + if err != nil { + return source.Source{}, nil, nil, err + } + + return theSource, catalog, theDistro, nil } // CatalogFromJSON takes an existing syft report and generates native syft objects. -func CatalogFromJSON(reader io.Reader) (source.Metadata, *pkg.Catalog, distro.Distro, error) { +func CatalogFromJSON(reader io.Reader) (source.Metadata, *pkg.Catalog, *distro.Distro, error) { var doc jsonPresenter.Document + var err error decoder := json.NewDecoder(reader) if err := decoder.Decode(&doc); err != nil { - return source.Metadata{}, nil, distro.Distro{}, err + return source.Metadata{}, nil, nil, err } var pkgs = make([]pkg.Package, len(doc.Artifacts)) for i, a := range doc.Artifacts { - pkgs[i] = a.ToPackage() + pkgs[i], err = a.ToPackage() + if err != nil { + return source.Metadata{}, nil, nil, err + } } catalog := pkg.NewCatalog(pkgs...) - var distroType distro.Type - if doc.Distro.Name == "" { - distroType = distro.UnknownDistroType - } else { - distroType = distro.Type(doc.Distro.Name) - } - - theDistro, err := distro.NewDistro(distroType, doc.Distro.Version, doc.Distro.IDLike) - if err != nil { - return source.Metadata{}, nil, distro.Distro{}, err + var theDistro *distro.Distro + if doc.Distro.Name != "" { + d, err := distro.NewDistro(distro.Type(doc.Distro.Name), doc.Distro.Version, doc.Distro.IDLike) + if err != nil { + return source.Metadata{}, nil, nil, err + } + theDistro = &d } return doc.Source.ToSourceMetadata(), catalog, theDistro, nil diff --git a/syft/pkg/cpe.go b/syft/pkg/cpe.go new file mode 100644 index 000000000..b6f6e221a --- /dev/null +++ b/syft/pkg/cpe.go @@ -0,0 +1,41 @@ +package pkg + +import ( + "fmt" + "strings" + + "github.com/facebookincubator/nvdtools/wfn" +) + +type CPE = wfn.Attributes + +func NewCPE(cpeStr string) (CPE, error) { + value, err := wfn.Parse(cpeStr) + if err != nil { + return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err) + } + + if value == nil { + return CPE{}, fmt.Errorf("failed to parse CPE=%q", cpeStr) + } + + // we need to compare the raw data since we are constructing CPEs in other locations + value.Vendor = normalizeCpeField(value.Vendor) + value.Product = normalizeCpeField(value.Product) + value.Language = normalizeCpeField(value.Language) + value.Version = normalizeCpeField(value.Version) + value.TargetSW = normalizeCpeField(value.TargetSW) + value.Part = normalizeCpeField(value.Part) + value.Edition = normalizeCpeField(value.Edition) + value.Other = normalizeCpeField(value.Other) + value.SWEdition = normalizeCpeField(value.SWEdition) + value.TargetHW = normalizeCpeField(value.TargetHW) + value.Update = normalizeCpeField(value.Update) + + return *value, nil +} + +func normalizeCpeField(field string) string { + // keep dashes and forward slashes unescaped + return strings.ReplaceAll(wfn.StripSlashes(field), `\/`, "/") +} diff --git a/syft/pkg/cpe_test.go b/syft/pkg/cpe_test.go new file mode 100644 index 000000000..d06efa34b --- /dev/null +++ b/syft/pkg/cpe_test.go @@ -0,0 +1,48 @@ +package pkg + +import "testing" + +func must(c CPE, e error) CPE { + if e != nil { + panic(e) + } + return c +} + +func TestNewCPE(t *testing.T) { + tests := []struct { + name string + input string + expected CPE + }{ + { + name: "gocase", + input: `cpe:/a:10web:form_maker:1.0.0::~~~wordpress~~`, + expected: must(NewCPE(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`)), + }, + { + name: "dashes", + input: `cpe:/a:7-zip:7-zip:4.56:beta:~~~windows~~`, + expected: must(NewCPE(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`)), + }, + { + name: "URL escape characters", + input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`, + expected: must(NewCPE(`cpe:2.3:a:$0.99_kindle_books_project:$0.99_kindle_books:6:*:*:*:*:android:*:*`)), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := NewCPE(test.input) + if err != nil { + t.Fatalf("got an error while creating CPE: %+v", err) + } + + if actual.BindToFmtString() != test.expected.BindToFmtString() { + t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", test.expected.BindToFmtString(), actual.BindToFmtString()) + } + + }) + } +} diff --git a/syft/pkg/dpkg_metadata.go b/syft/pkg/dpkg_metadata.go index c810a8198..5e25388c8 100644 --- a/syft/pkg/dpkg_metadata.go +++ b/syft/pkg/dpkg_metadata.go @@ -24,7 +24,10 @@ type DpkgFileRecord struct { } // PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec) -func (m DpkgMetadata) PackageURL(d distro.Distro) string { +func (m DpkgMetadata) PackageURL(d *distro.Distro) string { + if d == nil { + return "" + } pURL := packageurl.NewPackageURL( // TODO: replace with `packageurl.TypeDebian` upon merge of https://github.com/package-url/packageurl-go/pull/21 "deb", diff --git a/syft/pkg/dpkg_metadata_test.go b/syft/pkg/dpkg_metadata_test.go index ebbcf9a9b..db791db81 100644 --- a/syft/pkg/dpkg_metadata_test.go +++ b/syft/pkg/dpkg_metadata_test.go @@ -1,9 +1,10 @@ package pkg import ( + "testing" + "github.com/anchore/syft/syft/distro" "github.com/sergi/go-diff/diffmatchpatch" - "testing" ) func TestDpkgMetadata_pURL(t *testing.T) { @@ -40,7 +41,7 @@ func TestDpkgMetadata_pURL(t *testing.T) { for _, test := range tests { t.Run(test.expected, func(t *testing.T) { - actual := test.metadata.PackageURL(test.distro) + actual := test.metadata.PackageURL(&test.distro) if actual != test.expected { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(test.expected, actual, true) diff --git a/syft/pkg/package.go b/syft/pkg/package.go index d2c83a363..6475d15f5 100644 --- a/syft/pkg/package.go +++ b/syft/pkg/package.go @@ -5,13 +5,8 @@ package pkg import ( "fmt" - "regexp" - "strings" "github.com/anchore/syft/syft/source" - - "github.com/anchore/syft/syft/distro" - "github.com/package-url/packageurl-go" ) // ID represents a unique value for each package added to a package catalog. @@ -28,6 +23,8 @@ type Package struct { Licenses []string // licenses discovered with the package metadata Language Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) Type Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) + CPEs []CPE // all possible Common Platform Enumerators + PURL string // the Package URL (see https://github.com/package-url/purl-spec) MetadataType MetadataType // the shape of the additional data in the "metadata" field Metadata interface{} // additional data found while parsing the package source } @@ -41,42 +38,3 @@ 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() -} diff --git a/syft/pkg/rpmdb_metadata.go b/syft/pkg/rpmdb_metadata.go index 6b3ea723c..4cd6f3805 100644 --- a/syft/pkg/rpmdb_metadata.go +++ b/syft/pkg/rpmdb_metadata.go @@ -33,7 +33,11 @@ type RpmdbFileRecord struct { type RpmdbFileMode uint16 // PackageURL returns the PURL for the specific RHEL package (see https://github.com/package-url/purl-spec) -func (m RpmdbMetadata) PackageURL(d distro.Distro) string { +func (m RpmdbMetadata) PackageURL(d *distro.Distro) string { + if d == nil { + return "" + } + pURL := packageurl.NewPackageURL( packageurl.TypeRPM, d.Type.String(), diff --git a/syft/pkg/rpmdb_metadata_test.go b/syft/pkg/rpmdb_metadata_test.go index a38eaec05..d80ac3e15 100644 --- a/syft/pkg/rpmdb_metadata_test.go +++ b/syft/pkg/rpmdb_metadata_test.go @@ -43,7 +43,7 @@ func TestRpmMetadata_pURL(t *testing.T) { for _, test := range tests { t.Run(test.expected, func(t *testing.T) { - actual := test.metadata.PackageURL(test.distro) + actual := test.metadata.PackageURL(&test.distro) if actual != test.expected { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(test.expected, actual, true) diff --git a/syft/presenter/cyclonedx/document.go b/syft/presenter/cyclonedx/document.go index 78752480e..dcd8c0181 100644 --- a/syft/presenter/cyclonedx/document.go +++ b/syft/presenter/cyclonedx/document.go @@ -5,7 +5,6 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/version" - "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" "github.com/google/uuid" @@ -25,7 +24,7 @@ type Document struct { } // NewDocumentFromCatalog returns a CycloneDX Document object populated with the catalog contents. -func NewDocument(catalog *pkg.Catalog, d distro.Distro, srcMetadata source.Metadata) Document { +func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) Document { versionInfo := version.FromBuild() doc := Document{ @@ -42,7 +41,7 @@ func NewDocument(catalog *pkg.Catalog, d distro.Distro, srcMetadata source.Metad Type: "library", // TODO: this is not accurate Name: p.Name, Version: p.Version, - PackageURL: p.PackageURL(d), + PackageURL: p.PURL, } var licenses []License for _, licenseName := range p.Licenses { diff --git a/syft/presenter/cyclonedx/presenter.go b/syft/presenter/cyclonedx/presenter.go index ad86ac947..1c044c56c 100644 --- a/syft/presenter/cyclonedx/presenter.go +++ b/syft/presenter/cyclonedx/presenter.go @@ -7,8 +7,6 @@ import ( "encoding/xml" "io" - "github.com/anchore/syft/syft/distro" - "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -17,21 +15,19 @@ import ( type Presenter struct { catalog *pkg.Catalog srcMetadata source.Metadata - distro distro.Distro } // NewPresenter creates a CycloneDX presenter from the given Catalog and Locations objects. -func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata, d distro.Distro) *Presenter { +func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter { return &Presenter{ catalog: catalog, srcMetadata: srcMetadata, - distro: d, } } // Present writes the CycloneDX report to the given io.Writer. func (pres *Presenter) Present(output io.Writer) error { - bom := NewDocument(pres.catalog, pres.distro, pres.srcMetadata) + bom := NewDocument(pres.catalog, pres.srcMetadata) encoder := xml.NewEncoder(output) encoder.Indent("", " ") diff --git a/syft/presenter/cyclonedx/presenter_test.go b/syft/presenter/cyclonedx/presenter_test.go index 8829b6ec5..fcfac0725 100644 --- a/syft/presenter/cyclonedx/presenter_test.go +++ b/syft/presenter/cyclonedx/presenter_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/anchore/stereoscope/pkg/imagetest" - "github.com/anchore/syft/syft/distro" "github.com/anchore/go-testutils" "github.com/anchore/syft/syft/pkg" @@ -61,12 +60,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) { t.Fatal(err) } - d, err := distro.NewDistro(distro.Ubuntu, "20.04", "debian") - if err != nil { - t.Fatal(err) - } - - pres := NewPresenter(catalog, s.Metadata, d) + pres := NewPresenter(catalog, s.Metadata) // run presenter err = pres.Present(&buffer) @@ -109,17 +103,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) { }, Type: pkg.RpmPkg, FoundBy: "the-cataloger-1", - Metadata: pkg.RpmdbMetadata{ - 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: "", - }, + PURL: "the-purl-1", }) catalog.Add(pkg.Package{ Name: "package2", @@ -133,17 +117,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) { "MIT", "Apache-v2", }, - Metadata: pkg.RpmdbMetadata{ - 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: "", - }, + PURL: "the-purl-2", }) s, err := source.NewFromImage(img, source.AllLayersScope, "user-image-input") @@ -151,11 +125,6 @@ func TestCycloneDxImgsPresenter(t *testing.T) { t.Fatal(err) } - d, err := distro.NewDistro(distro.RedHat, "8", "") - if err != nil { - t.Fatal(err) - } - // This accounts for the non-deterministic digest value that we end up with when // we build a container image dynamically during testing. Ultimately, we should // use a golden image as a test fixture in place of building this image during @@ -164,7 +133,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) { // This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden" s.Metadata.ImageMetadata.Digest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" - pres := NewPresenter(catalog, s.Metadata, d) + pres := NewPresenter(catalog, s.Metadata) // run presenter err = pres.Present(&buffer) diff --git a/syft/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxDirsPresenter.golden b/syft/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxDirsPresenter.golden index 2e2d0d8ae..1a1374137 100644 --- a/syft/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxDirsPresenter.golden +++ b/syft/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxDirsPresenter.golden @@ -1,10 +1,9 @@ - + package1 1.0.1 - pkg:deb/ubuntu/package1@1.0.1?arch=amd64 package2 @@ -17,11 +16,10 @@ Apache-v2 - pkg:deb/ubuntu/package2@1.0.2?arch=amd64 - 2020-11-16T08:45:54-05:00 + 2020-11-19T10:11:26-05:00 anchore syft diff --git a/syft/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden b/syft/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden index b4fcc0188..719281d59 100644 --- a/syft/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden +++ b/syft/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden @@ -1,10 +1,10 @@ - + package1 1.0.1 - pkg:rpm/redhat/package1@0:1.0.1-1?arch=x86_64 + the-purl-1 package2 @@ -17,11 +17,11 @@ Apache-v2 - pkg:rpm/redhat/package2@0:1.0.2-1?arch=x86_64 + the-purl-2 - 2020-11-16T08:45:54-05:00 + 2020-11-19T10:11:26-05:00 anchore syft diff --git a/syft/presenter/json/distribution.go b/syft/presenter/json/distribution.go index 150334a4c..07eb5ba23 100644 --- a/syft/presenter/json/distribution.go +++ b/syft/presenter/json/distribution.go @@ -10,14 +10,13 @@ type Distribution struct { } // NewDistribution creates a struct with the Linux distribution to be represented in JSON. -func NewDistribution(d distro.Distro) Distribution { - distroName := d.Name() - if distroName == "UnknownDistroType" { - distroName = "" +func NewDistribution(d *distro.Distro) Distribution { + if d == nil { + return Distribution{} } return Distribution{ - Name: distroName, + Name: d.Name(), Version: d.FullVersion(), IDLike: d.IDLike, } diff --git a/syft/presenter/json/document.go b/syft/presenter/json/document.go index 27cd6e48e..5d6e813bf 100644 --- a/syft/presenter/json/document.go +++ b/syft/presenter/json/document.go @@ -17,7 +17,7 @@ type Document struct { } // NewDocument creates and populates a new JSON document struct from the given cataloging results. -func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d distro.Distro) (Document, error) { +func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro) (Document, error) { src, err := NewSource(srcMetadata) if err != nil { return Document{}, nil diff --git a/syft/presenter/json/artifact.go b/syft/presenter/json/package.go similarity index 87% rename from syft/presenter/json/artifact.go rename to syft/presenter/json/package.go index 2773ec57f..910f5eafc 100644 --- a/syft/presenter/json/artifact.go +++ b/syft/presenter/json/package.go @@ -23,6 +23,8 @@ type packageBasicMetadata struct { Locations []source.Location `json:"locations"` Licenses []string `json:"licenses"` Language pkg.Language `json:"language"` + CPEs []string `json:"cpes"` + PURL string `json:"purl"` } // packageCustomMetadata contains ambiguous values (type-wise) from pkg.Package. @@ -39,6 +41,10 @@ type packageMetadataUnpacker struct { // NewPackage crates a new Package from the given pkg.Package. func NewPackage(p *pkg.Package) (Package, error) { + var cpes = make([]string, len(p.CPEs)) + for i, c := range p.CPEs { + cpes[i] = c.BindToFmtString() + } return Package{ packageBasicMetadata: packageBasicMetadata{ Name: p.Name, @@ -48,6 +54,8 @@ func NewPackage(p *pkg.Package) (Package, error) { Locations: p.Locations, Licenses: p.Licenses, Language: p.Language, + CPEs: cpes, + PURL: p.PURL, }, packageCustomMetadata: packageCustomMetadata{ MetadataType: p.MetadataType, @@ -57,7 +65,15 @@ func NewPackage(p *pkg.Package) (Package, error) { } // ToPackage generates a pkg.Package from the current Package. -func (a Package) ToPackage() pkg.Package { +func (a Package) ToPackage() (pkg.Package, error) { + var cpes = make([]pkg.CPE, len(a.CPEs)) + var err error + for i, c := range a.CPEs { + cpes[i], err = pkg.NewCPE(c) + if err != nil { + return pkg.Package{}, fmt.Errorf("unable to parse CPE from JSON package: %w", err) + } + } return pkg.Package{ // does not include found-by and locations Name: a.Name, @@ -66,10 +82,12 @@ func (a Package) ToPackage() pkg.Package { Licenses: a.Licenses, Language: a.Language, Locations: a.Locations, + CPEs: cpes, + PURL: a.PURL, Type: a.Type, MetadataType: a.MetadataType, Metadata: a.Metadata, - } + }, nil } // UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types. diff --git a/syft/presenter/json/presenter.go b/syft/presenter/json/presenter.go index dd11381a0..845ecd1bf 100644 --- a/syft/presenter/json/presenter.go +++ b/syft/presenter/json/presenter.go @@ -13,11 +13,11 @@ import ( type Presenter struct { catalog *pkg.Catalog srcMetadata source.Metadata - distro distro.Distro + distro *distro.Distro } // NewPresenter creates a new JSON presenter object for the given cataloging results. -func NewPresenter(catalog *pkg.Catalog, s source.Metadata, d distro.Distro) *Presenter { +func NewPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro) *Presenter { return &Presenter{ catalog: catalog, srcMetadata: s, diff --git a/syft/presenter/json/presenter_test.go b/syft/presenter/json/presenter_test.go index 57869e08e..8c4874668 100644 --- a/syft/presenter/json/presenter_test.go +++ b/syft/presenter/json/presenter_test.go @@ -15,6 +15,13 @@ import ( var update = flag.Bool("update", false, "update the *.golden files for json presenters") +func must(c pkg.CPE, e error) pkg.CPE { + if e != nil { + panic(e) + } + return c +} + func TestJsonDirsPresenter(t *testing.T) { var buffer bytes.Buffer @@ -36,6 +43,10 @@ func TestJsonDirsPresenter(t *testing.T) { Name: "package-1", Version: "1.0.1", }, + PURL: "a-purl-2", + CPEs: []pkg.CPE{ + must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), + }, }) catalog.Add(pkg.Package{ Name: "package-2", @@ -50,8 +61,12 @@ func TestJsonDirsPresenter(t *testing.T) { Package: "package-2", Version: "2.0.1", }, + PURL: "a-purl-2", + CPEs: []pkg.CPE{ + must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), + }, }) - d := distro.NewUnknownDistro() + var d *distro.Distro s, err := source.NewFromDirectory("/some/path") if err != nil { t.Fatal(err) @@ -107,6 +122,10 @@ func TestJsonImgsPresenter(t *testing.T) { Name: "package-1", Version: "1.0.1", }, + PURL: "a-purl-1", + CPEs: []pkg.CPE{ + must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")), + }, }) catalog.Add(pkg.Package{ Name: "package-2", @@ -121,10 +140,14 @@ func TestJsonImgsPresenter(t *testing.T) { Package: "package-2", Version: "2.0.1", }, + PURL: "a-purl-2", + CPEs: []pkg.CPE{ + must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), + }, }) s, err := source.NewFromImage(img, source.AllLayersScope, "user-image-input") - d := distro.NewUnknownDistro() + var d *distro.Distro pres := NewPresenter(catalog, s.Metadata, d) // run presenter diff --git a/syft/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden b/syft/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden index 4876f5949..54988df9b 100644 --- a/syft/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden +++ b/syft/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden @@ -14,6 +14,10 @@ "MIT" ], "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", "metadataType": "PythonPackageMetadata", "metadata": { "name": "package-1", @@ -37,6 +41,10 @@ ], "licenses": null, "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", "metadataType": "DpkgMetadata", "metadata": { "package": "package-2", diff --git a/syft/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden b/syft/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden index 81ba778fd..f73f6796b 100644 --- a/syft/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden +++ b/syft/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden @@ -15,6 +15,10 @@ "MIT" ], "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + ], + "purl": "a-purl-1", "metadataType": "PythonPackageMetadata", "metadata": { "name": "package-1", @@ -39,6 +43,10 @@ ], "licenses": null, "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", "metadataType": "DpkgMetadata", "metadata": { "package": "package-2", diff --git a/syft/presenter/presenter.go b/syft/presenter/presenter.go index 6c32b60d0..8bf3e46fb 100644 --- a/syft/presenter/presenter.go +++ b/syft/presenter/presenter.go @@ -25,7 +25,7 @@ type Presenter interface { } // GetPresenter returns a presenter for images or directories -func GetPresenter(option Option, srcMetadata source.Metadata, catalog *pkg.Catalog, d distro.Distro) Presenter { +func GetPresenter(option Option, srcMetadata source.Metadata, catalog *pkg.Catalog, d *distro.Distro) Presenter { switch option { case JSONPresenter: return json.NewPresenter(catalog, srcMetadata, d) @@ -34,7 +34,7 @@ func GetPresenter(option Option, srcMetadata source.Metadata, catalog *pkg.Catal case TablePresenter: return table.NewPresenter(catalog) case CycloneDxPresenter: - return cyclonedx.NewPresenter(catalog, srcMetadata, d) + return cyclonedx.NewPresenter(catalog, srcMetadata) default: return nil } diff --git a/test/integration/distro_test.go b/test/integration/distro_test.go index e6a9ce297..126a1d7bd 100644 --- a/test/integration/distro_test.go +++ b/test/integration/distro_test.go @@ -26,7 +26,7 @@ func TestDistroImage(t *testing.T) { t.Fatalf("could not create distro: %+v", err) } - for _, d := range deep.Equal(actualDistro, expected) { + for _, d := range deep.Equal(actualDistro, &expected) { t.Errorf("found distro difference: %+v", d) } diff --git a/test/integration/json_schema_test.go b/test/integration/json_schema_test.go index 07b1a32cb..4b0ee830d 100644 --- a/test/integration/json_schema_test.go +++ b/test/integration/json_schema_test.go @@ -67,7 +67,7 @@ func testJsonSchema(t *testing.T, catalog *pkg.Catalog, theScope source.Source, t.Fatalf("bad distro: %+v", err) } - p := presenter.GetPresenter(presenter.JSONPresenter, theScope.Metadata, catalog, d) + p := presenter.GetPresenter(presenter.JSONPresenter, theScope.Metadata, catalog, &d) if p == nil { t.Fatal("unable to get presenter") }