mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
Split the sbom.Format interface by encode and decode use cases (#2186)
* split up sbom.Format into encode and decode ops Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update cmd pkg to inject format configs Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * bump cyclonedx schema to 1.5 Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * redact image metadata from github encoder tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add more testing around format decoder identify Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add test case for format version options Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix cli tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix CLI test Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * [wip] - review comments Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * keep encoder creation out of post load function Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * keep decider and identify functions Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add a few more doc comments Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * remove format encoder default function helpers Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * address PR feedback Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * move back to streaming based decode functions Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * with common convention for encoder constructors Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix tests and allow for encoders to be created from cli options Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix cli tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * buffer reads from stdin to support seeking Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
7315f83f9d
commit
7392d607b6
254 changed files with 10450 additions and 2714 deletions
|
@ -19,12 +19,10 @@ import (
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
"github.com/anchore/syft/syft/event/monitor"
|
"github.com/anchore/syft/syft/event/monitor"
|
||||||
"github.com/anchore/syft/syft/formats"
|
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||||
"github.com/anchore/syft/syft/formats/github"
|
"github.com/anchore/syft/syft/format/spdxjson"
|
||||||
"github.com/anchore/syft/syft/formats/syftjson"
|
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||||
"github.com/anchore/syft/syft/formats/table"
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
"github.com/anchore/syft/syft/formats/template"
|
|
||||||
"github.com/anchore/syft/syft/formats/text"
|
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
@ -37,30 +35,33 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type attestOptions struct {
|
type attestOptions struct {
|
||||||
options.Config `yaml:",inline" mapstructure:",squash"`
|
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||||
options.SingleOutput `yaml:",inline" mapstructure:",squash"`
|
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||||
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||||
options.Attest `yaml:",inline" mapstructure:",squash"`
|
options.Attest `yaml:",inline" mapstructure:",squash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Attest(app clio.Application) *cobra.Command {
|
func Attest(app clio.Application) *cobra.Command {
|
||||||
id := app.ID()
|
id := app.ID()
|
||||||
|
|
||||||
var allowableOutputs []string
|
|
||||||
for _, f := range formats.AllIDs() {
|
|
||||||
switch f {
|
|
||||||
case table.ID, text.ID, github.ID, template.ID:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
allowableOutputs = append(allowableOutputs, f.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := &attestOptions{
|
opts := &attestOptions{
|
||||||
UpdateCheck: options.DefaultUpdateCheck(),
|
UpdateCheck: options.DefaultUpdateCheck(),
|
||||||
SingleOutput: options.SingleOutput{
|
Output: options.Output{
|
||||||
AllowableOptions: allowableOutputs,
|
AllowMultipleOutputs: false,
|
||||||
Output: syftjson.ID.String(),
|
AllowableOptions: []string{
|
||||||
|
string(syftjson.ID),
|
||||||
|
string(cyclonedxjson.ID),
|
||||||
|
string(spdxjson.ID),
|
||||||
|
string(spdxtagvalue.ID),
|
||||||
|
},
|
||||||
|
Outputs: []string{syftjson.ID.String()},
|
||||||
|
OutputFile: options.OutputFile{ // nolint:staticcheck
|
||||||
|
Enabled: false, // explicitly not allowed
|
||||||
|
},
|
||||||
|
OutputTemplate: options.OutputTemplate{
|
||||||
|
Enabled: false, // explicitly not allowed
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Catalog: options.DefaultCatalog(),
|
Catalog: options.DefaultCatalog(),
|
||||||
}
|
}
|
||||||
|
@ -95,15 +96,13 @@ func runAttest(id clio.Identification, opts *attestOptions, userInput string) er
|
||||||
return fmt.Errorf("unable to build SBOM: %w", err)
|
return fmt.Errorf("unable to build SBOM: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
o := opts.Output
|
f, err := os.CreateTemp("", "syft-attest-")
|
||||||
|
|
||||||
f, err := os.CreateTemp("", o)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create temp file: %w", err)
|
return fmt.Errorf("unable to create temp file: %w", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(f.Name())
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
writer, err := opts.SBOMWriter(f.Name())
|
writer, err := opts.SBOMWriter()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create SBOM writer: %w", err)
|
return fmt.Errorf("unable to create SBOM writer: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -118,10 +117,21 @@ func runAttest(id clio.Identification, opts *attestOptions, userInput string) er
|
||||||
return fmt.Errorf("unable to find cosign in PATH; make sure you have it installed")
|
return fmt.Errorf("unable to find cosign in PATH; make sure you have it installed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outputNames := opts.OutputNameSet()
|
||||||
|
var outputName string
|
||||||
|
switch outputNames.Size() {
|
||||||
|
case 0:
|
||||||
|
return fmt.Errorf("no output format specified")
|
||||||
|
case 1:
|
||||||
|
outputName = outputNames.List()[0]
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("multiple output formats specified: %s", strings.Join(outputNames.List(), ", "))
|
||||||
|
}
|
||||||
|
|
||||||
// Select Cosign predicate type based on defined output type
|
// Select Cosign predicate type based on defined output type
|
||||||
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
|
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
|
||||||
var predicateType string
|
var predicateType string
|
||||||
switch strings.ToLower(o) {
|
switch strings.ToLower(outputName) {
|
||||||
case "cyclonedx-json":
|
case "cyclonedx-json":
|
||||||
predicateType = "cyclonedx"
|
predicateType = "cyclonedx"
|
||||||
case "spdx-tag-value", "spdx-tv":
|
case "spdx-tag-value", "spdx-tv":
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/formats"
|
"github.com/anchore/syft/syft/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -23,7 +23,7 @@ const (
|
||||||
|
|
||||||
type ConvertOptions struct {
|
type ConvertOptions struct {
|
||||||
options.Config `yaml:",inline" mapstructure:",squash"`
|
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||||
options.MultiOutput `yaml:",inline" mapstructure:",squash"`
|
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ func Convert(app clio.Application) *cobra.Command {
|
||||||
|
|
||||||
opts := &ConvertOptions{
|
opts := &ConvertOptions{
|
||||||
UpdateCheck: options.DefaultUpdateCheck(),
|
UpdateCheck: options.DefaultUpdateCheck(),
|
||||||
|
Output: options.DefaultOutput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.SetupCommand(&cobra.Command{
|
return app.SetupCommand(&cobra.Command{
|
||||||
|
@ -63,10 +64,13 @@ func RunConvert(opts *ConvertOptions, userInput string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var reader io.ReadCloser
|
var reader io.ReadSeekCloser
|
||||||
|
|
||||||
if userInput == "-" {
|
if userInput == "-" {
|
||||||
reader = os.Stdin
|
// though os.Stdin is an os.File, it does not support seeking
|
||||||
|
// you will get errors such as "seek /dev/stdin: illegal seek".
|
||||||
|
// We need to buffer what we read.
|
||||||
|
reader = internal.NewBufferedSeeker(os.Stdin)
|
||||||
} else {
|
} else {
|
||||||
f, err := os.Open(userInput)
|
f, err := os.Open(userInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -78,7 +82,7 @@ func RunConvert(opts *ConvertOptions, userInput string) error {
|
||||||
reader = f
|
reader = f
|
||||||
}
|
}
|
||||||
|
|
||||||
s, _, err := formats.Decode(reader)
|
s, _, _, err := format.Decode(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decode SBOM: %w", err)
|
return fmt.Errorf("failed to decode SBOM: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import (
|
||||||
"github.com/anchore/syft/internal/file"
|
"github.com/anchore/syft/internal/file"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/formats/template"
|
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
@ -55,14 +54,14 @@ const (
|
||||||
|
|
||||||
type packagesOptions struct {
|
type packagesOptions struct {
|
||||||
options.Config `yaml:",inline" mapstructure:",squash"`
|
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||||
options.MultiOutput `yaml:",inline" mapstructure:",squash"`
|
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||||
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultPackagesOptions() *packagesOptions {
|
func defaultPackagesOptions() *packagesOptions {
|
||||||
return &packagesOptions{
|
return &packagesOptions{
|
||||||
MultiOutput: options.DefaultOutput(),
|
Output: options.DefaultOutput(),
|
||||||
UpdateCheck: options.DefaultUpdateCheck(),
|
UpdateCheck: options.DefaultUpdateCheck(),
|
||||||
Catalog: options.DefaultCatalog(),
|
Catalog: options.DefaultCatalog(),
|
||||||
}
|
}
|
||||||
|
@ -108,11 +107,6 @@ func validateArgs(cmd *cobra.Command, args []string, error string) error {
|
||||||
|
|
||||||
// nolint:funlen
|
// nolint:funlen
|
||||||
func runPackages(id clio.Identification, opts *packagesOptions, userInput string) error {
|
func runPackages(id clio.Identification, opts *packagesOptions, userInput string) error {
|
||||||
err := validatePackageOutputOptions(&opts.MultiOutput)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
writer, err := opts.SBOMWriter()
|
writer, err := opts.SBOMWriter()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -235,19 +229,3 @@ func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []art
|
||||||
|
|
||||||
return relationships
|
return relationships
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePackageOutputOptions(cfg *options.MultiOutput) error {
|
|
||||||
var usesTemplateOutput bool
|
|
||||||
for _, o := range cfg.Outputs {
|
|
||||||
if o == template.ID.String() {
|
|
||||||
usesTemplateOutput = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if usesTemplateOutput && cfg.OutputTemplatePath == "" {
|
|
||||||
return fmt.Errorf(`must specify path to template file when using "template" output format`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/formats/syftjson"
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
@ -42,6 +42,9 @@ func PowerUser(app clio.Application) *cobra.Command {
|
||||||
pkgs.FileClassification.Cataloger.Enabled = true
|
pkgs.FileClassification.Cataloger.Enabled = true
|
||||||
opts := &powerUserOptions{
|
opts := &powerUserOptions{
|
||||||
Catalog: pkgs,
|
Catalog: pkgs,
|
||||||
|
OutputFile: options.OutputFile{ // nolint:staticcheck
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.SetupCommand(&cobra.Command{
|
return app.SetupCommand(&cobra.Command{
|
||||||
|
@ -62,7 +65,7 @@ func PowerUser(app clio.Application) *cobra.Command {
|
||||||
|
|
||||||
//nolint:funlen
|
//nolint:funlen
|
||||||
func runPowerUser(id clio.Identification, opts *powerUserOptions, userInput string) error {
|
func runPowerUser(id clio.Identification, opts *powerUserOptions, userInput string) error {
|
||||||
writer, err := opts.SBOMWriter(syftjson.Format())
|
writer, err := opts.SBOMWriter(syftjson.NewFormatEncoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,99 +2,232 @@ package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
"github.com/anchore/clio"
|
"github.com/anchore/clio"
|
||||||
"github.com/anchore/fangs"
|
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||||
"github.com/anchore/syft/syft/formats"
|
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||||
"github.com/anchore/syft/syft/formats/table"
|
"github.com/anchore/syft/syft/format/github"
|
||||||
"github.com/anchore/syft/syft/formats/template"
|
"github.com/anchore/syft/syft/format/spdxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||||
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/format/table"
|
||||||
|
"github.com/anchore/syft/syft/format/template"
|
||||||
|
"github.com/anchore/syft/syft/format/text"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MultiOutput has the standard output options syft accepts: multiple -o, --file, --template
|
|
||||||
type MultiOutput struct {
|
|
||||||
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
|
||||||
OutputFile `yaml:",inline" json:"" mapstructure:",squash"`
|
|
||||||
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ interface {
|
|
||||||
clio.FlagAdder
|
|
||||||
} = (*MultiOutput)(nil)
|
|
||||||
|
|
||||||
func DefaultOutput() MultiOutput {
|
|
||||||
return MultiOutput{
|
|
||||||
Outputs: []string{string(table.ID)},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *MultiOutput) AddFlags(flags clio.FlagSet) {
|
|
||||||
flags.StringArrayVarP(&o.Outputs, "output", "o",
|
|
||||||
fmt.Sprintf("report output format (<format>=<file> to output to a file), formats=%v", formats.AllIDs()))
|
|
||||||
|
|
||||||
flags.StringVarP(&o.OutputTemplatePath, "template", "t",
|
|
||||||
"specify the path to a Go template file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *MultiOutput) SBOMWriter() (sbom.Writer, error) {
|
|
||||||
return makeSBOMWriter(o.Outputs, o.File, o.OutputTemplatePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SingleOutput allows only 1 output to be specified, with a user able to set what options are allowed by setting AllowableOptions
|
|
||||||
type SingleOutput struct {
|
|
||||||
AllowableOptions []string `yaml:"-" json:"-" mapstructure:"-"`
|
|
||||||
Output string `yaml:"output" json:"output" mapstructure:"output"`
|
|
||||||
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ clio.FlagAdder = (*SingleOutput)(nil)
|
|
||||||
|
|
||||||
func (o *SingleOutput) AddFlags(flags clio.FlagSet) {
|
|
||||||
flags.StringVarP(&o.Output, "output", "o",
|
|
||||||
fmt.Sprintf("report output format, options=%v", o.AllowableOptions))
|
|
||||||
|
|
||||||
if slices.Contains(o.AllowableOptions, template.ID.String()) {
|
|
||||||
flags.StringVarP(&o.OutputTemplatePath, "template", "t",
|
|
||||||
"specify the path to a Go template file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *SingleOutput) SBOMWriter(file string) (sbom.Writer, error) {
|
|
||||||
return makeSBOMWriter([]string{o.Output}, file, o.OutputTemplatePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: OutputFile is only the --file argument
|
|
||||||
type OutputFile struct {
|
|
||||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ interface {
|
var _ interface {
|
||||||
clio.FlagAdder
|
clio.FlagAdder
|
||||||
clio.PostLoader
|
clio.PostLoader
|
||||||
} = (*OutputFile)(nil)
|
} = (*Output)(nil)
|
||||||
|
|
||||||
func (o *OutputFile) AddFlags(flags clio.FlagSet) {
|
// Output has the standard output options syft accepts: multiple -o, --file, --template
|
||||||
flags.StringVarP(&o.File, "file", "",
|
type Output struct {
|
||||||
"file to write the default report output to (default is STDOUT)")
|
AllowableOptions []string `yaml:"-" json:"-" mapstructure:"-"`
|
||||||
|
AllowMultipleOutputs bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||||
|
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
||||||
|
OutputFile `yaml:",inline" json:"" mapstructure:",squash"`
|
||||||
|
OutputTemplate `yaml:"template" json:"template" mapstructure:"template"`
|
||||||
|
}
|
||||||
|
|
||||||
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
func DefaultOutput() Output {
|
||||||
flagSet := pfp.PFlagSet()
|
return Output{
|
||||||
flagSet.Lookup("file").Deprecated = "use: output"
|
AllowMultipleOutputs: true,
|
||||||
|
Outputs: []string{string(table.ID)},
|
||||||
|
OutputFile: OutputFile{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
OutputTemplate: OutputTemplate{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OutputFile) PostLoad() error {
|
func (o *Output) AddFlags(flags clio.FlagSet) {
|
||||||
if o.File != "" {
|
var names []string
|
||||||
file, err := expandFilePath(o.File)
|
for _, id := range supportedIDs() {
|
||||||
|
names = append(names, id.String())
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
flags.StringArrayVarP(&o.Outputs, "output", "o",
|
||||||
|
fmt.Sprintf("report output format (<format>=<file> to output to a file), formats=%v", names))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Output) SBOMWriter() (sbom.Writer, error) {
|
||||||
|
names := o.OutputNameSet()
|
||||||
|
|
||||||
|
if len(o.Outputs) > 1 && !o.AllowMultipleOutputs {
|
||||||
|
return nil, fmt.Errorf("only one output format is allowed (given %d: %s)", len(o.Outputs), names)
|
||||||
|
}
|
||||||
|
|
||||||
|
usesTemplateOutput := names.Has(string(template.ID))
|
||||||
|
|
||||||
|
if usesTemplateOutput && o.OutputTemplate.Path == "" {
|
||||||
|
return nil, fmt.Errorf(`must specify path to template file when using "template" output format`)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoders, err := o.Encoders()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeSBOMWriter(o.Outputs, o.File, encoders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Output) Encoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
// setup all encoders based on the configuration
|
||||||
|
var list encoderList
|
||||||
|
|
||||||
|
// in the future there will be application configuration options that can be used to set the default output format
|
||||||
|
list.addWithErr(template.ID)(o.OutputTemplate.formatEncoders())
|
||||||
|
list.add(syftjson.ID)(syftjson.NewFormatEncoder())
|
||||||
|
list.add(table.ID)(table.NewFormatEncoder())
|
||||||
|
list.add(text.ID)(text.NewFormatEncoder())
|
||||||
|
list.add(github.ID)(github.NewFormatEncoder())
|
||||||
|
list.addWithErr(cyclonedxxml.ID)(cycloneDxXMLEncoders())
|
||||||
|
list.addWithErr(cyclonedxjson.ID)(cycloneDxJSONEncoders())
|
||||||
|
list.addWithErr(spdxjson.ID)(spdxJSONEncoders())
|
||||||
|
list.addWithErr(spdxtagvalue.ID)(spdxTagValueEncoders())
|
||||||
|
|
||||||
|
return list.encoders, list.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Output) OutputNameSet() *strset.Set {
|
||||||
|
names := strset.New()
|
||||||
|
for _, output := range o.Outputs {
|
||||||
|
fields := strings.Split(output, "=")
|
||||||
|
names.Add(fields[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderList struct {
|
||||||
|
encoders []sbom.FormatEncoder
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *encoderList) addWithErr(name sbom.FormatID) func([]sbom.FormatEncoder, error) {
|
||||||
|
return func(encs []sbom.FormatEncoder, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: %w", name, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, enc := range encs {
|
||||||
|
if enc == nil {
|
||||||
|
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: nil encoder returned", name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.encoders = append(l.encoders, enc)
|
||||||
}
|
}
|
||||||
o.File = file
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OutputFile) SBOMWriter(f sbom.Format) (sbom.Writer, error) {
|
func (l *encoderList) add(name sbom.FormatID) func(...sbom.FormatEncoder) {
|
||||||
return makeSBOMWriterForFormat(f, o.File)
|
return func(encs ...sbom.FormatEncoder) {
|
||||||
|
for _, enc := range encs {
|
||||||
|
if enc == nil {
|
||||||
|
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: nil encoder returned", name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.encoders = append(l.encoders, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when application configuration is made for this format then this should be ported to the options object
|
||||||
|
// that is created for that configuration (as done with the template output option)
|
||||||
|
func cycloneDxXMLEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
var (
|
||||||
|
encs []sbom.FormatEncoder
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
for _, v := range cyclonedxxml.SupportedVersions() {
|
||||||
|
enc, err := cyclonedxxml.NewFormatEncoderWithConfig(cyclonedxxml.EncoderConfig{Version: v})
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
} else {
|
||||||
|
encs = append(encs, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encs, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when application configuration is made for this format then this should be ported to the options object
|
||||||
|
// that is created for that configuration (as done with the template output option)
|
||||||
|
func cycloneDxJSONEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
var (
|
||||||
|
encs []sbom.FormatEncoder
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
for _, v := range cyclonedxjson.SupportedVersions() {
|
||||||
|
enc, err := cyclonedxjson.NewFormatEncoderWithConfig(cyclonedxjson.EncoderConfig{Version: v})
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
} else {
|
||||||
|
encs = append(encs, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encs, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when application configuration is made for this format then this should be ported to the options object
|
||||||
|
// that is created for that configuration (as done with the template output option)
|
||||||
|
func spdxJSONEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
var (
|
||||||
|
encs []sbom.FormatEncoder
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
for _, v := range spdxjson.SupportedVersions() {
|
||||||
|
enc, err := spdxjson.NewFormatEncoderWithConfig(spdxjson.EncoderConfig{Version: v})
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
} else {
|
||||||
|
encs = append(encs, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encs, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when application configuration is made for this format then this should be ported to the options object
|
||||||
|
// that is created for that configuration (as done with the template output option)
|
||||||
|
func spdxTagValueEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
var (
|
||||||
|
encs []sbom.FormatEncoder
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
for _, v := range spdxtagvalue.SupportedVersions() {
|
||||||
|
enc, err := spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.EncoderConfig{Version: v})
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
} else {
|
||||||
|
encs = append(encs, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encs, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportedIDs() []sbom.FormatID {
|
||||||
|
encs := []sbom.FormatID{
|
||||||
|
// encoders that support a single version
|
||||||
|
syftjson.ID,
|
||||||
|
github.ID,
|
||||||
|
table.ID,
|
||||||
|
text.ID,
|
||||||
|
template.ID,
|
||||||
|
|
||||||
|
// encoders that support multiple versions
|
||||||
|
cyclonedxxml.ID,
|
||||||
|
cyclonedxjson.ID,
|
||||||
|
spdxtagvalue.ID,
|
||||||
|
spdxjson.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return encs
|
||||||
}
|
}
|
||||||
|
|
56
cmd/syft/cli/options/output_file.go
Normal file
56
cmd/syft/cli/options/output_file.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/fangs"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
clio.FlagAdder
|
||||||
|
clio.PostLoader
|
||||||
|
} = (*OutputFile)(nil)
|
||||||
|
|
||||||
|
// Deprecated: OutputFile supports the --file to write the SBOM output to
|
||||||
|
type OutputFile struct {
|
||||||
|
Enabled bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||||
|
File string `yaml:"file" json:"file" mapstructure:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OutputFile) AddFlags(flags clio.FlagSet) {
|
||||||
|
if o.Enabled {
|
||||||
|
flags.StringVarP(&o.File, "file", "",
|
||||||
|
"file to write the default report output to (default is STDOUT)")
|
||||||
|
|
||||||
|
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||||
|
flagSet := pfp.PFlagSet()
|
||||||
|
flagSet.Lookup("file").Deprecated = "use: output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OutputFile) PostLoad() error {
|
||||||
|
if !o.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if o.File != "" {
|
||||||
|
file, err := expandFilePath(o.File)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.File = file
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OutputFile) SBOMWriter(f sbom.FormatEncoder) (sbom.Writer, error) {
|
||||||
|
if !o.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(f, o.File))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer, nil
|
||||||
|
}
|
31
cmd/syft/cli/options/output_template.go
Normal file
31
cmd/syft/cli/options/output_template.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/syft/format/template"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ clio.FlagAdder = (*OutputTemplate)(nil)
|
||||||
|
|
||||||
|
type OutputTemplate struct {
|
||||||
|
Enabled bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||||
|
Path string `yaml:"path" json:"path" mapstructure:"path"` // -t template file to use for output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OutputTemplate) AddFlags(flags clio.FlagSet) {
|
||||||
|
if o.Enabled {
|
||||||
|
flags.StringVarP(&o.Path, "template", "t",
|
||||||
|
"specify the path to a Go template file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o OutputTemplate) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
if !o.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
enc, err := template.NewFormatEncoder(template.EncoderConfig{
|
||||||
|
TemplatePath: o.Path,
|
||||||
|
})
|
||||||
|
return []sbom.FormatEncoder{enc}, err
|
||||||
|
}
|
179
cmd/syft/cli/options/output_test.go
Normal file
179
cmd/syft/cli/options/output_test.go
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||||
|
"github.com/anchore/syft/syft/format/github"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||||
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/format/table"
|
||||||
|
"github.com/anchore/syft/syft/format/template"
|
||||||
|
"github.com/anchore/syft/syft/format/text"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_getEncoders(t *testing.T) {
|
||||||
|
allDefaultEncoderNames := strset.New()
|
||||||
|
for _, id := range supportedIDs() {
|
||||||
|
allDefaultEncoderNames.Add(id.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := DefaultOutput()
|
||||||
|
opts.OutputTemplate.Path = "somewhere"
|
||||||
|
|
||||||
|
encoders, err := opts.Encoders()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, encoders)
|
||||||
|
|
||||||
|
encoderNames := strset.New()
|
||||||
|
for _, e := range encoders {
|
||||||
|
encoderNames.Add(e.ID().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, allDefaultEncoderNames.List(), encoderNames.List(), "not all encoders are expressed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_EncoderCollection_ByString_IDOnly_Defaults(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want sbom.FormatID
|
||||||
|
}{
|
||||||
|
// SPDX Tag-Value
|
||||||
|
{
|
||||||
|
name: "spdx",
|
||||||
|
want: spdxtagvalue.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdx-tag-value",
|
||||||
|
want: spdxtagvalue.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdx-tv",
|
||||||
|
want: spdxtagvalue.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdxtv", // clean variant
|
||||||
|
want: spdxtagvalue.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// SPDX JSON
|
||||||
|
{
|
||||||
|
name: "spdx-json",
|
||||||
|
want: spdxjson.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdxjson", // clean variant
|
||||||
|
want: spdxjson.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cyclonedx JSON
|
||||||
|
{
|
||||||
|
name: "cyclonedx-json",
|
||||||
|
want: cyclonedxjson.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclonedxjson", // clean variant
|
||||||
|
want: cyclonedxjson.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cyclonedx XML
|
||||||
|
{
|
||||||
|
name: "cdx",
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclone",
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclonedx",
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclonedx-xml",
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclonedxxml", // clean variant
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Syft Table
|
||||||
|
{
|
||||||
|
name: "table",
|
||||||
|
want: table.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "syft-table",
|
||||||
|
want: table.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Syft Text
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
want: text.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "syft-text",
|
||||||
|
want: text.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Syft JSON
|
||||||
|
{
|
||||||
|
name: "json",
|
||||||
|
want: syftjson.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "syft-json",
|
||||||
|
want: syftjson.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "syftjson", // clean variant
|
||||||
|
want: syftjson.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// GitHub JSON
|
||||||
|
{
|
||||||
|
name: "github",
|
||||||
|
want: github.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "github-json",
|
||||||
|
want: github.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Syft template
|
||||||
|
{
|
||||||
|
name: "template",
|
||||||
|
want: template.ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := DefaultOutput()
|
||||||
|
opts.OutputTemplate.Path = "somewhere"
|
||||||
|
|
||||||
|
defaultEncoders, err := opts.Encoders()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
encoders := format.NewEncoderCollection(defaultEncoders...)
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
f := encoders.GetByString(tt.name)
|
||||||
|
if tt.want == "" {
|
||||||
|
require.Nil(t, f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NotNil(t, f)
|
||||||
|
assert.Equal(t, tt.want, f.ID())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,16 +6,17 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/formats"
|
"github.com/anchore/syft/syft/format"
|
||||||
"github.com/anchore/syft/syft/formats/table"
|
"github.com/anchore/syft/syft/format/table"
|
||||||
"github.com/anchore/syft/syft/formats/template"
|
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,8 +29,8 @@ var _ interface {
|
||||||
|
|
||||||
// makeSBOMWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
|
// makeSBOMWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
|
||||||
// or an error but neither both and if there is no error, sbom.Writer.Close() should be called
|
// or an error but neither both and if there is no error, sbom.Writer.Close() should be called
|
||||||
func makeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
|
func makeSBOMWriter(outputs []string, defaultFile string, encoders []sbom.FormatEncoder) (sbom.Writer, error) {
|
||||||
outputOptions, err := parseSBOMOutputFlags(outputs, defaultFile, templateFilePath)
|
outputOptions, err := parseSBOMOutputFlags(outputs, defaultFile, encoders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -42,18 +43,10 @@ func makeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbo
|
||||||
return writer, nil
|
return writer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error.
|
|
||||||
func makeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) {
|
|
||||||
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(format, path))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return writer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSBOMOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file
|
// parseSBOMOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file
|
||||||
func parseSBOMOutputFlags(outputs []string, defaultFile, templateFilePath string) (out []sbomWriterDescription, errs error) {
|
func parseSBOMOutputFlags(outputs []string, defaultFile string, encoders []sbom.FormatEncoder) (out []sbomWriterDescription, errs error) {
|
||||||
|
encoderCollection := format.NewEncoderCollection(encoders...)
|
||||||
|
|
||||||
// always should have one option -- we generally get the default of "table", but just make sure
|
// always should have one option -- we generally get the default of "table", but just make sure
|
||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
outputs = append(outputs, table.ID.String())
|
outputs = append(outputs, table.ID.String())
|
||||||
|
@ -76,29 +69,77 @@ func parseSBOMOutputFlags(outputs []string, defaultFile, templateFilePath string
|
||||||
file = parts[1]
|
file = parts[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
format := formats.ByName(name)
|
enc := encoderCollection.GetByString(name)
|
||||||
if format == nil {
|
if enc == nil {
|
||||||
errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, formats.AllIDs()))
|
errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, formatVersionOptions(encoderCollection.NameVersions())))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if tmpl, ok := format.(template.OutputFormat); ok {
|
out = append(out, newSBOMWriterDescription(enc, file))
|
||||||
tmpl.SetTemplatePath(templateFilePath)
|
|
||||||
format = tmpl
|
|
||||||
}
|
|
||||||
|
|
||||||
out = append(out, newSBOMWriterDescription(format, file))
|
|
||||||
}
|
}
|
||||||
return out, errs
|
return out, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatVersionOptions takes a list like ["github-json", "syft-json@11.0.0", "cyclonedx-xml@1.0", "cyclondx-xml@1.1"...]
|
||||||
|
// and formats it into a human-readable string like:
|
||||||
|
//
|
||||||
|
// Available formats:
|
||||||
|
// - cyclonedx-json @ 1.2, 1.3, 1.4, 1.5
|
||||||
|
// - cyclonedx-xml @ 1.0, 1.1, 1.2, 1.3, 1.4, 1.5
|
||||||
|
// - github-json
|
||||||
|
// - spdx-json @ 2.2, 2.3
|
||||||
|
// - spdx-tag-value @ 2.1, 2.2, 2.3
|
||||||
|
// - syft-json
|
||||||
|
// - syft-table
|
||||||
|
// - syft-text
|
||||||
|
// - template
|
||||||
|
func formatVersionOptions(nameVersionPairs []string) string {
|
||||||
|
availableVersions := make(map[string][]string)
|
||||||
|
availableFormats := strset.New()
|
||||||
|
for _, nameVersion := range nameVersionPairs {
|
||||||
|
fields := strings.SplitN(nameVersion, "@", 2)
|
||||||
|
if len(fields) == 2 {
|
||||||
|
availableVersions[fields[0]] = append(availableVersions[fields[0]], fields[1])
|
||||||
|
}
|
||||||
|
availableFormats.Add(fields[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// find any formats with exactly one version -- remove them from the version map
|
||||||
|
for name, versions := range availableVersions {
|
||||||
|
if len(versions) == 1 {
|
||||||
|
delete(availableVersions, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedAvailableFormats := availableFormats.List()
|
||||||
|
sort.Strings(sortedAvailableFormats)
|
||||||
|
|
||||||
|
var s strings.Builder
|
||||||
|
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString("Available formats:")
|
||||||
|
|
||||||
|
for _, name := range sortedAvailableFormats {
|
||||||
|
s.WriteString("\n")
|
||||||
|
|
||||||
|
s.WriteString(fmt.Sprintf(" - %s", name))
|
||||||
|
|
||||||
|
if len(availableVersions[name]) > 0 {
|
||||||
|
s.WriteString(" @ ")
|
||||||
|
s.WriteString(strings.Join(availableVersions[name], ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
// sbomWriterDescription Format and path strings used to create sbom.Writer
|
// sbomWriterDescription Format and path strings used to create sbom.Writer
|
||||||
type sbomWriterDescription struct {
|
type sbomWriterDescription struct {
|
||||||
Format sbom.Format
|
Format sbom.FormatEncoder
|
||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSBOMWriterDescription(f sbom.Format, p string) sbomWriterDescription {
|
func newSBOMWriterDescription(f sbom.FormatEncoder, p string) sbomWriterDescription {
|
||||||
expandedPath, err := homedir.Expand(p)
|
expandedPath, err := homedir.Expand(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("could not expand given writer output path=%q: %w", p, err)
|
log.Warnf("could not expand given writer output path=%q: %w", p, err)
|
||||||
|
@ -171,7 +212,7 @@ func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) {
|
||||||
|
|
||||||
// sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
// sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
||||||
type sbomStreamWriter struct {
|
type sbomStreamWriter struct {
|
||||||
format sbom.Format
|
format sbom.FormatEncoder
|
||||||
out io.Writer
|
out io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +232,7 @@ func (w *sbomStreamWriter) Close() error {
|
||||||
|
|
||||||
// sbomPublisher implements sbom.Writer that publishes results to the event bus
|
// sbomPublisher implements sbom.Writer that publishes results to the event bus
|
||||||
type sbomPublisher struct {
|
type sbomPublisher struct {
|
||||||
format sbom.Format
|
format sbom.FormatEncoder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the provided SBOM to the data stream
|
// Write the provided SBOM to the data stream
|
||||||
|
|
|
@ -8,43 +8,71 @@ import (
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/homedir"
|
"github.com/docker/docker/pkg/homedir"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_MakeSBOMWriter(t *testing.T) {
|
func Test_MakeSBOMWriter(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
name string
|
||||||
outputs []string
|
outputs []string
|
||||||
wantErr assert.ErrorAssertionFunc
|
wantErr assert.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
name: "go case",
|
||||||
outputs: []string{"json"},
|
outputs: []string{"json"},
|
||||||
wantErr: assert.NoError,
|
wantErr: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "multiple",
|
||||||
outputs: []string{"table", "json"},
|
outputs: []string{"table", "json"},
|
||||||
wantErr: assert.NoError,
|
wantErr: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "unknown format",
|
||||||
outputs: []string{"unknown"},
|
outputs: []string{"unknown"},
|
||||||
wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool {
|
wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool {
|
||||||
return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are: [`)
|
return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are:`)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
_, err := makeSBOMWriter(tt.outputs, "", "")
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tt.wantErr(t, err)
|
opt := DefaultOutput()
|
||||||
|
encoders, err := opt.Encoders()
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = makeSBOMWriter(tt.outputs, "", encoders)
|
||||||
|
tt.wantErr(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dummyEncoder(io.Writer, sbom.SBOM) error {
|
func dummyFormat(name string) sbom.FormatEncoder {
|
||||||
|
return dummyEncoder{name: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ sbom.FormatEncoder = (*dummyEncoder)(nil)
|
||||||
|
|
||||||
|
type dummyEncoder struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dummyEncoder) ID() sbom.FormatID {
|
||||||
|
return sbom.FormatID(d.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dummyEncoder) Aliases() []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dummyFormat(name string) sbom.Format {
|
func (d dummyEncoder) Version() string {
|
||||||
return sbom.NewFormat(sbom.AnyVersion, dummyEncoder, nil, nil, sbom.FormatID(name))
|
return sbom.AnyVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dummyEncoder) Encode(writer io.Writer, s sbom.SBOM) error {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_newSBOMMultiWriter(t *testing.T) {
|
func Test_newSBOMMultiWriter(t *testing.T) {
|
||||||
|
@ -227,3 +255,39 @@ func Test_newSBOMWriterDescription(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_formatVersionOptions(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nameVersionPairs []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "gocase",
|
||||||
|
nameVersionPairs: []string{
|
||||||
|
"cyclonedx-json@1.2", "cyclonedx-json@1.3", "cyclonedx-json@1.4", "cyclonedx-json@1.5",
|
||||||
|
"cyclonedx-xml@1.0", "cyclonedx-xml@1.1", "cyclonedx-xml@1.2", "cyclonedx-xml@1.3",
|
||||||
|
"cyclonedx-xml@1.4", "cyclonedx-xml@1.5", "github-json", "spdx-json@2.2", "spdx-json@2.3",
|
||||||
|
"spdx-tag-value@2.1", "spdx-tag-value@2.2", "spdx-tag-value@2.3", "syft-json@11.0.0",
|
||||||
|
"syft-table", "syft-text", "template",
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
Available formats:
|
||||||
|
- cyclonedx-json @ 1.2, 1.3, 1.4, 1.5
|
||||||
|
- cyclonedx-xml @ 1.0, 1.1, 1.2, 1.3, 1.4, 1.5
|
||||||
|
- github-json
|
||||||
|
- spdx-json @ 2.2, 2.3
|
||||||
|
- spdx-tag-value @ 2.1, 2.2, 2.3
|
||||||
|
- syft-json
|
||||||
|
- syft-table
|
||||||
|
- syft-text
|
||||||
|
- template`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, formatVersionOptions(tt.nameVersionPairs))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
11
go.mod
11
go.mod
|
@ -25,7 +25,7 @@ require (
|
||||||
github.com/charmbracelet/lipgloss v0.9.1
|
github.com/charmbracelet/lipgloss v0.9.1
|
||||||
github.com/dave/jennifer v1.7.0
|
github.com/dave/jennifer v1.7.0
|
||||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/distribution/reference v0.5.0
|
||||||
github.com/docker/docker v24.0.6+incompatible
|
github.com/docker/docker v24.0.6+incompatible
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/facebookincubator/nvdtools v0.1.5
|
github.com/facebookincubator/nvdtools v0.1.5
|
||||||
|
@ -55,6 +55,7 @@ require (
|
||||||
github.com/pelletier/go-toml v1.9.5
|
github.com/pelletier/go-toml v1.9.5
|
||||||
github.com/saferwall/pe v1.4.7
|
github.com/saferwall/pe v1.4.7
|
||||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
|
||||||
|
github.com/sanity-io/litter v1.5.5
|
||||||
github.com/sassoftware/go-rpmutils v0.2.0
|
github.com/sassoftware/go-rpmutils v0.2.0
|
||||||
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
|
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
|
||||||
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
|
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
|
||||||
|
@ -71,16 +72,10 @@ require (
|
||||||
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||||
golang.org/x/mod v0.13.0
|
golang.org/x/mod v0.13.0
|
||||||
golang.org/x/net v0.17.0
|
golang.org/x/net v0.17.0
|
||||||
golang.org/x/term v0.13.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
modernc.org/sqlite v1.26.0
|
modernc.org/sqlite v1.26.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/distribution/reference v0.5.0
|
|
||||||
github.com/sanity-io/litter v1.5.5
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
|
||||||
|
@ -113,6 +108,7 @@ require (
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/docker/cli v24.0.0+incompatible // indirect
|
github.com/docker/cli v24.0.0+incompatible // indirect
|
||||||
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||||
|
@ -209,6 +205,7 @@ require (
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
golang.org/x/crypto v0.14.0 // indirect
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
golang.org/x/term v0.13.0 // indirect
|
||||||
golang.org/x/text v0.13.0 // indirect
|
golang.org/x/text v0.13.0 // indirect
|
||||||
golang.org/x/tools v0.13.0 // indirect
|
golang.org/x/tools v0.13.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
|
|
84
internal/buffered_seek_reader.go
Normal file
84
internal/buffered_seek_reader.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ io.ReadSeekCloser = (*bufferedSeekReader)(nil)
|
||||||
|
|
||||||
|
// bufferedSeekReader wraps an io.ReadCloser to provide io.Seeker functionality.
|
||||||
|
// It only supports seeking from the start and cannot seek past what has already been read.
|
||||||
|
type bufferedSeekReader struct {
|
||||||
|
r io.ReadCloser
|
||||||
|
buf *bytes.Reader
|
||||||
|
data []byte
|
||||||
|
pos int64
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBufferedSeeker(rc io.ReadCloser) io.ReadSeekCloser {
|
||||||
|
return &bufferedSeekReader{
|
||||||
|
r: rc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *bufferedSeekReader) Read(p []byte) (int, error) {
|
||||||
|
if bs.closed {
|
||||||
|
return 0, errors.New("cannot read from closed reader")
|
||||||
|
}
|
||||||
|
if bs.pos == int64(len(bs.data)) {
|
||||||
|
// if we're at the end of our buffer, read more data into it
|
||||||
|
tmp := make([]byte, len(p))
|
||||||
|
|
||||||
|
n, err := bs.r.Read(tmp)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return 0, err
|
||||||
|
} else if err == io.EOF {
|
||||||
|
bs.closed = true
|
||||||
|
}
|
||||||
|
bs.data = append(bs.data, tmp[:n]...)
|
||||||
|
bs.buf = bytes.NewReader(bs.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := bs.buf.ReadAt(p, bs.pos)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
log.WithFields("error", err).Trace("buffered seek reader failed to read from underlying reader")
|
||||||
|
}
|
||||||
|
bs.pos += int64(n)
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *bufferedSeekReader) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
var abs int64
|
||||||
|
switch whence {
|
||||||
|
case io.SeekStart:
|
||||||
|
abs = offset
|
||||||
|
case io.SeekCurrent:
|
||||||
|
abs = bs.pos + offset
|
||||||
|
case io.SeekEnd:
|
||||||
|
return 0, errors.New("'SeekEnd' not supported")
|
||||||
|
default:
|
||||||
|
return 0, errors.New("invalid seek option")
|
||||||
|
}
|
||||||
|
|
||||||
|
if abs < 0 {
|
||||||
|
return 0, errors.New("unable to seek before start")
|
||||||
|
}
|
||||||
|
|
||||||
|
if abs > int64(len(bs.data)) {
|
||||||
|
return 0, errors.New("unable to seek past read data")
|
||||||
|
}
|
||||||
|
|
||||||
|
bs.pos = abs
|
||||||
|
return bs.pos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *bufferedSeekReader) Close() error {
|
||||||
|
bs.closed = true
|
||||||
|
return bs.r.Close()
|
||||||
|
}
|
141
internal/buffered_seek_reader_test.go
Normal file
141
internal/buffered_seek_reader_test.go
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBufferedSeeker_Read(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initialData string
|
||||||
|
readLengths []int
|
||||||
|
expectedReads []string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "go case (read)",
|
||||||
|
initialData: "Hello, World!",
|
||||||
|
readLengths: []int{5},
|
||||||
|
expectedReads: []string{"Hello"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple reads",
|
||||||
|
initialData: "Hello, World!",
|
||||||
|
readLengths: []int{5, 8},
|
||||||
|
expectedReads: []string{"Hello", ", World!"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
bs := NewBufferedSeeker(io.NopCloser(bytes.NewBufferString(tt.initialData)))
|
||||||
|
|
||||||
|
for i, length := range tt.readLengths {
|
||||||
|
buf := make([]byte, length)
|
||||||
|
n, err := bs.Read(buf)
|
||||||
|
|
||||||
|
if !tt.expectError {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equalf(t, tt.expectedReads[i], string(buf[:n]), "read index %d", i)
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBufferedSeeker_Seek(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initialData string
|
||||||
|
readLengths []int
|
||||||
|
seekOffsets []int64
|
||||||
|
seekWhence []int
|
||||||
|
expectedReads []string
|
||||||
|
seekError require.ErrorAssertionFunc
|
||||||
|
readError require.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "seek start 0 without read first",
|
||||||
|
initialData: "Hello, World!",
|
||||||
|
readLengths: []int{5},
|
||||||
|
seekOffsets: []int64{0},
|
||||||
|
seekWhence: []int{io.SeekStart},
|
||||||
|
expectedReads: []string{"Hello"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read + seek back",
|
||||||
|
initialData: "Hello, World!",
|
||||||
|
readLengths: []int{5, 8, 8},
|
||||||
|
seekOffsets: []int64{-1, -1, 2},
|
||||||
|
seekWhence: []int{io.SeekStart, io.SeekStart, io.SeekStart},
|
||||||
|
expectedReads: []string{"Hello", ", World!", "llo, Wor"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seek past read data",
|
||||||
|
initialData: "Hello, World!",
|
||||||
|
readLengths: []int{5},
|
||||||
|
seekOffsets: []int64{20},
|
||||||
|
seekWhence: []int{io.SeekStart},
|
||||||
|
expectedReads: []string{""},
|
||||||
|
seekError: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seek to end",
|
||||||
|
initialData: "Hello, World!",
|
||||||
|
readLengths: []int{-1},
|
||||||
|
seekOffsets: []int64{20},
|
||||||
|
seekWhence: []int{io.SeekEnd},
|
||||||
|
expectedReads: []string{""},
|
||||||
|
seekError: require.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.seekError == nil {
|
||||||
|
tt.seekError = require.NoError
|
||||||
|
}
|
||||||
|
if tt.readError == nil {
|
||||||
|
tt.readError = require.NoError
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := NewBufferedSeeker(io.NopCloser(bytes.NewBufferString(tt.initialData)))
|
||||||
|
|
||||||
|
for i, length := range tt.readLengths {
|
||||||
|
if len(tt.seekOffsets) > i && tt.seekOffsets[i] >= 0 {
|
||||||
|
_, err := bs.Seek(tt.seekOffsets[i], tt.seekWhence[i])
|
||||||
|
tt.seekError(t, err)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if length >= 0 {
|
||||||
|
buf := make([]byte, length)
|
||||||
|
n, err := bs.Read(buf)
|
||||||
|
tt.readError(t, err)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.Equalf(t, tt.expectedReads[i], string(buf[:n]), "read index %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBufferedSeeker_Close(t *testing.T) {
|
||||||
|
bs := NewBufferedSeeker(io.NopCloser(bytes.NewBufferString("Hello, World!")))
|
||||||
|
err := bs.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
n, err := bs.Read(make([]byte, 1))
|
||||||
|
assert.Equal(t, 0, n)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
.DEFAULT_GOAL := validate-schema
|
.DEFAULT_GOAL := validate-schema
|
||||||
.PHONY: validate-schema
|
.PHONY: validate-schema
|
||||||
validate-schema:
|
validate-schema:
|
||||||
go run ../../cmd/syft/main.go ubuntu:latest -vv -o cyclonedx > bom.xml
|
@echo "Generating CycloneDX SBOMs..."
|
||||||
|
go run ../../cmd/syft/main.go ubuntu:latest -v -o cyclonedx-xml=bom.xml -o cyclonedx-json=bom.json
|
||||||
|
|
||||||
|
@echo "\nValidating CycloneDX XML..."
|
||||||
xmllint --noout --schema ./cyclonedx.xsd bom.xml
|
xmllint --noout --schema ./cyclonedx.xsd bom.xml
|
||||||
go run ../../cmd/syft/main.go ubuntu:latest -vv -o cyclonedx-json > bom.json
|
|
||||||
|
@echo "\nValidating CycloneDX JSON..."
|
||||||
../../.tool/yajsv -s cyclonedx.json bom.json
|
../../.tool/yajsv -s cyclonedx.json bom.json
|
||||||
|
|
|
@ -5,3 +5,5 @@
|
||||||
however, this tool does not know how to deal with references from HTTP, only the local filesystem.
|
however, this tool does not know how to deal with references from HTTP, only the local filesystem.
|
||||||
For this reason we've included a copy of all schemas needed to validate `syft` output, modified
|
For this reason we've included a copy of all schemas needed to validate `syft` output, modified
|
||||||
to reference local copies of dependent schemas.
|
to reference local copies of dependent schemas.
|
||||||
|
|
||||||
|
You can get the latest schemas from the [CycloneDX specifications repo](https://github.com/CycloneDX/specification/tree/master/schema).
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
@ -1,18 +0,0 @@
|
||||||
package syft
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/formats"
|
|
||||||
"github.com/anchore/syft/syft/sbom"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: deprecated, moved to syft/formats/formats.go. will be removed in v1.0.0
|
|
||||||
func Encode(s sbom.SBOM, f sbom.Format) ([]byte, error) {
|
|
||||||
return formats.Encode(s, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: deprecated, moved to syft/formats/formats.go. will be removed in v1.0.0
|
|
||||||
func Decode(reader io.Reader) (*sbom.SBOM, sbom.Format, error) {
|
|
||||||
return formats.Decode(reader)
|
|
||||||
}
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/anchore/packageurl-go"
|
"github.com/anchore/packageurl-go"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/formats/common"
|
"github.com/anchore/syft/syft/format/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,56 +2,18 @@ package cyclonedxhelpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/CycloneDX/cyclonedx-go"
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
|
||||||
"github.com/anchore/packageurl-go"
|
"github.com/anchore/packageurl-go"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/formats/common"
|
"github.com/anchore/syft/syft/format/common"
|
||||||
"github.com/anchore/syft/syft/linux"
|
"github.com/anchore/syft/syft/linux"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cycloneDXXmlSchema = "http://cyclonedx.org/schema/bom"
|
|
||||||
|
|
||||||
func GetValidator(format cyclonedx.BOMFileFormat) sbom.Validator {
|
|
||||||
return func(reader io.Reader) error {
|
|
||||||
bom := &cyclonedx.BOM{}
|
|
||||||
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
xmlWithoutNS := format == cyclonedx.BOMFileFormatXML && !strings.Contains(bom.XMLNS, cycloneDXXmlSchema)
|
|
||||||
xmlWithoutComponents := format == cyclonedx.BOMFileFormatXML && bom.Components == nil
|
|
||||||
if (cyclonedx.BOM{} == *bom || xmlWithoutComponents || xmlWithoutNS) {
|
|
||||||
return fmt.Errorf("not a valid CycloneDX document")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDecoder(format cyclonedx.BOMFileFormat) sbom.Decoder {
|
|
||||||
return func(reader io.Reader) (*sbom.SBOM, error) {
|
|
||||||
bom := &cyclonedx.BOM{
|
|
||||||
Components: &[]cyclonedx.Component{},
|
|
||||||
}
|
|
||||||
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s, err := ToSyftModel(bom)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
|
func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
|
||||||
if bom == nil {
|
if bom == nil {
|
||||||
return nil, fmt.Errorf("no content defined in CycloneDX BOM")
|
return nil, fmt.Errorf("no content defined in CycloneDX BOM")
|
|
@ -1,8 +1,6 @@
|
||||||
package cyclonedxhelpers
|
package cyclonedxhelpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -327,18 +325,6 @@ func Test_missingDataDecode(t *testing.T) {
|
||||||
assert.Equal(t, pkg.Licenses.Empty(), true)
|
assert.Equal(t, pkg.Licenses.Empty(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_missingComponentsDecode(t *testing.T) {
|
|
||||||
bom := &cyclonedx.BOM{
|
|
||||||
SpecVersion: cyclonedx.SpecVersion1_4,
|
|
||||||
}
|
|
||||||
bomBytes, _ := json.Marshal(&bom)
|
|
||||||
decode := GetDecoder(cyclonedx.BOMFileFormatJSON)
|
|
||||||
|
|
||||||
_, err := decode(bytes.NewReader(bomBytes))
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_decodeDependencies(t *testing.T) {
|
func Test_decodeDependencies(t *testing.T) {
|
||||||
c1 := cyclonedx.Component{
|
c1 := cyclonedx.Component{
|
||||||
Name: "c1",
|
Name: "c1",
|
|
@ -3,7 +3,7 @@ package cyclonedxhelpers
|
||||||
import (
|
import (
|
||||||
"github.com/CycloneDX/cyclonedx-go"
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/formats/common"
|
"github.com/anchore/syft/syft/format/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
|
@ -19,7 +19,7 @@ import (
|
||||||
"github.com/anchore/syft/internal/spdxlicense"
|
"github.com/anchore/syft/internal/spdxlicense"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/formats/common/util"
|
"github.com/anchore/syft/syft/format/common/util"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
|
@ -18,7 +18,7 @@ import (
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/cpe"
|
"github.com/anchore/syft/syft/cpe"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/formats/common/util"
|
"github.com/anchore/syft/syft/format/common/util"
|
||||||
"github.com/anchore/syft/syft/license"
|
"github.com/anchore/syft/syft/license"
|
||||||
"github.com/anchore/syft/syft/linux"
|
"github.com/anchore/syft/syft/linux"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
131
syft/format/cyclonedxjson/decoder.go
Normal file
131
syft/format/cyclonedxjson/decoder.go
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package cyclonedxjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/format/common/cyclonedxhelpers"
|
||||||
|
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ sbom.FormatDecoder = (*decoder)(nil)
|
||||||
|
|
||||||
|
type decoder struct {
|
||||||
|
decoder cyclonedxutil.Decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFormatDecoder() sbom.FormatDecoder {
|
||||||
|
return decoder{
|
||||||
|
decoder: cyclonedxutil.NewDecoder(cyclonedx.BOMFileFormatJSON),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d decoder) Decode(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, string, error) {
|
||||||
|
if reader == nil {
|
||||||
|
return nil, "", "", fmt.Errorf("no SBOM bytes provided")
|
||||||
|
}
|
||||||
|
id, version := d.Identify(reader)
|
||||||
|
if id != ID {
|
||||||
|
return nil, "", "", fmt.Errorf("not a cyclonedx json document")
|
||||||
|
}
|
||||||
|
if version == "" {
|
||||||
|
return nil, "", "", fmt.Errorf("unsupported cyclonedx json document version")
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := d.decoder.Decode(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, id, version, fmt.Errorf("unable to decode cyclonedx json document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := cyclonedxhelpers.ToSyftModel(doc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, id, version, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, id, version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d decoder) Identify(reader io.ReadSeeker) (sbom.FormatID, string) {
|
||||||
|
if reader == nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||||
|
log.Debugf("unable to seek to start of CycloneDX JSON SBOM: %+v", err)
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
JSONSchema string `json:"$schema"`
|
||||||
|
BOMFormat string `json:"bomFormat"`
|
||||||
|
SpecVersion string `json:"specVersion"`
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := json.NewDecoder(reader)
|
||||||
|
|
||||||
|
var doc Document
|
||||||
|
err := dec.Decode(&doc)
|
||||||
|
if err != nil {
|
||||||
|
// maybe not json? maybe not valid? doesn't matter, we won't process it.
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
id, version := getFormatInfo(doc.JSONSchema, doc.BOMFormat, doc.SpecVersion)
|
||||||
|
if version == "" || id != ID {
|
||||||
|
// not a cyclonedx json document that we support
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, version
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFormatInfo(schemaURI, bomFormat string, specVersion any) (sbom.FormatID, string) {
|
||||||
|
if !strings.Contains(schemaURI, "cyclonedx.org/schema/bom") {
|
||||||
|
// not a cyclonedx json document
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if bomFormat != "CycloneDX" {
|
||||||
|
// not a cyclonedx json document
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// by this point this looks to be valid cyclonedx json, but we need to know the version
|
||||||
|
|
||||||
|
var (
|
||||||
|
version string
|
||||||
|
spec cyclonedx.SpecVersion
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch s := specVersion.(type) {
|
||||||
|
case string:
|
||||||
|
version = s
|
||||||
|
spec, err = cyclonedxutil.SpecVersionFromString(version)
|
||||||
|
if err != nil {
|
||||||
|
// not a supported version, but is cyclonedx json
|
||||||
|
return ID, ""
|
||||||
|
}
|
||||||
|
case cyclonedx.SpecVersion:
|
||||||
|
spec = s
|
||||||
|
version = cyclonedxutil.VersionFromSpecVersion(spec)
|
||||||
|
if version == "" {
|
||||||
|
// not a supported version, but is cyclonedx json
|
||||||
|
return ID, ""
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// bad input provided for version info
|
||||||
|
return ID, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec < 0 {
|
||||||
|
// not a supported version, but is cyclonedx json
|
||||||
|
return ID, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return ID, version
|
||||||
|
}
|
118
syft/format/cyclonedxjson/decoder_test.go
Normal file
118
syft/format/cyclonedxjson/decoder_test.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package cyclonedxjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecoder_Decode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
file string
|
||||||
|
err bool
|
||||||
|
distro string
|
||||||
|
packages []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dir-scan",
|
||||||
|
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
|
||||||
|
distro: "debian:1.2.3",
|
||||||
|
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image-scan",
|
||||||
|
file: "snapshot/TestCycloneDxImageEncoder.golden",
|
||||||
|
distro: "debian:1.2.3",
|
||||||
|
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not-an-sbom",
|
||||||
|
file: "bad-sbom",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.file, func(t *testing.T) {
|
||||||
|
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dec := NewFormatDecoder()
|
||||||
|
|
||||||
|
formatID, formatVersion := dec.Identify(reader)
|
||||||
|
if test.err {
|
||||||
|
assert.Equal(t, sbom.FormatID(""), formatID)
|
||||||
|
assert.Equal(t, "", formatVersion)
|
||||||
|
|
||||||
|
_, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, sbom.FormatID(""), decodeID)
|
||||||
|
assert.Equal(t, "", decodeVersion)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, ID, formatID)
|
||||||
|
assert.NotEmpty(t, formatVersion)
|
||||||
|
|
||||||
|
bom, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||||
|
require.NotNil(t, bom)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, ID, decodeID)
|
||||||
|
assert.Equal(t, formatVersion, decodeVersion)
|
||||||
|
|
||||||
|
split := strings.SplitN(test.distro, ":", 2)
|
||||||
|
distroName := split[0]
|
||||||
|
distroVersion := split[1]
|
||||||
|
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, distroName)
|
||||||
|
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, distroVersion)
|
||||||
|
|
||||||
|
var pkgs []string
|
||||||
|
for p := range bom.Artifacts.Packages.Enumerate() {
|
||||||
|
pkgs = append(pkgs, fmt.Sprintf("%s:%s", p.Name, p.Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, test.packages, pkgs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecoder_Identify(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
file string
|
||||||
|
id sbom.FormatID
|
||||||
|
version string
|
||||||
|
}
|
||||||
|
|
||||||
|
var cases []testCase
|
||||||
|
|
||||||
|
for _, version := range SupportedVersions() {
|
||||||
|
cases = append(cases, testCase{
|
||||||
|
name: fmt.Sprintf("v%s schema", version),
|
||||||
|
file: fmt.Sprintf("test-fixtures/identify/%s.json", version),
|
||||||
|
id: ID,
|
||||||
|
version: version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range cases {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
reader, err := os.Open(test.file)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dec := NewFormatDecoder()
|
||||||
|
|
||||||
|
formatID, formatVersion := dec.Identify(reader)
|
||||||
|
assert.Equal(t, test.id, formatID)
|
||||||
|
assert.Equal(t, test.version, formatVersion)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
52
syft/format/cyclonedxjson/encoder.go
Normal file
52
syft/format/cyclonedxjson/encoder.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package cyclonedxjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ID = cyclonedxutil.JSONFormatID
|
||||||
|
|
||||||
|
func SupportedVersions() []string {
|
||||||
|
return cyclonedxutil.SupportedVersions(ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncoderConfig struct {
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoder struct {
|
||||||
|
cfg EncoderConfig
|
||||||
|
cyclonedxutil.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
|
||||||
|
enc, err := cyclonedxutil.NewEncoder(cfg.Version, cyclonedx.BOMFileFormatJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return encoder{
|
||||||
|
cfg: cfg,
|
||||||
|
Encoder: enc,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultEncoderConfig() EncoderConfig {
|
||||||
|
return EncoderConfig{
|
||||||
|
Version: cyclonedxutil.DefaultVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) ID() sbom.FormatID {
|
||||||
|
return ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) Aliases() []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) Version() string {
|
||||||
|
return e.cfg.Version
|
||||||
|
}
|
132
syft/format/cyclonedxjson/encoder_test.go
Normal file
132
syft/format/cyclonedxjson/encoder_test.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package cyclonedxjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var updateSnapshot = flag.Bool("update-cyclonedx-json", false, "update the *.golden files for cyclone-dx JSON encoders")
|
||||||
|
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
|
||||||
|
|
||||||
|
func getEncoder(t testing.TB) sbom.FormatEncoder {
|
||||||
|
enc, err := NewFormatEncoderWithConfig(DefaultEncoderConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
|
testutil.EncoderSnapshotTestConfig{
|
||||||
|
Subject: testutil.DirectoryInput(t, dir),
|
||||||
|
Format: getEncoder(t),
|
||||||
|
UpdateSnapshot: *updateSnapshot,
|
||||||
|
PersistRedactionsInSnapshot: true,
|
||||||
|
IsJSON: true,
|
||||||
|
Redactor: redactor(dir),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||||
|
testImage := "image-simple"
|
||||||
|
testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
|
testutil.ImageSnapshotTestConfig{
|
||||||
|
Image: testImage,
|
||||||
|
UpdateImageSnapshot: *updateImage,
|
||||||
|
},
|
||||||
|
testutil.EncoderSnapshotTestConfig{
|
||||||
|
Subject: testutil.ImageInput(t, testImage),
|
||||||
|
Format: getEncoder(t),
|
||||||
|
UpdateSnapshot: *updateSnapshot,
|
||||||
|
PersistRedactionsInSnapshot: true,
|
||||||
|
IsJSON: true,
|
||||||
|
Redactor: redactor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactor(values ...string) testutil.Redactor {
|
||||||
|
return testutil.NewRedactions().
|
||||||
|
WithValuesRedacted(values...).
|
||||||
|
WithPatternRedactors(
|
||||||
|
map[string]string{
|
||||||
|
// UUIDs
|
||||||
|
`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`: `urn:uuid:redacted`,
|
||||||
|
|
||||||
|
// timestamps
|
||||||
|
`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`: `timestamp:redacted`,
|
||||||
|
|
||||||
|
// image hashes
|
||||||
|
`sha256:[A-Fa-f0-9]{64}`: `sha256:redacted`,
|
||||||
|
|
||||||
|
// BOM refs
|
||||||
|
`"bom-ref":\s*"[^"]+"`: `"bom-ref":"redacted"`,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSupportedVersions(t *testing.T) {
|
||||||
|
encs := defaultFormatEncoders()
|
||||||
|
require.NotEmpty(t, encs)
|
||||||
|
|
||||||
|
versions := SupportedVersions()
|
||||||
|
require.Equal(t, len(versions), len(encs))
|
||||||
|
|
||||||
|
subject := testutil.DirectoryInput(t, t.TempDir())
|
||||||
|
dec := NewFormatDecoder()
|
||||||
|
|
||||||
|
for _, enc := range encs {
|
||||||
|
t.Run(enc.Version(), func(t *testing.T) {
|
||||||
|
require.Contains(t, versions, enc.Version())
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
require.NoError(t, enc.Encode(&buf, subject))
|
||||||
|
|
||||||
|
id, version := dec.Identify(bytes.NewReader(buf.Bytes()))
|
||||||
|
require.Equal(t, enc.ID(), id)
|
||||||
|
require.Equal(t, enc.Version(), version)
|
||||||
|
|
||||||
|
var s *sbom.SBOM
|
||||||
|
var err error
|
||||||
|
s, id, version, err = dec.Decode(bytes.NewReader(buf.Bytes()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, enc.ID(), id)
|
||||||
|
require.Equal(t, enc.Version(), version)
|
||||||
|
|
||||||
|
require.NotEmpty(t, s.Artifacts.Packages.PackageCount())
|
||||||
|
|
||||||
|
assert.Equal(t, len(subject.Relationships), len(s.Relationships), "mismatched relationship count")
|
||||||
|
|
||||||
|
if !assert.Equal(t, subject.Artifacts.Packages.PackageCount(), s.Artifacts.Packages.PackageCount(), "mismatched package count") {
|
||||||
|
t.Logf("expected: %d", subject.Artifacts.Packages.PackageCount())
|
||||||
|
for _, p := range subject.Artifacts.Packages.Sorted() {
|
||||||
|
t.Logf(" - %s", p.String())
|
||||||
|
}
|
||||||
|
t.Logf("actual: %d", s.Artifacts.Packages.PackageCount())
|
||||||
|
for _, p := range s.Artifacts.Packages.Sorted() {
|
||||||
|
t.Logf(" - %s", p.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatEncoders() []sbom.FormatEncoder {
|
||||||
|
var encs []sbom.FormatEncoder
|
||||||
|
for _, version := range SupportedVersions() {
|
||||||
|
enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: version})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
encs = append(encs, enc)
|
||||||
|
}
|
||||||
|
return encs
|
||||||
|
}
|
1
syft/format/cyclonedxjson/test-fixtures/bad-sbom
Normal file
1
syft/format/cyclonedxjson/test-fixtures/bad-sbom
Normal file
|
@ -0,0 +1 @@
|
||||||
|
not an sbom!
|
33
syft/format/cyclonedxjson/test-fixtures/identify/1.2.json
Normal file
33
syft/format/cyclonedxjson/test-fixtures/identify/1.2.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://cyclonedx.org/schema/bom-1.2.schema.json",
|
||||||
|
"bomFormat": "CycloneDX",
|
||||||
|
"specVersion": "1.2",
|
||||||
|
"serialNumber": "urn:uuid:8cfb48be-7ed8-4cea-8c23-8992e98d12be",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"timestamp": "2023-09-29T12:02:02-04:00",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"vendor": "anchore",
|
||||||
|
"name": "syft",
|
||||||
|
"version": "[not provided]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"component": {
|
||||||
|
"bom-ref": "a0ff99a6af10f11f",
|
||||||
|
"type": "file",
|
||||||
|
"name": "go.mod",
|
||||||
|
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
|
||||||
|
"type": "library",
|
||||||
|
"name": "github.com/wagoodman/go-partybus",
|
||||||
|
"version": "v0.0.0-20230516145632-8ccac152c651",
|
||||||
|
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
|
||||||
|
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
59
syft/format/cyclonedxjson/test-fixtures/identify/1.3.json
Normal file
59
syft/format/cyclonedxjson/test-fixtures/identify/1.3.json
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
|
||||||
|
"bomFormat": "CycloneDX",
|
||||||
|
"specVersion": "1.3",
|
||||||
|
"serialNumber": "urn:uuid:57ce2636-4ca6-46ba-b389-196af071d6fc",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"timestamp": "2023-09-29T12:02:02-04:00",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"vendor": "anchore",
|
||||||
|
"name": "syft",
|
||||||
|
"version": "[not provided]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"component": {
|
||||||
|
"bom-ref": "a0ff99a6af10f11f",
|
||||||
|
"type": "file",
|
||||||
|
"name": "go.mod",
|
||||||
|
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
|
||||||
|
"type": "library",
|
||||||
|
"name": "github.com/wagoodman/go-partybus",
|
||||||
|
"version": "v0.0.0-20230516145632-8ccac152c651",
|
||||||
|
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
|
||||||
|
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "syft:package:foundBy",
|
||||||
|
"value": "go-mod-file-cataloger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:package:language",
|
||||||
|
"value": "go"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:package:metadataType",
|
||||||
|
"value": "GolangModMetadata"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:package:type",
|
||||||
|
"value": "go-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:cpe23",
|
||||||
|
"value": "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:location:0:path",
|
||||||
|
"value": "/go.mod"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
59
syft/format/cyclonedxjson/test-fixtures/identify/1.4.json
Normal file
59
syft/format/cyclonedxjson/test-fixtures/identify/1.4.json
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
|
||||||
|
"bomFormat": "CycloneDX",
|
||||||
|
"specVersion": "1.4",
|
||||||
|
"serialNumber": "urn:uuid:6be7491f-6a9b-40f0-a60e-f74e2c0e23c3",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"timestamp": "2023-09-29T12:02:02-04:00",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"vendor": "anchore",
|
||||||
|
"name": "syft",
|
||||||
|
"version": "[not provided]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"component": {
|
||||||
|
"bom-ref": "a0ff99a6af10f11f",
|
||||||
|
"type": "file",
|
||||||
|
"name": "go.mod",
|
||||||
|
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
|
||||||
|
"type": "library",
|
||||||
|
"name": "github.com/wagoodman/go-partybus",
|
||||||
|
"version": "v0.0.0-20230516145632-8ccac152c651",
|
||||||
|
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
|
||||||
|
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "syft:package:foundBy",
|
||||||
|
"value": "go-mod-file-cataloger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:package:language",
|
||||||
|
"value": "go"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:package:metadataType",
|
||||||
|
"value": "GolangModMetadata"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:package:type",
|
||||||
|
"value": "go-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:cpe23",
|
||||||
|
"value": "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:location:0:path",
|
||||||
|
"value": "/go.mod"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
59
syft/format/cyclonedxjson/test-fixtures/identify/1.5.json
Normal file
59
syft/format/cyclonedxjson/test-fixtures/identify/1.5.json
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
||||||
|
"bomFormat": "CycloneDX",
|
||||||
|
"specVersion": "1.5",
|
||||||
|
"serialNumber": "urn:uuid:22c456c5-c2a0-4285-a8ef-01b4382822c3",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"timestamp": "2023-09-29T12:02:02-04:00",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"vendor": "anchore",
|
||||||
|
"name": "syft",
|
||||||
|
"version": "[not provided]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"component": {
|
||||||
|
"bom-ref": "a0ff99a6af10f11f",
|
||||||
|
"type": "file",
|
||||||
|
"name": "go.mod",
|
||||||
|
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
|
||||||
|
"type": "library",
|
||||||
|
"name": "github.com/wagoodman/go-partybus",
|
||||||
|
"version": "v0.0.0-20230516145632-8ccac152c651",
|
||||||
|
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
|
||||||
|
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "syft:package:foundBy",
|
||||||
|
"value": "go-mod-file-cataloger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:package:language",
|
||||||
|
"value": "go"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:package:metadataType",
|
||||||
|
"value": "GolangModMetadata"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:package:type",
|
||||||
|
"value": "go-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:cpe23",
|
||||||
|
"value": "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:location:0:path",
|
||||||
|
"value": "/go.mod"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
||||||
"bomFormat": "CycloneDX",
|
"bomFormat": "CycloneDX",
|
||||||
"specVersion": "1.4",
|
"specVersion": "1.5",
|
||||||
"serialNumber": "urn:uuid:redacted",
|
"serialNumber": "urn:uuid:redacted",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
||||||
"bomFormat": "CycloneDX",
|
"bomFormat": "CycloneDX",
|
||||||
"specVersion": "1.4",
|
"specVersion": "1.5",
|
||||||
"serialNumber": "urn:uuid:redacted",
|
"serialNumber": "urn:uuid:redacted",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
106
syft/format/cyclonedxxml/decoder.go
Normal file
106
syft/format/cyclonedxxml/decoder.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package cyclonedxxml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/format/common/cyclonedxhelpers"
|
||||||
|
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ sbom.FormatDecoder = (*decoder)(nil)
|
||||||
|
|
||||||
|
type decoder struct {
|
||||||
|
decoder cyclonedxutil.Decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFormatDecoder() sbom.FormatDecoder {
|
||||||
|
return decoder{
|
||||||
|
decoder: cyclonedxutil.NewDecoder(cyclonedx.BOMFileFormatXML),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d decoder) Decode(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, string, error) {
|
||||||
|
if reader == nil {
|
||||||
|
return nil, "", "", fmt.Errorf("no SBOM bytes provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, version := d.Identify(reader)
|
||||||
|
if id != ID {
|
||||||
|
return nil, "", "", fmt.Errorf("not a cyclonedx xml document")
|
||||||
|
}
|
||||||
|
if version == "" {
|
||||||
|
return nil, "", "", fmt.Errorf("unsupported cyclonedx xml document version")
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := d.decoder.Decode(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, id, version, fmt.Errorf("unable to decode cyclonedx xml document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := cyclonedxhelpers.ToSyftModel(doc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, id, version, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, id, version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d decoder) Identify(reader io.ReadSeeker) (sbom.FormatID, string) {
|
||||||
|
if reader == nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||||
|
log.Debugf("unable to seek to start of CycloneDX XML SBOM: %+v", err)
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
XMLNS string `xml:"xmlns,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := xml.NewDecoder(reader)
|
||||||
|
|
||||||
|
var doc Document
|
||||||
|
err := dec.Decode(&doc)
|
||||||
|
if err != nil {
|
||||||
|
// maybe not xml? maybe not valid? doesn't matter, we won't process it.
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
id, version := getFormatInfo(doc.XMLNS)
|
||||||
|
if version == "" || id != ID {
|
||||||
|
// not a cyclonedx xml document that we support
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, version
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFormatInfo(xmlns string) (sbom.FormatID, string) {
|
||||||
|
version := getVersionFromXMLNS(xmlns)
|
||||||
|
|
||||||
|
if !strings.Contains(xmlns, "cyclonedx.org/schema/bom") {
|
||||||
|
// not a cyclonedx xml document
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, err := cyclonedxutil.SpecVersionFromString(version)
|
||||||
|
if spec < 0 || err != nil {
|
||||||
|
// not a supported version, but is cyclonedx xml
|
||||||
|
return ID, ""
|
||||||
|
}
|
||||||
|
return ID, version
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVersionFromXMLNS(xmlns string) string {
|
||||||
|
fields := strings.Split(xmlns, "/")
|
||||||
|
return fields[len(fields)-1]
|
||||||
|
}
|
118
syft/format/cyclonedxxml/decoder_test.go
Normal file
118
syft/format/cyclonedxxml/decoder_test.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package cyclonedxxml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecoder_Decode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
file string
|
||||||
|
err bool
|
||||||
|
distro string
|
||||||
|
packages []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dir-scan",
|
||||||
|
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
|
||||||
|
distro: "debian:1.2.3",
|
||||||
|
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image-scan",
|
||||||
|
file: "snapshot/TestCycloneDxImageEncoder.golden",
|
||||||
|
distro: "debian:1.2.3",
|
||||||
|
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not-an-sbom",
|
||||||
|
file: "bad-sbom",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.file, func(t *testing.T) {
|
||||||
|
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dec := NewFormatDecoder()
|
||||||
|
|
||||||
|
formatID, formatVersion := dec.Identify(reader)
|
||||||
|
if test.err {
|
||||||
|
assert.Equal(t, sbom.FormatID(""), formatID)
|
||||||
|
assert.Equal(t, "", formatVersion)
|
||||||
|
|
||||||
|
_, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, sbom.FormatID(""), decodeID)
|
||||||
|
assert.Equal(t, "", decodeVersion)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, ID, formatID)
|
||||||
|
assert.NotEmpty(t, formatVersion)
|
||||||
|
|
||||||
|
bom, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||||
|
require.NotNil(t, bom)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, ID, decodeID)
|
||||||
|
assert.Equal(t, formatVersion, decodeVersion)
|
||||||
|
|
||||||
|
split := strings.SplitN(test.distro, ":", 2)
|
||||||
|
distroName := split[0]
|
||||||
|
distroVersion := split[1]
|
||||||
|
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, distroName)
|
||||||
|
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, distroVersion)
|
||||||
|
|
||||||
|
var pkgs []string
|
||||||
|
for p := range bom.Artifacts.Packages.Enumerate() {
|
||||||
|
pkgs = append(pkgs, fmt.Sprintf("%s:%s", p.Name, p.Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, test.packages, pkgs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecoder_Identify(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
file string
|
||||||
|
id sbom.FormatID
|
||||||
|
version string
|
||||||
|
}
|
||||||
|
|
||||||
|
var cases []testCase
|
||||||
|
|
||||||
|
for _, version := range SupportedVersions() {
|
||||||
|
cases = append(cases, testCase{
|
||||||
|
name: fmt.Sprintf("v%s schema", version),
|
||||||
|
file: fmt.Sprintf("test-fixtures/identify/%s.xml", version),
|
||||||
|
id: ID,
|
||||||
|
version: version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range cases {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
reader, err := os.Open(test.file)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dec := NewFormatDecoder()
|
||||||
|
|
||||||
|
formatID, formatVersion := dec.Identify(reader)
|
||||||
|
assert.Equal(t, test.id, formatID)
|
||||||
|
assert.Equal(t, test.version, formatVersion)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
58
syft/format/cyclonedxxml/encoder.go
Normal file
58
syft/format/cyclonedxxml/encoder.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package cyclonedxxml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ sbom.FormatEncoder = (*encoder)(nil)
|
||||||
|
|
||||||
|
const ID = cyclonedxutil.XMLFormatID
|
||||||
|
|
||||||
|
func SupportedVersions() []string {
|
||||||
|
return cyclonedxutil.SupportedVersions(ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncoderConfig struct {
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoder struct {
|
||||||
|
cfg EncoderConfig
|
||||||
|
cyclonedxutil.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
|
||||||
|
enc, err := cyclonedxutil.NewEncoder(cfg.Version, cyclonedx.BOMFileFormatXML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return encoder{
|
||||||
|
cfg: cfg,
|
||||||
|
Encoder: enc,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultEncoderConfig() EncoderConfig {
|
||||||
|
return EncoderConfig{
|
||||||
|
Version: cyclonedxutil.DefaultVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) ID() sbom.FormatID {
|
||||||
|
return ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) Aliases() []string {
|
||||||
|
return []string{
|
||||||
|
"cyclonedx",
|
||||||
|
"cyclone",
|
||||||
|
"cdx",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) Version() string {
|
||||||
|
return e.cfg.Version
|
||||||
|
}
|
129
syft/format/cyclonedxxml/encoder_test.go
Normal file
129
syft/format/cyclonedxxml/encoder_test.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
package cyclonedxxml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var updateSnapshot = flag.Bool("update-cyclonedx-xml", false, "update the *.golden files for cyclone-dx XML encoders")
|
||||||
|
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
|
||||||
|
|
||||||
|
func getEncoder(t testing.TB) sbom.FormatEncoder {
|
||||||
|
enc, err := NewFormatEncoderWithConfig(DefaultEncoderConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
|
testutil.EncoderSnapshotTestConfig{
|
||||||
|
Subject: testutil.DirectoryInput(t, dir),
|
||||||
|
Format: getEncoder(t),
|
||||||
|
UpdateSnapshot: *updateSnapshot,
|
||||||
|
PersistRedactionsInSnapshot: true,
|
||||||
|
IsJSON: false,
|
||||||
|
Redactor: redactor(dir),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||||
|
testImage := "image-simple"
|
||||||
|
testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
|
testutil.ImageSnapshotTestConfig{
|
||||||
|
Image: testImage,
|
||||||
|
UpdateImageSnapshot: *updateImage,
|
||||||
|
},
|
||||||
|
testutil.EncoderSnapshotTestConfig{
|
||||||
|
Subject: testutil.ImageInput(t, testImage),
|
||||||
|
Format: getEncoder(t),
|
||||||
|
UpdateSnapshot: *updateSnapshot,
|
||||||
|
PersistRedactionsInSnapshot: true,
|
||||||
|
IsJSON: false,
|
||||||
|
Redactor: redactor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactor(values ...string) testutil.Redactor {
|
||||||
|
return testutil.NewRedactions().
|
||||||
|
WithValuesRedacted(values...).
|
||||||
|
WithPatternRedactors(
|
||||||
|
map[string]string{
|
||||||
|
// dates
|
||||||
|
`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`: `redacted`,
|
||||||
|
|
||||||
|
// image hashes and BOM refs
|
||||||
|
`sha256:[A-Za-z0-9]{64}`: `sha256:redacted`,
|
||||||
|
|
||||||
|
// serial numbers and BOM refs
|
||||||
|
`(serialNumber|bom-ref)="[^"]+"`: `$1="redacted"`,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSupportedVersions(t *testing.T) {
|
||||||
|
encs := defaultFormatEncoders()
|
||||||
|
require.NotEmpty(t, encs)
|
||||||
|
|
||||||
|
versions := SupportedVersions()
|
||||||
|
require.Equal(t, len(versions), len(encs))
|
||||||
|
|
||||||
|
subject := testutil.DirectoryInput(t, t.TempDir())
|
||||||
|
dec := NewFormatDecoder()
|
||||||
|
|
||||||
|
for _, enc := range encs {
|
||||||
|
t.Run(enc.Version(), func(t *testing.T) {
|
||||||
|
require.Contains(t, versions, enc.Version())
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
require.NoError(t, enc.Encode(&buf, subject))
|
||||||
|
|
||||||
|
id, version := dec.Identify(bytes.NewReader(buf.Bytes()))
|
||||||
|
require.Equal(t, enc.ID(), id)
|
||||||
|
require.Equal(t, enc.Version(), version)
|
||||||
|
|
||||||
|
var s *sbom.SBOM
|
||||||
|
var err error
|
||||||
|
s, id, version, err = dec.Decode(bytes.NewReader(buf.Bytes()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, enc.ID(), id)
|
||||||
|
require.Equal(t, enc.Version(), version)
|
||||||
|
|
||||||
|
require.NotEmpty(t, s.Artifacts.Packages.PackageCount())
|
||||||
|
|
||||||
|
assert.Equal(t, len(subject.Relationships), len(s.Relationships), "mismatched relationship count")
|
||||||
|
|
||||||
|
if !assert.Equal(t, subject.Artifacts.Packages.PackageCount(), s.Artifacts.Packages.PackageCount(), "mismatched package count") {
|
||||||
|
t.Logf("expected: %d", subject.Artifacts.Packages.PackageCount())
|
||||||
|
for _, p := range subject.Artifacts.Packages.Sorted() {
|
||||||
|
t.Logf(" - %s", p.String())
|
||||||
|
}
|
||||||
|
t.Logf("actual: %d", s.Artifacts.Packages.PackageCount())
|
||||||
|
for _, p := range s.Artifacts.Packages.Sorted() {
|
||||||
|
t.Logf(" - %s", p.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFormatEncoders() []sbom.FormatEncoder {
|
||||||
|
var encs []sbom.FormatEncoder
|
||||||
|
for _, version := range SupportedVersions() {
|
||||||
|
enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: version})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
encs = append(encs, enc)
|
||||||
|
}
|
||||||
|
return encs
|
||||||
|
}
|
1
syft/format/cyclonedxxml/test-fixtures/bad-sbom
Normal file
1
syft/format/cyclonedxxml/test-fixtures/bad-sbom
Normal file
|
@ -0,0 +1 @@
|
||||||
|
not an sbom!
|
12
syft/format/cyclonedxxml/test-fixtures/identify/1.0.xml
Normal file
12
syft/format/cyclonedxxml/test-fixtures/identify/1.0.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
|
||||||
|
<components>
|
||||||
|
<component type="library">
|
||||||
|
<name>github.com/wagoodman/go-partybus</name>
|
||||||
|
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||||
|
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||||
|
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||||
|
<modified>false</modified>
|
||||||
|
</component>
|
||||||
|
</components>
|
||||||
|
</bom>
|
11
syft/format/cyclonedxxml/test-fixtures/identify/1.1.xml
Normal file
11
syft/format/cyclonedxxml/test-fixtures/identify/1.1.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" serialNumber="urn:uuid:32ad4aec-ddc3-4c26-90e7-796f7ca82a1d" version="1">
|
||||||
|
<components>
|
||||||
|
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||||
|
<name>github.com/wagoodman/go-partybus</name>
|
||||||
|
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||||
|
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||||
|
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||||
|
</component>
|
||||||
|
</components>
|
||||||
|
</bom>
|
25
syft/format/cyclonedxxml/test-fixtures/identify/1.2.xml
Normal file
25
syft/format/cyclonedxxml/test-fixtures/identify/1.2.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" serialNumber="urn:uuid:214033a9-6fe2-4f0b-88f2-8dbca251d0e0" version="1">
|
||||||
|
<metadata>
|
||||||
|
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
|
||||||
|
<tools>
|
||||||
|
<tool>
|
||||||
|
<vendor>anchore</vendor>
|
||||||
|
<name>syft</name>
|
||||||
|
<version>[not provided]</version>
|
||||||
|
</tool>
|
||||||
|
</tools>
|
||||||
|
<component bom-ref="a0ff99a6af10f11f" type="file">
|
||||||
|
<name>go.mod</name>
|
||||||
|
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
|
||||||
|
</component>
|
||||||
|
</metadata>
|
||||||
|
<components>
|
||||||
|
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||||
|
<name>github.com/wagoodman/go-partybus</name>
|
||||||
|
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||||
|
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||||
|
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||||
|
</component>
|
||||||
|
</components>
|
||||||
|
</bom>
|
33
syft/format/cyclonedxxml/test-fixtures/identify/1.3.xml
Normal file
33
syft/format/cyclonedxxml/test-fixtures/identify/1.3.xml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:7ec2b549-3d31-4c7b-85be-fad838c8f312" version="1">
|
||||||
|
<metadata>
|
||||||
|
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
|
||||||
|
<tools>
|
||||||
|
<tool>
|
||||||
|
<vendor>anchore</vendor>
|
||||||
|
<name>syft</name>
|
||||||
|
<version>[not provided]</version>
|
||||||
|
</tool>
|
||||||
|
</tools>
|
||||||
|
<component bom-ref="a0ff99a6af10f11f" type="file">
|
||||||
|
<name>go.mod</name>
|
||||||
|
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
|
||||||
|
</component>
|
||||||
|
</metadata>
|
||||||
|
<components>
|
||||||
|
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||||
|
<name>github.com/wagoodman/go-partybus</name>
|
||||||
|
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||||
|
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||||
|
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||||
|
<properties>
|
||||||
|
<property name="syft:package:foundBy">go-mod-file-cataloger</property>
|
||||||
|
<property name="syft:package:language">go</property>
|
||||||
|
<property name="syft:package:metadataType">GolangModMetadata</property>
|
||||||
|
<property name="syft:package:type">go-module</property>
|
||||||
|
<property name="syft:cpe23">cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</property>
|
||||||
|
<property name="syft:location:0:path">/go.mod</property>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
</components>
|
||||||
|
</bom>
|
33
syft/format/cyclonedxxml/test-fixtures/identify/1.4.xml
Normal file
33
syft/format/cyclonedxxml/test-fixtures/identify/1.4.xml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:3e5d8b2a-30cd-409f-9519-558c53e542c5" version="1">
|
||||||
|
<metadata>
|
||||||
|
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
|
||||||
|
<tools>
|
||||||
|
<tool>
|
||||||
|
<vendor>anchore</vendor>
|
||||||
|
<name>syft</name>
|
||||||
|
<version>[not provided]</version>
|
||||||
|
</tool>
|
||||||
|
</tools>
|
||||||
|
<component bom-ref="a0ff99a6af10f11f" type="file">
|
||||||
|
<name>go.mod</name>
|
||||||
|
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
|
||||||
|
</component>
|
||||||
|
</metadata>
|
||||||
|
<components>
|
||||||
|
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||||
|
<name>github.com/wagoodman/go-partybus</name>
|
||||||
|
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||||
|
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||||
|
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||||
|
<properties>
|
||||||
|
<property name="syft:package:foundBy">go-mod-file-cataloger</property>
|
||||||
|
<property name="syft:package:language">go</property>
|
||||||
|
<property name="syft:package:metadataType">GolangModMetadata</property>
|
||||||
|
<property name="syft:package:type">go-module</property>
|
||||||
|
<property name="syft:cpe23">cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</property>
|
||||||
|
<property name="syft:location:0:path">/go.mod</property>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
</components>
|
||||||
|
</bom>
|
33
syft/format/cyclonedxxml/test-fixtures/identify/1.5.xml
Normal file
33
syft/format/cyclonedxxml/test-fixtures/identify/1.5.xml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="urn:uuid:b2188bff-93b8-47e7-b86e-41c417e0d75e" version="1">
|
||||||
|
<metadata>
|
||||||
|
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
|
||||||
|
<tools>
|
||||||
|
<tool>
|
||||||
|
<vendor>anchore</vendor>
|
||||||
|
<name>syft</name>
|
||||||
|
<version>[not provided]</version>
|
||||||
|
</tool>
|
||||||
|
</tools>
|
||||||
|
<component bom-ref="a0ff99a6af10f11f" type="file">
|
||||||
|
<name>go.mod</name>
|
||||||
|
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
|
||||||
|
</component>
|
||||||
|
</metadata>
|
||||||
|
<components>
|
||||||
|
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||||
|
<name>github.com/wagoodman/go-partybus</name>
|
||||||
|
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||||
|
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||||
|
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||||
|
<properties>
|
||||||
|
<property name="syft:package:foundBy">go-mod-file-cataloger</property>
|
||||||
|
<property name="syft:package:language">go</property>
|
||||||
|
<property name="syft:package:metadataType">GolangModMetadata</property>
|
||||||
|
<property name="syft:package:type">go-module</property>
|
||||||
|
<property name="syft:cpe23">cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</property>
|
||||||
|
<property name="syft:location:0:path">/go.mod</property>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
</components>
|
||||||
|
</bom>
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="redacted" version="1">
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="redacted" version="1">
|
||||||
<metadata>
|
<metadata>
|
||||||
<timestamp>redacted</timestamp>
|
<timestamp>redacted</timestamp>
|
||||||
<tools>
|
<tools>
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="redacted" version="1">
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="redacted" version="1">
|
||||||
<metadata>
|
<metadata>
|
||||||
<timestamp>redacted</timestamp>
|
<timestamp>redacted</timestamp>
|
||||||
<tools>
|
<tools>
|
91
syft/format/decoders.go
Normal file
91
syft/format/decoders.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||||
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
staticDecoders sbom.FormatDecoder
|
||||||
|
_ sbom.FormatDecoder = (*DecoderCollection)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
staticDecoders = NewDecoderCollection(Decoders()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decoders() []sbom.FormatDecoder {
|
||||||
|
return []sbom.FormatDecoder{
|
||||||
|
syftjson.NewFormatDecoder(),
|
||||||
|
cyclonedxxml.NewFormatDecoder(),
|
||||||
|
cyclonedxjson.NewFormatDecoder(),
|
||||||
|
spdxtagvalue.NewFormatDecoder(),
|
||||||
|
spdxjson.NewFormatDecoder(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecoderCollection struct {
|
||||||
|
decoders []sbom.FormatDecoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDecoderCollection(decoders ...sbom.FormatDecoder) sbom.FormatDecoder {
|
||||||
|
return &DecoderCollection{
|
||||||
|
decoders: decoders,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode takes a set of bytes and attempts to decode it into an SBOM relative to the decoders in the collection.
|
||||||
|
func (c *DecoderCollection) Decode(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, string, error) {
|
||||||
|
if reader == nil {
|
||||||
|
return nil, "", "", fmt.Errorf("no SBOM bytes provided")
|
||||||
|
}
|
||||||
|
var bestID sbom.FormatID
|
||||||
|
for _, d := range c.decoders {
|
||||||
|
id, version := d.Identify(reader)
|
||||||
|
if id == "" || version == "" {
|
||||||
|
if id != "" {
|
||||||
|
bestID = id
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.Decode(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestID != "" {
|
||||||
|
return nil, bestID, "", fmt.Errorf("sbom format found to be %q but the version is not supported", bestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, "", "", fmt.Errorf("sbom format not recognized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify takes a set of bytes and attempts to identify the format of the SBOM relative to the decoders in the collection.
|
||||||
|
func (c *DecoderCollection) Identify(reader io.ReadSeeker) (sbom.FormatID, string) {
|
||||||
|
if reader == nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
for _, d := range c.decoders {
|
||||||
|
id, version := d.Identify(reader)
|
||||||
|
if id != "" && version != "" {
|
||||||
|
return id, version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify takes a set of bytes and attempts to identify the format of the SBOM.
|
||||||
|
func Identify(reader io.ReadSeeker) (sbom.FormatID, string) {
|
||||||
|
return staticDecoders.Identify(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode takes a set of bytes and attempts to decode it into an SBOM.
|
||||||
|
func Decode(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, string, error) {
|
||||||
|
return staticDecoders.Decode(reader)
|
||||||
|
}
|
62
syft/format/decoders_test.go
Normal file
62
syft/format/decoders_test.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIdentify(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
fixture string
|
||||||
|
id sbom.FormatID
|
||||||
|
version string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
fixture: "test-fixtures/alpine-syft.json",
|
||||||
|
id: syftjson.ID,
|
||||||
|
version: "1.1.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.fixture, func(t *testing.T) {
|
||||||
|
reader, err := os.Open(test.fixture)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
id, version := Identify(reader)
|
||||||
|
assert.Equal(t, test.id, id)
|
||||||
|
assert.Equal(t, test.version, version)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormats_EmptyInput(t *testing.T) {
|
||||||
|
for _, format := range Decoders() {
|
||||||
|
name := strings.Split(fmt.Sprintf("%#v", format), "{")[0]
|
||||||
|
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Run("Decode", func(t *testing.T) {
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
decodedSBOM, _, _, err := format.Decode(nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, decodedSBOM)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Identify", func(t *testing.T) {
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
id, version := format.Identify(nil)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
assert.Empty(t, version)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
143
syft/format/encoders.go
Normal file
143
syft/format/encoders.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EncoderCollection struct {
|
||||||
|
encoders []sbom.FormatEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEncoderCollection(encoders ...sbom.FormatEncoder) *EncoderCollection {
|
||||||
|
return &EncoderCollection{
|
||||||
|
encoders: encoders,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDs returns all format IDs represented in the collection.
|
||||||
|
func (e EncoderCollection) IDs() []sbom.FormatID {
|
||||||
|
idSet := strset.New()
|
||||||
|
for _, f := range e.encoders {
|
||||||
|
idSet.Add(string(f.ID()))
|
||||||
|
}
|
||||||
|
|
||||||
|
idList := idSet.List()
|
||||||
|
sort.Strings(idList)
|
||||||
|
|
||||||
|
var ids []sbom.FormatID
|
||||||
|
for _, id := range idList {
|
||||||
|
ids = append(ids, sbom.FormatID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameVersions returns all formats that are supported by the collection as a list of "name@version" strings.
|
||||||
|
func (e EncoderCollection) NameVersions() []string {
|
||||||
|
set := strset.New()
|
||||||
|
for _, f := range e.encoders {
|
||||||
|
if f.Version() == sbom.AnyVersion {
|
||||||
|
set.Add(string(f.ID()))
|
||||||
|
} else {
|
||||||
|
set.Add(fmt.Sprintf("%s@%s", f.ID(), f.Version()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list := set.List()
|
||||||
|
sort.Strings(list)
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aliases returns all format aliases represented in the collection (where an ID would be "spdx-tag-value" the alias would be "spdx").
|
||||||
|
func (e EncoderCollection) Aliases() []string {
|
||||||
|
aliases := strset.New()
|
||||||
|
for _, f := range e.encoders {
|
||||||
|
aliases.Add(f.Aliases()...)
|
||||||
|
}
|
||||||
|
lst := aliases.List()
|
||||||
|
sort.Strings(lst)
|
||||||
|
return lst
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the contained encoder for a given format name and version.
|
||||||
|
func (e EncoderCollection) Get(name string, version string) sbom.FormatEncoder {
|
||||||
|
log.WithFields("name", name, "version", version).Trace("looking for matching encoder")
|
||||||
|
|
||||||
|
name = cleanFormatName(name)
|
||||||
|
var mostRecentFormat sbom.FormatEncoder
|
||||||
|
|
||||||
|
for _, f := range e.encoders {
|
||||||
|
log.WithFields("name", f.ID(), "version", f.Version(), "aliases", f.Aliases()).Trace("considering format")
|
||||||
|
names := []string{string(f.ID())}
|
||||||
|
names = append(names, f.Aliases()...)
|
||||||
|
for _, n := range names {
|
||||||
|
if cleanFormatName(n) == name && versionMatches(f.Version(), version) {
|
||||||
|
if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
|
||||||
|
mostRecentFormat = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mostRecentFormat != nil {
|
||||||
|
log.WithFields("name", mostRecentFormat.ID(), "version", mostRecentFormat.Version()).Trace("found matching encoder")
|
||||||
|
} else {
|
||||||
|
log.WithFields("search-name", name, "search-version", version).Trace("no matching encoder found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mostRecentFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByString accepts a name@version string, such as:
|
||||||
|
// - json
|
||||||
|
// - spdx-json@2.1
|
||||||
|
// - cdx@1.5
|
||||||
|
func (e EncoderCollection) GetByString(s string) sbom.FormatEncoder {
|
||||||
|
parts := strings.SplitN(s, "@", 2)
|
||||||
|
version := sbom.AnyVersion
|
||||||
|
if len(parts) > 1 {
|
||||||
|
version = parts[1]
|
||||||
|
}
|
||||||
|
return e.Get(parts[0], version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionMatches(version string, match string) bool {
|
||||||
|
if version == sbom.AnyVersion || match == sbom.AnyVersion {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
match = strings.ReplaceAll(match, ".", "\\.")
|
||||||
|
match = strings.ReplaceAll(match, "*", ".*")
|
||||||
|
match = fmt.Sprintf("^%s(\\..*)*$", match)
|
||||||
|
matcher, err := regexp.Compile(match)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return matcher.MatchString(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanFormatName(name string) string {
|
||||||
|
r := strings.NewReplacer("-", "", "_", "")
|
||||||
|
return strings.ToLower(r.Replace(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode takes all SBOM elements and a format option and encodes an SBOM document.
|
||||||
|
func Encode(s sbom.SBOM, f sbom.FormatEncoder) ([]byte, error) {
|
||||||
|
buff := bytes.Buffer{}
|
||||||
|
|
||||||
|
if err := f.Encode(&buff, s); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to encode sbom: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buff.Bytes(), nil
|
||||||
|
}
|
111
syft/format/encoders_test.go
Normal file
111
syft/format/encoders_test.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_versionMatches(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
version string
|
||||||
|
match string
|
||||||
|
matches bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "any version matches number",
|
||||||
|
version: string(sbom.AnyVersion),
|
||||||
|
match: "6",
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number matches any version",
|
||||||
|
version: "6",
|
||||||
|
match: string(sbom.AnyVersion),
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same number matches",
|
||||||
|
version: "3",
|
||||||
|
match: "3",
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same major number matches",
|
||||||
|
version: "3.1",
|
||||||
|
match: "3",
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same minor number matches",
|
||||||
|
version: "3.1",
|
||||||
|
match: "3.1",
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard-version matches minor",
|
||||||
|
version: "7.1.3",
|
||||||
|
match: "7.*",
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard-version matches patch",
|
||||||
|
version: "7.4.8",
|
||||||
|
match: "7.4.*",
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sub-version matches major",
|
||||||
|
version: "7.19.11",
|
||||||
|
match: "7",
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sub-version matches minor",
|
||||||
|
version: "7.55.2",
|
||||||
|
match: "7.55",
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sub-version matches patch",
|
||||||
|
version: "7.32.6",
|
||||||
|
match: "7.32.6",
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
// negative tests
|
||||||
|
{
|
||||||
|
name: "different number does not match",
|
||||||
|
version: "3",
|
||||||
|
match: "4",
|
||||||
|
matches: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sub-version doesn't match major",
|
||||||
|
version: "7.2.5",
|
||||||
|
match: "8.2.5",
|
||||||
|
matches: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sub-version doesn't match minor",
|
||||||
|
version: "7.2.9",
|
||||||
|
match: "7.1",
|
||||||
|
matches: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sub-version doesn't match patch",
|
||||||
|
version: "7.32.6",
|
||||||
|
match: "7.32.5",
|
||||||
|
matches: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
matches := versionMatches(test.version, test.match)
|
||||||
|
assert.Equal(t, test.matches, matches)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue