feat: Allow specific versions of formats to be specified (#1543)

This commit is contained in:
Keith Zantow 2023-02-07 10:40:43 -05:00 committed by GitHub
parent 95201840d2
commit 9650473298
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 349 additions and 187 deletions

View File

@ -22,7 +22,6 @@ import (
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
@ -176,16 +175,9 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-
}
func ValidateOutputOptions(app *config.Application) error {
var usesTemplateOutput bool
for _, o := range app.Outputs {
if o == template.ID.String() {
usesTemplateOutput = true
break
}
}
if usesTemplateOutput && app.OutputTemplatePath == "" {
return fmt.Errorf(`must specify path to template file when using "template" output format`)
err := packages.ValidateOutputOptions(app)
if err != nil {
return err
}
if len(app.Outputs) > 1 {

View File

@ -8,7 +8,7 @@ import (
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats"
)
func Run(_ context.Context, app *config.Application, args []string) error {
@ -34,7 +34,7 @@ func Run(_ context.Context, app *config.Application, args []string) error {
_ = f.Close()
}()
sbom, _, err := syft.Decode(f)
sbom, _, err := formats.Decode(f)
if err != nil {
return fmt.Errorf("failed to decode SBOM: %w", err)
}

View File

@ -1,39 +0,0 @@
package options
import (
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/cyclonedxxml"
"github.com/anchore/syft/syft/formats/github"
"github.com/anchore/syft/syft/formats/spdxjson"
"github.com/anchore/syft/syft/formats/spdxtagvalue"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/formats/text"
"github.com/anchore/syft/syft/sbom"
)
func FormatAliases(ids ...sbom.FormatID) (aliases []string) {
for _, id := range ids {
switch id {
case syftjson.ID:
aliases = append(aliases, "syft-json")
case text.ID:
aliases = append(aliases, "text")
case table.ID:
aliases = append(aliases, "table")
case spdxjson.ID:
aliases = append(aliases, "spdx-json")
case spdxtagvalue.ID:
aliases = append(aliases, "spdx-tag-value")
case cyclonedxxml.ID:
aliases = append(aliases, "cyclonedx-xml")
case cyclonedxjson.ID:
aliases = append(aliases, "cyclonedx-json")
case github.ID:
aliases = append(aliases, "github", "github-json")
default:
aliases = append(aliases, string(id))
}
}
return aliases
}

View File

@ -7,7 +7,7 @@ import (
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/source"
@ -30,8 +30,8 @@ func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVarP(&o.Scope, "scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
cmd.Flags().StringArrayVarP(&o.Output, "output", "o", FormatAliases(table.ID),
fmt.Sprintf("report output format, options=%v", FormatAliases(syft.FormatIDs()...)))
cmd.Flags().StringArrayVarP(&o.Output, "output", "o", []string{string(table.ID)},
fmt.Sprintf("report output format, options=%v", formats.AllIDs()))
cmd.Flags().StringVarP(&o.File, "file", "", "",
"file to write the default report output to (default is STDOUT)")

View File

@ -6,7 +6,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/sbom"
@ -32,7 +32,7 @@ func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Wr
func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out []sbom.WriterOption, errs error) {
// always should have one option -- we generally get the default of "table", but just make sure
if len(outputs) == 0 {
outputs = append(outputs, string(table.ID))
outputs = append(outputs, table.ID.String())
}
for _, name := range outputs {
@ -52,9 +52,9 @@ func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out [
file = parts[1]
}
format := syft.FormatByName(name)
format := formats.ByName(name)
if format == nil {
errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, FormatAliases(syft.FormatIDs()...)))
errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, formats.AllIDs()))
continue
}

View File

@ -26,9 +26,10 @@ import (
"github.com/anchore/syft/syft/source"
)
func Run(ctx context.Context, app *config.Application, args []string) error {
func Run(_ context.Context, app *config.Application, args []string) error {
f := syftjson.Format()
writer, err := sbom.NewWriter(sbom.WriterOption{
Format: syftjson.Format(),
Format: f,
Path: app.File,
})
if err != nil {

View File

@ -30,12 +30,12 @@ const (
// TODO: deprecated, moved to syft/formats/formats.go. will be removed in v1.0.0
func FormatIDs() (ids []sbom.FormatID) {
return formats.IDs()
return formats.AllIDs()
}
// TODO: deprecated, moved to syft/formats/formats.go. will be removed in v1.0.0
func FormatByID(id sbom.FormatID) sbom.Format {
return formats.ByID(id)
return formats.ByNameAndVersion(string(id), "")
}
// TODO: deprecated, moved to syft/formats/formats.go. will be removed in v1.0.0

View File

@ -7,13 +7,14 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "cyclonedx-1-json"
const ID sbom.FormatID = "cyclonedx-json"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
encoder,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
ID,
)
}

View File

@ -7,13 +7,14 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "cyclonedx-1-xml"
const ID sbom.FormatID = "cyclonedx-xml"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
encoder,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
ID, "cyclonedx", "cyclone",
)
}

View File

@ -5,8 +5,11 @@ import (
"errors"
"fmt"
"io"
"regexp"
"strings"
"golang.org/x/exp/slices"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/cyclonedxxml"
@ -26,8 +29,11 @@ func Formats() []sbom.Format {
cyclonedxxml.Format(),
cyclonedxjson.Format(),
github.Format(),
spdxtagvalue.Format(),
spdxjson.Format(),
spdxtagvalue.Format2_1(),
spdxtagvalue.Format2_2(),
spdxtagvalue.Format2_3(),
spdxjson.Format2_2(),
spdxjson.Format2_3(),
table.Format(),
text.Format(),
template.Format(),
@ -47,53 +53,51 @@ func Identify(by []byte) sbom.Format {
return nil
}
// ByName accepts a name@version string, such as:
//
// spdx-json@2.1 or cyclonedx@2
func ByName(name string) sbom.Format {
cleanName := cleanFormatName(name)
for _, f := range Formats() {
if cleanFormatName(string(f.ID())) == cleanName {
return f
}
parts := strings.SplitN(name, "@", 2)
version := sbom.AnyVersion
if len(parts) > 1 {
version = parts[1]
}
// handle any aliases for any supported format
switch cleanName {
case "json", "syftjson":
return ByID(syftjson.ID)
case "cyclonedx", "cyclone", "cyclonedxxml":
return ByID(cyclonedxxml.ID)
case "cyclonedxjson":
return ByID(cyclonedxjson.ID)
case "github", "githubjson":
return ByID(github.ID)
case "spdx", "spdxtv", "spdxtagvalue":
return ByID(spdxtagvalue.ID)
case "spdxjson":
return ByID(spdxjson.ID)
case "table":
return ByID(table.ID)
case "text":
return ByID(text.ID)
case "template":
ByID(template.ID)
}
return nil
return ByNameAndVersion(parts[0], version)
}
func IDs() (ids []sbom.FormatID) {
func ByNameAndVersion(name string, version string) sbom.Format {
name = cleanFormatName(name)
var mostRecentFormat sbom.Format
for _, f := range Formats() {
ids = append(ids, f.ID())
}
return ids
}
func ByID(id sbom.FormatID) sbom.Format {
for _, f := range Formats() {
if f.ID() == id {
return f
for _, n := range f.IDs() {
if cleanFormatName(string(n)) == name && versionMatches(f.Version(), version) {
if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
mostRecentFormat = f
}
}
}
}
return nil
return mostRecentFormat
}
func versionMatches(version string, match string) bool {
if version == sbom.AnyVersion || match == sbom.AnyVersion {
return true
}
dots := strings.Count(match, ".")
if dots == 0 {
match += ".*"
}
match = strings.ReplaceAll(match, ".", "\\.")
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 {
@ -127,3 +131,13 @@ func Decode(reader io.Reader) (*sbom.SBOM, sbom.Format, error) {
s, err := f.Decode(bytes.NewReader(by))
return s, f, err
}
func AllIDs() (ids []sbom.FormatID) {
for _, f := range Formats() {
if slices.Contains(ids, f.ID()) {
continue
}
ids = append(ids, f.ID())
}
return ids
}

View File

@ -92,18 +92,6 @@ func TestByName(t *testing.T) {
name: "spdxtv", // clean variant
want: spdxtagvalue.ID,
},
{
name: "spdx-2-tag-value", // clean variant
want: spdxtagvalue.ID,
},
{
name: "spdx-2-tagvalue", // clean variant
want: spdxtagvalue.ID,
},
{
name: "spdx2-tagvalue", // clean variant
want: spdxtagvalue.ID,
},
// SPDX JSON
{
@ -111,7 +99,7 @@ func TestByName(t *testing.T) {
want: spdxjson.ID,
},
{
name: "spdx-2-json",
name: "spdxjson", // clean variant
want: spdxjson.ID,
},
@ -121,7 +109,7 @@ func TestByName(t *testing.T) {
want: cyclonedxjson.ID,
},
{
name: "cyclonedx-1-json",
name: "cyclonedxjson", // clean variant
want: cyclonedxjson.ID,
},
@ -135,7 +123,7 @@ func TestByName(t *testing.T) {
want: cyclonedxxml.ID,
},
{
name: "cyclonedx-1-xml",
name: "cyclonedxxml", // clean variant
want: cyclonedxxml.ID,
},
@ -144,7 +132,6 @@ func TestByName(t *testing.T) {
name: "table",
want: table.ID,
},
{
name: "syft-table",
want: table.ID,
@ -155,7 +142,6 @@ func TestByName(t *testing.T) {
name: "text",
want: text.ID,
},
{
name: "syft-text",
want: text.ID,
@ -166,23 +152,26 @@ func TestByName(t *testing.T) {
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,
@ -200,3 +189,56 @@ func TestByName(t *testing.T) {
})
}
}
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: "different number does not match",
version: "3",
match: "4",
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)
})
}
}

View File

@ -7,11 +7,11 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "github-0-json"
const ID sbom.FormatID = "github-json"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
func(writer io.Writer, sbom sbom.SBOM) error {
bom := toGithubModel(&sbom)
@ -25,5 +25,6 @@ func Format() sbom.Format {
},
nil,
nil,
ID, "github",
)
}

View File

@ -4,13 +4,31 @@ import (
"encoding/json"
"io"
"github.com/spdx/tools-golang/convert"
"github.com/spdx/tools-golang/spdx/v2/v2_2"
"github.com/anchore/syft/syft/formats/common/spdxhelpers"
"github.com/anchore/syft/syft/sbom"
)
func encoder(output io.Writer, s sbom.SBOM) error {
func encoder2_3(output io.Writer, s sbom.SBOM) error {
doc := spdxhelpers.ToFormatModel(s)
return encodeJSON(output, doc)
}
func encoder2_2(output io.Writer, s sbom.SBOM) error {
doc := spdxhelpers.ToFormatModel(s)
var out v2_2.Document
err := convert.Document(doc, &out)
if err != nil {
return err
}
return encodeJSON(output, out)
}
func encodeJSON(output io.Writer, doc interface{}) error {
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)

View File

@ -4,14 +4,30 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "spdx-2-json"
const ID sbom.FormatID = "spdx-json"
var IDs = []sbom.FormatID{ID}
// note: this format is LOSSY relative to the syftjson format
func Format() sbom.Format {
func Format2_2() sbom.Format {
return sbom.NewFormat(
ID,
encoder,
"2.2",
encoder2_2,
decoder,
validator,
IDs...,
)
}
func Format2_3() sbom.Format {
return sbom.NewFormat(
"2.3",
encoder2_3,
decoder,
validator,
IDs...,
)
}
var Format = Format2_3

View File

@ -3,13 +3,36 @@ package spdxtagvalue
import (
"io"
"github.com/spdx/tools-golang/convert"
"github.com/spdx/tools-golang/spdx/v2/v2_1"
"github.com/spdx/tools-golang/spdx/v2/v2_2"
"github.com/spdx/tools-golang/tagvalue"
"github.com/anchore/syft/syft/formats/common/spdxhelpers"
"github.com/anchore/syft/syft/sbom"
)
func encoder(output io.Writer, s sbom.SBOM) error {
func encoder2_3(output io.Writer, s sbom.SBOM) error {
model := spdxhelpers.ToFormatModel(s)
return tagvalue.Write(model, output)
}
func encoder2_2(output io.Writer, s sbom.SBOM) error {
model := spdxhelpers.ToFormatModel(s)
var out v2_2.Document
err := convert.Document(model, &out)
if err != nil {
return err
}
return tagvalue.Write(out, output)
}
func encoder2_1(output io.Writer, s sbom.SBOM) error {
model := spdxhelpers.ToFormatModel(s)
var out v2_1.Document
err := convert.Document(model, &out)
if err != nil {
return err
}
return tagvalue.Write(out, output)
}

View File

@ -4,14 +4,39 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "spdx-2-tag-value"
const ID sbom.FormatID = "spdx-tag-value"
// note: this format is LOSSY relative to the syftjson formation, which means that decoding and validation is not supported at this time
func Format() sbom.Format {
var IDs = []sbom.FormatID{ID, "spdx", "spdx-tv"}
// note: this format is LOSSY relative to the syftjson format
func Format2_1() sbom.Format {
return sbom.NewFormat(
ID,
encoder,
"2.1",
encoder2_1,
decoder,
validator,
IDs...,
)
}
func Format2_2() sbom.Format {
return sbom.NewFormat(
"2.2",
encoder2_2,
decoder,
validator,
IDs...,
)
}
func Format2_3() sbom.Format {
return sbom.NewFormat(
"2.3",
encoder2_3,
decoder,
validator,
IDs...,
)
}
var Format = Format2_3

View File

@ -4,13 +4,14 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "syft-6-json"
const ID sbom.FormatID = "syft-json"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
"6",
encoder,
decoder,
validator,
ID, "json", "syft",
)
}

View File

@ -15,8 +15,7 @@ import (
func Test_SyftJsonID_Compatibility(t *testing.T) {
jsonMajorVersion := strings.Split(internal.JSONSchemaVersion, ".")[0]
syftJsonIDVersion := strings.Split(string(ID), "-")[1]
assert.Equal(t, jsonMajorVersion, syftJsonIDVersion)
assert.Equal(t, jsonMajorVersion, string(Format().Version()))
}
func Test_toSourceModel(t *testing.T) {

View File

@ -8,9 +8,10 @@ const ID sbom.FormatID = "syft-table"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
encoder,
nil,
nil,
ID, "table",
)
}

View File

@ -1,6 +1,7 @@
package template
import (
"fmt"
"io"
"github.com/anchore/syft/syft/formats/syftjson"
@ -23,7 +24,19 @@ func (f OutputFormat) ID() sbom.FormatID {
return ID
}
func (f OutputFormat) Decode(reader io.Reader) (*sbom.SBOM, error) {
func (f OutputFormat) IDs() []sbom.FormatID {
return []sbom.FormatID{ID}
}
func (f OutputFormat) Version() string {
return sbom.AnyVersion
}
func (f OutputFormat) String() string {
return fmt.Sprintf("template: " + f.templateFilePath)
}
func (f OutputFormat) Decode(_ io.Reader) (*sbom.SBOM, error) {
return nil, sbom.ErrDecodingNotSupported
}
@ -37,7 +50,7 @@ func (f OutputFormat) Encode(output io.Writer, s sbom.SBOM) error {
return tmpl.Execute(output, doc)
}
func (f OutputFormat) Validate(reader io.Reader) error {
func (f OutputFormat) Validate(_ io.Reader) error {
return sbom.ErrValidationNotSupported
}
@ -45,3 +58,5 @@ func (f OutputFormat) Validate(reader io.Reader) error {
func (f *OutputFormat) SetTemplatePath(filePath string) {
f.templateFilePath = filePath
}
var _ sbom.Format = (*OutputFormat)(nil)

View File

@ -8,9 +8,10 @@ const ID sbom.FormatID = "syft-text"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
encoder,
nil,
nil,
ID, "text",
)
}

View File

@ -2,6 +2,7 @@ package sbom
import (
"errors"
"fmt"
"io"
)
@ -18,20 +19,41 @@ func (f FormatID) String() string {
return string(f)
}
const AnyVersion = ""
type Format interface {
ID() FormatID
IDs() []FormatID
Version() string
Encode(io.Writer, SBOM) error
Decode(io.Reader) (*SBOM, error)
Validate(io.Reader) error
fmt.Stringer
}
type format struct {
id FormatID
ids []FormatID
version string
encoder Encoder
decoder Decoder
validator Validator
}
func (f format) IDs() []FormatID {
return f.ids
}
func (f format) Version() string {
return f.version
}
func (f format) String() string {
if f.version == AnyVersion {
return f.ID().String()
}
return fmt.Sprintf("%s@%s", f.ID(), f.version)
}
// Decoder is a function that can convert an SBOM document of a specific format from a reader into Syft native objects.
type Decoder func(reader io.Reader) (*SBOM, error)
@ -47,9 +69,10 @@ type Encoder func(io.Writer, SBOM) error
// really represent a different format that also uses json)
type Validator func(reader io.Reader) error
func NewFormat(id FormatID, encoder Encoder, decoder Decoder, validator Validator) Format {
return &format{
id: id,
func NewFormat(version string, encoder Encoder, decoder Decoder, validator Validator, ids ...FormatID) Format {
return format{
ids: ids,
version: version,
encoder: encoder,
decoder: decoder,
validator: validator,
@ -57,7 +80,7 @@ func NewFormat(id FormatID, encoder Encoder, decoder Decoder, validator Validato
}
func (f format) ID() FormatID {
return f.id
return f.ids[0]
}
func (f format) Encode(output io.Writer, s SBOM) error {
@ -81,3 +104,5 @@ func (f format) Validate(reader io.Reader) error {
return f.validator(reader)
}
var _ Format = (*format)(nil)

View File

@ -15,7 +15,7 @@ func dummyEncoder(io.Writer, SBOM) error {
}
func dummyFormat(name string) Format {
return NewFormat(FormatID(name), dummyEncoder, nil, nil)
return NewFormat(AnyVersion, dummyEncoder, nil, nil, FormatID(name))
}
type writerConfig struct {

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/template"
)
@ -21,9 +21,9 @@ func TestAllFormatsExpressible(t *testing.T) {
},
assertSuccessfulReturnCode,
}
formats := syft.FormatIDs()
require.NotEmpty(t, formats)
for _, o := range formats {
formatNames := formats.AllIDs()
require.NotEmpty(t, formatNames)
for _, o := range formatNames {
t.Run(fmt.Sprintf("format:%s", o), func(t *testing.T) {
args := []string{"dir:./test-fixtures/image-pkg-coverage", "-o", string(o)}
if o == template.ID {

View File

@ -2,7 +2,7 @@ package integration
import (
"context"
"io/ioutil"
"fmt"
"os"
"testing"
@ -10,7 +10,7 @@ import (
"github.com/anchore/syft/cmd/syft/cli/convert"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/cyclonedxxml"
"github.com/anchore/syft/syft/formats/spdxjson"
@ -21,36 +21,61 @@ import (
"github.com/anchore/syft/syft/source"
)
var convertibleFormats = []sbom.Format{
syftjson.Format(),
spdxjson.Format(),
spdxtagvalue.Format(),
cyclonedxjson.Format(),
cyclonedxxml.Format(),
}
// TestConvertCmd tests if the converted SBOM is a valid document according
// to spec.
// TODO: This test can, but currently does not, check the converted SBOM content. It
// might be useful to do that in the future, once we gather a better understanding of
// what users expect from the convert command.
func TestConvertCmd(t *testing.T) {
for _, format := range convertibleFormats {
t.Run(format.ID().String(), func(t *testing.T) {
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope, nil)
format := syft.FormatByID(syftjson.ID)
tests := []struct {
name string
format sbom.Format
}{
{
name: "syft-json",
format: syftjson.Format(),
},
{
name: "spdx-json",
format: spdxjson.Format(),
},
{
name: "spdx-tag-value",
format: spdxtagvalue.Format(),
},
{
name: "cyclonedx-json",
format: cyclonedxjson.Format(),
},
{
name: "cyclonedx-xml",
format: cyclonedxxml.Format(),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
syftSbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope, nil)
syftFormat := syftjson.Format()
f, err := ioutil.TempFile("", "test-convert-sbom-")
syftFile, err := os.CreateTemp("", "test-convert-sbom-")
require.NoError(t, err)
defer func() {
os.Remove(f.Name())
_ = os.Remove(syftFile.Name())
}()
err = format.Encode(f, sbom)
err = syftFormat.Encode(syftFile, syftSbom)
require.NoError(t, err)
formatFile, err := os.CreateTemp("", "test-convert-sbom-")
require.NoError(t, err)
defer func() {
_ = os.Remove(syftFile.Name())
}()
ctx := context.Background()
app := &config.Application{Outputs: []string{format.ID().String()}}
app := &config.Application{
Outputs: []string{fmt.Sprintf("%s=%s", test.format.ID().String(), formatFile.Name())},
}
// stdout reduction of test noise
rescue := os.Stdout // keep backup of the real stdout
@ -59,17 +84,17 @@ func TestConvertCmd(t *testing.T) {
os.Stdout = rescue
}()
err = convert.Run(ctx, app, []string{f.Name()})
err = convert.Run(ctx, app, []string{syftFile.Name()})
require.NoError(t, err)
file, err := ioutil.ReadFile(f.Name())
contents, err := os.ReadFile(formatFile.Name())
require.NoError(t, err)
formatFound := syft.IdentifyFormat(file)
if format.ID() == table.ID {
formatFound := formats.Identify(contents)
if test.format.ID() == table.ID {
require.Nil(t, formatFound)
return
}
require.Equal(t, format.ID(), formatFound.ID())
require.Equal(t, test.format.ID(), formatFound.ID())
})
}
}

View File

@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/cyclonedxxml"
"github.com/anchore/syft/syft/formats/syftjson"
@ -68,17 +68,17 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
for _, image := range images {
originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope, nil)
format := syft.FormatByID(test.formatOption)
format := formats.ByName(string(test.formatOption))
require.NotNil(t, format)
by1, err := syft.Encode(originalSBOM, format)
by1, err := formats.Encode(originalSBOM, format)
assert.NoError(t, err)
newSBOM, newFormat, err := syft.Decode(bytes.NewReader(by1))
newSBOM, newFormat, err := formats.Decode(bytes.NewReader(by1))
assert.NoError(t, err)
assert.Equal(t, format.ID(), newFormat.ID())
by2, err := syft.Encode(*newSBOM, format)
by2, err := formats.Encode(*newSBOM, format)
assert.NoError(t, err)
if test.redactor != nil {