Remove MetadataType from core package object and normalize JSON metadataType values (#1983)

* [wip]

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* distinct the package metadata functions

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* remove metadata type from package core model

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* incorporate review feedback for names

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add RPM archive metadata and split parser helpers

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* clarify the python package metadata type

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rename the KB metadata type

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* break hackage and composer types by use case

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* linting fix

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix encoding and decoding for syft-json and cyclonedx

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* bump json schema to 11

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update cyclonedx-json snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update cyclonedx-xml snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update spdx-json snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update spdx-tv snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update syft-json snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* correct metadata type in stack yaml parser test

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix bom-ref redactor for cyclonedx-xml

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add tests for legacy package metadata names

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* regenerate json schema v11

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix legacy HackageMetadataType reflect type value check

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix linting

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* packagemetadata discovery should account for type shadowing

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix linting

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix cli tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* bump json schema version to v12

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update json schema to incorporate changes from main

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add syft-json legacy config option

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add tests around v11-v12 json decoding

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add docs for SYFT_JSON_LEGACY

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rename structs to be compliant with new naming scheme

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2023-10-30 12:12:04 -04:00
committed by GitHub
parent f442586ec9
commit 1aaa644007
261 changed files with 7010 additions and 4040 deletions
-11
View File
@@ -18,18 +18,7 @@ var _ sbom.FormatDecoder = (*decoder)(nil)
type decoder struct{}
type DecoderConfig struct {
}
func DefaultDecoderConfig() DecoderConfig {
return DecoderConfig{}
}
func NewFormatDecoder() sbom.FormatDecoder {
return NewFormatDecoderWithConfig(DefaultDecoderConfig())
}
func NewFormatDecoderWithConfig(DecoderConfig) sbom.FormatDecoder {
return decoder{}
}
+65 -37
View File
@@ -23,46 +23,75 @@ import (
)
func Test_EncodeDecodeCycle(t *testing.T) {
testImage := "image-simple"
originalSBOM := testutil.ImageInput(t, testImage)
enc := NewFormatEncoder()
dec := NewFormatDecoder()
var buf bytes.Buffer
assert.NoError(t, enc.Encode(&buf, originalSBOM))
actualSBOM, decodedID, decodedVersion, err := dec.Decode(bytes.NewReader(buf.Bytes()))
assert.NoError(t, err)
assert.Equal(t, ID, decodedID)
assert.Equal(t, internal.JSONSchemaVersion, decodedVersion)
for _, d := range deep.Equal(originalSBOM.Source, actualSBOM.Source) {
if strings.HasSuffix(d, "<nil slice> != []") {
// semantically the same
continue
}
t.Errorf("metadata difference: %+v", d)
table := []struct {
name string
fixtureImage string
cfg EncoderConfig
}{
{
name: "go case",
fixtureImage: "image-simple",
cfg: DefaultEncoderConfig(),
},
{
name: "default alpine",
fixtureImage: "image-alpine",
cfg: DefaultEncoderConfig(),
},
{
name: "legacy alpine",
fixtureImage: "image-alpine",
cfg: EncoderConfig{
Legacy: true,
},
},
}
actualPackages := actualSBOM.Artifacts.Packages.Sorted()
for idx, p := range originalSBOM.Artifacts.Packages.Sorted() {
if !assert.Equal(t, p.Name, actualPackages[idx].Name) {
t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name)
continue
}
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
originalSBOM := testutil.ImageInput(t, tt.fixtureImage)
for _, d := range deep.Equal(p, actualPackages[idx]) {
if strings.Contains(d, ".VirtualPath: ") {
// location.Virtual path is not exposed in the json output
continue
enc, err := NewFormatEncoderWithConfig(tt.cfg)
require.NoError(t, err)
dec := NewFormatDecoder()
var buf bytes.Buffer
assert.NoError(t, enc.Encode(&buf, originalSBOM))
actualSBOM, decodedID, decodedVersion, err := dec.Decode(bytes.NewReader(buf.Bytes()))
assert.NoError(t, err)
assert.Equal(t, ID, decodedID)
assert.Equal(t, internal.JSONSchemaVersion, decodedVersion)
for _, d := range deep.Equal(originalSBOM.Source, actualSBOM.Source) {
if strings.HasSuffix(d, "<nil slice> != []") {
// semantically the same
continue
}
t.Errorf("metadata difference: %+v", d)
}
if strings.HasSuffix(d, "<nil slice> != []") {
// semantically the same
continue
actualPackages := actualSBOM.Artifacts.Packages.Sorted()
for idx, p := range originalSBOM.Artifacts.Packages.Sorted() {
if !assert.Equal(t, p.Name, actualPackages[idx].Name) {
t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name)
continue
}
for _, d := range deep.Equal(p, actualPackages[idx]) {
if strings.Contains(d, ".VirtualPath: ") {
// location.Virtual path is not exposed in the json output
continue
}
if strings.HasSuffix(d, "<nil slice> != []") {
// semantically the same
continue
}
t.Errorf("%q package difference (%s): %+v", tt.fixtureImage, p.Name, d)
}
}
t.Errorf("package difference (%s): %+v", p.Name, d)
}
})
}
}
@@ -137,9 +166,8 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
Update: "update",
},
},
PURL: "pkg:generic/pkg@version",
MetadataType: "",
Metadata: nil,
PURL: "pkg:generic/pkg@version",
Metadata: nil,
}
p.SetID()
+23 -2
View File
@@ -12,11 +12,32 @@ var _ sbom.FormatEncoder = (*encoder)(nil)
const ID sbom.FormatID = "syft-json"
type EncoderConfig struct {
Legacy bool // transform the output to the legacy syft-json format (pre v1.0 changes, enumerated in the README.md)
}
type encoder struct {
cfg EncoderConfig
}
func NewFormatEncoder() sbom.FormatEncoder {
return encoder{}
enc, err := NewFormatEncoderWithConfig(DefaultEncoderConfig())
if err != nil {
panic(err)
}
return enc
}
func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
return encoder{
cfg: cfg,
}, nil
}
func DefaultEncoderConfig() EncoderConfig {
return EncoderConfig{
Legacy: false,
}
}
func (e encoder) ID() sbom.FormatID {
@@ -35,7 +56,7 @@ func (e encoder) Version() string {
}
func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
doc := ToFormatModel(s)
doc := ToFormatModel(s, e.cfg)
enc := json.NewEncoder(writer)
// prevent > and < from being escaped in the payload
+8 -10
View File
@@ -74,12 +74,11 @@ func TestEncodeFullJSONDocument(t *testing.T) {
RealPath: "/a/place/a",
}),
),
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
Metadata: pkg.PythonPackageMetadata{
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
Metadata: pkg.PythonPackage{
Name: "package-1",
Version: "1.0.1",
Files: []pkg.PythonFileRecord{},
@@ -98,10 +97,9 @@ func TestEncodeFullJSONDocument(t *testing.T) {
RealPath: "/b/place/b",
}),
),
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
Metadata: pkg.DpkgDBEntry{
Package: "package-2",
Version: "2.0.1",
Files: []pkg.DpkgFileRecord{},
+60 -26
View File
@@ -5,9 +5,11 @@ import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/packagemetadata"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
@@ -60,28 +62,28 @@ func newModelLicensesFromValues(licenses []string) (ml []License) {
}
func (f *licenses) UnmarshalJSON(b []byte) error {
var licenses []License
if err := json.Unmarshal(b, &licenses); err != nil {
var lics []License
if err := json.Unmarshal(b, &lics); err != nil {
var simpleLicense []string
if err := json.Unmarshal(b, &simpleLicense); err != nil {
return fmt.Errorf("unable to unmarshal license: %w", err)
}
licenses = newModelLicensesFromValues(simpleLicense)
lics = newModelLicensesFromValues(simpleLicense)
}
*f = licenses
*f = lics
return nil
}
// PackageCustomData contains ambiguous values (type-wise) from pkg.Package.
type PackageCustomData struct {
MetadataType pkg.MetadataType `json:"metadataType,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
MetadataType string `json:"metadataType,omitempty"`
Metadata any `json:"metadata,omitempty"`
}
// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
type packageMetadataUnpacker struct {
MetadataType pkg.MetadataType `json:"metadataType"`
Metadata json.RawMessage `json:"metadata"`
MetadataType string `json:"metadataType"`
Metadata json.RawMessage `json:"metadata"`
}
func (p *packageMetadataUnpacker) String() string {
@@ -112,32 +114,64 @@ func (p *Package) UnmarshalJSON(b []byte) error {
}
func unpackPkgMetadata(p *Package, unpacker packageMetadataUnpacker) error {
p.MetadataType = pkg.CleanMetadataType(unpacker.MetadataType)
typ, ok := pkg.MetadataTypeByName[p.MetadataType]
if ok {
val := reflect.New(typ).Interface()
if len(unpacker.Metadata) > 0 {
if err := json.Unmarshal(unpacker.Metadata, val); err != nil {
return err
}
}
p.Metadata = reflect.ValueOf(val).Elem().Interface()
if unpacker.MetadataType == "" {
return nil
}
// capture unknown metadata as a generic struct
if len(unpacker.Metadata) > 0 {
var val interface{}
if err := json.Unmarshal(unpacker.Metadata, &val); err != nil {
return err
// check for legacy correction cases from schema v11 -> v12
ty := unpacker.MetadataType
switch unpacker.MetadataType {
case "HackageMetadataType":
for _, l := range p.Locations {
if strings.HasSuffix(l.RealPath, ".yaml.lock") {
ty = "haskell-hackage-stack-lock-entry"
break
} else if strings.HasSuffix(l.RealPath, ".yaml") {
ty = "haskell-hackage-stack-entry"
break
}
}
case "RpmMetadata":
for _, l := range p.Locations {
if strings.HasSuffix(l.RealPath, ".rpm") {
ty = "rpm-archive"
break
}
}
case "RustCargoPackageMetadata":
var found bool
for _, l := range p.Locations {
if strings.HasSuffix(strings.ToLower(l.RealPath), "cargo.lock") {
ty = "rust-cargo-lock-entry"
found = true
break
}
}
if !found {
ty = "rust-cargo-audit-entry"
}
p.Metadata = val
}
if p.MetadataType != "" {
typ := packagemetadata.ReflectTypeFromJSONName(ty)
if typ == nil {
// capture unknown metadata as a generic struct
if len(unpacker.Metadata) > 0 {
var val interface{}
if err := json.Unmarshal(unpacker.Metadata, &val); err != nil {
return err
}
p.Metadata = val
}
return errUnknownMetadataType
}
val := reflect.New(typ).Interface()
if len(unpacker.Metadata) > 0 {
if err := json.Unmarshal(unpacker.Metadata, val); err != nil {
return err
}
}
p.Metadata = reflect.ValueOf(val).Elem().Interface()
return nil
}
+239 -34
View File
@@ -12,7 +12,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func TestUnmarshalPackageGolang(t *testing.T) {
func Test_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
packageData []byte
@@ -50,9 +50,9 @@ func TestUnmarshalPackageGolang(t *testing.T) {
}
}`),
assert: func(p *Package) {
assert.NotNil(t, p.Metadata)
golangMetadata := p.Metadata.(pkg.GolangBinMetadata)
assert.NotEmpty(t, golangMetadata)
require.NotNil(t, p.Metadata)
golangMetadata := p.Metadata.(pkg.GolangBinaryBuildinfoEntry)
require.NotEmpty(t, golangMetadata)
assert.Equal(t, "go1.18", golangMetadata.GoCompiledVersion)
},
},
@@ -169,6 +169,223 @@ func TestUnmarshalPackageGolang(t *testing.T) {
}, p.Licenses)
},
},
{
name: "breaking v11-v12 schema change: rpm db vs archive (select db)",
packageData: []byte(`{
"id": "739158935bfffc4d",
"name": "dbus",
"version": "1:1.12.8-12.el8",
"type": "rpm",
"foundBy": "rpm-db-cataloger",
"locations": [
{
"path": "/var/lib/rpm/Packages",
"layerID": "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a",
"annotations": {
"evidence": "primary"
}
}
],
"licenses": [],
"language": "",
"cpes": [],
"purl": "pkg:rpm/centos/dbus@1.12.8-12.el8?arch=aarch64&epoch=1&upstream=dbus-1.12.8-12.el8.src.rpm&distro=centos-8",
"metadataType": "RpmMetadata",
"metadata": {
"name": "dbus",
"version": "1.12.8",
"epoch": 1,
"architecture": "aarch64",
"release": "12.el8",
"sourceRpm": "dbus-1.12.8-12.el8.src.rpm",
"size": 0,
"vendor": "CentOS",
"modularityLabel": "",
"files": []
}
}
`),
assert: func(p *Package) {
assert.Equal(t, pkg.RpmPkg, p.Type)
assert.Equal(t, reflect.TypeOf(pkg.RpmDBEntry{}).Name(), reflect.TypeOf(p.Metadata).Name())
},
},
{
name: "breaking v11-v12 schema change: rpm db vs archive (select archive)",
packageData: []byte(`{
"id": "739158935bfffc4d",
"name": "dbus",
"version": "1:1.12.8-12.el8",
"type": "rpm",
"foundBy": "rpm-db-cataloger",
"locations": [
{
"path": "/var/cache/dbus-1.12.8-12.el8.rpm",
"layerID": "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a",
"annotations": {
"evidence": "primary"
}
}
],
"licenses": [],
"language": "",
"cpes": [],
"purl": "pkg:rpm/centos/dbus@1.12.8-12.el8?arch=aarch64&epoch=1&upstream=dbus-1.12.8-12.el8.src.rpm&distro=centos-8",
"metadataType": "RpmMetadata",
"metadata": {
"name": "dbus",
"version": "1.12.8",
"epoch": 1,
"architecture": "aarch64",
"release": "12.el8",
"sourceRpm": "dbus-1.12.8-12.el8.src.rpm",
"size": 0,
"vendor": "CentOS",
"modularityLabel": "",
"files": []
}
}
`),
assert: func(p *Package) {
assert.Equal(t, pkg.RpmPkg, p.Type)
assert.Equal(t, reflect.TypeOf(pkg.RpmArchive{}).Name(), reflect.TypeOf(p.Metadata).Name())
},
},
{
name: "breaking v11-v12 schema change: stack.yaml vs stack.yaml.lock (select stack.yaml)",
packageData: []byte(`{
"id": "46ff1a71f7715f38",
"name": "hspec-discover",
"version": "2.9.4",
"type": "hackage",
"foundBy": "haskell-cataloger",
"locations": [
{
"path": "/stack.yaml",
"annotations": {
"evidence": "primary"
}
}
],
"licenses": [],
"language": "haskell",
"cpes": [
"cpe:2.3:a:hspec-discover:hspec-discover:2.9.4:*:*:*:*:*:*:*",
"cpe:2.3:a:hspec-discover:hspec_discover:2.9.4:*:*:*:*:*:*:*",
"cpe:2.3:a:hspec_discover:hspec-discover:2.9.4:*:*:*:*:*:*:*",
"cpe:2.3:a:hspec_discover:hspec_discover:2.9.4:*:*:*:*:*:*:*",
"cpe:2.3:a:hspec:hspec-discover:2.9.4:*:*:*:*:*:*:*",
"cpe:2.3:a:hspec:hspec_discover:2.9.4:*:*:*:*:*:*:*"
],
"purl": "pkg:hackage/hspec-discover@2.9.4",
"metadataType": "HackageMetadataType",
"metadata": {
"name": "",
"version": "",
"pkgHash": "fbcf49ecfc3d4da53e797fd0275264cba776ffa324ee223e2a3f4ec2d2c9c4a6"
}
}`),
assert: func(p *Package) {
assert.Equal(t, pkg.HackagePkg, p.Type)
assert.Equal(t, reflect.TypeOf(pkg.HackageStackYamlEntry{}).Name(), reflect.TypeOf(p.Metadata).Name())
},
},
{
name: "breaking v11-v12 schema change: stack.yaml vs stack.yaml.lock (select stack.yaml.lock)",
packageData: []byte(`{
"id": "87939e95124ceb92",
"name": "optparse-applicative",
"version": "0.16.1.0",
"type": "hackage",
"foundBy": "haskell-cataloger",
"locations": [
{
"path": "/stack.yaml.lock",
"annotations": {
"evidence": "primary"
}
}
],
"licenses": [],
"language": "haskell",
"cpes": [
"cpe:2.3:a:optparse-applicative:optparse-applicative:0.16.1.0:*:*:*:*:*:*:*",
"cpe:2.3:a:optparse-applicative:optparse_applicative:0.16.1.0:*:*:*:*:*:*:*",
"cpe:2.3:a:optparse_applicative:optparse-applicative:0.16.1.0:*:*:*:*:*:*:*",
"cpe:2.3:a:optparse_applicative:optparse_applicative:0.16.1.0:*:*:*:*:*:*:*",
"cpe:2.3:a:optparse:optparse-applicative:0.16.1.0:*:*:*:*:*:*:*",
"cpe:2.3:a:optparse:optparse_applicative:0.16.1.0:*:*:*:*:*:*:*"
],
"purl": "pkg:hackage/optparse-applicative@0.16.1.0",
"metadataType": "HackageMetadataType",
"metadata": {
"name": "",
"version": "",
"pkgHash": "418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731",
"snapshotURL": "https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/14.yaml"
}
}`),
assert: func(p *Package) {
assert.Equal(t, pkg.HackagePkg, p.Type)
assert.Equal(t, reflect.TypeOf(pkg.HackageStackYamlLockEntry{}).Name(), reflect.TypeOf(p.Metadata).Name())
},
},
{
name: "breaking v11-v12 schema change: rust cargo.lock vs audit (select cargo.lock)",
packageData: []byte(`{
"id": "95124ceb9287939e",
"name": "optpkg",
"version": "1.16.1",
"type": "hackage",
"foundBy": "rust-cataloger",
"locations": [
{
"path": "/cargo.lock",
"annotations": {
"evidence": "primary"
}
}
],
"licenses": [],
"language": "rust",
"cpes": [],
"purl": "pkg:cargo/optpkg@1.16.1",
"metadataType": "RustCargoPackageMetadata",
"metadata": {}
}`),
assert: func(p *Package) {
assert.Equal(t, pkg.HackagePkg, p.Type)
assert.Equal(t, reflect.TypeOf(pkg.RustCargoLockEntry{}).Name(), reflect.TypeOf(p.Metadata).Name())
},
},
{
name: "breaking v11-v12 schema change: rust cargo.lock vs audit (select audit binary)",
packageData: []byte(`{
"id": "95124ceb9287939e",
"name": "optpkg",
"version": "1.16.1",
"type": "hackage",
"foundBy": "rust-cataloger",
"locations": [
{
"path": "/my-binary",
"annotations": {
"evidence": "primary"
}
}
],
"licenses": [],
"language": "rust",
"cpes": [],
"purl": "pkg:cargo/optpkg@1.16.1",
"metadataType": "RustCargoPackageMetadata",
"metadata": {}
}`),
assert: func(p *Package) {
assert.Equal(t, pkg.HackagePkg, p.Type)
assert.Equal(t, reflect.TypeOf(pkg.RustBinaryAuditEntry{}).Name(), reflect.TypeOf(p.Metadata).Name())
},
},
}
for _, test := range tests {
@@ -185,13 +402,12 @@ func Test_unpackMetadata(t *testing.T) {
tests := []struct {
name string
packageData []byte
metadataType pkg.MetadataType
wantMetadata interface{}
wantMetadata any
wantErr require.ErrorAssertionFunc
}{
{
name: "unmarshal package metadata",
metadataType: pkg.GolangBinMetadataType,
wantMetadata: pkg.GolangBinaryBuildinfoEntry{},
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
@@ -217,7 +433,7 @@ func Test_unpackMetadata(t *testing.T) {
},
{
name: "can handle package without metadata",
metadataType: "",
wantMetadata: nil,
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
@@ -237,7 +453,7 @@ func Test_unpackMetadata(t *testing.T) {
},
{
name: "can handle RpmdbMetadata",
metadataType: pkg.RpmMetadataType,
wantMetadata: pkg.RpmDBEntry{},
packageData: []byte(`{
"id": "4ac699c3b8fe1835",
"name": "acl",
@@ -272,9 +488,8 @@ func Test_unpackMetadata(t *testing.T) {
}`),
},
{
name: "bad metadata type is an error",
metadataType: "BOGOSITY",
wantErr: require.Error,
name: "bad metadata type is an error",
wantErr: require.Error,
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
@@ -301,8 +516,7 @@ func Test_unpackMetadata(t *testing.T) {
"thing": "thing-1"
}
}`),
wantErr: require.Error,
metadataType: "NewMetadataType",
wantErr: require.Error,
wantMetadata: map[string]interface{}{
"thing": "thing-1",
},
@@ -312,39 +526,31 @@ func Test_unpackMetadata(t *testing.T) {
packageData: []byte(`{
"metadataType": "GolangBinMetadata"
}`),
metadataType: pkg.GolangBinMetadataType,
wantMetadata: pkg.GolangBinMetadata{},
wantMetadata: pkg.GolangBinaryBuildinfoEntry{},
},
{
name: "can handle package with golang bin metadata type",
packageData: []byte(`{
"metadataType": "GolangBinMetadata"
}`),
metadataType: pkg.GolangBinMetadataType,
wantMetadata: pkg.GolangBinMetadata{},
wantMetadata: pkg.GolangBinaryBuildinfoEntry{},
},
{
name: "can handle package with unknonwn metadata type and missing metadata",
name: "can handle package with unknown metadata type and missing metadata",
packageData: []byte(`{
"metadataType": "BadMetadata"
}`),
wantErr: require.Error,
metadataType: "BadMetadata",
wantMetadata: nil,
wantErr: require.Error,
},
{
name: "can handle package with unknonwn metadata type and metadata",
name: "can handle package with unknown metadata type and metadata",
packageData: []byte(`{
"metadataType": "BadMetadata",
"metadata": {
"random": "thing"
}
}`),
wantErr: require.Error,
metadataType: "BadMetadata",
wantMetadata: map[string]interface{}{
"random": "thing",
},
wantErr: require.Error,
},
}
@@ -355,19 +561,18 @@ func Test_unpackMetadata(t *testing.T) {
}
p := &Package{}
var basic PackageBasicData
require.NoError(t, json.Unmarshal(test.packageData, &basic))
p.PackageBasicData = basic
var unpacker packageMetadataUnpacker
require.NoError(t, json.Unmarshal(test.packageData, &unpacker))
err := unpackPkgMetadata(p, unpacker)
assert.Equal(t, test.metadataType, p.MetadataType)
test.wantErr(t, err)
if test.wantMetadata != nil {
assert.True(t, reflect.DeepEqual(test.wantMetadata, p.Metadata))
if p.Metadata == nil {
t.Fatalf("expected metadata to be populated")
return
}
assert.Equal(t, reflect.TypeOf(test.wantMetadata).Name(), reflect.TypeOf(p.Metadata).Name())
}
})
}
@@ -0,0 +1 @@
FROM alpine:3.18.4@sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "9265397e5e15168a",
"id": "fb6bef15e281ea43",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -25,7 +25,7 @@
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "PythonPackageMetadata",
"metadataType": "python-package",
"metadata": {
"name": "package-1",
"version": "1.0.1",
@@ -41,7 +41,7 @@
}
},
{
"id": "ad5013466727018f",
"id": "39392bb5e270f669",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@@ -57,7 +57,7 @@
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "pkg:deb/debian/package-2@2.0.1",
"metadataType": "DpkgMetadata",
"metadataType": "dpkg-db-entry",
"metadata": {
"package": "package-2",
"source": "",
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "271e49ba46e0b601",
"id": "d748d4614750058d",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -25,7 +25,7 @@
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
],
"purl": "a-purl-1",
"metadataType": "PythonPackageMetadata",
"metadataType": "python-package",
"metadata": {
"name": "package-1",
"version": "1.0.1",
@@ -36,7 +36,7 @@
}
},
{
"id": "aa0ca2c331576dfd",
"id": "fa4ec37eccd65756",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@@ -52,7 +52,7 @@
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "DpkgMetadata",
"metadataType": "dpkg-db-entry",
"metadata": {
"package": "package-2",
"source": "",
@@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "125840abc1c66dd7",
"id": "80210ebcba92e632",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@@ -26,7 +26,7 @@
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
],
"purl": "a-purl-1",
"metadataType": "PythonPackageMetadata",
"metadataType": "python-package",
"metadata": {
"name": "package-1",
"version": "1.0.1",
@@ -37,7 +37,7 @@
}
},
{
"id": "f27313b22a5ba330",
"id": "4b756c6f6fb127a3",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@@ -54,7 +54,7 @@
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "pkg:deb/debian/package-2@2.0.1",
"metadataType": "DpkgMetadata",
"metadataType": "dpkg-db-entry",
"metadata": {
"package": "package-2",
"source": "",
+14 -6
View File
@@ -12,6 +12,7 @@ import (
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/syftjson/model"
"github.com/anchore/syft/syft/internal/packagemetadata"
"github.com/anchore/syft/syft/internal/sourcemetadata"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
@@ -20,9 +21,9 @@ import (
)
// ToFormatModel transforms the sbom import a format-specific model.
func ToFormatModel(s sbom.SBOM) model.Document {
func ToFormatModel(s sbom.SBOM, cfg EncoderConfig) model.Document {
return model.Document{
Artifacts: toPackageModels(s.Artifacts.Packages),
Artifacts: toPackageModels(s.Artifacts.Packages, cfg),
ArtifactRelationships: toRelationshipModel(s.Relationships),
Files: toFile(s),
Secrets: toSecrets(s.Artifacts.Secrets),
@@ -196,13 +197,13 @@ func toFileType(ty stereoscopeFile.Type) string {
}
}
func toPackageModels(catalog *pkg.Collection) []model.Package {
func toPackageModels(catalog *pkg.Collection, cfg EncoderConfig) []model.Package {
artifacts := make([]model.Package, 0)
if catalog == nil {
return artifacts
}
for _, p := range catalog.Sorted() {
artifacts = append(artifacts, toPackageModel(p))
artifacts = append(artifacts, toPackageModel(p, cfg))
}
return artifacts
}
@@ -233,7 +234,7 @@ func toLicenseModel(pkgLicenses []pkg.License) (modelLicenses []model.License) {
}
// toPackageModel crates a new Package from the given pkg.Package.
func toPackageModel(p pkg.Package) model.Package {
func toPackageModel(p pkg.Package, cfg EncoderConfig) model.Package {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = cpe.String(c)
@@ -246,6 +247,13 @@ func toPackageModel(p pkg.Package) model.Package {
licenses = toLicenseModel(p.Licenses.ToSlice())
}
var ty string
if cfg.Legacy {
ty = packagemetadata.JSONLegacyName(p.Metadata)
} else {
ty = packagemetadata.JSONName(p.Metadata)
}
return model.Package{
PackageBasicData: model.PackageBasicData{
ID: string(p.ID()),
@@ -260,7 +268,7 @@ func toPackageModel(p pkg.Package) model.Package {
PURL: p.PURL,
},
PackageCustomData: model.PackageCustomData{
MetadataType: p.MetadataType,
MetadataType: ty,
Metadata: p.Metadata,
},
}
@@ -4,6 +4,8 @@ import (
"encoding/json"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -11,6 +13,7 @@ import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/syftjson/model"
"github.com/anchore/syft/syft/internal/sourcemetadata"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
@@ -303,3 +306,48 @@ func Test_toFileMetadataEntry(t *testing.T) {
})
}
}
func Test_toPackageModel_metadataType(t *testing.T) {
tests := []struct {
name string
p pkg.Package
cfg EncoderConfig
want model.Package
}{
{
name: "empty config",
p: pkg.Package{
Metadata: pkg.RpmDBEntry{},
},
cfg: EncoderConfig{},
want: model.Package{
PackageCustomData: model.PackageCustomData{
MetadataType: "rpm-db-entry",
Metadata: pkg.RpmDBEntry{},
},
},
},
{
name: "legacy config",
p: pkg.Package{
Metadata: pkg.RpmDBEntry{},
},
cfg: EncoderConfig{
Legacy: true,
},
want: model.Package{
PackageCustomData: model.PackageCustomData{
MetadataType: "RpmMetadata",
Metadata: pkg.RpmDBEntry{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if d := cmp.Diff(tt.want, toPackageModel(tt.p, tt.cfg), cmpopts.EquateEmpty()); d != "" {
t.Errorf("unexpected package (-want +got):\n%s", d)
}
})
}
}
+10 -11
View File
@@ -311,17 +311,16 @@ func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
}
out := pkg.Package{
Name: p.Name,
Version: p.Version,
FoundBy: p.FoundBy,
Locations: file.NewLocationSet(p.Locations...),
Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...),
Language: p.Language,
Type: p.Type,
CPEs: cpes,
PURL: p.PURL,
MetadataType: p.MetadataType,
Metadata: p.Metadata,
Name: p.Name,
Version: p.Version,
FoundBy: p.FoundBy,
Locations: file.NewLocationSet(p.Locations...),
Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...),
Language: p.Language,
Type: p.Type,
CPEs: cpes,
PURL: p.PURL,
Metadata: p.Metadata,
}
// we don't know if this package ID is truly unique, however, we need to trust the user input in case there are