move source metadata upstream and fix tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2020-11-13 16:30:31 -05:00
parent aa0d444fd4
commit 6f7a4fd3e4
No known key found for this signature in database
GPG Key ID: 5CB45AE22BAB7EA7
36 changed files with 305 additions and 294 deletions

View File

@ -50,7 +50,7 @@ func setGlobalCliOptions() {
flag := "scope"
rootCmd.Flags().StringP(
"scope", "s", source.SquashedScope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.Options))
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)

View File

@ -100,7 +100,7 @@ func startWorker(userInput string) <-chan error {
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: presenter.GetPresenter(appConfig.PresenterOpt, *scope, catalog, distro),
Value: presenter.GetPresenter(appConfig.PresenterOpt, scope.Metadata, catalog, distro),
})
}()
return errs

View File

@ -5,11 +5,8 @@
"items": {
"properties": {
"foundBy": {
"items": {
"type": "string"
},
"type": "array"
},
"language": {
"type": "string"
},
@ -27,33 +24,21 @@
]
},
"locations": {
"anyOf": [
{
"type": "null"
},
{
"items": {
"properties": {
"layerID": {
"type": "string"
},
"layerIndex": {
"type": "integer"
},
"path": {
"type": "string"
}
},
"required": [
"layerID",
"layerIndex",
"path"
],
"type": "object"
},
"type": "array"
}
]
},
"metadata": {
"properties": {
@ -354,7 +339,7 @@
"name": {
"type": "string"
},
"reportTimestamp": {
"scope": {
"type": "string"
},
"version": {
@ -363,7 +348,7 @@
},
"required": [
"name",
"reportTimestamp",
"scope",
"version"
],
"type": "object"
@ -432,6 +417,9 @@
"type": "string"
},
"type": "array"
},
"userInput": {
"type": "string"
}
},
"required": [
@ -439,7 +427,8 @@
"layers",
"mediaType",
"size",
"tags"
"tags",
"userInput"
],
"type": "object"
}

View File

