mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 23:16:30 +01:00
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:
@@ -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{}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user