syft/syft/format/syftjson/model/package_test.go
William Murphy ce67927a98
Fix: unmarshal key values in Java, Go, and Conan metadata (#2603)
Previously, Syft represented several metadata fields as map[string]string,
however this representation erased ordering, so Syft now represents these values
as []KeyValue. Add custom unmarshaling so that JSON that was written by
older versions of Syft using the map[string]string representation can be parsed
into the new []KeyValue representation.

Signed-off-by: Will Murphy <will.murphy@anchore.com>
2024-02-07 11:26:23 -05:00

717 lines
19 KiB
Go

package model
import (
"encoding/json"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
func Test_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
packageData []byte
assert func(*Package)
}{
{
name: "unmarshal package metadata",
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
"version": "v2.6.0",
"type": "go-module",
"foundBy": "go-module-binary-cataloger",
"locations": [
{
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": []
}
],
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0",
"metadataType": "GolangBinMetadata",
"metadata": {
"goCompiledVersion": "go1.18",
"architecture": "amd64",
"h1Digest": "h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI="
}
}`),
assert: func(p *Package) {
require.NotNil(t, p.Metadata)
golangMetadata := p.Metadata.(pkg.GolangBinaryBuildinfoEntry)
require.NotEmpty(t, golangMetadata)
assert.Equal(t, "go1.18", golangMetadata.GoCompiledVersion)
},
},
{
name: "can handle package without metadata",
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
"version": "v2.6.0",
"type": "go-module",
"foundBy": "go-mod-cataloger",
"locations": [
{
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": ["https://www.github.com"]
},
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"locations": [{"path": "/Users/hal/go/bin/syft"}]
}
],
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
}`),
assert: func(p *Package) {
assert.Empty(t, p.MetadataType)
assert.Empty(t, p.Metadata)
},
},
{
name: "can handle package with []string licenses",
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
"version": "v2.6.0",
"type": "go-module",
"foundBy": "go-mod-cataloger",
"locations": [
{
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": ["MIT", "Apache-2.0"],
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
}`),
assert: func(p *Package) {
assert.Equal(t, licenses{
{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
},
{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: license.Declared,
},
}, p.Licenses)
},
},
{
name: "can handle package with []pkg.License licenses",
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
"version": "v2.6.0",
"type": "go-module",
"foundBy": "go-mod-cataloger",
"locations": [
{
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared"
},
{
"value": "Apache-2.0",
"spdxExpression": "Apache-2.0",
"type": "declared"
}
],
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
}`),
assert: func(p *Package) {
assert.Equal(t, licenses{
{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
},
{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: license.Declared,
},
}, 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())
},
},
{
name: "map-based java metadata",
packageData: []byte(`{
"id": "e6f845bdaa69ddb2",
"name": "SparseBitSet",
"version": "1.2",
"type": "java-archive",
"foundBy": "java-archive-cataloger",
"locations": [],
"licenses": [],
"language": "java",
"cpes": [],
"purl": "pkg:maven/com.zaxxer/SparseBitSet@1.2",
"metadataType": "java-archive",
"metadata": {
"virtualPath": "/opt/solr-9.4.1/modules/extraction/lib/SparseBitSet-1.2.jar",
"manifest": {
"main": {
"Archiver-Version": "Plexus Archiver",
"Build-Jdk": "1.8.0_73",
"Built-By": "lbayer",
"Created-By": "Apache Maven 3.5.0",
"Manifest-Version": "1.0"
},
"namedSections": {
"META-INF/mailcap": {
"SHA-256-Digest": "kXN4VupOQOJhduMGwxumj4ijmD/YAlz97a9Mp7CVXtk="
},
"META-INF/versions/9/module-info.class": {
"SHA-256-Digest": "cMeIRa5l8DWPgrVWavr/6TKVBUGixVKGcu6yOTZMlKk="
}
}
}
}
}`),
assert: func(p *Package) {
meta := p.Metadata.(pkg.JavaArchive)
manifest := meta.Manifest
assert.Equal(t, "1.8.0_73", manifest.Main.MustGet("Build-Jdk"))
require.Equal(t, 2, len(manifest.Sections))
assert.Equal(t, "META-INF/mailcap", manifest.Sections[0].MustGet("Name"))
assert.Equal(t, "kXN4VupOQOJhduMGwxumj4ijmD/YAlz97a9Mp7CVXtk=", manifest.Sections[0].MustGet("SHA-256-Digest"))
assert.Equal(t, "META-INF/versions/9/module-info.class", manifest.Sections[1].MustGet("Name"))
assert.Equal(t, "cMeIRa5l8DWPgrVWavr/6TKVBUGixVKGcu6yOTZMlKk=", manifest.Sections[1].MustGet("SHA-256-Digest"))
},
},
{
name: "pre key-value golang metadata",
packageData: []byte(`{
"id": "e348ed25484a94c9",
"name": "github.com/anchore/syft",
"version": "v0.101.1-SNAPSHOT-4c777834",
"type": "go-module",
"foundBy": "go-module-binary-cataloger",
"locations": [
{
"path": "/syft",
"layerID": "sha256:2274947a5f3527e48d8725a96646aefdcce3d99340c1eefb1e7c894043863c92",
"accessPath": "/syft",
"annotations": {
"evidence": "primary"
}
}
],
"licenses": [],
"language": "go",
"cpes": [
"cpe:2.3:a:anchore:syft:v0.101.1-SNAPSHOT-4c777834:*:*:*:*:*:*:*"
],
"purl": "pkg:golang/github.com/anchore/syft@v0.101.1-SNAPSHOT-4c777834",
"metadataType": "go-module-buildinfo-entry",
"metadata": {
"goBuildSettings": {
"-buildmode": "exe",
"-compiler": "gc",
"-ldflags": "-w -s -extldflags '-static' -X main.version=0.101.1-SNAPSHOT-4c777834 -X main.gitCommit=4c777834618b2ad8ad94cd200a45d6670bc1c013 -X main.buildDate=2024-01-22T16:43:49Z -X main.gitDescription=v0.101.1-4-g4c777834 ",
"CGO_ENABLED": "0",
"GOAMD64": "v1",
"GOARCH": "amd64",
"GOOS": "linux",
"vcs": "git",
"vcs.modified": "false",
"vcs.revision": "4c777834618b2ad8ad94cd200a45d6670bc1c013",
"vcs.time": "2024-01-22T16:31:41Z"
},
"goCompiledVersion": "go1.21.2",
"architecture": "amd64",
"mainModule": "github.com/anchore/syft"
}
}
`),
assert: func(p *Package) {
buildInfo := p.Metadata.(pkg.GolangBinaryBuildinfoEntry)
assert.Equal(t, "exe", buildInfo.BuildSettings.MustGet("-buildmode"))
},
},
{
name: "conan lock with legacy options",
packageData: []byte(`{
"id": "75eb35307226c921",
"name": "boost",
"version": "1.75.0",
"type": "conan",
"foundBy": "conan-cataloger",
"locations": [
{
"path": "/conan.lock",
"accessPath": "/conan.lock",
"annotations": {
"evidence": "primary"
}
}
],
"licenses": [],
"language": "c++",
"cpes": [
"cpe:2.3:a:boost:boost:1.75.0:*:*:*:*:*:*:*"
],
"purl": "pkg:conan/boost@1.75.0",
"metadataType": "c-conan-lock-entry",
"metadata": {
"ref": "boost/1.75.0#a9c318f067216f900900e044e7af4ab1",
"package_id": "dc8aedd23a0f0a773a5fcdcfe1ae3e89c4205978",
"prev": "b9d7912e6131dfa453c725593b36c808",
"options": {
"addr2line_location": "/usr/bin/addr2line",
"asio_no_deprecated": "False",
"zstd": "False"
},
"context": "host"
}
}`),
assert: func(p *Package) {
metadata := p.Metadata.(pkg.ConanV1LockEntry)
assert.Equal(t, "False", metadata.Options.MustGet("asio_no_deprecated"))
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
p := &Package{}
err := p.UnmarshalJSON(test.packageData)
require.NoError(t, err)
test.assert(p)
})
}
}
func Test_unpackMetadata(t *testing.T) {
tests := []struct {
name string
packageData []byte
wantMetadata any
wantErr require.ErrorAssertionFunc
}{
{
name: "unmarshal package metadata",
wantMetadata: pkg.GolangBinaryBuildinfoEntry{},
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
"version": "v2.6.0",
"type": "go-module",
"foundBy": "go-module-binary-cataloger",
"locations": [
{
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": [],
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0",
"metadataType": "GolangBinMetadata",
"metadata": {
"goCompiledVersion": "go1.18",
"architecture": "amd64",
"h1Digest": "h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI="
}
}`),
},
{
name: "can handle package without metadata",
wantMetadata: nil,
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
"version": "v2.6.0",
"type": "go-module",
"foundBy": "go-mod-cataloger",
"locations": [
{
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": [],
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
}`),
},
{
name: "can handle RpmdbMetadata",
wantMetadata: pkg.RpmDBEntry{},
packageData: []byte(`{
"id": "4ac699c3b8fe1835",
"name": "acl",
"version": "2.2.53-1.el8",
"type": "rpm",
"foundBy": "rpm-db-cataloger",
"locations": [
{
"path": "/var/lib/rpm/Packages",
"layerID": "sha256:74ddd0ec08fa43d09f32636ba91a0a3053b02cb4627c35051aff89f853606b59"
}
],
"language": "",
"cpes": [
"cpe:2.3:a:centos:acl:2.2.53-1.el8:*:*:*:*:*:*:*",
"cpe:2.3:a:acl:acl:2.2.53-1.el8:*:*:*:*:*:*:*"
],
"purl": "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
"metadataType": "RpmdbMetadata",
"metadata": {
"name": "acl",
"version": "2.2.53",
"epoch": null,
"architecture": "x86_64",
"release": "1.el8",
"sourceRpm": "acl-2.2.53-1.el8.src.rpm",
"size": 205740,
"license": "GPLv2+",
"vendor": "CentOS",
"modularityLabel": ""
}
}`),
},
{
name: "bad metadata type is an error",
wantErr: require.Error,
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
"version": "v2.6.0",
"type": "go-module",
"foundBy": "go-mod-cataloger",
"locations": [
{
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": [],
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0",
"metadataType": "BOGOSITY"
}`),
},
{
name: "unknown metadata type",
packageData: []byte(`{
"metadataType": "NewMetadataType",
"metadata": {
"thing": "thing-1"
}
}`),
wantErr: require.Error,
wantMetadata: map[string]interface{}{
"thing": "thing-1",
},
},
{
name: "can handle package with metadata type but missing metadata",
packageData: []byte(`{
"metadataType": "GolangBinMetadata"
}`),
wantMetadata: pkg.GolangBinaryBuildinfoEntry{},
},
{
name: "can handle package with golang bin metadata type",
packageData: []byte(`{
"metadataType": "GolangBinMetadata"
}`),
wantMetadata: pkg.GolangBinaryBuildinfoEntry{},
},
{
name: "can handle package with unknown metadata type and missing metadata",
packageData: []byte(`{
"metadataType": "BadMetadata"
}`),
wantErr: require.Error,
},
{
name: "can handle package with unknown metadata type and metadata",
packageData: []byte(`{
"metadataType": "BadMetadata",
"metadata": {
"random": "thing"
}
}`),
wantErr: require.Error,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.wantErr == nil {
test.wantErr = require.NoError
}
p := &Package{}
var unpacker packageMetadataUnpacker
require.NoError(t, json.Unmarshal(test.packageData, &unpacker))
err := unpackPkgMetadata(p, unpacker)
test.wantErr(t, err)
if test.wantMetadata != nil {
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())
}
})
}
}