Skip to content

Commit

Permalink
port to generic cataloger and add relationship to original file
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
  • Loading branch information
wagoodman committed Nov 14, 2022
1 parent 336fa11 commit e407663
Show file tree
Hide file tree
Showing 5 changed files with 3,573 additions and 30 deletions.
3 changes: 3 additions & 0 deletions syft/artifact/relationship.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const (

// DependencyOfRelationship is a proxy for the SPDX 2.2.1 DEPENDENCY_OF relationship.
DependencyOfRelationship RelationshipType = "dependency-of"

// DescribedByRelationship is a proxy for the SPDX 2.2.2 DESCRIBED_BY relationship.
DescribedByRelationship RelationshipType = "described-by"
)

type RelationshipType string
Expand Down
5 changes: 5 additions & 0 deletions syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ func (p *CatalogTester) IgnoreLocationLayer() *CatalogTester {
return p
}

func (p *CatalogTester) IgnorePackageFields(fields ...string) *CatalogTester {
p.compareOptions = append(p.compareOptions, cmpopts.IgnoreFields(pkg.Package{}, fields...))
return p
}

func (p *CatalogTester) Expects(pkgs []pkg.Package, relationships []artifact.Relationship) *CatalogTester {
p.expectedPkgs = pkgs
p.expectedRelationships = relationships
Expand Down
64 changes: 34 additions & 30 deletions syft/pkg/cataloger/sbom/cataloger.go
Original file line number Diff line number Diff line change
@@ -1,53 +1,57 @@
package sbom

import (
"bytes"
"fmt"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/cyclonedxxml"
"github.com/anchore/syft/syft/formats/spdx22json"
"github.com/anchore/syft/syft/formats/spdx22tagvalue"
"github.com/anchore/syft/syft/formats/syftjson"
"io"

"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)

const catalogerName = "sbom-cataloger"

// NewSBOMCataloger returns a new SBOM cataloger object loaded from saved SBOM JSON.
func NewSBOMCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/*.syft.json": makeParser(syftjson.Format()),
"**/bom.json": makeParser(cyclonedxjson.Format()),
"**/bom.xml": makeParser(cyclonedxxml.Format()),
"**/*.cdx.json": makeParser(cyclonedxjson.Format()),
"**/*.cdx.xml": makeParser(cyclonedxxml.Format()),
"**/*.spdx.json": makeParser(spdx22json.Format()),
"**/*.spdx": makeParser(spdx22tagvalue.Format()),
}
return common.NewGenericCataloger(nil, globParsers, "sbom-cataloger")
}
func NewSBOMCataloger() *generic.Cataloger {

func makeParser(format sbom.Format) func(string, io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
return func(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
by, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("unable to read sbom: %w", err)
}
return generic.NewCataloger(catalogerName).
WithParserByGlobs(makeParser(syftjson.Format()), "**/*.syft.json").
WithParserByGlobs(makeParser(cyclonedxjson.Format()), "**/bom.json", "**/*.cdx.json").
WithParserByGlobs(makeParser(cyclonedxxml.Format()), "**/bom.xml", "**/*.cdx.xml").
WithParserByGlobs(makeParser(spdx22json.Format()), "**/*.spdx.json").
WithParserByGlobs(makeParser(spdx22tagvalue.Format()), "**/*.spdx", "**/*.spdx.tv")

s, err := format.Decode(bytes.NewReader(by))
}

func makeParser(format sbom.Format) generic.Parser {
return func(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
s, err := format.Decode(reader)
if err != nil {
return nil, nil, fmt.Errorf("unable to decode sbom: %w", err)
return nil, nil, err
}

var packages []*pkg.Package
var pkgs []pkg.Package
var relationships []artifact.Relationship
for _, p := range s.Artifacts.PackageCatalog.Sorted() {
x := p // copy
packages = append(packages, &x)
// replace all locations on the pacakge with the location of the SBOM file.
// Why not keep the original list of locations? Since the "locations" field is meant to capture
// where there is evidence of this file, and the catalogers have not run against any file other than,
// the SBOM, this is the only location that is relevant for this cataloger.
p.Locations = source.NewLocationSet(reader.Location)
p.FoundBy = catalogerName

pkgs = append(pkgs, p)
relationships = append(relationships, artifact.Relationship{
From: p,
To: reader.Location.Coordinates,
Type: artifact.DescribedByRelationship,
})
}

return packages, nil, nil
return pkgs, relationships, nil
}
}
289 changes: 289 additions & 0 deletions syft/pkg/cataloger/sbom/cataloger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
package sbom

import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/require"
"testing"
)

