mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
migrate syft/presenter to internal/presenter
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
4666ca8469
commit
ff4ed40d50
69
internal/presenter/packages/cyclonedx_bom_descriptor.go
Normal file
69
internal/presenter/packages/cyclonedx_bom_descriptor.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Source: https://cyclonedx.org/ext/bom-descriptor/
|
||||||
|
|
||||||
|
// CycloneDxBomDescriptor represents all metadata surrounding the BOM report (such as when the BOM was made, with which tool, and the item being cataloged).
|
||||||
|
type CycloneDxBomDescriptor struct {
|
||||||
|
XMLName xml.Name `xml:"metadata"`
|
||||||
|
Timestamp string `xml:"timestamp,omitempty"` // The date and time (timestamp) when the document was created
|
||||||
|
Tools []CycloneDxBdTool `xml:"tools>tool"` // The tool used to create the BOM.
|
||||||
|
Component *CycloneDxBdComponent `xml:"component"` // The component that the BOM describes.
|
||||||
|
}
|
||||||
|
|
||||||
|
// CycloneDxBdTool represents the tool that created the BOM report.
|
||||||
|
type CycloneDxBdTool struct {
|
||||||
|
XMLName xml.Name `xml:"tool"`
|
||||||
|
Vendor string `xml:"vendor,omitempty"` // The vendor of the tool used to create the BOM.
|
||||||
|
Name string `xml:"name,omitempty"` // The name of the tool used to create the BOM.
|
||||||
|
Version string `xml:"version,omitempty"` // The version of the tool used to create the BOM.
|
||||||
|
// TODO: hashes, author, manufacture, supplier
|
||||||
|
// TODO: add user-defined fields for the remaining build/version parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// CycloneDxBdComponent represents the software/package being cataloged.
|
||||||
|
type CycloneDxBdComponent struct {
|
||||||
|
XMLName xml.Name `xml:"component"`
|
||||||
|
CycloneDxComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCycloneDxBomDescriptor returns a new CycloneDxBomDescriptor tailored for the current time and "syft" tool details.
|
||||||
|
func NewCycloneDxBomDescriptor(name, version string, srcMetadata source.Metadata) *CycloneDxBomDescriptor {
|
||||||
|
descriptor := CycloneDxBomDescriptor{
|
||||||
|
XMLName: xml.Name{},
|
||||||
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
Tools: []CycloneDxBdTool{
|
||||||
|
{
|
||||||
|
Vendor: "anchore",
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch srcMetadata.Scheme {
|
||||||
|
case source.ImageScheme:
|
||||||
|
descriptor.Component = &CycloneDxBdComponent{
|
||||||
|
CycloneDxComponent: CycloneDxComponent{
|
||||||
|
Type: "container",
|
||||||
|
Name: srcMetadata.ImageMetadata.UserInput,
|
||||||
|
Version: srcMetadata.ImageMetadata.ManifestDigest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case source.DirectoryScheme:
|
||||||
|
descriptor.Component = &CycloneDxBdComponent{
|
||||||
|
CycloneDxComponent: CycloneDxComponent{
|
||||||
|
Type: "file",
|
||||||
|
Name: srcMetadata.Path,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &descriptor
|
||||||
|
}
|
||||||
27
internal/presenter/packages/cyclonedx_component.go
Normal file
27
internal/presenter/packages/cyclonedx_component.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package packages
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// CycloneDxComponent represents a single element in the CycloneDX BOM
|
||||||
|
type CycloneDxComponent struct {
|
||||||
|
XMLName xml.Name `xml:"component"`
|
||||||
|
Type string `xml:"type,attr"` // Required; Describes if the component is a library, framework, application, container, operating system, firmware, hardware device, or file
|
||||||
|
Supplier string `xml:"supplier,omitempty"` // The organization that supplied the component. The supplier may often be the manufacture, but may also be a distributor or repackager.
|
||||||
|
Author string `xml:"author,omitempty"` // The person(s) or organization(s) that authored the component
|
||||||
|
Publisher string `xml:"publisher,omitempty"` // The person(s) or organization(s) that published the component
|
||||||
|
Group string `xml:"group,omitempty"` // The high-level classification that a project self-describes as. This will often be a shortened, single name of the company or project that produced the component, or the source package or domain name.
|
||||||
|
Name string `xml:"name"` // Required; The name of the component as defined by the project
|
||||||
|
Version string `xml:"version"` // Required; The version of the component as defined by the project
|
||||||
|
Description string `xml:"description,omitempty"` // A description of the component
|
||||||
|
Licenses *[]CycloneDxLicense `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions
|
||||||
|
PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec
|
||||||
|
// TODO: source, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
|
||||||
|
// TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CycloneDxLicense represents a single software license for a CycloneDxComponent
|
||||||
|
type CycloneDxLicense struct {
|
||||||
|
XMLName xml.Name `xml:"license"`
|
||||||
|
ID string `xml:"id,omitempty"` // A valid SPDX license ID
|
||||||
|
Name string `xml:"name,omitempty"` // If SPDX does not define the license used, this field may be used to provide the license name
|
||||||
|
}
|
||||||
57
internal/presenter/packages/cyclonedx_document.go
Normal file
57
internal/presenter/packages/cyclonedx_document.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/version"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Source: https://github.com/CycloneDX/specification
|
||||||
|
|
||||||
|
// CycloneDxDocument represents a CycloneDX BOM CycloneDxDocument.
|
||||||
|
type CycloneDxDocument struct {
|
||||||
|
XMLName xml.Name `xml:"bom"`
|
||||||
|
XMLNs string `xml:"xmlns,attr"`
|
||||||
|
Version int `xml:"version,attr"`
|
||||||
|
SerialNumber string `xml:"serialNumber,attr"`
|
||||||
|
BomDescriptor *CycloneDxBomDescriptor `xml:"metadata"` // The BOM descriptor extension
|
||||||
|
Components []CycloneDxComponent `xml:"components>component"` // The BOM contents
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCycloneDxDocument returns a CycloneDX CycloneDxDocument object populated with the catalog contents.
|
||||||
|
func NewCycloneDxDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) CycloneDxDocument {
|
||||||
|
versionInfo := version.FromBuild()
|
||||||
|
|
||||||
|
doc := CycloneDxDocument{
|
||||||
|
XMLNs: "http://cyclonedx.org/schema/bom/1.2",
|
||||||
|
Version: 1,
|
||||||
|
SerialNumber: uuid.New().URN(),
|
||||||
|
BomDescriptor: NewCycloneDxBomDescriptor(internal.ApplicationName, versionInfo.Version, srcMetadata),
|
||||||
|
}
|
||||||
|
|
||||||
|
// attach components
|
||||||
|
for p := range catalog.Enumerate() {
|
||||||
|
component := CycloneDxComponent{
|
||||||
|
Type: "library", // TODO: this is not accurate
|
||||||
|
Name: p.Name,
|
||||||
|
Version: p.Version,
|
||||||
|
PackageURL: p.PURL,
|
||||||
|
}
|
||||||
|
var licenses []CycloneDxLicense
|
||||||
|
for _, licenseName := range p.Licenses {
|
||||||
|
licenses = append(licenses, CycloneDxLicense{
|
||||||
|
Name: licenseName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(licenses) > 0 {
|
||||||
|
component.Licenses = &licenses
|
||||||
|
}
|
||||||
|
doc.Components = append(doc.Components, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Package cyclonedx is responsible for generating a CycloneDX XML report for the given container image or file system.
|
Package cyclonedx is responsible for generating a CycloneDX XML report for the given container image or file system.
|
||||||
*/
|
*/
|
||||||
package cyclonedx
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@ -11,23 +11,23 @@ import (
|
|||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Presenter writes a CycloneDX report from the given Catalog and Locations contents
|
// CycloneDxPresenter writes a CycloneDX report from the given Catalog and Locations contents
|
||||||
type Presenter struct {
|
type CycloneDxPresenter struct {
|
||||||
catalog *pkg.Catalog
|
catalog *pkg.Catalog
|
||||||
srcMetadata source.Metadata
|
srcMetadata source.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPresenter creates a CycloneDX presenter from the given Catalog and Locations objects.
|
// NewCycloneDxPresenter creates a CycloneDX presenter from the given Catalog and Locations objects.
|
||||||
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter {
|
func NewCycloneDxPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *CycloneDxPresenter {
|
||||||
return &Presenter{
|
return &CycloneDxPresenter{
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
srcMetadata: srcMetadata,
|
srcMetadata: srcMetadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present writes the CycloneDX report to the given io.Writer.
|
// Present writes the CycloneDX report to the given io.Writer.
|
||||||
func (pres *Presenter) Present(output io.Writer) error {
|
func (pres *CycloneDxPresenter) Present(output io.Writer) error {
|
||||||
bom := NewDocument(pres.catalog, pres.srcMetadata)
|
bom := NewCycloneDxDocument(pres.catalog, pres.srcMetadata)
|
||||||
|
|
||||||
encoder := xml.NewEncoder(output)
|
encoder := xml.NewEncoder(output)
|
||||||
encoder.Indent("", " ")
|
encoder.Indent("", " ")
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package cyclonedx
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -62,7 +62,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pres := NewPresenter(catalog, s.Metadata)
|
pres := NewCycloneDxPresenter(catalog, s.Metadata)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -93,8 +93,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
|
|||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||||
@ -125,7 +124,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
|
|||||||
PURL: "the-purl-2",
|
PURL: "the-purl-2",
|
||||||
})
|
})
|
||||||
|
|
||||||
s, err := source.NewFromImage(img, source.SquashedScope, "user-image-input")
|
s, err := source.NewFromImage(img, "user-image-input")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -138,7 +137,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
|
|||||||
// This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden"
|
// This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden"
|
||||||
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||||
|
|
||||||
pres := NewPresenter(catalog, s.Metadata)
|
pres := NewCycloneDxPresenter(catalog, s.Metadata)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -1,21 +1,21 @@
|
|||||||
package json
|
package packages
|
||||||
|
|
||||||
import "github.com/anchore/syft/syft/distro"
|
import "github.com/anchore/syft/syft/distro"
|
||||||
|
|
||||||
// Distribution provides information about a detected Linux Distribution.
|
// JSONDistribution provides information about a detected Linux JSONDistribution.
|
||||||
type Distribution struct {
|
type JSONDistribution struct {
|
||||||
Name string `json:"name"` // Name of the Linux distribution
|
Name string `json:"name"` // Name of the Linux distribution
|
||||||
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
|
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
|
||||||
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
|
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDistribution creates a struct with the Linux distribution to be represented in JSON.
|
// NewJSONDistribution creates a struct with the Linux distribution to be represented in JSON.
|
||||||
func NewDistribution(d *distro.Distro) Distribution {
|
func NewJSONDistribution(d *distro.Distro) JSONDistribution {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return Distribution{}
|
return JSONDistribution{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Distribution{
|
return JSONDistribution{
|
||||||
Name: d.Name(),
|
Name: d.Name(),
|
||||||
Version: d.FullVersion(),
|
Version: d.FullVersion(),
|
||||||
IDLike: d.IDLike,
|
IDLike: d.IDLike,
|
||||||
62
internal/presenter/packages/json_document.go
Normal file
62
internal/presenter/packages/json_document.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/version"
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONDocument represents the syft cataloging findings as a JSON document
|
||||||
|
type JSONDocument struct {
|
||||||
|
Artifacts []JSONPackage `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
|
||||||
|
ArtifactRelationships []JSONRelationship `json:"artifactRelationships"`
|
||||||
|
Source JSONSource `json:"source"` // Source represents the original object that was cataloged
|
||||||
|
Distro JSONDistribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
|
||||||
|
Descriptor JSONDescriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
|
||||||
|
Schema JSONSchema `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
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
|
||||||
|
func NewJSONDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro, scope source.Scope, configuration interface{}) (JSONDocument, error) {
|
||||||
|
src, err := NewJSONSource(srcMetadata, scope)
|
||||||
|
if err != nil {
|
||||||
|
return JSONDocument{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
artifacts, err := NewJSONPackages(catalog)
|
||||||
|
if err != nil {
|
||||||
|
return JSONDocument{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSONDocument{
|
||||||
|
Artifacts: artifacts,
|
||||||
|
ArtifactRelationships: newJSONRelationships(pkg.NewRelationships(catalog)),
|
||||||
|
Source: src,
|
||||||
|
Distro: NewJSONDistribution(d),
|
||||||
|
Descriptor: JSONDescriptor{
|
||||||
|
Name: internal.ApplicationName,
|
||||||
|
Version: version.FromBuild().Version,
|
||||||
|
Configuration: configuration,
|
||||||
|
},
|
||||||
|
Schema: JSONSchema{
|
||||||
|
Version: internal.JSONSchemaVersion,
|
||||||
|
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONDescriptor describes what created the document as well as surrounding metadata
|
||||||
|
type JSONDescriptor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Configuration interface{} `json:"configuration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONSchema struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
68
internal/presenter/packages/json_package.go
Normal file
68
internal/presenter/packages/json_package.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONPackage represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
|
||||||
|
type JSONPackage struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
FoundBy string `json:"foundBy"`
|
||||||
|
Locations []source.Location `json:"locations"`
|
||||||
|
Licenses []string `json:"licenses"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
CPEs []string `json:"cpes"`
|
||||||
|
PURL string `json:"purl"`
|
||||||
|
MetadataType string `json:"metadataType"`
|
||||||
|
Metadata interface{} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJSONPackages(catalog *pkg.Catalog) ([]JSONPackage, error) {
|
||||||
|
artifacts := make([]JSONPackage, 0)
|
||||||
|
for _, p := range catalog.Sorted() {
|
||||||
|
art, err := NewJSONPackage(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
artifacts = append(artifacts, art)
|
||||||
|
}
|
||||||
|
return artifacts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSONPackage crates a new JSONPackage from the given pkg.Package.
|
||||||
|
func NewJSONPackage(p *pkg.Package) (JSONPackage, error) {
|
||||||
|
var cpes = make([]string, len(p.CPEs))
|
||||||
|
for i, c := range p.CPEs {
|
||||||
|
cpes[i] = c.BindToFmtString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure collections are never nil for presentation reasons
|
||||||
|
var locations = make([]source.Location, 0)
|
||||||
|
if p.Locations != nil {
|
||||||
|
locations = p.Locations
|
||||||
|
}
|
||||||
|
|
||||||
|
var licenses = make([]string, 0)
|
||||||
|
if p.Licenses != nil {
|
||||||
|
licenses = p.Licenses
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSONPackage{
|
||||||
|
ID: string(p.ID),
|
||||||
|
Name: p.Name,
|
||||||
|
Version: p.Version,
|
||||||
|
Type: string(p.Type),
|
||||||
|
FoundBy: p.FoundBy,
|
||||||
|
Locations: locations,
|
||||||
|
Licenses: licenses,
|
||||||
|
Language: string(p.Language),
|
||||||
|
CPEs: cpes,
|
||||||
|
PURL: p.PURL,
|
||||||
|
MetadataType: string(p.MetadataType),
|
||||||
|
Metadata: p.Metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
43
internal/presenter/packages/json_presenter.go
Normal file
43
internal/presenter/packages/json_presenter.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONPresenter is a JSON presentation object for the syft results
|
||||||
|
type JSONPresenter struct {
|
||||||
|
catalog *pkg.Catalog
|
||||||
|
srcMetadata source.Metadata
|
||||||
|
distro *distro.Distro
|
||||||
|
scope source.Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
|
||||||
|
func NewJSONPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro, scope source.Scope) *JSONPresenter {
|
||||||
|
return &JSONPresenter{
|
||||||
|
catalog: catalog,
|
||||||
|
srcMetadata: s,
|
||||||
|
distro: d,
|
||||||
|
scope: scope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present the catalog results to the given writer.
|
||||||
|
func (pres *JSONPresenter) Present(output io.Writer) error {
|
||||||
|
// we do not pass in configuration for backwards compatibility
|
||||||
|
doc, err := NewJSONDocument(pres.catalog, pres.srcMetadata, pres.distro, pres.scope, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(output)
|
||||||
|
// prevent > and < from being escaped in the payload
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(&doc)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package json
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
)
|
)
|
||||||
|
|
||||||
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
|
var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters")
|
||||||
|
|
||||||
func must(c pkg.CPE, e error) pkg.CPE {
|
func must(c pkg.CPE, e error) pkg.CPE {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
@ -24,7 +24,7 @@ func must(c pkg.CPE, e error) pkg.CPE {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJsonDirsPresenter(t *testing.T) {
|
func TestJSONDirsPresenter(t *testing.T) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
@ -75,7 +75,7 @@ func TestJsonDirsPresenter(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pres := NewPresenter(catalog, s.Metadata, d)
|
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -84,7 +84,7 @@ func TestJsonDirsPresenter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
if *update {
|
if *updateJSONGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,12 +98,12 @@ func TestJsonDirsPresenter(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJsonImgsPresenter(t *testing.T) {
|
func TestJSONImgsPresenter(t *testing.T) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
|
|
||||||
if *update {
|
if *updateJSONGoldenFiles {
|
||||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,9 +158,9 @@ func TestJsonImgsPresenter(t *testing.T) {
|
|||||||
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
||||||
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||||
|
|
||||||
s, err := source.NewFromImage(img, source.SquashedScope, "user-image-input")
|
s, err := source.NewFromImage(img, "user-image-input")
|
||||||
var d *distro.Distro
|
var d *distro.Distro
|
||||||
pres := NewPresenter(catalog, s.Metadata, d)
|
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -169,7 +169,7 @@ func TestJsonImgsPresenter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
if *update {
|
if *updateJSONGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,18 +1,18 @@
|
|||||||
package json
|
package packages
|
||||||
|
|
||||||
import "github.com/anchore/syft/syft/pkg"
|
import "github.com/anchore/syft/syft/pkg"
|
||||||
|
|
||||||
type Relationship struct {
|
type JSONRelationship struct {
|
||||||
Parent string `json:"parent"`
|
Parent string `json:"parent"`
|
||||||
Child string `json:"child"`
|
Child string `json:"child"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Metadata interface{} `json:"metadata"`
|
Metadata interface{} `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRelationships(relationships []pkg.Relationship) []Relationship {
|
func newJSONRelationships(relationships []pkg.Relationship) []JSONRelationship {
|
||||||
result := make([]Relationship, len(relationships))
|
result := make([]JSONRelationship, len(relationships))
|
||||||
for i, r := range relationships {
|
for i, r := range relationships {
|
||||||
result[i] = Relationship{
|
result[i] = JSONRelationship{
|
||||||
Parent: string(r.Parent),
|
Parent: string(r.Parent),
|
||||||
Child: string(r.Child),
|
Child: string(r.Child),
|
||||||
Type: string(r.Type),
|
Type: string(r.Type),
|
||||||
39
internal/presenter/packages/json_source.go
Normal file
39
internal/presenter/packages/json_source.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONSource object represents the thing that was cataloged
|
||||||
|
type JSONSource struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Target interface{} `json:"target"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONImageSource struct {
|
||||||
|
source.ImageMetadata
|
||||||
|
Scope source.Scope `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSONSource creates a new source object to be represented into JSON.
|
||||||
|
func NewJSONSource(src source.Metadata, scope source.Scope) (JSONSource, error) {
|
||||||
|
switch src.Scheme {
|
||||||
|
case source.ImageScheme:
|
||||||
|
return JSONSource{
|
||||||
|
Type: "image",
|
||||||
|
Target: JSONImageSource{
|
||||||
|
Scope: scope,
|
||||||
|
ImageMetadata: src.ImageMetadata,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
case source.DirectoryScheme:
|
||||||
|
return JSONSource{
|
||||||
|
Type: "directory",
|
||||||
|
Target: src.Path,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return JSONSource{}, fmt.Errorf("unsupported source: %q", src.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
internal/presenter/packages/presenter.go
Normal file
25
internal/presenter/packages/presenter.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Defines a Presenter interface for displaying catalog results to an io.Writer as well as a helper utility to obtain
|
||||||
|
a specific Presenter implementation given user configuration.
|
||||||
|
*/
|
||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/internal/presenter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Presenter returns a presenter for images or directories
|
||||||
|
func Presenter(option PresenterOption, config PresenterConfig) presenter.Presenter {
|
||||||
|
switch option {
|
||||||
|
case JSONPresenterOption:
|
||||||
|
return NewJSONPresenter(config.Catalog, config.SourceMetadata, config.Distro, config.Scope)
|
||||||
|
case TextPresenterOption:
|
||||||
|
return NewTextPresenter(config.Catalog, config.SourceMetadata)
|
||||||
|
case TablePresenterOption:
|
||||||
|
return NewTablePresenter(config.Catalog)
|
||||||
|
case CycloneDxPresenterOption:
|
||||||
|
return NewCycloneDxPresenter(config.Catalog, config.SourceMetadata)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
14
internal/presenter/packages/presenter_config.go
Normal file
14
internal/presenter/packages/presenter_config.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PresenterConfig struct {
|
||||||
|
SourceMetadata source.Metadata
|
||||||
|
Catalog *pkg.Catalog
|
||||||
|
Distro *distro.Distro
|
||||||
|
Scope source.Scope
|
||||||
|
}
|
||||||
35
internal/presenter/packages/presenter_option.go
Normal file
35
internal/presenter/packages/presenter_option.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package packages
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
UnknownPresenterOption PresenterOption = "UnknownPresenterOption"
|
||||||
|
JSONPresenterOption PresenterOption = "json"
|
||||||
|
TextPresenterOption PresenterOption = "text"
|
||||||
|
TablePresenterOption PresenterOption = "table"
|
||||||
|
CycloneDxPresenterOption PresenterOption = "cyclonedx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllPresenters = []PresenterOption{
|
||||||
|
JSONPresenterOption,
|
||||||
|
TextPresenterOption,
|
||||||
|
TablePresenterOption,
|
||||||
|
CycloneDxPresenterOption,
|
||||||
|
}
|
||||||
|
|
||||||
|
type PresenterOption string
|
||||||
|
|
||||||
|
func ParsePresenterOption(userStr string) PresenterOption {
|
||||||
|
switch strings.ToLower(userStr) {
|
||||||
|
case string(JSONPresenterOption):
|
||||||
|
return JSONPresenterOption
|
||||||
|
case string(TextPresenterOption):
|
||||||
|
return TextPresenterOption
|
||||||
|
case string(TablePresenterOption):
|
||||||
|
return TablePresenterOption
|
||||||
|
case string(CycloneDxPresenterOption), "cyclone", "cyclone-dx":
|
||||||
|
return CycloneDxPresenterOption
|
||||||
|
default:
|
||||||
|
return UnknownPresenterOption
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package table
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -11,17 +11,17 @@ import (
|
|||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Presenter struct {
|
type TablePresenter struct {
|
||||||
catalog *pkg.Catalog
|
catalog *pkg.Catalog
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPresenter(catalog *pkg.Catalog) *Presenter {
|
func NewTablePresenter(catalog *pkg.Catalog) *TablePresenter {
|
||||||
return &Presenter{
|
return &TablePresenter{
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pres *Presenter) Present(output io.Writer) error {
|
func (pres *TablePresenter) Present(output io.Writer) error {
|
||||||
rows := make([][]string, 0)
|
rows := make([][]string, 0)
|
||||||
|
|
||||||
columns := []string{"Name", "Version", "Type"}
|
columns := []string{"Name", "Version", "Type"}
|
||||||
@ -42,7 +42,7 @@ func (pres *Presenter) Present(output io.Writer) error {
|
|||||||
// sort by name, version, then type
|
// sort by name, version, then type
|
||||||
sort.SliceStable(rows, func(i, j int) bool {
|
sort.SliceStable(rows, func(i, j int) bool {
|
||||||
for col := 0; col < len(columns); col++ {
|
for col := 0; col < len(columns); col++ {
|
||||||
if rows[i][0] != rows[j][0] {
|
if rows[i][col] != rows[j][col] {
|
||||||
return rows[i][col] < rows[j][col]
|
return rows[i][col] < rows[j][col]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package table
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
)
|
)
|
||||||
|
|
||||||
var update = flag.Bool("update", false, "update the *.golden files for table presenters")
|
var updateTablePresenterGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table presenters")
|
||||||
|
|
||||||
func TestTablePresenter(t *testing.T) {
|
func TestTablePresenter(t *testing.T) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
@ -24,8 +24,7 @@ func TestTablePresenter(t *testing.T) {
|
|||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
|
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||||
@ -48,7 +47,7 @@ func TestTablePresenter(t *testing.T) {
|
|||||||
Type: pkg.DebPkg,
|
Type: pkg.DebPkg,
|
||||||
})
|
})
|
||||||
|
|
||||||
pres := NewPresenter(catalog)
|
pres := NewTablePresenter(catalog)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err := pres.Present(&buffer)
|
err := pres.Present(&buffer)
|
||||||
@ -57,7 +56,7 @@ func TestTablePresenter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
if *update {
|
if *updateTablePresenterGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package text
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -10,22 +10,22 @@ import (
|
|||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Presenter is a human-friendly text presenter to represent package and source data.
|
// TextPresenter is a human-friendly text presenter to represent package and source data.
|
||||||
type Presenter struct {
|
type TextPresenter struct {
|
||||||
catalog *pkg.Catalog
|
catalog *pkg.Catalog
|
||||||
srcMetadata source.Metadata
|
srcMetadata source.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPresenter creates a new presenter for the given set of catalog and image data.
|
// NewTextPresenter creates a new presenter for the given set of catalog and image data.
|
||||||
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter {
|
func NewTextPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *TextPresenter {
|
||||||
return &Presenter{
|
return &TextPresenter{
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
srcMetadata: srcMetadata,
|
srcMetadata: srcMetadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present is a method that is in charge of writing to an output buffer
|
// Present is a method that is in charge of writing to an output buffer
|
||||||
func (pres *Presenter) Present(output io.Writer) error {
|
func (pres *TextPresenter) Present(output io.Writer) error {
|
||||||
// init the tabular writer
|
// init the tabular writer
|
||||||
w := new(tabwriter.Writer)
|
w := new(tabwriter.Writer)
|
||||||
w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight)
|
w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package text
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
)
|
)
|
||||||
|
|
||||||
var update = flag.Bool("update", false, "update the *.golden files for text presenters")
|
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
|
||||||
|
|
||||||
func TestTextDirPresenter(t *testing.T) {
|
func TestTextDirPresenter(t *testing.T) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
@ -37,7 +37,7 @@ func TestTextDirPresenter(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create source: %+v", err)
|
t.Fatalf("unable to create source: %+v", err)
|
||||||
}
|
}
|
||||||
pres := NewPresenter(catalog, s.Metadata)
|
pres := NewTextPresenter(catalog, s.Metadata)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -46,7 +46,7 @@ func TestTextDirPresenter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
if *update {
|
if *updateTextPresenterGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,8 +69,7 @@ func TestTextImgPresenter(t *testing.T) {
|
|||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||||
@ -102,18 +101,18 @@ func TestTextImgPresenter(t *testing.T) {
|
|||||||
l.Metadata.Digest = "sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53"
|
l.Metadata.Digest = "sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53"
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := source.NewFromImage(img, source.SquashedScope, "user-image-input")
|
s, err := source.NewFromImage(img, "user-image-input")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pres := NewPresenter(catalog, s.Metadata)
|
pres := NewTextPresenter(catalog, s.Metadata)
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
if *update {
|
if *updateTextPresenterGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
32
internal/presenter/poweruser/json_document.go
Normal file
32
internal/presenter/poweruser/json_document.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package poweruser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/internal/presenter/packages"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONDocument struct {
|
||||||
|
// note: poweruser.JSONDocument is meant to always be a superset of packages.JSONDocument, any additional fields
|
||||||
|
// here should be optional by supplying "omitempty" on these fields hint to the jsonschema generator to not
|
||||||
|
// require these fields. As an accepted rule in this repo all collections should still be initialized in the
|
||||||
|
// context of being used in a JSON document.
|
||||||
|
FileMetadata []JSONFileMetadata `json:"fileMetadata,omitempty"`
|
||||||
|
packages.JSONDocument
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
|
||||||
|
func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) {
|
||||||
|
pkgsDoc, err := packages.NewJSONDocument(config.PackageCatalog, config.SourceMetadata, config.Distro, config.ApplicationConfig.Packages.ScopeOpt, config.ApplicationConfig)
|
||||||
|
if err != nil {
|
||||||
|
return JSONDocument{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileMetadata, err := NewJSONFileMetadata(config.FileMetadata, config.FileDigests)
|
||||||
|
if err != nil {
|
||||||
|
return JSONDocument{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSONDocument{
|
||||||
|
FileMetadata: fileMetadata,
|
||||||
|
JSONDocument: pkgsDoc,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
18
internal/presenter/poweruser/json_document_config.go
Normal file
18
internal/presenter/poweruser/json_document_config.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package poweruser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/internal/config"
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONDocumentConfig struct {
|
||||||
|
ApplicationConfig config.Application
|
||||||
|
PackageCatalog *pkg.Catalog
|
||||||
|
FileMetadata map[source.Location]source.FileMetadata
|
||||||
|
FileDigests map[source.Location][]file.Digest
|
||||||
|
Distro *distro.Distro
|
||||||
|
SourceMetadata source.Metadata
|
||||||
|
}
|
||||||
50
internal/presenter/poweruser/json_file_metadata.go
Normal file
50
internal/presenter/poweruser/json_file_metadata.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package poweruser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONFileMetadata struct {
|
||||||
|
Location source.Location `json:"location"`
|
||||||
|
Metadata JSONFileMetadataEntry `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONFileMetadataEntry struct {
|
||||||
|
Mode int `json:"mode"`
|
||||||
|
Type source.FileType `json:"type"`
|
||||||
|
UserID int `json:"userID"`
|
||||||
|
GroupID int `json:"groupID"`
|
||||||
|
Digests []file.Digest `json:"digests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests map[source.Location][]file.Digest) ([]JSONFileMetadata, error) {
|
||||||
|
results := make([]JSONFileMetadata, 0)
|
||||||
|
for location, metadata := range data {
|
||||||
|
mode, err := strconv.Atoi(fmt.Sprintf("%o", metadata.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid mode found in file catalog @ location=%+v mode=%q: %w", location, metadata.Mode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
digestResults := make([]file.Digest, 0)
|
||||||
|
if digestsForLocation, exists := digests[location]; exists {
|
||||||
|
digestResults = digestsForLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, JSONFileMetadata{
|
||||||
|
Location: location,
|
||||||
|
Metadata: JSONFileMetadataEntry{
|
||||||
|
Mode: mode,
|
||||||
|
Type: metadata.Type,
|
||||||
|
UserID: metadata.UserID,
|
||||||
|
GroupID: metadata.GroupID,
|
||||||
|
Digests: digestResults,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
32
internal/presenter/poweruser/json_presenter.go
Normal file
32
internal/presenter/poweruser/json_presenter.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package poweruser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONPresenter is a JSON presentation object for the syft results
|
||||||
|
type JSONPresenter struct {
|
||||||
|
config JSONDocumentConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
|
||||||
|
func NewJSONPresenter(config JSONDocumentConfig) *JSONPresenter {
|
||||||
|
return &JSONPresenter{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present the PackageCatalog results to the given writer.
|
||||||
|
func (p *JSONPresenter) Present(output io.Writer) error {
|
||||||
|
doc, err := NewJSONDocument(p.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(output)
|
||||||
|
// prevent > and < from being escaped in the payload
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(&doc)
|
||||||
|
}
|
||||||
9
internal/presenter/presenter.go
Normal file
9
internal/presenter/presenter.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package presenter
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// Presenter defines the expected behavior for an object responsible for displaying arbitrary input and processed data
|
||||||
|
// to a given io.Writer.
|
||||||
|
type Presenter interface {
|
||||||
|
Present(io.Writer) error
|
||||||
|
}
|
||||||
@ -1,69 +0,0 @@
|
|||||||
package cyclonedx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Source: https://cyclonedx.org/ext/bom-descriptor/
|
|
||||||
|
|
||||||
// BomDescriptor represents all metadata surrounding the BOM report (such as when the BOM was made, with which tool, and the item being cataloged).
|
|
||||||
type BomDescriptor struct {
|
|
||||||
XMLName xml.Name `xml:"metadata"`
|
|
||||||
Timestamp string `xml:"timestamp,omitempty"` // The date and time (timestamp) when the document was created
|
|
||||||
Tools []BdTool `xml:"tools>tool"` // The tool used to create the BOM.
|
|
||||||
Component *BdComponent `xml:"component"` // The component that the BOM describes.
|
|
||||||
}
|
|
||||||
|
|
||||||
// BdTool represents the tool that created the BOM report.
|
|
||||||
type BdTool struct {
|
|
||||||
XMLName xml.Name `xml:"tool"`
|
|
||||||
Vendor string `xml:"vendor,omitempty"` // The vendor of the tool used to create the BOM.
|
|
||||||
Name string `xml:"name,omitempty"` // The name of the tool used to create the BOM.
|
|
||||||
Version string `xml:"version,omitempty"` // The version of the tool used to create the BOM.
|
|
||||||
// TODO: hashes, author, manufacture, supplier
|
|
||||||
// TODO: add user-defined fields for the remaining build/version parameters
|
|
||||||
}
|
|
||||||
|
|
||||||
// BdComponent represents the software/package being cataloged.
|
|
||||||
type BdComponent struct {
|
|
||||||
XMLName xml.Name `xml:"component"`
|
|
||||||
Component
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
|
|
||||||
func NewBomDescriptor(name, version string, srcMetadata source.Metadata) *BomDescriptor {
|
|
||||||
descriptor := BomDescriptor{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
Timestamp: time.Now().Format(time.RFC3339),
|
|
||||||
Tools: []BdTool{
|
|
||||||
{
|
|
||||||
Vendor: "anchore",
|
|
||||||
Name: name,
|
|
||||||
Version: version,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
switch srcMetadata.Scheme {
|
|
||||||
case source.ImageScheme:
|
|
||||||
descriptor.Component = &BdComponent{
|
|
||||||
Component: Component{
|
|
||||||
Type: "container",
|
|
||||||
Name: srcMetadata.ImageMetadata.UserInput,
|
|
||||||
Version: srcMetadata.ImageMetadata.ManifestDigest,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
case source.DirectoryScheme:
|
|
||||||
descriptor.Component = &BdComponent{
|
|
||||||
Component: Component{
|
|
||||||
Type: "file",
|
|
||||||
Name: srcMetadata.Path,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &descriptor
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
package cyclonedx
|
|
||||||
|
|
||||||
import "encoding/xml"
|
|
||||||
|
|
||||||
// Component represents a single element in the CycloneDX BOM
|
|
||||||
type Component struct {
|
|
||||||
XMLName xml.Name `xml:"component"`
|
|
||||||
Type string `xml:"type,attr"` // Required; Describes if the component is a library, framework, application, container, operating system, firmware, hardware device, or file
|
|
||||||
Supplier string `xml:"supplier,omitempty"` // The organization that supplied the component. The supplier may often be the manufacture, but may also be a distributor or repackager.
|
|
||||||
Author string `xml:"author,omitempty"` // The person(s) or organization(s) that authored the component
|
|
||||||
Publisher string `xml:"publisher,omitempty"` // The person(s) or organization(s) that published the component
|
|
||||||
Group string `xml:"group,omitempty"` // The high-level classification that a project self-describes as. This will often be a shortened, single name of the company or project that produced the component, or the source package or domain name.
|
|
||||||
Name string `xml:"name"` // Required; The name of the component as defined by the project
|
|
||||||
Version string `xml:"version"` // Required; The version of the component as defined by the project
|
|
||||||
Description string `xml:"description,omitempty"` // A description of the component
|
|
||||||
Licenses *[]License `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions
|
|
||||||
PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec
|
|
||||||
// TODO: source, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
|
|
||||||
// TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.)
|
|
||||||
}
|
|
||||||
|
|
||||||
// License represents a single software license for a Component
|
|
||||||
type License struct {
|
|
||||||
XMLName xml.Name `xml:"license"`
|
|
||||||
ID string `xml:"id,omitempty"` // A valid SPDX license ID
|
|
||||||
Name string `xml:"name,omitempty"` // If SPDX does not define the license used, this field may be used to provide the license name
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
package cyclonedx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
"github.com/anchore/syft/internal/version"
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Source: https://github.com/CycloneDX/specification
|
|
||||||
|
|
||||||
// Document represents a CycloneDX BOM Document.
|
|
||||||
type Document struct {
|
|
||||||
XMLName xml.Name `xml:"bom"`
|
|
||||||
XMLNs string `xml:"xmlns,attr"`
|
|
||||||
Version int `xml:"version,attr"`
|
|
||||||
SerialNumber string `xml:"serialNumber,attr"`
|
|
||||||
BomDescriptor *BomDescriptor `xml:"metadata"` // The BOM descriptor extension
|
|
||||||
Components []Component `xml:"components>component"` // The BOM contents
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDocumentFromCatalog returns a CycloneDX Document object populated with the catalog contents.
|
|
||||||
func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) Document {
|
|
||||||
versionInfo := version.FromBuild()
|
|
||||||
|
|
||||||
doc := Document{
|
|
||||||
XMLNs: "http://cyclonedx.org/schema/bom/1.2",
|
|
||||||
Version: 1,
|
|
||||||
SerialNumber: uuid.New().URN(),
|
|
||||||
BomDescriptor: NewBomDescriptor(internal.ApplicationName, versionInfo.Version, srcMetadata),
|
|
||||||
}
|
|
||||||
|
|
||||||
// attach components
|
|
||||||
for p := range catalog.Enumerate() {
|
|
||||||
component := Component{
|
|
||||||
Type: "library", // TODO: this is not accurate
|
|
||||||
Name: p.Name,
|
|
||||||
Version: p.Version,
|
|
||||||
PackageURL: p.PURL,
|
|
||||||
}
|
|
||||||
var licenses []License
|
|
||||||
for _, licenseName := range p.Licenses {
|
|
||||||
licenses = append(licenses, License{
|
|
||||||
Name: licenseName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(licenses) > 0 {
|
|
||||||
component.Licenses = &licenses
|
|
||||||
}
|
|
||||||
doc.Components = append(doc.Components, component)
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
|
|
||||||
FROM scratch
|
|
||||||
ADD file-1.txt /somefile-1.txt
|
|
||||||
ADD file-2.txt /somefile-2.txt
|
|
||||||
# note: adding a directory will behave differently on docker engine v18 vs v19
|
|
||||||
ADD target /
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
another file!
|
|
||||||
with lines...
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
// Descriptor describes what created the document as well as surrounding metadata
|
|
||||||
type Descriptor struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
"github.com/anchore/syft/internal/version"
|
|
||||||
"github.com/anchore/syft/syft/distro"
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
Source Source `json:"source"` // Source represents the original object that was cataloged
|
|
||||||
Distro Distribution `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
|
|
||||||
ArtifactRelationships []Relationship `json:"artifactRelationships"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDocument creates and populates a new JSON document struct from the given cataloging results.
|
|
||||||
func NewDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro) (Document, error) {
|
|
||||||
src, err := NewSource(srcMetadata)
|
|
||||||
if err != nil {
|
|
||||||
return Document{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
doc := Document{
|
|
||||||
Artifacts: make([]Package, 0),
|
|
||||||
Source: src,
|
|
||||||
Distro: NewDistribution(d),
|
|
||||||
Descriptor: Descriptor{
|
|
||||||
Name: internal.ApplicationName,
|
|
||||||
Version: version.FromBuild().Version,
|
|
||||||
},
|
|
||||||
Schema: Schema{
|
|
||||||
Version: internal.JSONSchemaVersion,
|
|
||||||
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
|
|
||||||
},
|
|
||||||
ArtifactRelationships: newRelationships(pkg.NewRelationships(catalog)),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range catalog.Sorted() {
|
|
||||||
art, err := NewPackage(p)
|
|
||||||
if err != nil {
|
|
||||||
return Document{}, err
|
|
||||||
}
|
|
||||||
doc.Artifacts = append(doc.Artifacts, art)
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc, nil
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/distro"
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Presenter is a JSON presentation object for the syft results
|
|
||||||
type Presenter struct {
|
|
||||||
catalog *pkg.Catalog
|
|
||||||
srcMetadata source.Metadata
|
|
||||||
distro *distro.Distro
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPresenter creates a new JSON presenter object for the given cataloging results.
|
|
||||||
func NewPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro) *Presenter {
|
|
||||||
return &Presenter{
|
|
||||||
catalog: catalog,
|
|
||||||
srcMetadata: s,
|
|
||||||
distro: d,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Present the catalog results to the given writer.
|
|
||||||
func (pres *Presenter) Present(output io.Writer) error {
|
|
||||||
doc, err := NewDocument(pres.catalog, pres.srcMetadata, pres.distro)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := json.NewEncoder(output)
|
|
||||||
// prevent > and < from being escaped in the payload
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
return enc.Encode(&doc)
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
type Schema struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Source object represents the thing that was cataloged
|
|
||||||
type Source struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Target interface{} `json:"target"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// sourceUnpacker is used to unmarshal Source objects
|
|
||||||
type sourceUnpacker struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Target json.RawMessage `json:"target"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSource creates a new source object to be represented into JSON.
|
|
||||||
func NewSource(src source.Metadata) (Source, error) {
|
|
||||||
switch src.Scheme {
|
|
||||||
case source.ImageScheme:
|
|
||||||
return Source{
|
|
||||||
Type: "image",
|
|
||||||
Target: src.ImageMetadata,
|
|
||||||
}, nil
|
|
||||||
case source.DirectoryScheme:
|
|
||||||
return Source{
|
|
||||||
Type: "directory",
|
|
||||||
Target: src.Path,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return Source{}, fmt.Errorf("unsupported source: %T", src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON populates a source object from JSON bytes.
|
|
||||||
func (s *Source) UnmarshalJSON(b []byte) error {
|
|
||||||
var unpacker sourceUnpacker
|
|
||||||
if err := json.Unmarshal(b, &unpacker); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Type = unpacker.Type
|
|
||||||
|
|
||||||
switch s.Type {
|
|
||||||
case "image":
|
|
||||||
var payload source.ImageMetadata
|
|
||||||
if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Target = payload
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported package metadata type: %+v", s.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToSourceMetadata takes a source object represented from JSON and creates a source.Metadata object.
|
|
||||||
func (s *Source) ToSourceMetadata() source.Metadata {
|
|
||||||
var metadata source.Metadata
|
|
||||||
switch s.Type {
|
|
||||||
case "directory":
|
|
||||||
metadata.Scheme = source.DirectoryScheme
|
|
||||||
metadata.Path = s.Target.(string)
|
|
||||||
case "image":
|
|
||||||
metadata.Scheme = source.ImageScheme
|
|
||||||
metadata.ImageMetadata = s.Target.(source.ImageMetadata)
|
|
||||||
}
|
|
||||||
return metadata
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
|
|
||||||
FROM scratch
|
|
||||||
ADD file-1.txt /somefile-1.txt
|
|
||||||
ADD file-2.txt /somefile-2.txt
|
|
||||||
# note: adding a directory will behave differently on docker engine v18 vs v19
|
|
||||||
ADD target /
|
|
||||||
@ -1 +0,0 @@
|
|||||||
this file has contents
|
|
||||||
@ -1 +0,0 @@
|
|||||||
file-2 contents!
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
another file!
|
|
||||||
with lines...
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
{
|
|
||||||
"artifacts": [
|
|
||||||
{
|
|
||||||
"id": "package-1-id",
|
|
||||||
"name": "package-1",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"type": "python",
|
|
||||||
"foundBy": "the-cataloger-1",
|
|
||||||
"locations": [
|
|
||||||
{
|
|
||||||
"path": "/some/path/pkg1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"licenses": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"language": "python",
|
|
||||||
"cpes": [
|
|
||||||
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
|
||||||
],
|
|
||||||
"purl": "a-purl-2",
|
|
||||||
"metadataType": "PythonPackageMetadata",
|
|
||||||
"metadata": {
|
|
||||||
"name": "package-1",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"license": "",
|
|
||||||
"author": "",
|
|
||||||
"authorEmail": "",
|
|
||||||
"platform": "",
|
|
||||||
"sitePackagesRootPath": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "package-2-id",
|
|
||||||
"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": "a-purl-2",
|
|
||||||
"metadataType": "DpkgMetadata",
|
|
||||||
"metadata": {
|
|
||||||
"package": "package-2",
|
|
||||||
"source": "",
|
|
||||||
"version": "2.0.1",
|
|
||||||
"sourceVersion": "",
|
|
||||||
"architecture": "",
|
|
||||||
"maintainer": "",
|
|
||||||
"installedSize": 0,
|
|
||||||
"files": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": {
|
|
||||||
"type": "directory",
|
|
||||||
"target": "/some/path"
|
|
||||||
},
|
|
||||||
"distro": {
|
|
||||||
"name": "",
|
|
||||||
"version": "",
|
|
||||||
"idLike": ""
|
|
||||||
},
|
|
||||||
"descriptor": {
|
|
||||||
"name": "syft",
|
|
||||||
"version": "[not provided]"
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.3.json"
|
|
||||||
},
|
|
||||||
"artifactRelationships": []
|
|
||||||
}
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
{
|
|
||||||
"artifacts": [
|
|
||||||
{
|
|
||||||
"id": "package-1-id",
|
|
||||||
"name": "package-1",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"type": "python",
|
|
||||||
"foundBy": "the-cataloger-1",
|
|
||||||
"locations": [
|
|
||||||
{
|
|
||||||
"path": "/somefile-1.txt",
|
|
||||||
"layerID": "sha256:2abd6dc7d143b384177da9a30f5311eb1f6c4e920eb6e218ec32b58c1a8806b1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"licenses": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"language": "python",
|
|
||||||
"cpes": [
|
|
||||||
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
|
|
||||||
],
|
|
||||||
"purl": "a-purl-1",
|
|
||||||
"metadataType": "PythonPackageMetadata",
|
|
||||||
"metadata": {
|
|
||||||
"name": "package-1",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"license": "",
|
|
||||||
"author": "",
|
|
||||||
"authorEmail": "",
|
|
||||||
"platform": "",
|
|
||||||
"sitePackagesRootPath": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "package-2-id",
|
|
||||||
"name": "package-2",
|
|
||||||
"version": "2.0.1",
|
|
||||||
"type": "deb",
|
|
||||||
"foundBy": "the-cataloger-2",
|
|
||||||
"locations": [
|
|
||||||
{
|
|
||||||
"path": "/somefile-2.txt",
|
|
||||||
"layerID": "sha256:63574b0cbd9fcfc799d82a449c0975ee3f3560747b0957f042938bdec902c4cc"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": {
|
|
||||||
"type": "image",
|
|
||||||
"target": {
|
|
||||||
"userInput": "user-image-input",
|
|
||||||
"imageID": "sha256:0a7326bbb1c3c467969c7a73deb6366b1e34eb034616b97f8614ad662cad1ce1",
|
|
||||||
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
|
||||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
|
||||||
"tags": [
|
|
||||||
"stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"
|
|
||||||
],
|
|
||||||
"imageSize": 65,
|
|
||||||
"scope": "Squashed",
|
|
||||||
"layers": [
|
|
||||||
{
|
|
||||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
|
||||||
"digest": "sha256:2abd6dc7d143b384177da9a30f5311eb1f6c4e920eb6e218ec32b58c1a8806b1",
|
|
||||||
"size": 22
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
|
||||||
"digest": "sha256:63574b0cbd9fcfc799d82a449c0975ee3f3560747b0957f042938bdec902c4cc",
|
|
||||||
"size": 16
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
|
||||||
"digest": "sha256:b102400548006e8dfa3abf6e2b2e6f1c440e29936d91a36e145736e2fd8cf0a1",
|
|
||||||
"size": 27
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxODA4LCJkaWdlc3QiOiJzaGEyNTY6MGE3MzI2YmJiMWMzYzQ2Nzk2OWM3YTczZGViNjM2NmIxZTM0ZWIwMzQ2MTZiOTdmODYxNGFkNjYyY2FkMWNlMSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMDQ4LCJkaWdlc3QiOiJzaGEyNTY6MmFiZDZkYzdkMTQzYjM4NDE3N2RhOWEzMGY1MzExZWIxZjZjNGU5MjBlYjZlMjE4ZWMzMmI1OGMxYTg4MDZiMSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1Njo2MzU3NGIwY2JkOWZjZmM3OTlkODJhNDQ5YzA5NzVlZTNmMzU2MDc0N2IwOTU3ZjA0MjkzOGJkZWM5MDJjNGNjIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MzU4NCwiZGlnZXN0Ijoic2hhMjU2OmIxMDI0MDA1NDgwMDZlOGRmYTNhYmY2ZTJiMmU2ZjFjNDQwZTI5OTM2ZDkxYTM2ZTE0NTczNmUyZmQ4Y2YwYTEifV19",
|
|
||||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpudWxsLCJJbWFnZSI6InNoYTI1Njo4YjlhNTM1MmM3YzU3M2RmNTkxYTYwMzA0NzIxMmU3ZmYzOGI3YjRiYzZiMzk1NmI3Zjk2Nzk0MWU3NGMyMjhkIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6bnVsbH0sImNvbnRhaW5lcl9jb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IiIsIkF0dGFjaFN0ZGluIjpmYWxzZSwiQXR0YWNoU3Rkb3V0IjpmYWxzZSwiQXR0YWNoU3RkZXJyIjpmYWxzZSwiVHR5IjpmYWxzZSwiT3BlblN0ZGluIjpmYWxzZSwiU3RkaW5PbmNlIjpmYWxzZSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIl0sIkNtZCI6WyIvYmluL3NoIiwiLWMiLCIjKG5vcCkgQUREIGRpcjpjOTM3YzZhYTUwODkwN2UyODUwOWI2NDRhMTJmOGQ2YzY3ZDM0ZTk2OWY4M2IxNGRlZTkzZWExN2Q3NjkwMjhhIGluIC8gIl0sIkltYWdlIjoic2hhMjU2OjhiOWE1MzUyYzdjNTczZGY1OTFhNjAzMDQ3MjEyZTdmZjM4YjdiNGJjNmIzOTU2YjdmOTY3OTQxZTc0YzIyOGQiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMDMtMjJUMTM6MDg6MzUuNDkyOTI1MzAzWiIsImRvY2tlcl92ZXJzaW9uIjoiMjAuMTAuMiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTAzLTIyVDEzOjA4OjM1LjA5NDMxMjQ3NloiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YWMzMmRhMjNkNTFlODAxZjAyZjkyNDEyM2VkMzA5OTBlYjNmMGZlYzFiOWVkNGYwYjA2YzI0ZTg4YjljMzY5NSBpbiAvc29tZWZpbGUtMS50eHQgIn0seyJjcmVhdGVkIjoiMjAyMS0wMy0yMlQxMzowODozNS4yODk5MjIwNTFaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOmRmM2I3NDRmNTRhOWIxNmI5YjlhZWQ0MGUzZTk4ZDljYTJiNDlmNWE3N2Q5ZmE4YTk3NjkwZDdiYWY1ODg4MjAgaW4gL3NvbWVmaWxlLTIudHh0ICJ9LHsiY3JlYXRlZCI6IjIwMjEtMDMtMjJUMTM6MDg6MzUuNDkyOTI1MzAzWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZGlyOmM5MzdjNmFhNTA4OTA3ZTI4NTA5YjY0NGExMmY4ZDZjNjdkMzRlOTY5ZjgzYjE0ZGVlOTNlYTE3ZDc2OTAyOGEgaW4gLyAifV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1NjoyYWJkNmRjN2QxNDNiMzg0MTc3ZGE5YTMwZjUzMTFlYjFmNmM0ZTkyMGViNmUyMThlYzMyYjU4YzFhODgwNmIxIiwic2hhMjU2OjYzNTc0YjBjYmQ5ZmNmYzc5OWQ4MmE0NDljMDk3NWVlM2YzNTYwNzQ3YjA5NTdmMDQyOTM4YmRlYzkwMmM0Y2MiLCJzaGEyNTY6YjEwMjQwMDU0ODAwNmU4ZGZhM2FiZjZlMmIyZTZmMWM0NDBlMjk5MzZkOTFhMzZlMTQ1NzM2ZTJmZDhjZjBhMSJdfX0="
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"distro": {
|
|
||||||
"name": "",
|
|
||||||
"version": "",
|
|
||||||
"idLike": ""
|
|
||||||
},
|
|
||||||
"descriptor": {
|
|
||||||
"name": "syft",
|
|
||||||
"version": "[not provided]"
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.3.json"
|
|
||||||
},
|
|
||||||
"artifactRelationships": []
|
|
||||||
}
|
|
||||||
Binary file not shown.
@ -1,35 +0,0 @@
|
|||||||
package presenter
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
const (
|
|
||||||
UnknownPresenter Option = "UnknownPresenter"
|
|
||||||
JSONPresenter Option = "json"
|
|
||||||
TextPresenter Option = "text"
|
|
||||||
TablePresenter Option = "table"
|
|
||||||
CycloneDxPresenter Option = "cyclonedx"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Options = []Option{
|
|
||||||
JSONPresenter,
|
|
||||||
TextPresenter,
|
|
||||||
TablePresenter,
|
|
||||||
CycloneDxPresenter,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option string
|
|
||||||
|
|
||||||
func ParseOption(userStr string) Option {
|
|
||||||
switch strings.ToLower(userStr) {
|
|
||||||
case string(JSONPresenter):
|
|
||||||
return JSONPresenter
|
|
||||||
case string(TextPresenter):
|
|
||||||
return TextPresenter
|
|
||||||
case string(TablePresenter):
|
|
||||||
return TablePresenter
|
|
||||||
case string(CycloneDxPresenter), "cyclone", "cyclone-dx":
|
|
||||||
return CycloneDxPresenter
|
|
||||||
default:
|
|
||||||
return UnknownPresenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
Defines a Presenter interface for displaying catalog results to an io.Writer as well as a helper utility to obtain
|
|
||||||
a specific Presenter implementation given user configuration.
|
|
||||||
*/
|
|
||||||
package presenter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/distro"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/presenter/cyclonedx"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/anchore/syft/syft/presenter/json"
|
|
||||||
"github.com/anchore/syft/syft/presenter/table"
|
|
||||||
"github.com/anchore/syft/syft/presenter/text"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Presenter defines the expected behavior for an object responsible for displaying arbitrary input and processed data
|
|
||||||
// to a given io.Writer.
|
|
||||||
type Presenter interface {
|
|
||||||
Present(io.Writer) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPresenter returns a presenter for images or directories
|
|
||||||
func GetPresenter(option Option, srcMetadata source.Metadata, catalog *pkg.Catalog, d *distro.Distro) Presenter {
|
|
||||||
switch option {
|
|
||||||
case JSONPresenter:
|
|
||||||
return json.NewPresenter(catalog, srcMetadata, d)
|
|
||||||
case TextPresenter:
|
|
||||||
return text.NewPresenter(catalog, srcMetadata)
|
|
||||||
case TablePresenter:
|
|
||||||
return table.NewPresenter(catalog)
|
|
||||||
case CycloneDxPresenter:
|
|
||||||
return cyclonedx.NewPresenter(catalog, srcMetadata)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
|
|
||||||
FROM scratch
|
|
||||||
ADD file-1.txt /somefile-1.txt
|
|
||||||
ADD file-2.txt /somefile-2.txt
|
|
||||||
# note: adding a directory will behave differently on docker engine v18 vs v19
|
|
||||||
ADD target /
|
|
||||||
@ -1 +0,0 @@
|
|||||||
this file has contents
|
|
||||||
@ -1 +0,0 @@
|
|||||||
file-2 contents!
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
another file!
|
|
||||||
with lines...
|
|
||||||
@ -1 +0,0 @@
|
|||||||
this file has contents
|
|
||||||
@ -1 +0,0 @@
|
|||||||
file-2 contents!
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","cataloger":"","sources":[],"metadata":null},{"name":"package-2","version":"2.0.1","type":"deb","cataloger":"","sources":[],"metadata":null}],"Source":"/some/path"}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user