mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
Port cyclonedx presenter to format object (#589)
* add new cyclonedx format object Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * remove cyclonedx presenter Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * remove cyclonedx presenter call Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * remove dependence on golden images for format tests Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * wire up new formt + rename all-presenters ref Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * add CLI test to ensure that all formats can be expressed as report output Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * add cyclonedx version and encoding format to package name Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * optionally preserve format snapshot images Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * fix linting + text unit tests Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
3286a4d4cc
commit
4a2d1d7225
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
29
internal/formats/cyclonedx12xml/encoder.go
Normal file
29
internal/formats/cyclonedx12xml/encoder.go
Normal file
@ -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
|
||||
}
|
||||
@ -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,
|
||||
12
internal/formats/cyclonedx12xml/format.go
Normal file
12
internal/formats/cyclonedx12xml/format.go
Normal file
@ -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,
|
||||
)
|
||||
}
|
||||
31
internal/formats/cyclonedx12xml/model/bom_descriptor.go
Normal file
31
internal/formats/cyclonedx12xml/model/bom_descriptor.go
Normal file
@ -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
|
||||
}
|
||||
20
internal/formats/cyclonedx12xml/model/component.go
Normal file
20
internal/formats/cyclonedx12xml/model/component.go
Normal file
@ -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.)
|
||||
}
|
||||
17
internal/formats/cyclonedx12xml/model/document.go
Normal file
17
internal/formats/cyclonedx12xml/model/document.go
Normal file
@ -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
|
||||
}
|
||||
10
internal/formats/cyclonedx12xml/model/license.go
Normal file
10
internal/formats/cyclonedx12xml/model/license.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
@ -0,0 +1 @@
|
||||
this file has contents
|
||||
@ -0,0 +1 @@
|
||||
file-2 contents!
|
||||
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:5404937f-72d6-44a2-8e9b-954305ecb4f6">
|
||||
<metadata>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component type="file">
|
||||
<name>/some/path</name>
|
||||
<version></version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e34bad2e-cd27-483c-86dc-f4e26d6103b0">
|
||||
<metadata>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component type="container">
|
||||
<name>user-image-input</name>
|
||||
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>a-purl-1</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
97
internal/formats/cyclonedx12xml/to_format_model.go
Normal file
97
internal/formats/cyclonedx12xml/to_format_model.go
Normal file
@ -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
|
||||
}
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -12,7 +12,7 @@ const (
|
||||
SPDXJSONOption Option = "spdx-json"
|
||||
)
|
||||
|
||||
var AllPresenters = []Option{
|
||||
var AllOptions = []Option{
|
||||
JSONOption,
|
||||
TextOption,
|
||||
TableOption,
|
||||
|
||||
@ -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:
|
||||
|
||||
35
test/cli/all_formats_expressible_test.go
Normal file
35
test/cli/all_formats_expressible_test.go
Normal file
@ -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, " "))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user