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