func mustCPEs(s ...string) (c []pkg.CPE) {
for _, i := range s {
c = append(c, mustCPE(i))
}
return
}

func mustCPE(c string) pkg.CPE {
return must(pkg.NewCPE(c))
}
func must(c pkg.CPE, e error) pkg.CPE {
if e != nil {
panic(e)
}
return c
}

func Test_parseSBOM(t *testing.T) {

expectedPkgs := []pkg.Package{
{
Name: "alpine-baselayout",
Version: "3.2.0-r23",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"GPL-2.0-only"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/alpine-baselayout@3.2.0-r23?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:alpine-baselayout:alpine-baselayout:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine-baselayout:alpine_baselayout:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine_baselayout:alpine-baselayout:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine_baselayout:alpine_baselayout:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine:alpine-baselayout:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine:alpine_baselayout:3.2.0-r23:*:*:*:*:*:*:*",
),
},
{
Name: "alpine-baselayout-data",
Version: "3.2.0-r23",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"GPL-2.0-only"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/alpine-baselayout-data@3.2.0-r23?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:alpine-baselayout-data:alpine-baselayout-data:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine-baselayout-data:alpine_baselayout_data:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine_baselayout_data:alpine-baselayout-data:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine_baselayout_data:alpine_baselayout_data:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine-baselayout:alpine-baselayout-data:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine-baselayout:alpine_baselayout_data:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine_baselayout:alpine-baselayout-data:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine_baselayout:alpine_baselayout_data:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine:alpine-baselayout-data:3.2.0-r23:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine:alpine_baselayout_data:3.2.0-r23:*:*:*:*:*:*:*",
),
},
{
Name: "alpine-keys",
Version: "2.4-r1",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"MIT"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/alpine-keys@2.4-r1?arch=x86_64&upstream=alpine-keys&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:alpine-keys:alpine-keys:2.4-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine-keys:alpine_keys:2.4-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine_keys:alpine-keys:2.4-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine_keys:alpine_keys:2.4-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine:alpine-keys:2.4-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine:alpine_keys:2.4-r1:*:*:*:*:*:*:*",
),
},
{
Name: "apk-tools",
Version: "2.12.9-r3",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"GPL-2.0-only"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/apk-tools@2.12.9-r3?arch=x86_64&upstream=apk-tools&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:apk-tools:apk-tools:2.12.9-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:apk-tools:apk_tools:2.12.9-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:apk_tools:apk-tools:2.12.9-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:apk_tools:apk_tools:2.12.9-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:apk:apk-tools:2.12.9-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:apk:apk_tools:2.12.9-r3:*:*:*:*:*:*:*",
),
},
{
Name: "busybox",
Version: "1.35.0-r17",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"GPL-2.0-only"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/busybox@1.35.0-r17?arch=x86_64&upstream=busybox&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:busybox:busybox:1.35.0-r17:*:*:*:*:*:*:*",
),
},
{
Name: "ca-certificates-bundle",
Version: "20220614-r0",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"MPL-2.0", "AND", "MIT"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/ca-certificates-bundle@20220614-r0?arch=x86_64&upstream=ca-certificates&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:ca-certificates-bundle:ca-certificates-bundle:20220614-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:ca-certificates-bundle:ca_certificates_bundle:20220614-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:ca_certificates_bundle:ca-certificates-bundle:20220614-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:ca_certificates_bundle:ca_certificates_bundle:20220614-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:ca-certificates:ca-certificates-bundle:20220614-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:ca-certificates:ca_certificates_bundle:20220614-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:ca_certificates:ca-certificates-bundle:20220614-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:ca_certificates:ca_certificates_bundle:20220614-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:ca:ca-certificates-bundle:20220614-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:ca:ca_certificates_bundle:20220614-r0:*:*:*:*:*:*:*",
),
},
{
Name: "libc-utils",
Version: "0.7.2-r3",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"BSD-2-Clause", "AND", "BSD-3-Clause"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/libc-utils@0.7.2-r3?arch=x86_64&upstream=libc-dev&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:libc-utils:libc-utils:0.7.2-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:libc-utils:libc_utils:0.7.2-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:libc_utils:libc-utils:0.7.2-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:libc_utils:libc_utils:0.7.2-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:libc:libc-utils:0.7.2-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:libc:libc_utils:0.7.2-r3:*:*:*:*:*:*:*",
),
},
{
Name: "libcrypto1.1",
Version: "1.1.1s-r0",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"OpenSSL"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/libcrypto1.1@1.1.1s-r0?arch=x86_64&upstream=openssl&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:libcrypto1.1:libcrypto1.1:1.1.1s-r0:*:*:*:*:*:*:*",
),
},
{
Name: "libssl1.1",
Version: "1.1.1s-r0",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"OpenSSL"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/libssl1.1@1.1.1s-r0?arch=x86_64&upstream=openssl&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:libssl1.1:libssl1.1:1.1.1s-r0:*:*:*:*:*:*:*",
),
},
{
Name: "musl",
Version: "1.2.3-r1",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"MIT"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/musl@1.2.3-r1?arch=x86_64&upstream=musl&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:musl:musl:1.2.3-r1:*:*:*:*:*:*:*",
),
},
{
Name: "musl-utils",
Version: "1.2.3-r1",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"MIT", "BSD", "GPL2+"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/musl-utils@1.2.3-r1?arch=x86_64&upstream=musl&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:musl-utils:musl-utils:1.2.3-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:musl-utils:musl_utils:1.2.3-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:musl_utils:musl-utils:1.2.3-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:musl_utils:musl_utils:1.2.3-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:musl:musl-utils:1.2.3-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:musl:musl_utils:1.2.3-r1:*:*:*:*:*:*:*",
),
},
{
Name: "scanelf",
Version: "1.3.4-r0",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"GPL-2.0-only"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/scanelf@1.3.4-r0?arch=x86_64&upstream=pax-utils&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:scanelf:scanelf:1.3.4-r0:*:*:*:*:*:*:*",
),
},
{
Name: "ssl_client",
Version: "1.35.0-r17",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"GPL-2.0-only"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/ssl_client@1.35.0-r17?arch=x86_64&upstream=busybox&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:ssl-client:ssl-client:1.35.0-r17:*:*:*:*:*:*:*",
"cpe:2.3:a:ssl-client:ssl_client:1.35.0-r17:*:*:*:*:*:*:*",
"cpe:2.3:a:ssl_client:ssl-client:1.35.0-r17:*:*:*:*:*:*:*",
"cpe:2.3:a:ssl_client:ssl_client:1.35.0-r17:*:*:*:*:*:*:*",
"cpe:2.3:a:ssl:ssl-client:1.35.0-r17:*:*:*:*:*:*:*",
"cpe:2.3:a:ssl:ssl_client:1.35.0-r17:*:*:*:*:*:*:*",
),
},
{
Name: "zlib",
Version: "1.2.12-r3",
Type: "apk",
Locations: source.NewLocationSet(source.NewLocation("sbom.syft.json")),
Licenses: []string{"Zlib"},
FoundBy: "sbom-cataloger",
PURL: "pkg:alpine/zlib@1.2.12-r3?arch=x86_64&upstream=zlib&distro=alpine-3.16.3",
CPEs: mustCPEs(
"cpe:2.3:a:zlib:zlib:1.2.12-r3:*:*:*:*:*:*:*",
),
},
}

var expectedRelationships []artifact.Relationship

for _, p := range expectedPkgs {
expectedRelationships = append(expectedRelationships, artifact.Relationship{
From: p,
To: source.Coordinates{
RealPath: "sbom.syft.json",
},
Type: artifact.DescribedByRelationship,
})
}

tests := []struct {
name string
format sbom.Format
fixture string
wantPkgs []pkg.Package
wantRelationships []artifact.Relationship
wantErr require.ErrorAssertionFunc
}{
{
name: "parse syft JSON",
format: syftjson.Format(),
fixture: "test-fixtures/alpine/syft-json",
wantPkgs: expectedPkgs,
wantRelationships: expectedRelationships,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pkgtest.NewCatalogTester().
FromDirectory(t, tt.fixture).
IgnorePackageFields("Metadata", "MetadataType").
Expects(tt.wantPkgs, tt.wantRelationships).
TestCataloger(t, NewSBOMCataloger())
})
}
}
Loading

0 comments on commit e407663

Please sign in to comment.