@ -54,7 +54,7 @@ func TestDpkgCataloger(t *testing.T) {
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-dpkg")
defer cleanup()
s, err := source.NewFromImage(img, source.AllLayersScope)
s, err := source.NewFromImage(img, source.AllLayersScope, "")
if err != nil {
t.Fatal(err)
}
@ -79,7 +79,7 @@ func TestDpkgCataloger(t *testing.T) {
// we will test the sources separately
var sourcesList = make([]string, len(a.Locations))
for i, s := range a.Locations {
sourcesList[i] = string(s.Path)
sourcesList[i] = s.Path
}
a.Locations = nil

View File

@ -13,6 +13,7 @@ import (
const (
packagesGlob = "**/var/lib/rpm/Packages"
catalogerName = "rpmdb-cataloger"
)
type Cataloger struct{}
@ -24,7 +25,7 @@ func NewRpmdbCataloger() *Cataloger {
// Name returns a string that uniquely describes a cataloger
func (c *Cataloger) Name() string {
return "rpmdb-cataloger"
return catalogerName
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing rpm db installation.

View File

@ -55,6 +55,7 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader
Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does
//Version: fmt.Sprintf("%d:%s-%s.%s", entry.Epoch, entry.Version, entry.Release, entry.Arch),
Locations: []source.Location{dbLocation},
FoundBy: catalogerName,
Type: pkg.RpmPkg,
MetadataType: pkg.RpmdbMetadataType,
Metadata: pkg.RpmdbMetadata{

View File

@ -43,7 +43,7 @@ func (r *rpmdbTestFileResolverMock) RelativeFileByPath(source.Location, string)
}
func TestParseRpmDB(t *testing.T) {
dbRef := file.NewFileReference("test-path")
dbLocation := source.NewLocation("test-path")
tests := []struct {
fixture string
@ -58,7 +58,8 @@ func TestParseRpmDB(t *testing.T) {
"dive": {
Name: "dive",
Version: "0.9.2-1",
Source: []file.Reference{dbRef},
Locations: []source.Location{dbLocation},
FoundBy: catalogerName,
Type: pkg.RpmPkg,
MetadataType: pkg.RpmdbMetadataType,
Metadata: pkg.RpmdbMetadata{
@ -84,7 +85,8 @@ func TestParseRpmDB(t *testing.T) {
"dive": {
Name: "dive",
Version: "0.9.2-1",
Source: []file.Reference{dbRef},
Locations: []source.Location{dbLocation},
FoundBy: catalogerName,
Type: pkg.RpmPkg,
MetadataType: pkg.RpmdbMetadataType,
Metadata: pkg.RpmdbMetadata{
@ -120,7 +122,7 @@ func TestParseRpmDB(t *testing.T) {
fileResolver := newTestFileResolver(test.ignorePaths)
actual, err := parseRpmDB(fileResolver, dbRef, fixture)
actual, err := parseRpmDB(fileResolver, dbLocation, fixture)
if err != nil {
t.Fatalf("failed to parse rpmdb: %+v", err)
}

View File

@ -36,7 +36,7 @@ import (
// set of packages, the identified Linux distribution, and the source object used to wrap the data source.
func Catalog(userInput string, scoptOpt source.Scope) (*pkg.Catalog, *source.Source, *distro.Distro, error) {
log.Info("cataloging image")
s, cleanup, err := source.NewSource(userInput, scoptOpt)
s, cleanup, err := source.New(userInput, scoptOpt)
defer cleanup()
if err != nil {
return nil, nil, nil, err
@ -70,13 +70,13 @@ func CatalogFromScope(s source.Source) (*pkg.Catalog, error) {
// conditionally have two sets of catalogers
var catalogers []cataloger.Cataloger
switch s.Scheme {
switch s.Metadata.Scheme {
case source.ImageScheme:
catalogers = cataloger.ImageCatalogers()
case source.DirectoryScheme:
catalogers = cataloger.DirectoryCatalogers()
default:
return nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", s.Scheme)
return nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", s.Metadata.Scheme)
}
return cataloger.Catalog(s.Resolver, catalogers...)

View File

@ -9,7 +9,7 @@ import (
// at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section.
type DpkgMetadata struct {
Package string `mapstructure:"Package" json:"package"`
Source string `mapstructure:"Locations" json:"source"`
Source string `mapstructure:"Source" json:"source"`
Version string `mapstructure:"Version" json:"version"`
Architecture string `mapstructure:"Architecture" json:"architecture"`
Maintainer string `mapstructure:"Maintainer" json:"maintainer"`

View File

@ -19,16 +19,16 @@ type ID int64
// Package represents an application or library that has been bundled into a distributable format.
type Package struct {
id ID // uniquely identifies a package, set by the cataloger
Name string `json:"manifest"` // the package name
Version string `json:"version"` // the version of the package
FoundBy string `json:"foundBy"` // the specific cataloger that discovered this package
Locations []source.Location `json:"-"` // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)
Name string // the package name
Version string // the version of the package
FoundBy string // the specific cataloger that discovered this package
Locations []source.Location // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)
// TODO: should we move licenses into metadata?
Licenses []string `json:"licenses"` // licenses discovered with the package metadata
Language Language `json:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc)
Type Type `json:"type"` // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc)
MetadataType MetadataType `json:"metadataType,omitempty"` // the shape of the additional data in the "metadata" field
Metadata interface{} `json:"metadata,omitempty"` // additional data found while parsing the package source
Licenses []string // licenses discovered with the package metadata
Language Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc)
Type Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc)
MetadataType MetadataType // the shape of the additional data in the "metadata" field
Metadata interface{} // additional data found while parsing the package source
}
// ID returns the package ID, which is unique relative to a package catalog.

View File

@ -17,15 +17,15 @@ import (
// Presenter writes a CycloneDX report from the given Catalog and Locations contents
type Presenter struct {
catalog *pkg.Catalog
source source.Source
srcMetadata source.Metadata
distro distro.Distro
}
// NewPresenter creates a CycloneDX presenter from the given Catalog and Locations objects.
func NewPresenter(catalog *pkg.Catalog, s source.Source, d distro.Distro) *Presenter {
func NewPresenter(catalog *pkg.Catalog, s source.Metadata, d distro.Distro) *Presenter {
return &Presenter{
catalog: catalog,
source: s,
srcMetadata: s,
distro: d,
}
}
@ -34,33 +34,26 @@ func NewPresenter(catalog *pkg.Catalog, s source.Source, d distro.Distro) *Prese
func (pres *Presenter) Present(output io.Writer) error {
bom := NewDocumentFromCatalog(pres.catalog, pres.distro)
switch src := pres.source.Target.(type) {
case source.DirSource:
switch pres.srcMetadata.Scheme {
case source.DirectoryScheme:
bom.BomDescriptor.Component = &BdComponent{
Component: Component{
Type: "file",
Name: src.Path,
Name: pres.srcMetadata.Path,
Version: "",
},
}
case source.ImageSource:
var imageID string
var versionStr string
if len(src.Img.Metadata.Tags) > 0 {
imageID = src.Img.Metadata.Tags[0].Context().Name()
versionStr = src.Img.Metadata.Tags[0].TagStr()
} else {
imageID = src.Img.Metadata.Digest
}
case source.ImageScheme:
// TODO: can we use the tags a bit better?
bom.BomDescriptor.Component = &BdComponent{
Component: Component{
Type: "container",
Name: imageID,
Version: versionStr,
Name: pres.srcMetadata.ImageMetadata.UserInput,
Version: pres.srcMetadata.ImageMetadata.Digest,
},
}
default:
return fmt.Errorf("unsupported source: %T", src)
return fmt.Errorf("unsupported source: %T", pres.srcMetadata.Scheme)
}
encoder := xml.NewEncoder(output)

View File

@ -66,7 +66,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
t.Fatal(err)
}
pres := NewPresenter(catalog, s, d)
pres := NewPresenter(catalog, s.Metadata, d)
// run presenter
err = pres.Present(&buffer)
@ -146,7 +146,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
},
})
s, err := source.NewFromImage(img, source.AllLayersScope)
s, err := source.NewFromImage(img, source.AllLayersScope, "user-image-input")
if err != nil {
t.Fatal(err)
}
@ -156,7 +156,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
t.Fatal(err)
}
pres := NewPresenter(catalog, s, d)
pres := NewPresenter(catalog, s.Metadata, d)
// run presenter
err = pres.Present(&buffer)
@ -177,7 +177,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}

View File

@ -16,11 +16,11 @@ type Artifact struct {
type ArtifactBasicMetadata struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
FoundBy []string `json:"foundBy"`
Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Language string `json:"language"`
Language pkg.Language `json:"language"`
}
type ArtifactCustomMetadata struct {
@ -33,17 +33,17 @@ type ArtifactMetadataUnpacker struct {
Metadata json.RawMessage `json:"metadata"`
}
func NewArtifact(p *pkg.Package, s source.Source) (Artifact, error) {
func NewArtifact(p *pkg.Package) (Artifact, error) {
return Artifact{
ArtifactBasicMetadata: ArtifactBasicMetadata{
Name: p.Name,
Version: p.Version,
Type: string(p.Type),
FoundBy: []string{p.FoundBy},
Type: p.Type,
FoundBy: p.FoundBy,
Locations: p.Locations,
Licenses: p.Licenses,
Language: string(p.Language),
Language: p.Language,
},
ArtifactCustomMetadata: ArtifactCustomMetadata{
MetadataType: p.MetadataType,
@ -57,9 +57,11 @@ func (a Artifact) ToPackage() pkg.Package {
// does not include found-by and locations
Name: a.Name,
Version: a.Version,
FoundBy: a.FoundBy,
Licenses: a.Licenses,
Language: pkg.Language(a.Language),
Type: pkg.Type(a.Type),
Language: a.Language,
Locations: a.Locations,
Type: a.Type,
MetadataType: a.MetadataType,
Metadata: a.Metadata,
}

View File

@ -1,8 +1,6 @@
package json
import (
"time"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/distro"
@ -21,8 +19,7 @@ type Document struct {
type Descriptor struct {
Name string `json:"name"`
Version string `json:"version"`
ReportTimestamp string `json:"reportTimestamp"`
// TODO: we should include source option here as well (or in source)
Scope string `json:"scope"`
}
// Distribution provides information about a detected Linux Distribution
@ -32,8 +29,8 @@ type Distribution struct {
IDLike string `json:"idLike"`
}
func NewDocument(catalog *pkg.Catalog, s source.Source, d distro.Distro) (Document, error) {
src, err := NewSource(s)
func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d distro.Distro) (Document, error) {
src, err := NewSource(srcMetadata)
if err != nil {
return Document{}, nil
}
@ -54,12 +51,12 @@ func NewDocument(catalog *pkg.Catalog, s source.Source, d distro.Distro) (Docume
Descriptor: Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
ReportTimestamp: time.Now().Format(time.RFC3339),
Scope: srcMetadata.Scope.String(),
},
}
for _, p := range catalog.Sorted() {
art, err := NewArtifact(p, s)
art, err := NewArtifact(p)
if err != nil {
return Document{}, err
}

View File

@ -1,44 +0,0 @@
package json
import (
"github.com/anchore/syft/syft/source"
)
type Image struct {
Layers []Layer `json:"layers"`
Size int64 `json:"size"`
Digest string `json:"digest"`
MediaType string `json:"mediaType"`
Tags []string `json:"tags"`
}
type Layer struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
func NewImage(src source.ImageSource) *Image {
// populate artifacts...
tags := make([]string, len(src.Img.Metadata.Tags))
for idx, tag := range src.Img.Metadata.Tags {
tags[idx] = tag.String()
}
img := Image{
Digest: src.Img.Metadata.Digest,
Size: src.Img.Metadata.Size,
MediaType: string(src.Img.Metadata.MediaType),
Tags: tags,
Layers: make([]Layer, len(src.Img.Layers)),
}
// populate image metadata
for idx, l := range src.Img.Layers {
img.Layers[idx] = Layer{
MediaType: string(l.Metadata.MediaType),
Digest: l.Metadata.Digest,
Size: l.Metadata.Size,
}
}
return &img
}

View File

@ -11,20 +11,20 @@ import (
type Presenter struct {
catalog *pkg.Catalog
source source.Source
srcMetadata source.Metadata
distro distro.Distro
}
func NewPresenter(catalog *pkg.Catalog, s source.Source, d distro.Distro) *Presenter {
func NewPresenter(catalog *pkg.Catalog, s source.Metadata, d distro.Distro) *Presenter {
return &Presenter{
catalog: catalog,
source: s,
srcMetadata: s,
distro: d,
}
}
func (pres *Presenter) Present(output io.Writer) error {
doc, err := NewDocument(pres.catalog, pres.source, pres.distro)
doc, err := NewDocument(pres.catalog, pres.srcMetadata, pres.distro)
if err != nil {
return err
}

View File

@ -56,7 +56,7 @@ func TestJsonDirsPresenter(t *testing.T) {
if err != nil {
t.Fatal(err)
}
pres := NewPresenter(catalog, s, d)
pres := NewPresenter(catalog, s.Metadata, d)
// run presenter
err = pres.Present(&buffer)
@ -73,7 +73,7 @@ func TestJsonDirsPresenter(t *testing.T) {
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
@ -123,9 +123,9 @@ func TestJsonImgsPresenter(t *testing.T) {
},
})
s, err := source.NewFromImage(img, source.AllLayersScope)
s, err := source.NewFromImage(img, source.AllLayersScope, "user-image-input")
d := distro.NewUnknownDistro()
pres := NewPresenter(catalog, s, d)
pres := NewPresenter(catalog, s.Metadata, d)
// run presenter
err = pres.Present(&buffer)
@ -142,7 +142,7 @@ func TestJsonImgsPresenter(t *testing.T) {
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}

View File

@ -17,14 +17,14 @@ type SourceUnpacker struct {
Target json.RawMessage `json:"target"`
}
func NewSource(s source.Source) (Source, error) {
switch src := s.Target.(type) {
case source.ImageSource:
func NewSource(src source.Metadata) (Source, error) {
switch src.Scheme {
case source.ImageScheme:
return Source{
Type: "image",
Target: NewImage(src),
Target: src.ImageMetadata,
}, nil
case source.DirSource:
case source.DirectoryScheme:
return Source{
Type: "directory",
Target: src.Path,
@ -44,7 +44,7 @@ func (s *Source) UnmarshalJSON(b []byte) error {
switch s.Type {
case "image":
var payload Image
var payload source.ImageMetadata
if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
return err
}

View File

@ -3,14 +3,29 @@
{
"name": "package-1",
"version": "1.0.1",
"type": "deb",
"type": "python",
"foundBy": [
"the-cataloger-1"
],
"locations": [
"/some/path/pkg1"
{
"path": "/some/path/pkg1"
}
],
"licenses": null
"licenses": [
"MIT"
],
"language": "python",
"metadataType": "python-package-metadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
"sitePackagesRootPath": ""
}
},
{
"name": "package-2",
@ -20,9 +35,22 @@
"the-cataloger-2"
],
"locations": [
"/some/path/pkg1"
{
"path": "/some/path/pkg1"
}
],
"licenses": null
"licenses": null,
"language": "",
"metadataType": "dpkg-metadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": null
}
}
],
"source": {
@ -33,5 +61,10 @@
"name": "",
"version": "",
"idLike": ""
},
"descriptor": {
"name": "syft",
"version": "[not provided]",
"scope": ""
}
}

View File

@ -3,17 +3,30 @@
{
"name": "package-1",
"version": "1.0.1",
"type": "deb",
"type": "python",
"foundBy": [
"the-cataloger-1"
],
"locations": [
{
"path": "/somefile-1.txt",
"layerIndex": 0
"layerID": "sha256:e158b57d6f5a96ef5fd22f2fe76c70b5ba6ff5b2619f9d83125b2aad0492ac7b"
}
],
"licenses": null
"licenses": [
"MIT"
],
"language": "python",
"metadataType": "python-package-metadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
"sitePackagesRootPath": ""
}
},
{
"name": "package-2",
@ -25,10 +38,21 @@
"locations": [
{
"path": "/somefile-2.txt",
"layerIndex": 1
"layerID": "sha256:da21056e7bf4308ecea0c0836848a7fe92f38fdcf35bc09ee6d98e7ab7beeebf"
}
],
"licenses": null
"licenses": null,
"language": "",
"metadataType": "dpkg-metadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": null
}
}
],
"source": {
@ -37,22 +61,22 @@
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:78783bfc74fef84f899b4977561ad1172f87753f82cc2157b06bf097e56dfbce",
"digest": "sha256:e158b57d6f5a96ef5fd22f2fe76c70b5ba6ff5b2619f9d83125b2aad0492ac7b",
"size": 22
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:54ec7f643dafbf9f27032a5e60afe06248c0e99b50aed54bb0fe28ea4825ccaf",
"digest": "sha256:da21056e7bf4308ecea0c0836848a7fe92f38fdcf35bc09ee6d98e7ab7beeebf",
"size": 16
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:ec4775a139c45b1ddf9ea8e1cb43385e92e5c0bf6ec2e3f4192372785b18c106",
"digest": "sha256:f0e18aa6032c24659a9c741fc36ca56f589782ea132061ccf6f52b952403da94",
"size": 27
}
],
"size": 65,
"digest": "sha256:fedd7bcc0b90f071501b662d8e7c9ac7548b88daba6b3deedfdf33f22ed8d95b",
"digest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
"stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"
@ -63,5 +87,10 @@
"name": "",
"version": "",
"idLike": ""
},
"descriptor": {
"name": "syft",
"version": "[not provided]",
"scope": "AllLayers"
}
}

View File

@ -25,16 +25,16 @@ type Presenter interface {
}
// GetPresenter returns a presenter for images or directories
func GetPresenter(option Option, s source.Source, catalog *pkg.Catalog, d *distro.Distro) Presenter {
func GetPresenter(option Option, srcMetadata source.Metadata, catalog *pkg.Catalog, d *distro.Distro) Presenter {
switch option {
case JSONPresenter:
return json.NewPresenter(catalog, s, *d)
return json.NewPresenter(catalog, srcMetadata, *d)
case TextPresenter:
return text.NewPresenter(catalog, s)
return text.NewPresenter(catalog, srcMetadata)
case TablePresenter:
return table.NewPresenter(catalog, s)
return table.NewPresenter(catalog)
case CycloneDxPresenter:
return cyclonedx.NewPresenter(catalog, s, *d)
return cyclonedx.NewPresenter(catalog, srcMetadata, *d)
default:
return nil
}

View File

@ -9,18 +9,15 @@ import (
"github.com/olekukonko/tablewriter"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
type Presenter struct {
catalog *pkg.Catalog
source source.Source
}
func NewPresenter(catalog *pkg.Catalog, s source.Source) *Presenter {
func NewPresenter(catalog *pkg.Catalog) *Presenter {
return &Presenter{
catalog: catalog,
source: s,
}
}

View File

@ -43,11 +43,10 @@ func TestTablePresenter(t *testing.T) {
Type: pkg.DebPkg,
})
s, err := source.NewFromImage(img, source.AllLayersScope)
pres := NewPresenter(catalog, s)
pres := NewPresenter(catalog)
// run presenter
err = pres.Present(&buffer)
err := pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}

View File

@ -12,13 +12,13 @@ import (
type Presenter struct {
catalog *pkg.Catalog
source source.Source
srcMetadata source.Metadata
}
func NewPresenter(catalog *pkg.Catalog, s source.Source) *Presenter {
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter {
return &Presenter{
catalog: catalog,
source: s,
srcMetadata: srcMetadata,
}
}
@ -28,22 +28,22 @@ func (pres *Presenter) Present(output io.Writer) error {
w := new(tabwriter.Writer)
w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight)
switch src := pres.source.Target.(type) {
case source.DirSource:
fmt.Fprintln(w, fmt.Sprintf("[Path: %s]", src.Path))
case source.ImageSource:
switch pres.srcMetadata.Scheme {
case source.DirectoryScheme:
fmt.Fprintln(w, fmt.Sprintf("[Path: %s]", pres.srcMetadata.Path))
case source.ImageScheme:
fmt.Fprintln(w, "[Image]")
for idx, l := range src.Img.Layers {
for idx, l := range pres.srcMetadata.ImageMetadata.Layers {
fmt.Fprintln(w, " Layer:\t", idx)
fmt.Fprintln(w, " Digest:\t", l.Metadata.Digest)
fmt.Fprintln(w, " Size:\t", l.Metadata.Size)
fmt.Fprintln(w, " MediaType:\t", l.Metadata.MediaType)
fmt.Fprintln(w, " Digest:\t", l.Digest)
fmt.Fprintln(w, " Size:\t", l.Size)
fmt.Fprintln(w, " MediaType:\t", l.MediaType)
fmt.Fprintln(w)
w.Flush()
}
default:
return fmt.Errorf("unsupported source: %T", src)
return fmt.Errorf("unsupported source: %T", pres.srcMetadata.Scheme)
}
// populate artifacts...

View File

@ -35,7 +35,7 @@ func TestTextDirPresenter(t *testing.T) {
if err != nil {
t.Fatalf("unable to create source: %+v", err)
}
pres := NewPresenter(catalog, s)
pres := NewPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
@ -97,11 +97,11 @@ func TestTextImgPresenter(t *testing.T) {
l.Metadata.Digest = "sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53"
}
s, err := source.NewFromImage(img, source.AllLayersScope)
s, err := source.NewFromImage(img, source.AllLayersScope, "user-image-input")
if err != nil {
t.Fatal(err)
}
pres := NewPresenter(catalog, s)
pres := NewPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
if err != nil {

View File

@ -0,0 +1,44 @@
package source
import "github.com/anchore/stereoscope/pkg/image"
type ImageMetadata struct {
UserInput string `json:"userInput"`
Layers []LayerMetadata `json:"layers"`
Size int64 `json:"size"`
Digest string `json:"digest"`
MediaType string `json:"mediaType"`
Tags []string `json:"tags"`
}
type LayerMetadata struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
func NewImageMetadata(img *image.Image, userInput string) ImageMetadata {
// populate artifacts...
tags := make([]string, len(img.Metadata.Tags))
for idx, tag := range img.Metadata.Tags {
tags[idx] = tag.String()
}
theImg := ImageMetadata{
UserInput: userInput,
Digest: img.Metadata.Digest,
Size: img.Metadata.Size,
MediaType: string(img.Metadata.MediaType),
Tags: tags,
Layers: make([]LayerMetadata, len(img.Layers)),
}
// populate image metadata
for idx, l := range img.Layers {
theImg.Layers[idx] = LayerMetadata{
MediaType: string(l.Metadata.MediaType),
Digest: l.Metadata.Digest,
Size: l.Metadata.Size,
}
}
return theImg
}

View File

@ -9,8 +9,7 @@ import (
type Location struct {
Path string `json:"path"`
LayerIndex uint `json:"layerIndex"`
LayerID string `json:"layerID"`
FileSystemID string `json:"layerID,omitempty"` // TODO: comment
ref file.Reference
}
@ -32,8 +31,7 @@ func NewLocationFromImage(ref file.Reference, img *image.Image) Location {
return Location{
Path: string(ref.Path),
LayerIndex: entry.Source.Metadata.Index,
LayerID: entry.Source.Metadata.Digest,
FileSystemID: entry.Source.Metadata.Digest,
ref: ref,
}
}

8
syft/source/metadata.go Normal file
View File

@ -0,0 +1,8 @@
package source
type Metadata struct {
Scope Scope // specific perspective to catalog
Scheme Scheme // the source data scheme type (directory or image)
ImageMetadata ImageMetadata // all image info (image only)
Path string // the root path to be cataloged (directory only)
}

View File

@ -2,21 +2,15 @@ package source
import "strings"
type Scope string
const (
UnknownScope Scope = iota
SquashedScope
AllLayersScope
UnknownScope Scope = "UnknownScope"
SquashedScope Scope = "Squashed"
AllLayersScope Scope = "AllLayers"
)
type Scope int
var optionStr = []string{
"UnknownScope",
"Squashed",
"AllLayers",
}
var Options = []Scope{
var AllScopes = []Scope{
SquashedScope,
AllLayersScope,
}
@ -32,9 +26,5 @@ func ParseScope(userStr string) Scope {
}
func (o Scope) String() string {
if int(o) >= len(optionStr) || o < 0 {
return optionStr[0]
}
return optionStr[o]
return string(o)
}

View File

@ -1,17 +0,0 @@
package source
import (
"fmt"
"testing"
)
func TestOptionStringerBoundary(t *testing.T) {
var _ fmt.Stringer = Scope(0)
for _, c := range []int{-1, 0, 3} {
option := Scope(c)
if option.String() != UnknownScope.String() {
t.Errorf("expected Scope(%d) to be unknown, found '%+v'", c, option)
}
}
}

View File

@ -15,29 +15,18 @@ import (
"github.com/anchore/stereoscope/pkg/image"
)
// ImageSource represents a data source that is a container image
type ImageSource struct {
Img *image.Image // the image object to be cataloged
}
// DirSource represents a data source that is a filesystem directory tree
type DirSource struct {
Path string // the root path to be cataloged
}
// Source is an object that captures the data source to be cataloged, configuration, and a specific resolver used
// in cataloging (based on the data source and configuration)
type Source struct {
Scope Scope // specific perspective to catalog
Resolver Resolver // a Resolver object to use in file path/glob resolution and file contents resolution
Target interface{} // the specific source object to be cataloged
Scheme Scheme // the source data scheme type (directory or image)
Image *image.Image // the image object to be cataloged (image only)
Metadata Metadata
}
type sourceDetector func(string) (image.Source, string, error)
// NewSource produces a Source based on userInput like dir: or image:tag
func NewSource(userInput string, o Scope) (Source, func(), error) {
// New produces a Source based on userInput like dir: or image:tag
func New(userInput string, o Scope) (Source, func(), error) {
fs := afero.NewOsFs()
parsedScheme, location, err := detectScheme(fs, image.DetectSource, userInput)
if err != nil {
@ -71,7 +60,7 @@ func NewSource(userInput string, o Scope) (Source, func(), error) {
return Source{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", location, err)
}
s, err := NewFromImage(img, o)
s, err := NewFromImage(img, o, location)
if err != nil {
return Source{}, cleanup, fmt.Errorf("could not populate source with image: %w", err)
}
@ -87,16 +76,16 @@ func NewFromDirectory(path string) (Source, error) {
Resolver: &DirectoryResolver{
Path: path,
},
Target: DirSource{
Metadata: Metadata{
Scheme: DirectoryScheme,
Path: path,
},
Scheme: DirectoryScheme,
}, nil
}
// NewFromImage creates a new source object tailored to catalog a given container image, relative to the
// option given (e.g. all-layers, squashed, etc)
func NewFromImage(img *image.Image, option Scope) (Source, error) {
func NewFromImage(img *image.Image, option Scope, userImageStr string) (Source, error) {
if img == nil {
return Source{}, fmt.Errorf("no image given")
}
@ -107,11 +96,12 @@ func NewFromImage(img *image.Image, option Scope) (Source, error) {
}
return Source{
Scope: option,
Resolver: resolver,
Target: ImageSource{
Img: img,
},
Image: img,
Metadata: Metadata{
Scope: option,
Scheme: ImageScheme,
ImageMetadata: NewImageMetadata(img, userImageStr),
},
}, nil
}

View File

@ -9,41 +9,41 @@ import (
"github.com/spf13/afero"
)
func TestNewScopeFromImageFails(t *testing.T) {
func TestNewFromImageFails(t *testing.T) {
t.Run("no image given", func(t *testing.T) {
_, err := NewFromImage(nil, AllLayersScope)
_, err := NewFromImage(nil, AllLayersScope, "")
if err == nil {
t.Errorf("expected an error condition but none was given")
}
})
}
func TestNewScopeFromImageUnknownOption(t *testing.T) {
func TestNewFromImageUnknownOption(t *testing.T) {
img := image.Image{}
t.Run("unknown option is an error", func(t *testing.T) {
_, err := NewFromImage(&img, UnknownScope)
_, err := NewFromImage(&img, UnknownScope, "")
if err == nil {
t.Errorf("expected an error condition but none was given")
}
})
}
func TestNewScopeFromImage(t *testing.T) {
func TestNewFromImage(t *testing.T) {
layer := image.NewLayer(nil)
img := image.Image{
Layers: []*image.Layer{layer},
}
t.Run("create a new Locations object from image", func(t *testing.T) {
_, err := NewFromImage(&img, AllLayersScope)
_, err := NewFromImage(&img, AllLayersScope, "")
if err != nil {
t.Errorf("unexpected error when creating a new Locations from img: %+v", err)
}
})
}
func TestDirectoryScope(t *testing.T) {
func TestNewFromDirectory(t *testing.T) {
testCases := []struct {
desc string
input string
@ -78,16 +78,16 @@ func TestDirectoryScope(t *testing.T) {
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
p, err := NewFromDirectory(test.input)
src, err := NewFromDirectory(test.input)
if err != nil {
t.Errorf("could not create NewDirScope: %+v", err)
}
if p.Target.(DirSource).Path != test.input {
t.Errorf("mismatched stringer: '%s' != '%s'", p.Target.(DirSource).Path, test.input)
if src.Metadata.Path != test.input {
t.Errorf("mismatched stringer: '%s' != '%s'", src.Metadata.Path, test.input)
}
refs, err := p.Resolver.FilesByPath(test.inputPaths...)
refs, err := src.Resolver.FilesByPath(test.inputPaths...)
if err != nil {
t.Errorf("FilesByPath call produced an error: %+v", err)
}
@ -100,7 +100,7 @@ func TestDirectoryScope(t *testing.T) {
}
}
func TestMultipleFileContentsByRefContents(t *testing.T) {
func TestMultipleFileContentsByLocation(t *testing.T) {
testCases := []struct {
desc string
input string
@ -136,7 +136,8 @@ func TestMultipleFileContentsByRefContents(t *testing.T) {
}
location := locations[0]
content, err := p.Resolver.FileContentsByLocation(location)
contents, err := p.Resolver.MultipleFileContentsByLocation([]Location{location})
content := contents[location]
if content != test.expected {
t.Errorf("unexpected contents from file: '%s' != '%s'", content, test.expected)
@ -146,7 +147,7 @@ func TestMultipleFileContentsByRefContents(t *testing.T) {
}
}
func TestMultipleFileContentsByRefNoContents(t *testing.T) {
func TestFilesByPathDoesNotExist(t *testing.T) {
testCases := []struct {
desc string
input string

View File

@ -37,7 +37,7 @@ func TestCatalogFromJSON(t *testing.T) {
}
var buf bytes.Buffer
jsonPres := json.NewPresenter(expectedCatalog, *s, *expectedDistro)
jsonPres := json.NewPresenter(expectedCatalog, s.Metadata, *expectedDistro)
if err = jsonPres.Present(&buf); err != nil {
t.Fatalf("failed to write to presenter: %+v", err)
}
@ -73,8 +73,6 @@ func TestCatalogFromJSON(t *testing.T) {
a := actualPackages[i]
// omit fields that should be missing
e.Locations = nil
e.FoundBy = ""
if e.MetadataType == pkg.JavaMetadataType {
metadata := e.Metadata.(pkg.JavaMetadata)
metadata.Parent = nil

View File

@ -67,7 +67,7 @@ func testJsonSchema(t *testing.T, catalog *pkg.Catalog, theScope *source.Source,
t.Fatalf("bad distro: %+v", err)
}
p := presenter.GetPresenter(presenter.JSONPresenter, *theScope, catalog, &d)
p := presenter.GetPresenter(presenter.JSONPresenter, theScope.Metadata, catalog, &d)
if p == nil {
t.Fatal("unable to get presenter")
}