diff --git a/cmd/packages.go b/cmd/packages.go index 81af8daca..b18919415 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -103,7 +103,7 @@ func setPackageFlags(flags *pflag.FlagSet) { flags.StringP( "output", "o", string(format.TableOption), - fmt.Sprintf("report output formatter, options=%v", format.AllPresenters), + fmt.Sprintf("report output formatter, options=%v", format.AllOptions), ) flags.StringP( diff --git a/internal/formats/common/testutils/utils.go b/internal/formats/common/testutils/utils.go index b8ca909a7..a28aeb8bc 100644 --- a/internal/formats/common/testutils/utils.go +++ b/internal/formats/common/testutils/utils.go @@ -4,13 +4,13 @@ import ( "bytes" "testing" - "github.com/anchore/syft/syft/presenter" - "github.com/anchore/go-testutils" "github.com/anchore/stereoscope/pkg/filetree" + "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/presenter" "github.com/anchore/syft/syft/source" "github.com/sergi/go-diff/diffmatchpatch" "github.com/stretchr/testify/assert" @@ -18,6 +18,18 @@ import ( type redactor func(s []byte) []byte +type imageCfg struct { + fromSnapshot bool +} + +type ImageOption func(cfg *imageCfg) + +func FromSnapshot() ImageOption { + return func(cfg *imageCfg) { + cfg.fromSnapshot = true + } +} + func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) { var buffer bytes.Buffer @@ -78,11 +90,37 @@ func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter } } -func ImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) { +func ImageInput(t testing.TB, testImage string, options ...ImageOption) (*pkg.Catalog, source.Metadata, *distro.Distro) { t.Helper() catalog := pkg.NewCatalog() - img := imagetest.GetGoldenFixtureImage(t, testImage) + var cfg imageCfg + var img *image.Image + for _, opt := range options { + opt(&cfg) + } + switch cfg.fromSnapshot { + case true: + img = imagetest.GetGoldenFixtureImage(t, testImage) + default: + img = imagetest.GetFixtureImage(t, "docker-archive", testImage) + } + + populateImageCatalog(catalog, img) + + // this is a hard coded value that is not given by the fixture helper and must be provided manually + img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" + + src, err := source.NewFromImage(img, "user-image-input") + assert.NoError(t, err) + + dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!") + assert.NoError(t, err) + + return catalog, src.Metadata, &dist +} + +func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) { _, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks) _, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks) @@ -127,20 +165,21 @@ func ImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, }) +} - // this is a hard coded value that is not given by the fixture helper and must be provided manually - img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" - - src, err := source.NewFromImage(img, "user-image-input") - assert.NoError(t, err) +func DirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) { + catalog := newDirectoryCatalog() dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!") assert.NoError(t, err) + src, err := source.NewFromDirectory("/some/path") + assert.NoError(t, err) + return catalog, src.Metadata, &dist } -func DirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) { +func newDirectoryCatalog() *pkg.Catalog { catalog := pkg.NewCatalog() // populate catalog with test data @@ -190,11 +229,5 @@ func DirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro }, }) - dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!") - assert.NoError(t, err) - - src, err := source.NewFromDirectory("/some/path") - assert.NoError(t, err) - - return catalog, src.Metadata, &dist + return catalog } diff --git a/internal/formats/cyclonedx12xml/encoder.go b/internal/formats/cyclonedx12xml/encoder.go new file mode 100644 index 000000000..db60f8d6d --- /dev/null +++ b/internal/formats/cyclonedx12xml/encoder.go @@ -0,0 +1,29 @@ +package cyclonedx12xml + +import ( + "encoding/xml" + "io" + + "github.com/anchore/syft/syft/distro" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func encoder(output io.Writer, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope) error { + enc := xml.NewEncoder(output) + enc.Indent("", " ") + + _, err := output.Write([]byte(xml.Header)) + if err != nil { + return err + } + + err = enc.Encode(toFormatModel(catalog, srcMetadata, d, scope)) + if err != nil { + return err + } + + _, err = output.Write([]byte("\n")) + return err +} diff --git a/internal/presenter/packages/cyclonedx_presenter_test.go b/internal/formats/cyclonedx12xml/encoder_test.go similarity index 84% rename from internal/presenter/packages/cyclonedx_presenter_test.go rename to internal/formats/cyclonedx12xml/encoder_test.go index 083a72022..7e3ccadea 100644 --- a/internal/presenter/packages/cyclonedx_presenter_test.go +++ b/internal/formats/cyclonedx12xml/encoder_test.go @@ -1,10 +1,12 @@ -package packages +package cyclonedx12xml import ( "flag" "regexp" "testing" + "github.com/anchore/syft/syft/source" + "github.com/anchore/syft/internal/formats/common/testutils" ) @@ -13,7 +15,7 @@ var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden func TestCycloneDxDirectoryPresenter(t *testing.T) { catalog, metadata, _ := testutils.DirectoryInput(t) testutils.AssertPresenterAgainstGoldenSnapshot(t, - NewCycloneDxPresenter(catalog, metadata), + Format().Presenter(catalog, &metadata, nil, source.SquashedScope), *updateCycloneDx, cycloneDxRedactor, ) @@ -23,7 +25,7 @@ func TestCycloneDxImagePresenter(t *testing.T) { testImage := "image-simple" catalog, metadata, _ := testutils.ImageInput(t, testImage) testutils.AssertPresenterAgainstGoldenImageSnapshot(t, - NewCycloneDxPresenter(catalog, metadata), + Format().Presenter(catalog, &metadata, nil, source.SquashedScope), testImage, *updateCycloneDx, cycloneDxRedactor, diff --git a/internal/formats/cyclonedx12xml/format.go b/internal/formats/cyclonedx12xml/format.go new file mode 100644 index 000000000..8ca2608ae --- /dev/null +++ b/internal/formats/cyclonedx12xml/format.go @@ -0,0 +1,12 @@ +package cyclonedx12xml + +import "github.com/anchore/syft/syft/format" + +func Format() format.Format { + return format.NewFormat( + format.CycloneDxOption, + encoder, + nil, + nil, + ) +} diff --git a/internal/formats/cyclonedx12xml/model/bom_descriptor.go b/internal/formats/cyclonedx12xml/model/bom_descriptor.go new file mode 100644 index 000000000..77406f5a5 --- /dev/null +++ b/internal/formats/cyclonedx12xml/model/bom_descriptor.go @@ -0,0 +1,31 @@ +package model + +import ( + "encoding/xml" +) + +// 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 []BomDescriptorTool `xml:"tools>tool"` // The tool used to create the BOM. + Component *BomDescriptorComponent `xml:"component"` // The component that the BOM describes. +} + +// BomDescriptorTool represents the tool that created the BOM report. +type BomDescriptorTool 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 +} + +// BomDescriptorComponent represents the software/package being cataloged. +type BomDescriptorComponent struct { + XMLName xml.Name `xml:"component"` + Component +} diff --git a/internal/formats/cyclonedx12xml/model/component.go b/internal/formats/cyclonedx12xml/model/component.go new file mode 100644 index 000000000..ea5efbae1 --- /dev/null +++ b/internal/formats/cyclonedx12xml/model/component.go @@ -0,0 +1,20 @@ +package model + +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.) +} diff --git a/internal/formats/cyclonedx12xml/model/document.go b/internal/formats/cyclonedx12xml/model/document.go new file mode 100644 index 000000000..6ba8a08ca --- /dev/null +++ b/internal/formats/cyclonedx12xml/model/document.go @@ -0,0 +1,17 @@ +package model + +import ( + "encoding/xml" +) + +// 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 +} diff --git a/internal/formats/cyclonedx12xml/model/license.go b/internal/formats/cyclonedx12xml/model/license.go new file mode 100644 index 000000000..c833e28d0 --- /dev/null +++ b/internal/formats/cyclonedx12xml/model/license.go @@ -0,0 +1,10 @@ +package model + +import "encoding/xml" + +// 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 +} diff --git a/internal/formats/cyclonedx12xml/test-fixtures/image-simple/Dockerfile b/internal/formats/cyclonedx12xml/test-fixtures/image-simple/Dockerfile new file mode 100644 index 000000000..79cfa759e --- /dev/null +++ b/internal/formats/cyclonedx12xml/test-fixtures/image-simple/Dockerfile @@ -0,0 +1,4 @@ +# 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 diff --git a/internal/formats/cyclonedx12xml/test-fixtures/image-simple/file-1.txt b/internal/formats/cyclonedx12xml/test-fixtures/image-simple/file-1.txt new file mode 100644 index 000000000..985d3408e --- /dev/null +++ b/internal/formats/cyclonedx12xml/test-fixtures/image-simple/file-1.txt @@ -0,0 +1 @@ +this file has contents \ No newline at end of file diff --git a/internal/formats/cyclonedx12xml/test-fixtures/image-simple/file-2.txt b/internal/formats/cyclonedx12xml/test-fixtures/image-simple/file-2.txt new file mode 100644 index 000000000..396d08bbc --- /dev/null +++ b/internal/formats/cyclonedx12xml/test-fixtures/image-simple/file-2.txt @@ -0,0 +1 @@ +file-2 contents! \ No newline at end of file diff --git a/internal/formats/cyclonedx12xml/test-fixtures/snapshot/TestCycloneDxDirectoryPresenter.golden b/internal/formats/cyclonedx12xml/test-fixtures/snapshot/TestCycloneDxDirectoryPresenter.golden new file mode 100644 index 000000000..5c473495a --- /dev/null +++ b/internal/formats/cyclonedx12xml/test-fixtures/snapshot/TestCycloneDxDirectoryPresenter.golden @@ -0,0 +1,34 @@ + + + + 2021-06-23T13:40:33-04:00 + + + anchore + syft + [not provided] + + + + /some/path + + + + + + package-1 + 1.0.1 + + + MIT + + + a-purl-2 + + + package-2 + 2.0.1 + a-purl-2 + + + diff --git a/internal/formats/cyclonedx12xml/test-fixtures/snapshot/TestCycloneDxImagePresenter.golden b/internal/formats/cyclonedx12xml/test-fixtures/snapshot/TestCycloneDxImagePresenter.golden new file mode 100644 index 000000000..d81de3a32 --- /dev/null +++ b/internal/formats/cyclonedx12xml/test-fixtures/snapshot/TestCycloneDxImagePresenter.golden @@ -0,0 +1,34 @@ + + + + 2021-06-23T13:40:33-04:00 + + + anchore + syft + [not provided] + + + + user-image-input + sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368 + + + + + package-1 + 1.0.1 + + + MIT + + + a-purl-1 + + + package-2 + 2.0.1 + a-purl-2 + + + diff --git a/internal/formats/cyclonedx12xml/to_format_model.go b/internal/formats/cyclonedx12xml/to_format_model.go new file mode 100644 index 000000000..88de067b4 --- /dev/null +++ b/internal/formats/cyclonedx12xml/to_format_model.go @@ -0,0 +1,97 @@ +package cyclonedx12xml + +import ( + "encoding/xml" + "time" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/formats/cyclonedx12xml/model" + "github.com/anchore/syft/internal/version" + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" + "github.com/google/uuid" +) + +// toFormatModel creates and populates a new in-memory representation of a CycloneDX 1.2 document +func toFormatModel(catalog *pkg.Catalog, srcMetadata *source.Metadata, _ *distro.Distro, _ source.Scope) model.Document { + versionInfo := version.FromBuild() + + doc := model.Document{ + XMLNs: "http://cyclonedx.org/schema/bom/1.2", + Version: 1, + SerialNumber: uuid.New().URN(), + BomDescriptor: toBomDescriptor(internal.ApplicationName, versionInfo.Version, srcMetadata), + } + + // attach components + for _, p := range catalog.Sorted() { + doc.Components = append(doc.Components, toComponent(p)) + } + + return doc +} + +func toComponent(p *pkg.Package) model.Component { + return model.Component{ + Type: "library", // TODO: this is not accurate + Name: p.Name, + Version: p.Version, + PackageURL: p.PURL, + Licenses: toLicenses(p.Licenses), + } +} + +// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details. +func toBomDescriptor(name, version string, srcMetadata *source.Metadata) *model.BomDescriptor { + return &model.BomDescriptor{ + XMLName: xml.Name{}, + Timestamp: time.Now().Format(time.RFC3339), + Tools: []model.BomDescriptorTool{ + { + Vendor: "anchore", + Name: name, + Version: version, + }, + }, + Component: toBomDescriptorComponent(srcMetadata), + } +} + +func toBomDescriptorComponent(srcMetadata *source.Metadata) *model.BomDescriptorComponent { + if srcMetadata == nil { + return nil + } + switch srcMetadata.Scheme { + case source.ImageScheme: + return &model.BomDescriptorComponent{ + Component: model.Component{ + Type: "container", + Name: srcMetadata.ImageMetadata.UserInput, + Version: srcMetadata.ImageMetadata.ManifestDigest, + }, + } + case source.DirectoryScheme: + return &model.BomDescriptorComponent{ + Component: model.Component{ + Type: "file", + Name: srcMetadata.Path, + }, + } + } + return nil +} + +func toLicenses(licenses []string) *[]model.License { + if len(licenses) == 0 { + return nil + } + + var result []model.License + for _, licenseName := range licenses { + result = append(result, model.License{ + Name: licenseName, + }) + } + return &result +} diff --git a/internal/formats/formats.go b/internal/formats/formats.go index f8cb24b22..ba9a42d0c 100644 --- a/internal/formats/formats.go +++ b/internal/formats/formats.go @@ -3,6 +3,8 @@ package formats import ( "bytes" + "github.com/anchore/syft/internal/formats/cyclonedx12xml" + "github.com/anchore/syft/internal/formats/table" "github.com/anchore/syft/internal/formats/syftjson" @@ -14,6 +16,7 @@ func All() []format.Format { return []format.Format{ syftjson.Format(), table.Format(), + cyclonedx12xml.Format(), } } diff --git a/internal/formats/syftjson/encoder_test.go b/internal/formats/syftjson/encoder_test.go index 7376451ac..f1f41eecc 100644 --- a/internal/formats/syftjson/encoder_test.go +++ b/internal/formats/syftjson/encoder_test.go @@ -22,7 +22,7 @@ func TestDirectoryPresenter(t *testing.T) { func TestImagePresenter(t *testing.T) { testImage := "image-simple" - catalog, metadata, distro := testutils.ImageInput(t, testImage) + catalog, metadata, distro := testutils.ImageInput(t, testImage, testutils.FromSnapshot()) testutils.AssertPresenterAgainstGoldenImageSnapshot(t, format.NewPresenter(encoder, catalog, &metadata, distro, source.SquashedScope), testImage, diff --git a/internal/presenter/packages/cyclonedx_bom_descriptor.go b/internal/presenter/packages/cyclonedx_bom_descriptor.go deleted file mode 100644 index eeaac61f7..000000000 --- a/internal/presenter/packages/cyclonedx_bom_descriptor.go +++ /dev/null @@ -1,69 +0,0 @@ -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 -} diff --git a/internal/presenter/packages/cyclonedx_component.go b/internal/presenter/packages/cyclonedx_component.go deleted file mode 100644 index 9ff1877dd..000000000 --- a/internal/presenter/packages/cyclonedx_component.go +++ /dev/null @@ -1,27 +0,0 @@ -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 -} diff --git a/internal/presenter/packages/cyclonedx_document.go b/internal/presenter/packages/cyclonedx_document.go deleted file mode 100644 index b11bb8006..000000000 --- a/internal/presenter/packages/cyclonedx_document.go +++ /dev/null @@ -1,57 +0,0 @@ -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.Sorted() { - 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 -} diff --git a/internal/presenter/packages/cyclonedx_presenter.go b/internal/presenter/packages/cyclonedx_presenter.go deleted file mode 100644 index f844fbf02..000000000 --- a/internal/presenter/packages/cyclonedx_presenter.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -Package cyclonedx is responsible for generating a CycloneDX XML report for the given container image or file system. -*/ -package packages - -import ( - "encoding/xml" - "io" - - "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" -) - -// CycloneDxPresenter writes a CycloneDX report from the given Catalog and Locations contents -type CycloneDxPresenter struct { - catalog *pkg.Catalog - srcMetadata source.Metadata -} - -// NewCycloneDxPresenter creates a CycloneDX presenter from the given Catalog and Locations objects. -func NewCycloneDxPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *CycloneDxPresenter { - return &CycloneDxPresenter{ - catalog: catalog, - srcMetadata: srcMetadata, - } -} - -// Present writes the CycloneDX report to the given io.Writer. -func (pres *CycloneDxPresenter) Present(output io.Writer) error { - bom := NewCycloneDxDocument(pres.catalog, pres.srcMetadata) - - encoder := xml.NewEncoder(output) - encoder.Indent("", " ") - - _, err := output.Write([]byte(xml.Header)) - if err != nil { - return err - } - - err = encoder.Encode(bom) - if err != nil { - return err - } - - _, err = output.Write([]byte("\n")) - return err -} diff --git a/internal/presenter/packages/text_presenter_test.go b/internal/presenter/packages/text_presenter_test.go index a6533d356..d561f3357 100644 --- a/internal/presenter/packages/text_presenter_test.go +++ b/internal/presenter/packages/text_presenter_test.go @@ -19,7 +19,7 @@ func TestTextDirectoryPresenter(t *testing.T) { func TestTextImagePresenter(t *testing.T) { testImage := "image-simple" - catalog, metadata, _ := testutils.ImageInput(t, testImage) + catalog, metadata, _ := testutils.ImageInput(t, testImage, testutils.FromSnapshot()) testutils.AssertPresenterAgainstGoldenImageSnapshot(t, NewTextPresenter(catalog, metadata), testImage, diff --git a/syft/format/option.go b/syft/format/option.go index 09ae7268d..184649e91 100644 --- a/syft/format/option.go +++ b/syft/format/option.go @@ -12,7 +12,7 @@ const ( SPDXJSONOption Option = "spdx-json" ) -var AllPresenters = []Option{ +var AllOptions = []Option{ JSONOption, TextOption, TableOption, diff --git a/syft/presenter/packages/presenter.go b/syft/presenter/packages/presenter.go index 117533121..341555c69 100644 --- a/syft/presenter/packages/presenter.go +++ b/syft/presenter/packages/presenter.go @@ -16,8 +16,6 @@ func Presenter(option format.Option, config PresenterConfig) presenter.Presenter switch option { case format.TextOption: return packages.NewTextPresenter(config.Catalog, config.SourceMetadata) - case format.CycloneDxOption: - return packages.NewCycloneDxPresenter(config.Catalog, config.SourceMetadata) case format.SPDXTagValueOption: return packages.NewSPDXTagValuePresenter(config.Catalog, config.SourceMetadata) case format.SPDXJSONOption: diff --git a/test/cli/all_formats_expressible_test.go b/test/cli/all_formats_expressible_test.go new file mode 100644 index 000000000..924c2a0cb --- /dev/null +++ b/test/cli/all_formats_expressible_test.go @@ -0,0 +1,35 @@ +package cli + +import ( + "fmt" + "strings" + "testing" + + "github.com/anchore/syft/syft/format" +) + +func TestAllFormatsExpressible(t *testing.T) { + commonAssertions := []traitAssertion{ + func(tb testing.TB, stdout, _ string, _ int) { + tb.Helper() + if len(stdout) < 1000 { + tb.Errorf("there may not be any report output (len=%d)", len(stdout)) + } + }, + assertSuccessfulReturnCode, + } + + for _, o := range format.AllOptions { + t.Run(fmt.Sprintf("format:%s", o), func(t *testing.T) { + cmd, stdout, stderr := runSyft(t, nil, "dir:./test-fixtures/image-pkg-coverage", "-o", string(o)) + for _, traitFn := range commonAssertions { + traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) + } + if t.Failed() { + t.Log("STDOUT:\n", stdout) + t.Log("STDERR:\n", stderr) + t.Log("COMMAND:", strings.Join(cmd.Args, " ")) + } + }) + } +}