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, " "))
+ }
+ })
+ }
+}