diff --git a/cmd/cmd.go b/cmd/cmd.go index 0c4a36327..70197c968 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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) diff --git a/cmd/root.go b/cmd/root.go index ffa936f90..3abccd404 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 diff --git a/schema/json/schema.json b/schema/json/schema.json index 556fd50e7..01f9e3e10 100644 --- a/schema/json/schema.json +++ b/schema/json/schema.json @@ -5,10 +5,7 @@ "items": { "properties": { "foundBy": { - "items": { - "type": "string" - }, - "type": "array" + "type": "string" }, "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" + "items": { + "properties": { + "layerID": { + "type": "string" }, - "type": "array" - } - ] + "path": { + "type": "string" + } + }, + "required": [ + "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" } diff --git a/syft/cataloger/deb/cataloger_test.go b/syft/cataloger/deb/cataloger_test.go index d340316ed..9a3e390c7 100644 --- a/syft/cataloger/deb/cataloger_test.go +++ b/syft/cataloger/deb/cataloger_test.go @@ -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 diff --git a/syft/cataloger/rpmdb/cataloger.go b/syft/cataloger/rpmdb/cataloger.go index 3ec99e112..c9234a1c7 100644 --- a/syft/cataloger/rpmdb/cataloger.go +++ b/syft/cataloger/rpmdb/cataloger.go @@ -12,7 +12,8 @@ import ( ) const ( - packagesGlob = "**/var/lib/rpm/Packages" + 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. diff --git a/syft/cataloger/rpmdb/parse_rpmdb.go b/syft/cataloger/rpmdb/parse_rpmdb.go index 05b6ec157..fcf251cd0 100644 --- a/syft/cataloger/rpmdb/parse_rpmdb.go +++ b/syft/cataloger/rpmdb/parse_rpmdb.go @@ -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{ diff --git a/syft/cataloger/rpmdb/parse_rpmdb_test.go b/syft/cataloger/rpmdb/parse_rpmdb_test.go index 2b89e7dff..a80fa2523 100644 --- a/syft/cataloger/rpmdb/parse_rpmdb_test.go +++ b/syft/cataloger/rpmdb/parse_rpmdb_test.go @@ -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) } diff --git a/syft/lib.go b/syft/lib.go index 930669371..9a1d81675 100644 --- a/syft/lib.go +++ b/syft/lib.go @@ -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...) diff --git a/syft/pkg/dpkg_metadata.go b/syft/pkg/dpkg_metadata.go index a29bf8169..f1e80802e 100644 --- a/syft/pkg/dpkg_metadata.go +++ b/syft/pkg/dpkg_metadata.go @@ -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"` diff --git a/syft/pkg/package.go b/syft/pkg/package.go index 525e5e893..7e97ee8a8 100644 --- a/syft/pkg/package.go +++ b/syft/pkg/package.go @@ -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. diff --git a/syft/presenter/cyclonedx/presenter.go b/syft/presenter/cyclonedx/presenter.go index b73052d51..2a28b8db7 100644 --- a/syft/presenter/cyclonedx/presenter.go +++ b/syft/presenter/cyclonedx/presenter.go @@ -16,17 +16,17 @@ import ( // Presenter writes a CycloneDX report from the given Catalog and Locations contents type Presenter struct { - catalog *pkg.Catalog - source source.Source - distro distro.Distro + catalog *pkg.Catalog + 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, - distro: d, + catalog: catalog, + 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) diff --git a/syft/presenter/cyclonedx/presenter_test.go b/syft/presenter/cyclonedx/presenter_test.go index 90ca5b309..ee29ca62c 100644 --- a/syft/presenter/cyclonedx/presenter_test.go +++ b/syft/presenter/cyclonedx/presenter_test.go @@ -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)) } } diff --git a/syft/presenter/json/artifact.go b/syft/presenter/json/artifact.go index 8a9e586d7..3aeea21ef 100644 --- a/syft/presenter/json/artifact.go +++ b/syft/presenter/json/artifact.go @@ -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, } diff --git a/syft/presenter/json/document.go b/syft/presenter/json/document.go index 78cb55ce2..2c20a1a38 100644 --- a/syft/presenter/json/document.go +++ b/syft/presenter/json/document.go @@ -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" @@ -19,10 +17,9 @@ type Document struct { // Descriptor describes what created the document as well as surrounding metadata 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) + Name string `json:"name"` + Version string `json:"version"` + 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 } @@ -52,14 +49,14 @@ func NewDocument(catalog *pkg.Catalog, s source.Source, d distro.Distro) (Docume IDLike: d.IDLike, }, Descriptor: Descriptor{ - Name: internal.ApplicationName, - Version: version.FromBuild().Version, - ReportTimestamp: time.Now().Format(time.RFC3339), + Name: internal.ApplicationName, + Version: version.FromBuild().Version, + Scope: srcMetadata.Scope.String(), }, } for _, p := range catalog.Sorted() { - art, err := NewArtifact(p, s) + art, err := NewArtifact(p) if err != nil { return Document{}, err } diff --git a/syft/presenter/json/image.go b/syft/presenter/json/image.go deleted file mode 100644 index ee7796421..000000000 --- a/syft/presenter/json/image.go +++ /dev/null @@ -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 -} diff --git a/syft/presenter/json/presenter.go b/syft/presenter/json/presenter.go index beb2e689b..00af4ef4e 100644 --- a/syft/presenter/json/presenter.go +++ b/syft/presenter/json/presenter.go @@ -10,21 +10,21 @@ import ( ) type Presenter struct { - catalog *pkg.Catalog - source source.Source - distro distro.Distro + catalog *pkg.Catalog + 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, - distro: d, + catalog: catalog, + 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 } diff --git a/syft/presenter/json/presenter_test.go b/syft/presenter/json/presenter_test.go index 94d052dae..57869e08e 100644 --- a/syft/presenter/json/presenter_test.go +++ b/syft/presenter/json/presenter_test.go @@ -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)) } } diff --git a/syft/presenter/json/source.go b/syft/presenter/json/source.go index 6172f9bb4..14561a24f 100644 --- a/syft/presenter/json/source.go +++ b/syft/presenter/json/source.go @@ -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 } diff --git a/syft/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden b/syft/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden index 2193e7875..7324c5349 100644 --- a/syft/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden +++ b/syft/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden @@ -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": "" } } diff --git a/syft/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden b/syft/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden index f3c7f56a2..91b28d91e 100644 --- a/syft/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden +++ b/syft/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden @@ -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" } } diff --git a/syft/presenter/json/test-fixtures/snapshot/anchore-fixture-image-simple.golden b/syft/presenter/json/test-fixtures/snapshot/anchore-fixture-image-simple.golden deleted file mode 100644 index 3e1d2daaa..000000000 Binary files a/syft/presenter/json/test-fixtures/snapshot/anchore-fixture-image-simple.golden and /dev/null differ diff --git a/syft/presenter/json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/syft/presenter/json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index c98a9207c..4e7ce36e0 100644 Binary files a/syft/presenter/json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden and b/syft/presenter/json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/syft/presenter/presenter.go b/syft/presenter/presenter.go index 56f00f9e6..4f1b86224 100644 --- a/syft/presenter/presenter.go +++ b/syft/presenter/presenter.go @@ -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 } diff --git a/syft/presenter/table/presenter.go b/syft/presenter/table/presenter.go index 6af222559..c7303e8a5 100644 --- a/syft/presenter/table/presenter.go +++ b/syft/presenter/table/presenter.go @@ -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, } } diff --git a/syft/presenter/table/presenter_test.go b/syft/presenter/table/presenter_test.go index 160e95169..02dd2f5d4 100644 --- a/syft/presenter/table/presenter_test.go +++ b/syft/presenter/table/presenter_test.go @@ -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) } diff --git a/syft/presenter/text/presenter.go b/syft/presenter/text/presenter.go index aeaf26d67..57764eb8b 100644 --- a/syft/presenter/text/presenter.go +++ b/syft/presenter/text/presenter.go @@ -11,14 +11,14 @@ import ( ) type Presenter struct { - catalog *pkg.Catalog - source source.Source + catalog *pkg.Catalog + 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, + catalog: catalog, + 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... diff --git a/syft/presenter/text/presenter_test.go b/syft/presenter/text/presenter_test.go index 9144977de..4ca2f756b 100644 --- a/syft/presenter/text/presenter_test.go +++ b/syft/presenter/text/presenter_test.go @@ -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 { diff --git a/syft/source/image_metadata.go b/syft/source/image_metadata.go new file mode 100644 index 000000000..2a9224d7a --- /dev/null +++ b/syft/source/image_metadata.go @@ -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 +} diff --git a/syft/source/location.go b/syft/source/location.go index f9287d0ab..1fc1250ae 100644 --- a/syft/source/location.go +++ b/syft/source/location.go @@ -8,10 +8,9 @@ import ( ) type Location struct { - Path string `json:"path"` - LayerIndex uint `json:"layerIndex"` - LayerID string `json:"layerID"` - ref file.Reference + Path string `json:"path"` + FileSystemID string `json:"layerID,omitempty"` // TODO: comment + ref file.Reference } func NewLocation(path string) Location { @@ -31,9 +30,8 @@ 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, - ref: ref, + Path: string(ref.Path), + FileSystemID: entry.Source.Metadata.Digest, + ref: ref, } } diff --git a/syft/source/metadata.go b/syft/source/metadata.go new file mode 100644 index 000000000..188118b7b --- /dev/null +++ b/syft/source/metadata.go @@ -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) +} diff --git a/syft/source/scope.go b/syft/source/scope.go index 0df5d7f23..92cb0b9b1 100644 --- a/syft/source/scope.go +++ b/syft/source/scope.go @@ -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) } diff --git a/syft/source/scope_test.go b/syft/source/scope_test.go deleted file mode 100644 index 0c210e013..000000000 --- a/syft/source/scope_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/syft/source/source.go b/syft/source/source.go index b3e49d22d..91ce1009e 100644 --- a/syft/source/source.go +++ b/syft/source/source.go @@ -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) + Resolver Resolver // a Resolver object to use in file path/glob resolution and file contents resolution + 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{ - Path: path, + 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), }, - Scheme: ImageScheme, }, nil } diff --git a/syft/source/source_test.go b/syft/source/source_test.go index c96aadfaf..30913c91d 100644 --- a/syft/source/source_test.go +++ b/syft/source/source_test.go @@ -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 diff --git a/test/integration/document_import_test.go b/test/integration/document_import_test.go index 438cce148..50be36c9c 100644 --- a/test/integration/document_import_test.go +++ b/test/integration/document_import_test.go @@ -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 diff --git a/test/integration/json_schema_test.go b/test/integration/json_schema_test.go index 15970ee0e..b84bf26f8 100644 --- a/test/integration/json_schema_test.go +++ b/test/integration/json_schema_test.go @@ -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") }