Split the sbom.Format interface by encode and decode use cases (#2186)

* split up sbom.Format into encode and decode ops

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

* update cmd pkg to inject format configs

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

* bump cyclonedx schema to 1.5

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

* redact image metadata from github encoder tests

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

* add more testing around format decoder identify

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

* add test case for format version options

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

* fix cli tests

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

* fix CLI test

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

* [wip] - review comments

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

* keep encoder creation out of post load function

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

* keep decider and identify functions

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

* add a few more doc comments

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

* remove format encoder default function helpers

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

* address PR feedback

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

* move back to streaming based decode functions

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

* with common convention for encoder constructors

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

* fix tests and allow for encoders to be created from cli options

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

* fix cli tests

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

* fix linting

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

* buffer reads from stdin to support seeking

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-25 13:43:06 +00:00
committed by GitHub
parent 7315f83f9d
commit 7392d607b6
254 changed files with 10450 additions and 2714 deletions
+113
View File
@@ -0,0 +1,113 @@
package syftjson
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/Masterminds/semver"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/format/syftjson/model"
"github.com/anchore/syft/syft/sbom"
)
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{}
}
func (d decoder) Decode(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, string, error) {
if reader == nil {
return nil, "", "", fmt.Errorf("no SBOM bytes provided")
}
id, version := d.Identify(reader)
if version == "" || id != ID {
return nil, "", "", fmt.Errorf("not a syft-json document")
}
var doc model.Document
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return nil, "", "", fmt.Errorf("unable to seek to start of Syft JSON SBOM: %+v", err)
}
dec := json.NewDecoder(reader)
err := dec.Decode(&doc)
if err != nil {
return nil, "", "", fmt.Errorf("unable to decode syft-json document: %w", err)
}
if err := checkSupportedSchema(doc.Schema.Version, internal.JSONSchemaVersion); err != nil {
log.Warn(err)
}
return toSyftModel(doc), ID, doc.Schema.Version, nil
}
func (d decoder) Identify(reader io.ReadSeeker) (sbom.FormatID, string) {
if reader == nil {
return "", ""
}
if _, err := reader.Seek(0, io.SeekStart); err != nil {
log.Debugf("unable to seek to start of Syft JSON SBOM: %+v", err)
return "", ""
}
type Document struct {
Schema model.Schema `json:"schema"`
}
dec := json.NewDecoder(reader)
var doc Document
err := dec.Decode(&doc)
if err != nil {
// maybe not json? maybe not valid? doesn't matter, we won't process it.
return "", ""
}
if !strings.Contains(doc.Schema.URL, "anchore/syft") {
// not a syft-json document
return "", ""
}
// note: we support all previous schema versions
return ID, doc.Schema.Version
}
func checkSupportedSchema(documentVersion string, parserVersion string) error {
documentV, err := semver.NewVersion(documentVersion)
if err != nil {
return fmt.Errorf("error comparing document schema version with parser schema version: %w", err)
}
parserV, err := semver.NewVersion(parserVersion)
if err != nil {
return fmt.Errorf("error comparing document schema version with parser schema version: %w", err)
}
if documentV.GreaterThan(parserV) {
return fmt.Errorf("document has schema version %s, but parser has older schema version (%s)", documentVersion, parserVersion)
}
return nil
}
+249
View File
@@ -0,0 +1,249 @@
package syftjson
import (
"bytes"
"errors"
"fmt"
"strings"
"testing"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal/testutil"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
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)
}
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("package difference (%s): %+v", p.Name, d)
}
}
}
func TestOutOfDateParser(t *testing.T) {
tests := []struct {
name string
documentVersion string
parserVersion string
want error
}{{
name: "no warning when doc version is older",
documentVersion: "1.0.9",
parserVersion: "3.1.0",
}, {
name: "warning when parser is older",
documentVersion: "4.3.2",
parserVersion: "3.1.0",
want: fmt.Errorf("document has schema version %s, but parser has older schema version (%s)", "4.3.2", "3.1.0"),
}, {
name: "warning when document version is unparseable",
documentVersion: "some-nonsense",
parserVersion: "3.1.0",
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("Invalid Semantic Version")),
}, {
name: "warning when parser version is unparseable",
documentVersion: "7.1.0",
parserVersion: "some-nonsense",
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("Invalid Semantic Version")),
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := checkSupportedSchema(tt.documentVersion, tt.parserVersion)
assert.Equal(t, tt.want, got)
})
}
}
func Test_encodeDecodeFileMetadata(t *testing.T) {
p := pkg.Package{
Name: "pkg",
Version: "version",
FoundBy: "something",
Locations: file.NewLocationSet(file.Location{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
RealPath: "/somewhere",
FileSystemID: "id",
},
},
LocationMetadata: file.LocationMetadata{
Annotations: map[string]string{
"key": "value",
},
},
}),
Licenses: pkg.NewLicenseSet(pkg.License{
Value: "MIT",
SPDXExpression: "MIT",
Type: "MIT",
URLs: []string{"https://example.org/license"},
Locations: file.LocationSet{},
}),
Language: "language",
Type: "type",
CPEs: []cpe.CPE{
{
Part: "a",
Vendor: "vendor",
Product: "product",
Version: "version",
Update: "update",
},
},
PURL: "pkg:generic/pkg@version",
MetadataType: "",
Metadata: nil,
}
p.SetID()
c := file.Coordinates{
RealPath: "some-file",
FileSystemID: "some-fs-id",
}
s := sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(p),
FileMetadata: map[file.Coordinates]file.Metadata{
c: {
FileInfo: stereoscopeFile.ManualInfo{
NameValue: c.RealPath,
ModeValue: 0644,
SizeValue: 7,
},
Path: c.RealPath,
Type: stereoscopeFile.TypeRegular,
UserID: 1,
GroupID: 2,
MIMEType: "text/plain",
},
},
FileDigests: map[file.Coordinates][]file.Digest{
c: {
{
Algorithm: "sha1",
Value: "d34db33f",
},
},
},
FileContents: map[file.Coordinates]string{
c: "some contents",
},
FileLicenses: map[file.Coordinates][]file.License{
c: {
{
Value: "MIT",
SPDXExpression: "MIT",
Type: "MIT",
LicenseEvidence: &file.LicenseEvidence{
Confidence: 1,
Offset: 2,
Extent: 3,
},
},
},
},
LinuxDistribution: &linux.Release{
PrettyName: "some os",
Name: "os",
ID: "os-id",
IDLike: []string{"os"},
Version: "version",
VersionID: "version",
VersionCodename: "codename",
BuildID: "build-id",
ImageID: "image-id",
ImageVersion: "image-version",
Variant: "variant",
VariantID: "variant-id",
HomeURL: "https://example.org/os",
SupportURL: "https://example.org/os/support",
BugReportURL: "https://example.org/os/bugs",
PrivacyPolicyURL: "https://example.org/os/privacy",
CPEName: "os-cpe",
SupportEnd: "now",
},
},
Relationships: nil,
Source: source.Description{
ID: "some-id",
Name: "some-name",
Version: "some-version",
Metadata: source.FileSourceMetadata{
Path: "/some-file-source-path",
Digests: []file.Digest{
{
Algorithm: "sha1",
Value: "d34db33f",
},
},
MIMEType: "file/zip",
},
},
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "this-version",
},
}
buf := &bytes.Buffer{}
enc := NewFormatEncoder()
err := enc.Encode(buf, s)
require.NoError(t, err)
dec := NewFormatDecoder()
got, decodedID, decodedVersion, err := dec.Decode(bytes.NewReader(buf.Bytes()))
require.NoError(t, err)
assert.Equal(t, ID, decodedID)
assert.Equal(t, internal.JSONSchemaVersion, decodedVersion)
require.Equal(t, s, *got)
}
+46
View File
@@ -0,0 +1,46 @@
package syftjson
import (
"encoding/json"
"io"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/sbom"
)
var _ sbom.FormatEncoder = (*encoder)(nil)
const ID sbom.FormatID = "syft-json"
type encoder struct {
}
func NewFormatEncoder() sbom.FormatEncoder {
return encoder{}
}
func (e encoder) ID() sbom.FormatID {
return ID
}
func (e encoder) Aliases() []string {
return []string{
"json",
"syft",
}
}
func (e encoder) Version() string {
return internal.JSONSchemaVersion
}
func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
doc := ToFormatModel(s)
enc := json.NewEncoder(writer)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}
+256
View File
@@ -0,0 +1,256 @@
package syftjson
import (
"flag"
"testing"
stereoFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal/testutil"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
var updateSnapshot = flag.Bool("update-json", false, "update the *.golden files for json encoders")
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
func TestDefaultNameAndVersion(t *testing.T) {
expectedID, expectedVersion := ID, internal.JSONSchemaVersion
enc := NewFormatEncoder()
if enc.ID() != expectedID {
t.Errorf("expected ID %q, got %q", expectedID, enc.ID())
}
if enc.Version() != expectedVersion {
t.Errorf("expected version %q, got %q", expectedVersion, enc.Version())
}
}
func TestDirectoryEncoder(t *testing.T) {
dir := t.TempDir()
testutil.AssertEncoderAgainstGoldenSnapshot(t,
testutil.EncoderSnapshotTestConfig{
Subject: testutil.DirectoryInput(t, dir),
Format: NewFormatEncoder(),
UpdateSnapshot: *updateSnapshot,
PersistRedactionsInSnapshot: true,
IsJSON: true,
Redactor: redactor(dir),
},
)
}
func TestImageEncoder(t *testing.T) {
testImage := "image-simple"
testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
testutil.ImageSnapshotTestConfig{
Image: testImage,
UpdateImageSnapshot: *updateImage,
},
testutil.EncoderSnapshotTestConfig{
Subject: testutil.ImageInput(t, testImage, testutil.FromSnapshot()),
Format: NewFormatEncoder(),
UpdateSnapshot: *updateSnapshot,
PersistRedactionsInSnapshot: true,
IsJSON: true,
Redactor: redactor(),
},
)
}
func TestEncodeFullJSONDocument(t *testing.T) {
catalog := pkg.NewCollection()
p1 := pkg.Package{
Name: "package-1",
Version: "1.0.1",
Locations: file.NewLocationSet(
file.NewLocationFromCoordinates(file.Coordinates{
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{
Name: "package-1",
Version: "1.0.1",
Files: []pkg.PythonFileRecord{},
},
PURL: "a-purl-1",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
},
}
p2 := pkg.Package{
Name: "package-2",
Version: "2.0.1",
Locations: file.NewLocationSet(
file.NewLocationFromCoordinates(file.Coordinates{
RealPath: "/b/place/b",
}),
),
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
Package: "package-2",
Version: "2.0.1",
Files: []pkg.DpkgFileRecord{},
},
PURL: "a-purl-2",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
}
catalog.Add(p1)
catalog.Add(p2)
s := sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: catalog,
FileMetadata: map[file.Coordinates]file.Metadata{
file.NewLocation("/a/place").Coordinates: {
FileInfo: stereoFile.ManualInfo{
NameValue: "/a/place",
ModeValue: 0775,
},
Type: stereoFile.TypeDirectory,
UserID: 0,
GroupID: 0,
},
file.NewLocation("/a/place/a").Coordinates: {
FileInfo: stereoFile.ManualInfo{
NameValue: "/a/place/a",
ModeValue: 0775,
},
Type: stereoFile.TypeRegular,
UserID: 0,
GroupID: 0,
},
file.NewLocation("/b").Coordinates: {
FileInfo: stereoFile.ManualInfo{
NameValue: "/b",
ModeValue: 0775,
},
Type: stereoFile.TypeSymLink,
LinkDestination: "/c",
UserID: 0,
GroupID: 0,
},
file.NewLocation("/b/place/b").Coordinates: {
FileInfo: stereoFile.ManualInfo{
NameValue: "/b/place/b",
ModeValue: 0644,
},
Type: stereoFile.TypeRegular,
UserID: 1,
GroupID: 2,
},
},
FileDigests: map[file.Coordinates][]file.Digest{
file.NewLocation("/a/place/a").Coordinates: {
{
Algorithm: "sha256",
Value: "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
},
},
file.NewLocation("/b/place/b").Coordinates: {
{
Algorithm: "sha256",
Value: "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c",
},
},
},
FileContents: map[file.Coordinates]string{
file.NewLocation("/a/place/a").Coordinates: "the-contents",
},
LinuxDistribution: &linux.Release{
ID: "redhat",
Version: "7",
VersionID: "7",
IDLike: []string{
"rhel",
},
},
},
Relationships: []artifact.Relationship{
{
From: p1,
To: p2,
Type: artifact.OwnershipByFileOverlapRelationship,
Data: map[string]string{
"file": "path",
},
},
},
Source: source.Description{
ID: "c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "user-image-input",
ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Tags: []string{
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b",
},
Size: 38,
Layers: []source.StereoscopeLayerMetadata{
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b",
Size: 22,
},
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
Size: 16,
},
},
RawManifest: []byte("eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJh..."),
RawConfig: []byte("eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZp..."),
RepoDigests: []string{},
},
},
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "v0.42.0-bogus",
// the application configuration should be persisted here, however, we do not want to import
// the application configuration in this package (it's reserved only for ingestion by the cmd package)
Configuration: map[string]string{
"config-key": "config-value",
},
},
}
testutil.AssertEncoderAgainstGoldenSnapshot(t,
testutil.EncoderSnapshotTestConfig{
Subject: s,
Format: NewFormatEncoder(),
UpdateSnapshot: *updateSnapshot,
PersistRedactionsInSnapshot: true,
IsJSON: true,
Redactor: redactor(),
},
)
}
func redactor(values ...string) testutil.Redactor {
return testutil.NewRedactions().
WithValuesRedacted(values...).
WithPatternRedactors(
map[string]string{
// remove schema version (don't even show the key or value)
`,?\s*"schema":\s*\{[^}]*}`: "",
},
)
}
+25
View File
@@ -0,0 +1,25 @@
package model
// Document represents the syft cataloging findings as a JSON document
type Document struct {
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
ArtifactRelationships []Relationship `json:"artifactRelationships"`
Files []File `json:"files,omitempty"` // note: must have omitempty
Secrets []Secrets `json:"secrets,omitempty"` // note: must have omitempty
Source Source `json:"source"` // Source represents the original object that was cataloged
Distro LinuxRelease `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
}
// Descriptor describes what created the document as well as surrounding metadata
type Descriptor struct {
Name string `json:"name"`
Version string `json:"version"`
Configuration interface{} `json:"configuration,omitempty"`
}
type Schema struct {
Version string `json:"version"`
URL string `json:"url"`
}
+38
View File
@@ -0,0 +1,38 @@
package model
import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/license"
)
type File struct {
ID string `json:"id"`
Location file.Coordinates `json:"location"`
Metadata *FileMetadataEntry `json:"metadata,omitempty"`
Contents string `json:"contents,omitempty"`
Digests []file.Digest `json:"digests,omitempty"`
Licenses []FileLicense `json:"licenses,omitempty"`
}
type FileMetadataEntry struct {
Mode int `json:"mode"`
Type string `json:"type"`
LinkDestination string `json:"linkDestination,omitempty"`
UserID int `json:"userID"`
GroupID int `json:"groupID"`
MIMEType string `json:"mimeType"`
Size int64 `json:"size"`
}
type FileLicense struct {
Value string `json:"value"`
SPDXExpression string `json:"spdxExpression"`
Type license.Type `json:"type"`
Evidence *FileLicenseEvidence `json:"evidence,omitempty"`
}
type FileLicenseEvidence struct {
Confidence int `json:"confidence"`
Offset int `json:"offset"`
Extent int `json:"extent"`
}
@@ -0,0 +1,43 @@
package model
import (
"encoding/json"
)
type IDLikes []string
type LinuxRelease struct {
PrettyName string `json:"prettyName,omitempty"`
Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"`
IDLike IDLikes `json:"idLike,omitempty"`
Version string `json:"version,omitempty"`
VersionID string `json:"versionID,omitempty"`
VersionCodename string `json:"versionCodename,omitempty"`
BuildID string `json:"buildID,omitempty"`
ImageID string `json:"imageID,omitempty"`
ImageVersion string `json:"imageVersion,omitempty"`
Variant string `json:"variant,omitempty"`
VariantID string `json:"variantID,omitempty"`
HomeURL string `json:"homeURL,omitempty"`
SupportURL string `json:"supportURL,omitempty"`
BugReportURL string `json:"bugReportURL,omitempty"`
PrivacyPolicyURL string `json:"privacyPolicyURL,omitempty"`
CPEName string `json:"cpeName,omitempty"`
SupportEnd string `json:"supportEnd,omitempty"`
}
func (s *IDLikes) UnmarshalJSON(data []byte) error {
var str string
var strSlice []string
// we support unmarshalling from a single value to support syft json schema v2
if err := json.Unmarshal(data, &str); err == nil {
*s = []string{str}
} else if err := json.Unmarshal(data, &strSlice); err == nil {
*s = strSlice
} else {
return err
}
return nil
}
@@ -0,0 +1,47 @@
package model
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIDLikes_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
data interface{}
expected IDLikes
}{
{
name: "single string",
data: "well hello there!",
expected: IDLikes{
"well hello there!",
},
},
{
name: "multiple strings",
data: []string{
"well hello there!",
"...hello there, john!",
},
expected: IDLikes{
"well hello there!",
"...hello there, john!",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(&tt.data)
require.NoError(t, err)
var obj IDLikes
require.NoError(t, json.Unmarshal(data, &obj))
assert.Equal(t, tt.expected, obj)
})
}
}
+143
View File
@@ -0,0 +1,143 @@
package model
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
var errUnknownMetadataType = errors.New("unknown metadata type")
// Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling.
type Package struct {
PackageBasicData
PackageCustomData
}
// PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package.
type PackageBasicData struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"`
Locations []file.Location `json:"locations"`
Licenses licenses `json:"licenses"`
Language pkg.Language `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
}
type licenses []License
type License struct {
Value string `json:"value"`
SPDXExpression string `json:"spdxExpression"`
Type license.Type `json:"type"`
URLs []string `json:"urls"`
Locations []file.Location `json:"locations"`
}
func newModelLicensesFromValues(licenses []string) (ml []License) {
for _, v := range licenses {
expression, err := license.ParseExpression(v)
if err != nil {
log.Trace("could not find valid spdx expression for %s: %w", v, err)
}
ml = append(ml, License{
Value: v,
SPDXExpression: expression,
Type: license.Declared,
})
}
return ml
}
func (f *licenses) UnmarshalJSON(b []byte) error {
var licenses []License
if err := json.Unmarshal(b, &licenses); 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)
}
*f = licenses
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"`
}
// 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"`
}
func (p *packageMetadataUnpacker) String() string {
return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata))
}
// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types.
func (p *Package) UnmarshalJSON(b []byte) error {
var basic PackageBasicData
if err := json.Unmarshal(b, &basic); err != nil {
return err
}
p.PackageBasicData = basic
var unpacker packageMetadataUnpacker
if err := json.Unmarshal(b, &unpacker); err != nil {
log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err)
return err
}
err := unpackPkgMetadata(p, unpacker)
if errors.Is(err, errUnknownMetadataType) {
log.Warnf("unknown package metadata type=%q for packageID=%q", p.MetadataType, p.ID)
return nil
}
return err
}
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()
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
}
p.Metadata = val
}
if p.MetadataType != "" {
return errUnknownMetadataType
}
return nil
}
+374
View File
@@ -0,0 +1,374 @@
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 TestUnmarshalPackageGolang(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) {
assert.NotNil(t, p.Metadata)
golangMetadata := p.Metadata.(pkg.GolangBinMetadata)
assert.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)
},
},
}
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
metadataType pkg.MetadataType
wantMetadata interface{}
wantErr require.ErrorAssertionFunc
}{
{
name: "unmarshal package metadata",
metadataType: pkg.GolangBinMetadataType,
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",
metadataType: "",
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",
metadataType: pkg.RpmMetadataType,
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",
metadataType: "BOGOSITY",
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,
metadataType: "NewMetadataType",
wantMetadata: map[string]interface{}{
"thing": "thing-1",
},
},
{
name: "can handle package with metadata type but missing metadata",
packageData: []byte(`{
"metadataType": "GolangBinMetadata"
}`),
metadataType: pkg.GolangBinMetadataType,
wantMetadata: pkg.GolangBinMetadata{},
},
{
name: "can handle package with golang bin metadata type",
packageData: []byte(`{
"metadataType": "GolangBinMetadata"
}`),
metadataType: pkg.GolangBinMetadataType,
wantMetadata: pkg.GolangBinMetadata{},
},
{
name: "can handle package with unknonwn metadata type and missing metadata",
packageData: []byte(`{
"metadataType": "BadMetadata"
}`),
wantErr: require.Error,
metadataType: "BadMetadata",
wantMetadata: nil,
},
{
name: "can handle package with unknonwn metadata type and metadata",
packageData: []byte(`{
"metadataType": "BadMetadata",
"metadata": {
"random": "thing"
}
}`),
wantErr: require.Error,
metadataType: "BadMetadata",
wantMetadata: map[string]interface{}{
"random": "thing",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.wantErr == nil {
test.wantErr = require.NoError
}
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))
}
})
}
}
@@ -0,0 +1,8 @@
package model
type Relationship struct {
Parent string `json:"parent"`
Child string `json:"child"`
Type string `json:"type"`
Metadata interface{} `json:"metadata,omitempty"`
}
+10
View File
@@ -0,0 +1,10 @@
package model
import (
"github.com/anchore/syft/syft/file"
)
type Secrets struct {
Location file.Coordinates `json:"location"`
Secrets []file.SearchResult `json:"secrets"`
}
+116
View File
@@ -0,0 +1,116 @@
package model
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/anchore/syft/syft/internal/sourcemetadata"
"github.com/anchore/syft/syft/source"
)
// Source object represents the thing that was cataloged
type Source struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Metadata interface{} `json:"metadata"`
}
// sourceUnpacker is used to unmarshal Source objects
type sourceUnpacker struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Metadata json.RawMessage `json:"metadata"`
Target json.RawMessage `json:"target"` // pre-v9 schema support
}
// UnmarshalJSON populates a source object from JSON bytes.
func (s *Source) UnmarshalJSON(b []byte) error {
var unpacker sourceUnpacker
err := json.Unmarshal(b, &unpacker)
if err != nil {
return err
}
s.Name = unpacker.Name
s.Version = unpacker.Version
s.Type = unpacker.Type
s.ID = unpacker.ID
if len(unpacker.Target) > 0 {
s.Type = cleanPreSchemaV9MetadataType(s.Type)
s.Metadata, err = extractPreSchemaV9Metadata(s.Type, unpacker.Target)
if err != nil {
return fmt.Errorf("unable to extract pre-schema-v9 source metadata: %w", err)
}
return nil
}
return unpackSrcMetadata(s, unpacker)
}
func unpackSrcMetadata(s *Source, unpacker sourceUnpacker) error {
rt := sourcemetadata.ReflectTypeFromJSONName(s.Type)
if rt == nil {
return fmt.Errorf("unable to find source metadata type=%q", s.Type)
}
val := reflect.New(rt).Interface()
if len(unpacker.Metadata) > 0 {
if err := json.Unmarshal(unpacker.Metadata, val); err != nil {
return err
}
}
s.Metadata = reflect.ValueOf(val).Elem().Interface()
return nil
}
func cleanPreSchemaV9MetadataType(t string) string {
t = strings.ToLower(t)
if t == "dir" {
return "directory"
}
return t
}
func extractPreSchemaV9Metadata(t string, target []byte) (interface{}, error) {
switch t {
case "directory", "dir":
cleanTarget, err := strconv.Unquote(string(target))
if err != nil {
cleanTarget = string(target)
}
return source.DirectorySourceMetadata{
Path: cleanTarget,
}, nil
case "file":
cleanTarget, err := strconv.Unquote(string(target))
if err != nil {
cleanTarget = string(target)
}
return source.FileSourceMetadata{
Path: cleanTarget,
}, nil
case "image":
var payload source.StereoscopeImageSourceMetadata
if err := json.Unmarshal(target, &payload); err != nil {
return nil, err
}
return payload, nil
default:
return nil, fmt.Errorf("unsupported package metadata type: %+v", t)
}
}
+324
View File
@@ -0,0 +1,324 @@
package model
import (
"encoding/json"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/sourcemetadata"
"github.com/anchore/syft/syft/source"
)
func TestSource_UnmarshalJSON(t *testing.T) {
tracker := sourcemetadata.NewCompletionTester(t)
cases := []struct {
name string
input []byte
expected *Source
wantErr require.ErrorAssertionFunc
}{
{
name: "directory",
input: []byte(`{
"id": "foobar",
"type": "directory",
"metadata": {"path": "/var/lib/foo", "base":"/nope"}
}`),
expected: &Source{
ID: "foobar",
Type: "directory",
Metadata: source.DirectorySourceMetadata{
Path: "/var/lib/foo",
//Base: "/nope", // note: should be ignored entirely
},
},
},
{
name: "image",
input: []byte(`{
"id": "foobar",
"type": "image",
"metadata": {
"userInput": "alpine:3.10",
"imageID": "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
"manifestDigest": "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [],
"imageSize": 5576169,
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
"size": 5576169
}
],
"manifest": "ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ3MiwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6ZTdiMzAwYWVlOWY5YmYzNDMzZDMyYmM5MzA1YmZkZDIyMTgzYmViNTlkOTMzYjQ4ZDc3YWI1NmJhNTNhMTk3YSIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDI3OTgzMzgsCiAgICAgICAgICJkaWdlc3QiOiAic2hhMjU2OjM5NmMzMTgzNzExNmFjMjkwNDU4YWZjYjkyOGY2OGI2Y2MxYzdiZGQ2OTYzZmM3MmY1MmYzNjVhMmE4OWMxYjUiCiAgICAgIH0KICAgXQp9",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiSW1hZ2UiOiJzaGEyNTY6ZWIyMDgwYzQ1NWU5NGMyMmFlMzViM2FlZjllMDc4YzQ5MmEwMDc5NTQxMmUwMjZlNGQ2YjQxZWY2NGJjN2RkOCIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOm51bGx9LCJjb250YWluZXIiOiJmZGI3ZTgwZTMzMzllOGQwNTk5MjgyZTYwNmM5MDdhYTU4ODFlZTRjNjY4YTY4MTM2MTE5ZTZkZmFjNmNlM2E0IiwiY29udGFpbmVyX2NvbmZpZyI6eyJIb3N0bmFtZSI6ImZkYjdlODBlMzMzOSIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giLCItYyIsIiMobm9wKSAiLCJDTUQgW1wiL2Jpbi9zaFwiXSJdLCJJbWFnZSI6InNoYTI1NjplYjIwODBjNDU1ZTk0YzIyYWUzNWIzYWVmOWUwNzhjNDkyYTAwNzk1NDEyZTAyNmU0ZDZiNDFlZjY0YmM3ZGQ4IiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6e319LCJjcmVhdGVkIjoiMjAyMS0wNC0xNFQxOToyMDowNS4zMzgzOTc3NjFaIiwiZG9ja2VyX3ZlcnNpb24iOiIxOS4wMy4xMiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA0LTE0VDE5OjIwOjA0Ljk4NzIxOTEyNFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YzUzNzdlYWE5MjZiZjQxMmRkOGQ0YTA4YjBhMWYyMzk5Y2ZkNzA4NzQzNTMzYjBhYTAzYjUzZDE0Y2I0YmI0ZSBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjEtMDQtMTRUMTk6MjA6MDUuMzM4Mzk3NzYxWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcIi9iaW4vc2hcIl0iLCJlbXB0eV9sYXllciI6dHJ1ZX1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6OWZiM2FhMmY4YjgwMjNhNGJlYmJmOTJhYTU2N2NhZjg4ZTM4ZTk2OWFkYTlmMGFjMTI2NDNiMjg0NzM5MTYzNSJdfX0=",
"repoDigests": [
"index.docker.io/library/alpine@sha256:451eee8bedcb2f029756dc3e9d73bab0e7943c1ac55cff3a4861c52a0fdd3e98"
]
}
}`),
expected: &Source{
ID: "foobar",
Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "alpine:3.10",
ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Tags: []string{},
Size: 5576169,
Layers: []source.StereoscopeLayerMetadata{
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
Size: 5576169,
},
},
RawManifest: []byte(`{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1472,
"digest": "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2798338,
"digest": "sha256:396c31837116ac290458afcb928f68b6cc1c7bdd6963fc72f52f365a2a89c1b5"
}
]
}`),
RawConfig: []byte(`{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"],"Image":"sha256:eb2080c455e94c22ae35b3aef9e078c492a00795412e026e4d6b41ef64bc7dd8","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"fdb7e80e3339e8d0599282e606c907aa5881ee4c668a68136119e6dfac6ce3a4","container_config":{"Hostname":"fdb7e80e3339","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/bin/sh\"]"],"Image":"sha256:eb2080c455e94c22ae35b3aef9e078c492a00795412e026e4d6b41ef64bc7dd8","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2021-04-14T19:20:05.338397761Z","docker_version":"19.03.12","history":[{"created":"2021-04-14T19:20:04.987219124Z","created_by":"/bin/sh -c #(nop) ADD file:c5377eaa926bf412dd8d4a08b0a1f2399cfd708743533b0aa03b53d14cb4bb4e in / "},{"created":"2021-04-14T19:20:05.338397761Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635"]}}`),
RepoDigests: []string{
"index.docker." +
"io/library/alpine@sha256:451eee8bedcb2f029756dc3e9d73bab0e7943c1ac55cff3a4861c52a0fdd3e98",
},
},
},
},
{
name: "file",
input: []byte(`{
"id": "foobar",
"type": "file",
"metadata": {
"path": "/var/lib/foo/go.mod",
"mimeType": "text/plain",
"digests": [
{
"algorithm": "sha256",
"value": "e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a"
}
]
}
}`),
expected: &Source{
ID: "foobar",
Type: "file",
Metadata: source.FileSourceMetadata{
Path: "/var/lib/foo/go.mod",
Digests: []file.Digest{
{
Algorithm: "sha256",
Value: "e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
},
},
MIMEType: "text/plain",
},
},
},
{
name: "unknown source type",
input: []byte(`{
"id": "foobar",
"type": "unknown-thing",
"target":"/var/lib/foo"
}`),
expected: &Source{
ID: "foobar",
Type: "unknown-thing",
},
wantErr: require.Error,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
var src Source
err := json.Unmarshal(tt.input, &src)
tt.wantErr(t, err)
if diff := cmp.Diff(tt.expected, &src); diff != "" {
t.Errorf("unexpected result from Source unmarshaling (-want +got)\n%s", diff)
}
tracker.Tested(t, tt.expected.Metadata)
})
}
}
func TestSource_UnmarshalJSON_PreSchemaV9(t *testing.T) {
cases := []struct {
name string
input []byte
expectedSource *Source
errAssertion assert.ErrorAssertionFunc
}{
{
name: "abbreviated directory",
input: []byte(`{
"id": "foobar",
"type": "dir",
"target":"/var/lib/foo"
}`),
expectedSource: &Source{
ID: "foobar",
Type: "directory",
Metadata: source.DirectorySourceMetadata{
Path: "/var/lib/foo",
},
},
errAssertion: assert.NoError,
},
{
name: "directory",
input: []byte(`{
"id": "foobar",
"type": "directory",
"target":"/var/lib/foo"
}`),
expectedSource: &Source{
ID: "foobar",
Type: "directory",
Metadata: source.DirectorySourceMetadata{
Path: "/var/lib/foo",
},
},
errAssertion: assert.NoError,
},
{
name: "image",
input: []byte(`{
"id": "foobar",
"type": "image",
"target": {
"userInput": "alpine:3.10",
"imageID": "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
"manifestDigest": "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [],
"imageSize": 5576169,
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
"size": 5576169
}
],
"manifest": "ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ3MiwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6ZTdiMzAwYWVlOWY5YmYzNDMzZDMyYmM5MzA1YmZkZDIyMTgzYmViNTlkOTMzYjQ4ZDc3YWI1NmJhNTNhMTk3YSIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDI3OTgzMzgsCiAgICAgICAgICJkaWdlc3QiOiAic2hhMjU2OjM5NmMzMTgzNzExNmFjMjkwNDU4YWZjYjkyOGY2OGI2Y2MxYzdiZGQ2OTYzZmM3MmY1MmYzNjVhMmE4OWMxYjUiCiAgICAgIH0KICAgXQp9",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiSW1hZ2UiOiJzaGEyNTY6ZWIyMDgwYzQ1NWU5NGMyMmFlMzViM2FlZjllMDc4YzQ5MmEwMDc5NTQxMmUwMjZlNGQ2YjQxZWY2NGJjN2RkOCIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOm51bGx9LCJjb250YWluZXIiOiJmZGI3ZTgwZTMzMzllOGQwNTk5MjgyZTYwNmM5MDdhYTU4ODFlZTRjNjY4YTY4MTM2MTE5ZTZkZmFjNmNlM2E0IiwiY29udGFpbmVyX2NvbmZpZyI6eyJIb3N0bmFtZSI6ImZkYjdlODBlMzMzOSIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giLCItYyIsIiMobm9wKSAiLCJDTUQgW1wiL2Jpbi9zaFwiXSJdLCJJbWFnZSI6InNoYTI1NjplYjIwODBjNDU1ZTk0YzIyYWUzNWIzYWVmOWUwNzhjNDkyYTAwNzk1NDEyZTAyNmU0ZDZiNDFlZjY0YmM3ZGQ4IiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6e319LCJjcmVhdGVkIjoiMjAyMS0wNC0xNFQxOToyMDowNS4zMzgzOTc3NjFaIiwiZG9ja2VyX3ZlcnNpb24iOiIxOS4wMy4xMiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA0LTE0VDE5OjIwOjA0Ljk4NzIxOTEyNFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YzUzNzdlYWE5MjZiZjQxMmRkOGQ0YTA4YjBhMWYyMzk5Y2ZkNzA4NzQzNTMzYjBhYTAzYjUzZDE0Y2I0YmI0ZSBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjEtMDQtMTRUMTk6MjA6MDUuMzM4Mzk3NzYxWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcIi9iaW4vc2hcIl0iLCJlbXB0eV9sYXllciI6dHJ1ZX1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6OWZiM2FhMmY4YjgwMjNhNGJlYmJmOTJhYTU2N2NhZjg4ZTM4ZTk2OWFkYTlmMGFjMTI2NDNiMjg0NzM5MTYzNSJdfX0=",
"repoDigests": [
"index.docker.io/library/alpine@sha256:451eee8bedcb2f029756dc3e9d73bab0e7943c1ac55cff3a4861c52a0fdd3e98"
]
}
}`),
expectedSource: &Source{
ID: "foobar",
Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "alpine:3.10",
ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Tags: []string{},
Size: 5576169,
Layers: []source.StereoscopeLayerMetadata{
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
Size: 5576169,
},
},
RawManifest: []byte(`{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1472,
"digest": "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2798338,
"digest": "sha256:396c31837116ac290458afcb928f68b6cc1c7bdd6963fc72f52f365a2a89c1b5"
}
]
}`),
RawConfig: []byte(`{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"],"Image":"sha256:eb2080c455e94c22ae35b3aef9e078c492a00795412e026e4d6b41ef64bc7dd8","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"fdb7e80e3339e8d0599282e606c907aa5881ee4c668a68136119e6dfac6ce3a4","container_config":{"Hostname":"fdb7e80e3339","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/bin/sh\"]"],"Image":"sha256:eb2080c455e94c22ae35b3aef9e078c492a00795412e026e4d6b41ef64bc7dd8","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2021-04-14T19:20:05.338397761Z","docker_version":"19.03.12","history":[{"created":"2021-04-14T19:20:04.987219124Z","created_by":"/bin/sh -c #(nop) ADD file:c5377eaa926bf412dd8d4a08b0a1f2399cfd708743533b0aa03b53d14cb4bb4e in / "},{"created":"2021-04-14T19:20:05.338397761Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635"]}}`),
RepoDigests: []string{
"index.docker." +
"io/library/alpine@sha256:451eee8bedcb2f029756dc3e9d73bab0e7943c1ac55cff3a4861c52a0fdd3e98",
},
},
},
errAssertion: assert.NoError,
},
{
name: "file",
input: []byte(`{
"id": "foobar",
"type": "file",
"target":"/var/lib/foo/go.mod"
}`),
expectedSource: &Source{
ID: "foobar",
Type: "file",
Metadata: source.FileSourceMetadata{
Path: "/var/lib/foo/go.mod",
},
},
errAssertion: assert.NoError,
},
{
name: "unknown source type",
input: []byte(`{
"id": "foobar",
"type": "unknown-thing",
"target":"/var/lib/foo"
}`),
expectedSource: &Source{
ID: "foobar",
Type: "unknown-thing",
},
errAssertion: assert.Error,
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
src := new(Source)
err := json.Unmarshal(testCase.input, src)
testCase.errAssertion(t, err)
if diff := cmp.Diff(testCase.expectedSource, src); diff != "" {
t.Errorf("unexpected result from Source unmarshaling (-want +got)\n%s", diff)
}
})
}
}
@@ -0,0 +1,152 @@
{
"artifacts": [
{
"id": "2ff71a67fb024c86",
"name": "github.com/wagoodman/go-partybus",
"version": "v0.0.0-20230516145632-8ccac152c651",
"type": "go-module",
"foundBy": "go-mod-file-cataloger",
"locations": [
{
"path": "/go.mod",
"annotations": {
"evidence": "primary"
}
}
],
"licenses": [],
"language": "go",
"cpes": [
"cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
"cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*"
],
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651",
"metadataType": "GolangModMetadata",
"metadata": {}
}
],
"artifactRelationships": [
{
"parent": "0bc762d3cbc1598aacb33550d32b58cf8a6f6c4b639a8520f1a8987a6747c288",
"child": "2ff71a67fb024c86",
"type": "contains"
},
{
"parent": "2ff71a67fb024c86",
"child": "3fc5a8d3d86e9790",
"type": "evident-by"
}
],
"files": [
{
"id": "3fc5a8d3d86e9790",
"location": {
"path": "/go.mod"
}
}
],
"source": {
"id": "0bc762d3cbc1598aacb33550d32b58cf8a6f6c4b639a8520f1a8987a6747c288",
"name": "go.mod",
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd",
"type": "file",
"metadata": {
"path": ".tmp/go.mod",
"digests": [
{
"algorithm": "sha256",
"value": "dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
}
],
"mimeType": "text/plain"
}
},
"distro": {},
"descriptor": {
"name": "syft",
"version": "0.91.0",
"configuration": {
"catalogers": null,
"package": {
"cataloger": {
"enabled": true,
"scope": "Squashed"
},
"search-unindexed-archives": false,
"search-indexed-archives": true
},
"golang": {
"search-local-mod-cache-licenses": false,
"local-mod-cache-dir": "",
"search-remote-licenses": false,
"proxy": "",
"no-proxy": ""
},
"linux-kernel": {
"catalog-modules": true
},
"python": {
"guess-unpinned-requirements": false
},
"file-metadata": {
"cataloger": {
"enabled": false,
"scope": "Squashed"
},
"digests": [
"sha256"
]
},
"file-classification": {
"cataloger": {
"enabled": false,
"scope": "Squashed"
}
},
"file-contents": {
"cataloger": {
"enabled": false,
"scope": "Squashed"
},
"skip-files-above-size": 1048576,
"globs": null
},
"secrets": {
"cataloger": {
"enabled": false,
"scope": "AllLayers"
},
"additional-patterns": null,
"exclude-pattern-names": null,
"reveal-values": false,
"skip-files-above-size": 1048576
},
"registry": {
"insecure-skip-tls-verify": false,
"insecure-use-http": false,
"auth": null,
"ca-cert": ""
},
"exclude": [],
"platform": "",
"name": "",
"source": {
"name": "",
"version": "",
"file": {
"digests": [
"sha256"
]
}
},
"parallelism": 1,
"default-image-pull-source": "",
"base-path": "",
"exclude-binary-overlap-by-ownership": true
}
},
"schema": {
"version": "11.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-11.0.0.json"
}
}
@@ -0,0 +1,100 @@
{
"artifacts": [
{
"id": "9265397e5e15168a",
"name": "package-1",
"version": "1.0.1",
"type": "python",
"foundBy": "the-cataloger-1",
"locations": [
{
"path": "/some/path/pkg1"
}
],
"licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"urls": [],
"locations": []
}
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"author": "",
"authorEmail": "",
"platform": "",
"files": [
{
"path": "/some/path/pkg1/dependencies/foo"
}
],
"sitePackagesRootPath": ""
}
},
{
"id": "ad5013466727018f",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
"foundBy": "the-cataloger-2",
"locations": [
{
"path": "/some/path/pkg1"
}
],
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "pkg:deb/debian/package-2@2.0.1",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"sourceVersion": "",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": null
}
}
],
"artifactRelationships": [],
"source": {
"id": "d1563248892cd59af469f406eee907c76fa4f9041f5410d45b93aef903bc4216",
"name": "some/path",
"version": "",
"type": "directory",
"metadata": {
"path": "redacted/some/path"
}
},
"distro": {
"prettyName": "debian",
"name": "debian",
"id": "debian",
"idLike": [
"like!"
],
"version": "1.2.3",
"versionID": "1.2.3"
},
"descriptor": {
"name": "syft",
"version": "v0.42.0-bogus",
"configuration": {
"config-key": "config-value"
}
}
}
@@ -0,0 +1,198 @@
{
"artifacts": [
{
"id": "271e49ba46e0b601",
"name": "package-1",
"version": "1.0.1",
"type": "python",
"foundBy": "the-cataloger-1",
"locations": [
{
"path": "/a/place/a"
}
],
"licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"urls": [],
"locations": []
}
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
],
"purl": "a-purl-1",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"author": "",
"authorEmail": "",
"platform": "",
"sitePackagesRootPath": ""
}
},
{
"id": "aa0ca2c331576dfd",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
"foundBy": "the-cataloger-2",
"locations": [
{
"path": "/b/place/b"
}
],
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"sourceVersion": "",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": []
}
}
],
"artifactRelationships": [
{
"parent": "",
"child": "",
"type": "ownership-by-file-overlap",
"metadata": {
"file": "path"
}
}
],
"files": [
{
"id": "913b4592e2c2ebdf",
"location": {
"path": "/a/place"
},
"metadata": {
"mode": 775,
"type": "Directory",
"userID": 0,
"groupID": 0,
"mimeType": "",
"size": 0
}
},
{
"id": "0e7c88bd18e11b0b",
"location": {
"path": "/a/place/a"
},
"metadata": {
"mode": 775,
"type": "RegularFile",
"userID": 0,
"groupID": 0,
"mimeType": "",
"size": 0
},
"contents": "the-contents",
"digests": [
{
"algorithm": "sha256",
"value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703"
}
]
},
{
"id": "5c3dc6885f48b5a1",
"location": {
"path": "/b"
},
"metadata": {
"mode": 775,
"type": "SymbolicLink",
"linkDestination": "/c",
"userID": 0,
"groupID": 0,
"mimeType": "",
"size": 0
}
},
{
"id": "799d2f12da0bcec4",
"location": {
"path": "/b/place/b"
},
"metadata": {
"mode": 644,
"type": "RegularFile",
"userID": 1,
"groupID": 2,
"mimeType": "",
"size": 0
},
"digests": [
{
"algorithm": "sha256",
"value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c"
}
]
}
],
"source": {
"id": "c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
"name": "",
"version": "",
"type": "image",
"metadata": {
"userInput": "user-image-input",
"imageID": "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b"
],
"imageSize": 38,
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b",
"size": 22
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
"size": 16
}
],
"manifest": "ZXlKelkyaGxiV0ZXWlhKemFXOXVJam95TENKdFpXUnBZVlI1Y0dVaU9pSmguLi4=",
"config": "ZXlKaGNtTm9hWFJsWTNSMWNtVWlPaUpoYldRMk5DSXNJbU52Ym1acC4uLg==",
"repoDigests": [],
"architecture": "",
"os": ""
}
},
"distro": {
"id": "redhat",
"idLike": [
"rhel"
],
"version": "7",
"versionID": "7"
},
"descriptor": {
"name": "syft",
"version": "v0.42.0-bogus",
"configuration": {
"config-key": "config-value"
}
}
}
@@ -0,0 +1,121 @@
{
"artifacts": [
{
"id": "125840abc1c66dd7",
"name": "package-1",
"version": "1.0.1",
"type": "python",
"foundBy": "the-cataloger-1",
"locations": [
{
"path": "/somefile-1.txt",
"layerID": "sha256:100d5a55f9032faead28b7427fa3e650e4f0158f86ea89d06e1489df00cb8c6f"
}
],
"licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"urls": [],
"locations": []
}
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
],
"purl": "a-purl-1",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"author": "",
"authorEmail": "",
"platform": "",
"sitePackagesRootPath": ""
}
},
{
"id": "f27313b22a5ba330",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
"foundBy": "the-cataloger-2",
"locations": [
{
"path": "/somefile-2.txt",
"layerID": "sha256:000fb9200890d3a19138478b20023023c0dce1c54352007c2863716780f049eb"
}
],
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "pkg:deb/debian/package-2@2.0.1",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"sourceVersion": "",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": null
}
}
],
"artifactRelationships": [],
"source": {
"id": "34d40fdc6ca13e9a3fa18415db216b50bff047716fae7d95a225c09732fe83fb",
"name": "user-image-input",
"version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"type": "image",
"metadata": {
"userInput": "user-image-input",
"imageID": "sha256:bf783ea304a3f02b5c7d2ece521800f5e2182e65ed5bb5116f578e17d6e82be4",
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b"
],
"imageSize": 38,
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:100d5a55f9032faead28b7427fa3e650e4f0158f86ea89d06e1489df00cb8c6f",
"size": 22
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:000fb9200890d3a19138478b20023023c0dce1c54352007c2863716780f049eb",
"size": 16
}
],
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzIsImRpZ2VzdCI6InNoYTI1NjpiZjc4M2VhMzA0YTNmMDJiNWM3ZDJlY2U1MjE4MDBmNWUyMTgyZTY1ZWQ1YmI1MTE2ZjU3OGUxN2Q2ZTgyYmU0In0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjoxMDBkNWE1NWY5MDMyZmFlYWQyOGI3NDI3ZmEzZTY1MGU0ZjAxNThmODZlYTg5ZDA2ZTE0ODlkZjAwY2I4YzZmIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjAwMGZiOTIwMDg5MGQzYTE5MTM4NDc4YjIwMDIzMDIzYzBkY2UxYzU0MzUyMDA3YzI4NjM3MTY3ODBmMDQ5ZWIifV19",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjMtMDktMjhUMTI6MjM6MzUuNDAwNjcyODg1WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIzLTA5LTI4VDEyOjIzOjM1LjM5Mzk4NjUxWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMy0wOS0yOFQxMjoyMzozNS40MDA2NzI4ODVaIiwiY3JlYXRlZF9ieSI6IkFERCBmaWxlLTIudHh0IC9zb21lZmlsZS0yLnR4dCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1NjoxMDBkNWE1NWY5MDMyZmFlYWQyOGI3NDI3ZmEzZTY1MGU0ZjAxNThmODZlYTg5ZDA2ZTE0ODlkZjAwY2I4YzZmIiwic2hhMjU2OjAwMGZiOTIwMDg5MGQzYTE5MTM4NDc4YjIwMDIzMDIzYzBkY2UxYzU0MzUyMDA3YzI4NjM3MTY3ODBmMDQ5ZWIiXX19",
"repoDigests": [],
"architecture": "",
"os": ""
}
},
"distro": {
"prettyName": "debian",
"name": "debian",
"id": "debian",
"idLike": [
"like!"
],
"version": "1.2.3",
"versionID": "1.2.3"
},
"descriptor": {
"name": "syft",
"version": "v0.42.0-bogus",
"configuration": {
"config-key": "config-value"
}
}
}
+313
View File
@@ -0,0 +1,313 @@
package syftjson
import (
"fmt"
"sort"
"strconv"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"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/sourcemetadata"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
// ToFormatModel transforms the sbom import a format-specific model.
func ToFormatModel(s sbom.SBOM) model.Document {
return model.Document{
Artifacts: toPackageModels(s.Artifacts.Packages),
ArtifactRelationships: toRelationshipModel(s.Relationships),
Files: toFile(s),
Secrets: toSecrets(s.Artifacts.Secrets),
Source: toSourceModel(s.Source),
Distro: toLinuxReleaser(s.Artifacts.LinuxDistribution),
Descriptor: toDescriptor(s.Descriptor),
Schema: model.Schema{
Version: internal.JSONSchemaVersion,
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
},
}
}
func toLinuxReleaser(d *linux.Release) model.LinuxRelease {
if d == nil {
return model.LinuxRelease{}
}
return model.LinuxRelease{
PrettyName: d.PrettyName,
Name: d.Name,
ID: d.ID,
IDLike: d.IDLike,
Version: d.Version,
VersionID: d.VersionID,
VersionCodename: d.VersionCodename,
BuildID: d.BuildID,
ImageID: d.ImageID,
ImageVersion: d.ImageVersion,
Variant: d.Variant,
VariantID: d.VariantID,
HomeURL: d.HomeURL,
SupportURL: d.SupportURL,
BugReportURL: d.BugReportURL,
PrivacyPolicyURL: d.PrivacyPolicyURL,
CPEName: d.CPEName,
SupportEnd: d.SupportEnd,
}
}
func toDescriptor(d sbom.Descriptor) model.Descriptor {
return model.Descriptor{
Name: d.Name,
Version: d.Version,
Configuration: d.Configuration,
}
}
func toSecrets(data map[file.Coordinates][]file.SearchResult) []model.Secrets {
results := make([]model.Secrets, 0)
for coordinates, secrets := range data {
results = append(results, model.Secrets{
Location: coordinates,
Secrets: secrets,
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
return results[i].Location.RealPath < results[j].Location.RealPath
})
return results
}
func toFile(s sbom.SBOM) []model.File {
results := make([]model.File, 0)
artifacts := s.Artifacts
for _, coordinates := range s.AllCoordinates() {
var metadata *file.Metadata
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
metadata = &metadataForLocation
}
var digests []file.Digest
if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists {
digests = digestsForLocation
}
var contents string
if contentsForLocation, exists := artifacts.FileContents[coordinates]; exists {
contents = contentsForLocation
}
var licenses []model.FileLicense
for _, l := range artifacts.FileLicenses[coordinates] {
var evidence *model.FileLicenseEvidence
if e := l.LicenseEvidence; e != nil {
evidence = &model.FileLicenseEvidence{
Confidence: e.Confidence,
Offset: e.Offset,
Extent: e.Extent,
}
}
licenses = append(licenses, model.FileLicense{
Value: l.Value,
SPDXExpression: l.SPDXExpression,
Type: l.Type,
Evidence: evidence,
})
}
results = append(results, model.File{
ID: string(coordinates.ID()),
Location: coordinates,
Metadata: toFileMetadataEntry(coordinates, metadata),
Digests: digests,
Contents: contents,
Licenses: licenses,
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
return results[i].Location.RealPath < results[j].Location.RealPath
})
return results
}
func toFileMetadataEntry(coordinates file.Coordinates, metadata *file.Metadata) *model.FileMetadataEntry {
if metadata == nil {
return nil
}
var mode int
var size int64
if metadata != nil && metadata.FileInfo != nil {
var err error
mode, err = strconv.Atoi(fmt.Sprintf("%o", metadata.Mode()))
if err != nil {
log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coordinates, metadata.Mode, err)
mode = 0
}
size = metadata.Size()
}
return &model.FileMetadataEntry{
Mode: mode,
Type: toFileType(metadata.Type),
LinkDestination: metadata.LinkDestination,
UserID: metadata.UserID,
GroupID: metadata.GroupID,
MIMEType: metadata.MIMEType,
Size: size,
}
}
func toFileType(ty stereoscopeFile.Type) string {
switch ty {
case stereoscopeFile.TypeSymLink:
return "SymbolicLink"
case stereoscopeFile.TypeHardLink:
return "HardLink"
case stereoscopeFile.TypeDirectory:
return "Directory"
case stereoscopeFile.TypeSocket:
return "Socket"
case stereoscopeFile.TypeBlockDevice:
return "BlockDevice"
case stereoscopeFile.TypeCharacterDevice:
return "CharacterDevice"
case stereoscopeFile.TypeFIFO:
return "FIFONode"
case stereoscopeFile.TypeRegular:
return "RegularFile"
case stereoscopeFile.TypeIrregular:
return "IrregularFile"
default:
return "Unknown"
}
}
func toPackageModels(catalog *pkg.Collection) []model.Package {
artifacts := make([]model.Package, 0)
if catalog == nil {
return artifacts
}
for _, p := range catalog.Sorted() {
artifacts = append(artifacts, toPackageModel(p))
}
return artifacts
}
func toLicenseModel(pkgLicenses []pkg.License) (modelLicenses []model.License) {
for _, l := range pkgLicenses {
// guarantee collection
locations := make([]file.Location, 0)
if v := l.Locations.ToSlice(); v != nil {
locations = v
}
// format model must have allocated collections
urls := l.URLs
if urls == nil {
urls = []string{}
}
modelLicenses = append(modelLicenses, model.License{
Value: l.Value,
SPDXExpression: l.SPDXExpression,
Type: l.Type,
URLs: urls,
Locations: locations,
})
}
return
}
// toPackageModel crates a new Package from the given pkg.Package.
func toPackageModel(p pkg.Package) model.Package {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = cpe.String(c)
}
// we want to make sure all catalogers are
// initializing the array; this is a good choke point for this check
var licenses = make([]model.License, 0)
if !p.Licenses.Empty() {
licenses = toLicenseModel(p.Licenses.ToSlice())
}
return model.Package{
PackageBasicData: model.PackageBasicData{
ID: string(p.ID()),
Name: p.Name,
Version: p.Version,
Type: p.Type,
FoundBy: p.FoundBy,
Locations: p.Locations.ToSlice(),
Licenses: licenses,
Language: p.Language,
CPEs: cpes,
PURL: p.PURL,
},
PackageCustomData: model.PackageCustomData{
MetadataType: p.MetadataType,
Metadata: p.Metadata,
},
}
}
func toRelationshipModel(relationships []artifact.Relationship) []model.Relationship {
result := make([]model.Relationship, len(relationships))
for i, r := range relationships {
result[i] = model.Relationship{
Parent: string(r.From.ID()),
Child: string(r.To.ID()),
Type: string(r.Type),
Metadata: r.Data,
}
}
sort.Slice(result, func(i, j int) bool {
if iParent, jParent := result[i].Parent, result[j].Parent; iParent != jParent {
return iParent < jParent
}
if iChild, jChild := result[i].Child, result[j].Child; iChild != jChild {
return iChild < jChild
}
return result[i].Type < result[j].Type
})
return result
}
// toSourceModel creates a new source object to be represented into JSON.
func toSourceModel(src source.Description) model.Source {
m := model.Source{
ID: src.ID,
Name: src.Name,
Version: src.Version,
Type: sourcemetadata.JSONName(src.Metadata),
Metadata: src.Metadata,
}
if metadata, ok := src.Metadata.(source.StereoscopeImageSourceMetadata); ok {
// ensure that empty collections are not shown as null
if metadata.RepoDigests == nil {
metadata.RepoDigests = []string{}
}
if metadata.Tags == nil {
metadata.Tags = []string{}
}
m.Metadata = metadata
}
return m
}
@@ -0,0 +1,305 @@
package syftjson
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"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/source"
)
func Test_toSourceModel_IgnoreBase(t *testing.T) {
tests := []struct {
name string
src source.Description
}{
{
name: "directory",
src: source.Description{
ID: "test-id",
Metadata: source.DirectorySourceMetadata{
Path: "some/path",
Base: "some/base",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// assert the model transformation is correct
actual := toSourceModel(test.src)
by, err := json.Marshal(actual)
require.NoError(t, err)
assert.NotContains(t, string(by), "some/base")
})
}
}
func Test_toSourceModel(t *testing.T) {
tracker := sourcemetadata.NewCompletionTester(t)
tests := []struct {
name string
src source.Description
expected model.Source
}{
{
name: "directory",
src: source.Description{
ID: "test-id",
Name: "some-name",
Version: "some-version",
Metadata: source.DirectorySourceMetadata{
Path: "some/path",
Base: "some/base",
},
},
expected: model.Source{
ID: "test-id",
Name: "some-name",
Version: "some-version",
Type: "directory",
Metadata: source.DirectorySourceMetadata{
Path: "some/path",
Base: "some/base",
},
},
},
{
name: "file",
src: source.Description{
ID: "test-id",
Name: "some-name",
Version: "some-version",
Metadata: source.FileSourceMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
},
},
expected: model.Source{
ID: "test-id",
Name: "some-name",
Version: "some-version",
Type: "file",
Metadata: source.FileSourceMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
},
},
},
{
name: "image",
src: source.Description{
ID: "test-id",
Name: "some-name",
Version: "some-version",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
expected: model.Source{
ID: "test-id",
Name: "some-name",
Version: "some-version",
Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
RepoDigests: []string{},
Tags: []string{},
},
},
},
// below are regression tests for when the name/version are not provided
// historically we've hoisted up the name/version from the metadata, now it is a simple pass-through
{
name: "directory - no name/version",
src: source.Description{
ID: "test-id",
Metadata: source.DirectorySourceMetadata{
Path: "some/path",
Base: "some/base",
},
},
expected: model.Source{
ID: "test-id",
Type: "directory",
Metadata: source.DirectorySourceMetadata{
Path: "some/path",
Base: "some/base",
},
},
},
{
name: "file - no name/version",
src: source.Description{
ID: "test-id",
Metadata: source.FileSourceMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
},
},
expected: model.Source{
ID: "test-id",
Type: "file",
Metadata: source.FileSourceMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
},
},
},
{
name: "image - no name/version",
src: source.Description{
ID: "test-id",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
expected: model.Source{
ID: "test-id",
Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
RepoDigests: []string{},
Tags: []string{},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// assert the model transformation is correct
actual := toSourceModel(test.src)
assert.Equal(t, test.expected, actual)
// track each scheme tested (passed or not)
tracker.Tested(t, test.expected.Metadata)
})
}
}
func Test_toFileType(t *testing.T) {
badType := stereoscopeFile.Type(0x1337)
var allTypesTested []stereoscopeFile.Type
tests := []struct {
ty stereoscopeFile.Type
name string
}{
{
ty: stereoscopeFile.TypeRegular,
name: "RegularFile",
},
{
ty: stereoscopeFile.TypeDirectory,
name: "Directory",
},
{
ty: stereoscopeFile.TypeSymLink,
name: "SymbolicLink",
},
{
ty: stereoscopeFile.TypeHardLink,
name: "HardLink",
},
{
ty: stereoscopeFile.TypeSocket,
name: "Socket",
},
{
ty: stereoscopeFile.TypeCharacterDevice,
name: "CharacterDevice",
},
{
ty: stereoscopeFile.TypeBlockDevice,
name: "BlockDevice",
},
{
ty: stereoscopeFile.TypeFIFO,
name: "FIFONode",
},
{
ty: stereoscopeFile.TypeIrregular,
name: "IrregularFile",
},
{
ty: badType,
name: "Unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.name, toFileType(tt.ty), "toFileType(%v)", tt.ty)
if tt.ty != badType {
allTypesTested = append(allTypesTested, tt.ty)
}
})
}
assert.ElementsMatch(t, allTypesTested, stereoscopeFile.AllTypes(), "not all file.Types are under test")
}
func Test_toFileMetadataEntry(t *testing.T) {
coords := file.Coordinates{
RealPath: "/path",
FileSystemID: "x",
}
tests := []struct {
name string
metadata *file.Metadata
want *model.FileMetadataEntry
}{
{
name: "no metadata",
},
{
name: "no file info",
metadata: &file.Metadata{
FileInfo: nil,
},
want: &model.FileMetadataEntry{
Type: stereoscopeFile.TypeRegular.String(),
},
},
{
name: "with file info",
metadata: &file.Metadata{
FileInfo: &stereoscopeFile.ManualInfo{
ModeValue: 1,
},
},
want: &model.FileMetadataEntry{
Mode: 1,
Type: stereoscopeFile.TypeRegular.String(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, toFileMetadataEntry(coords, tt.metadata))
})
}
}
+339
View File
@@ -0,0 +1,339 @@
package syftjson
import (
"fmt"
"os"
"path"
"strconv"
"strings"
"github.com/google/go-cmp/cmp"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"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/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func toSyftModel(doc model.Document) *sbom.SBOM {
idAliases := make(map[string]string)
catalog := toSyftCatalog(doc.Artifacts, idAliases)
fileArtifacts := toSyftFiles(doc.Files)
return &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: catalog,
FileMetadata: fileArtifacts.FileMetadata,
FileDigests: fileArtifacts.FileDigests,
FileContents: fileArtifacts.FileContents,
FileLicenses: fileArtifacts.FileLicenses,
LinuxDistribution: toSyftLinuxRelease(doc.Distro),
},
Source: *toSyftSourceData(doc.Source),
Descriptor: toSyftDescriptor(doc.Descriptor),
Relationships: warnConversionErrors(toSyftRelationships(&doc, catalog, doc.ArtifactRelationships, idAliases)),
}
}
func warnConversionErrors[T any](converted []T, errors []error) []T {
errorMessages := deduplicateErrors(errors)
for _, msg := range errorMessages {
log.Warn(msg)
}
return converted
}
func deduplicateErrors(errors []error) []string {
errorCounts := make(map[string]int)
var errorMessages []string
for _, e := range errors {
errorCounts[e.Error()] = errorCounts[e.Error()] + 1
}
for msg, count := range errorCounts {
errorMessages = append(errorMessages, fmt.Sprintf("%q occurred %d time(s)", msg, count))
}
return errorMessages
}
func toSyftFiles(files []model.File) sbom.Artifacts {
ret := sbom.Artifacts{
FileMetadata: make(map[file.Coordinates]file.Metadata),
FileDigests: make(map[file.Coordinates][]file.Digest),
FileContents: make(map[file.Coordinates]string),
FileLicenses: make(map[file.Coordinates][]file.License),
}
for _, f := range files {
coord := f.Location
if f.Metadata != nil {
mode, err := strconv.ParseInt(strconv.Itoa(f.Metadata.Mode), 8, 64)
if err != nil {
log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coord, f.Metadata.Mode, err)
mode = 0
}
fm := os.FileMode(mode)
ret.FileMetadata[coord] = file.Metadata{
FileInfo: stereoscopeFile.ManualInfo{
NameValue: path.Base(coord.RealPath),
SizeValue: f.Metadata.Size,
ModeValue: fm,
},
Path: coord.RealPath,
LinkDestination: f.Metadata.LinkDestination,
UserID: f.Metadata.UserID,
GroupID: f.Metadata.GroupID,
Type: toSyftFileType(f.Metadata.Type),
MIMEType: f.Metadata.MIMEType,
}
}
for _, d := range f.Digests {
ret.FileDigests[coord] = append(ret.FileDigests[coord], file.Digest{
Algorithm: d.Algorithm,
Value: d.Value,
})
}
if f.Contents != "" {
ret.FileContents[coord] = f.Contents
}
for _, l := range f.Licenses {
var evidence *file.LicenseEvidence
if e := l.Evidence; e != nil {
evidence = &file.LicenseEvidence{
Confidence: e.Confidence,
Offset: e.Offset,
Extent: e.Extent,
}
}
ret.FileLicenses[coord] = append(ret.FileLicenses[coord], file.License{
Value: l.Value,
SPDXExpression: l.SPDXExpression,
Type: l.Type,
LicenseEvidence: evidence,
})
}
}
return ret
}
func toSyftLicenses(m []model.License) (p []pkg.License) {
for _, l := range m {
p = append(p, pkg.License{
Value: l.Value,
SPDXExpression: l.SPDXExpression,
Type: l.Type,
URLs: l.URLs,
Locations: file.NewLocationSet(l.Locations...),
})
}
return
}
func toSyftFileType(ty string) stereoscopeFile.Type {
switch ty {
case "SymbolicLink":
return stereoscopeFile.TypeSymLink
case "HardLink":
return stereoscopeFile.TypeHardLink
case "Directory":
return stereoscopeFile.TypeDirectory
case "Socket":
return stereoscopeFile.TypeSocket
case "BlockDevice":
return stereoscopeFile.TypeBlockDevice
case "CharacterDevice":
return stereoscopeFile.TypeCharacterDevice
case "FIFONode":
return stereoscopeFile.TypeFIFO
case "RegularFile":
return stereoscopeFile.TypeRegular
case "IrregularFile":
return stereoscopeFile.TypeIrregular
default:
return stereoscopeFile.TypeIrregular
}
}
func toSyftLinuxRelease(d model.LinuxRelease) *linux.Release {
if cmp.Equal(d, model.LinuxRelease{}) {
return nil
}
return &linux.Release{
PrettyName: d.PrettyName,
Name: d.Name,
ID: d.ID,
IDLike: d.IDLike,
Version: d.Version,
VersionID: d.VersionID,
VersionCodename: d.VersionCodename,
BuildID: d.BuildID,
ImageID: d.ImageID,
ImageVersion: d.ImageVersion,
Variant: d.Variant,
VariantID: d.VariantID,
HomeURL: d.HomeURL,
SupportURL: d.SupportURL,
BugReportURL: d.BugReportURL,
PrivacyPolicyURL: d.PrivacyPolicyURL,
CPEName: d.CPEName,
SupportEnd: d.SupportEnd,
}
}
func toSyftRelationships(doc *model.Document, catalog *pkg.Collection, relationships []model.Relationship, idAliases map[string]string) ([]artifact.Relationship, []error) {
idMap := make(map[string]interface{})
for _, p := range catalog.Sorted() {
idMap[string(p.ID())] = p
locations := p.Locations.ToSlice()
for _, l := range locations {
idMap[string(l.Coordinates.ID())] = l.Coordinates
}
}
// set source metadata in identifier map
idMap[doc.Source.ID] = toSyftSource(doc.Source)
for _, f := range doc.Files {
idMap[f.ID] = f.Location
}
var out []artifact.Relationship
var conversionErrors []error
for _, r := range relationships {
syftRelationship, err := toSyftRelationship(idMap, r, idAliases)
if err != nil {
conversionErrors = append(conversionErrors, err)
}
if syftRelationship != nil {
out = append(out, *syftRelationship)
}
}
return out, conversionErrors
}
func toSyftSource(s model.Source) source.Source {
description := toSyftSourceData(s)
if description == nil {
return nil
}
return source.FromDescription(*description)
}
func toSyftRelationship(idMap map[string]interface{}, relationship model.Relationship, idAliases map[string]string) (*artifact.Relationship, error) {
id := func(id string) string {
aliased, ok := idAliases[id]
if ok {
return aliased
}
return id
}
from, ok := idMap[id(relationship.Parent)].(artifact.Identifiable)
if !ok {
return nil, fmt.Errorf("relationship mapping from key %s is not a valid artifact.Identifiable type: %+v", relationship.Parent, idMap[relationship.Parent])
}
to, ok := idMap[id(relationship.Child)].(artifact.Identifiable)
if !ok {
return nil, fmt.Errorf("relationship mapping to key %s is not a valid artifact.Identifiable type: %+v", relationship.Child, idMap[relationship.Child])
}
typ := artifact.RelationshipType(relationship.Type)
switch typ {
case artifact.OwnershipByFileOverlapRelationship, artifact.ContainsRelationship, artifact.DependencyOfRelationship, artifact.EvidentByRelationship:
default:
if !strings.Contains(string(typ), "dependency-of") {
return nil, fmt.Errorf("unknown relationship type: %s", string(typ))
}
// lets try to stay as compatible as possible with similar relationship types without dropping the relationship
log.Warnf("assuming %q for relationship type %q", artifact.DependencyOfRelationship, typ)
typ = artifact.DependencyOfRelationship
}
return &artifact.Relationship{
From: from,
To: to,
Type: typ,
Data: relationship.Metadata,
}, nil
}
func toSyftDescriptor(d model.Descriptor) sbom.Descriptor {
return sbom.Descriptor{
Name: d.Name,
Version: d.Version,
Configuration: d.Configuration,
}
}
func toSyftSourceData(s model.Source) *source.Description {
return &source.Description{
ID: s.ID,
Name: s.Name,
Version: s.Version,
Metadata: s.Metadata,
}
}
func toSyftCatalog(pkgs []model.Package, idAliases map[string]string) *pkg.Collection {
catalog := pkg.NewCollection()
for _, p := range pkgs {
catalog.Add(toSyftPackage(p, idAliases))
}
return catalog
}
func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
var cpes []cpe.CPE
for _, c := range p.CPEs {
value, err := cpe.New(c)
if err != nil {
log.Warnf("excluding invalid CPE %q: %v", c, err)
continue
}
cpes = append(cpes, value)
}
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,
}
// we don't know if this package ID is truly unique, however, we need to trust the user input in case there are
// external references to it. That is, we can't derive our own ID (using pkg.SetID()) since consumers won't
// be able to historically interact with data that references the IDs from the original SBOM document being decoded now.
out.OverrideID(artifact.ID(p.ID))
// this alias mapping is currently defunct, but could be useful in the future.
id := string(out.ID())
if id != p.ID {
idAliases[p.ID] = id
}
return out
}
+436
View File
@@ -0,0 +1,436 @@
package syftjson
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
stereoFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/artifact"
"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/sbom"
"github.com/anchore/syft/syft/source"
)
func Test_toSyftSourceData(t *testing.T) {
tracker := sourcemetadata.NewCompletionTester(t)
tests := []struct {
name string
src model.Source
expected *source.Description
}{
{
name: "directory",
src: model.Source{
ID: "the-id",
Name: "some-name",
Version: "some-version",
Type: "directory",
Metadata: source.DirectorySourceMetadata{
Path: "some/path",
Base: "some/base",
},
},
expected: &source.Description{
ID: "the-id",
Name: "some-name",
Version: "some-version",
Metadata: source.DirectorySourceMetadata{
Path: "some/path",
Base: "some/base",
},
},
},
{
name: "file",
src: model.Source{
ID: "the-id",
Name: "some-name",
Version: "some-version",
Type: "file",
Metadata: source.FileSourceMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
},
},
expected: &source.Description{
ID: "the-id",
Name: "some-name",
Version: "some-version",
Metadata: source.FileSourceMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
},
},
},
{
name: "image",
src: model.Source{
ID: "the-id",
Name: "some-name",
Version: "some-version",
Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
expected: &source.Description{
ID: "the-id",
Name: "some-name",
Version: "some-version",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
},
// below are regression tests for when the name/version are not provided
// historically we've hoisted up the name/version from the metadata, now it is a simple pass-through
{
name: "directory - no name/version",
src: model.Source{
ID: "the-id",
Type: "directory",
Metadata: source.DirectorySourceMetadata{
Path: "some/path",
Base: "some/base",
},
},
expected: &source.Description{
ID: "the-id",
Metadata: source.DirectorySourceMetadata{
Path: "some/path",
Base: "some/base",
},
},
},
{
name: "file - no name/version",
src: model.Source{
ID: "the-id",
Type: "file",
Metadata: source.FileSourceMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
},
},
expected: &source.Description{
ID: "the-id",
Metadata: source.FileSourceMetadata{
Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
MIMEType: "text/plain",
},
},
},
{
name: "image - no name/version",
src: model.Source{
ID: "the-id",
Type: "image",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
expected: &source.Description{
ID: "the-id",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "user-input",
ID: "id...",
ManifestDigest: "digest...",
MediaType: "type...",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// assert the model transformation is correct
actual := toSyftSourceData(test.src)
assert.Equal(t, test.expected, actual)
tracker.Tested(t, test.expected.Metadata)
})
}
}
func Test_idsHaveChanged(t *testing.T) {
s := toSyftModel(model.Document{
Source: model.Source{
Type: "file",
Metadata: source.FileSourceMetadata{Path: "some/path"},
},
Artifacts: []model.Package{
{
PackageBasicData: model.PackageBasicData{
ID: "1",
Name: "pkg-1",
},
},
{
PackageBasicData: model.PackageBasicData{
ID: "2",
Name: "pkg-2",
},
},
},
ArtifactRelationships: []model.Relationship{
{
Parent: "1",
Child: "2",
Type: string(artifact.ContainsRelationship),
},
},
})
require.Len(t, s.Relationships, 1)
r := s.Relationships[0]
from := s.Artifacts.Packages.Package(r.From.ID())
require.NotNil(t, from)
assert.Equal(t, "pkg-1", from.Name)
to := s.Artifacts.Packages.Package(r.To.ID())
require.NotNil(t, to)
assert.Equal(t, "pkg-2", to.Name)
}
func Test_toSyftFiles(t *testing.T) {
coord := file.Coordinates{
RealPath: "/somerwhere/place",
FileSystemID: "abc",
}
tests := []struct {
name string
files []model.File
want sbom.Artifacts
}{
{
name: "empty",
files: []model.File{},
want: sbom.Artifacts{
FileMetadata: map[file.Coordinates]file.Metadata{},
FileDigests: map[file.Coordinates][]file.Digest{},
},
},
{
name: "no metadata",
files: []model.File{
{
ID: string(coord.ID()),
Location: coord,
Metadata: nil,
Digests: []file.Digest{
{
Algorithm: "sha256",
Value: "123",
},
},
},
},
want: sbom.Artifacts{
FileMetadata: map[file.Coordinates]file.Metadata{},
FileDigests: map[file.Coordinates][]file.Digest{
coord: {
{
Algorithm: "sha256",
Value: "123",
},
},
},
},
},
{
name: "single file",
files: []model.File{
{
ID: string(coord.ID()),
Location: coord,
Metadata: &model.FileMetadataEntry{
Mode: 777,
Type: "RegularFile",
LinkDestination: "",
UserID: 42,
GroupID: 32,
MIMEType: "text/plain",
Size: 92,
},
Digests: []file.Digest{
{
Algorithm: "sha256",
Value: "123",
},
},
},
},
want: sbom.Artifacts{
FileMetadata: map[file.Coordinates]file.Metadata{
coord: {
FileInfo: stereoFile.ManualInfo{
NameValue: "place",
SizeValue: 92,
ModeValue: 511, // 777 octal = 511 decimal
},
Path: coord.RealPath,
LinkDestination: "",
UserID: 42,
GroupID: 32,
Type: stereoFile.TypeRegular,
MIMEType: "text/plain",
},
},
FileDigests: map[file.Coordinates][]file.Digest{
coord: {
{
Algorithm: "sha256",
Value: "123",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.want.FileContents = make(map[file.Coordinates]string)
tt.want.FileLicenses = make(map[file.Coordinates][]file.License)
assert.Equal(t, tt.want, toSyftFiles(tt.files))
})
}
}
func Test_toSyfRelationship(t *testing.T) {
packageWithId := func(id string) *pkg.Package {
p := &pkg.Package{}
p.OverrideID(artifact.ID(id))
return p
}
childPackage := packageWithId("some-child-id")
parentPackage := packageWithId("some-parent-id")
tests := []struct {
name string
idMap map[string]interface{}
idAliases map[string]string
relationships model.Relationship
want *artifact.Relationship
wantError error
}{
{
name: "one relationship no warnings",
idMap: map[string]interface{}{
"some-child-id": childPackage,
"some-parent-id": parentPackage,
},
idAliases: map[string]string{},
relationships: model.Relationship{
Parent: "some-parent-id",
Child: "some-child-id",
Type: string(artifact.ContainsRelationship),
},
want: &artifact.Relationship{
To: childPackage,
From: parentPackage,
Type: artifact.ContainsRelationship,
},
},
{
name: "relationship unknown type one warning",
idMap: map[string]interface{}{
"some-child-id": childPackage,
"some-parent-id": parentPackage,
},
idAliases: map[string]string{},
relationships: model.Relationship{
Parent: "some-parent-id",
Child: "some-child-id",
Type: "some-unknown-relationship-type",
},
wantError: errors.New(
"unknown relationship type: some-unknown-relationship-type",
),
},
{
name: "relationship missing child ID one warning",
idMap: map[string]interface{}{
"some-parent-id": parentPackage,
},
idAliases: map[string]string{},
relationships: model.Relationship{
Parent: "some-parent-id",
Child: "some-child-id",
Type: string(artifact.ContainsRelationship),
},
wantError: errors.New(
"relationship mapping to key some-child-id is not a valid artifact.Identifiable type: <nil>",
),
},
{
name: "relationship missing parent ID one warning",
idMap: map[string]interface{}{
"some-child-id": childPackage,
},
idAliases: map[string]string{},
relationships: model.Relationship{
Parent: "some-parent-id",
Child: "some-child-id",
Type: string(artifact.ContainsRelationship),
},
wantError: errors.New("relationship mapping from key some-parent-id is not a valid artifact.Identifiable type: <nil>"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, gotErr := toSyftRelationship(tt.idMap, tt.relationships, tt.idAliases)
assert.Equal(t, tt.want, got)
assert.Equal(t, tt.wantError, gotErr)
})
}
}
func Test_deduplicateErrors(t *testing.T) {
tests := []struct {
name string
errors []error
want []string
}{
{
name: "no errors, nil slice",
},
{
name: "deduplicates errors",
errors: []error{
errors.New("some error"),
errors.New("some error"),
},
want: []string{
`"some error" occurred 2 time(s)`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := deduplicateErrors(tt.errors)
assert.Equal(t, tt.want, got)
})
}
}