Migrate format definitions to sbom package (#864)

This commit is contained in:
Alex Goodman 2022-03-04 17:22:40 -05:00 committed by GitHub
parent 640099ce2e
commit 4af32c5bee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 580 additions and 401 deletions

View file

@ -9,6 +9,10 @@ import (
"os" "os"
"strings" "strings"
"github.com/anchore/syft/internal/formats/cyclonedx13json"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/stereoscope" "github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
@ -17,7 +21,7 @@ import (
"github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft" "github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/in-toto/in-toto-golang/in_toto" "github.com/in-toto/in-toto-golang/in_toto"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -49,7 +53,11 @@ const (
intotoJSONDsseType = `application/vnd.in-toto+json` intotoJSONDsseType = `application/vnd.in-toto+json`
) )
var attestFormats = []format.Option{format.SPDXJSONOption, format.CycloneDxJSONOption, format.JSONOption} var attestFormats = []sbom.FormatID{
syftjson.ID,
spdx22json.ID,
cyclonedx13json.ID,
}
var ( var (
attestCmd = &cobra.Command{ attestCmd = &cobra.Command{
@ -149,10 +157,10 @@ func attestExec(ctx context.Context, _ *cobra.Command, args []string) error {
return fmt.Errorf("unable to generate attestation for more than one output") return fmt.Errorf("unable to generate attestation for more than one output")
} }
output := format.ParseOption(appConfig.Output[0]) format := syft.FormatByName(appConfig.Output[0])
predicateType := assertPredicateType(output) predicateType := formatPredicateType(format)
if predicateType == "" { if predicateType == "" {
return fmt.Errorf("could not produce attestation predicate for given format: %q. Available formats: %+v", output, attestFormats) return fmt.Errorf("could not produce attestation predicate for given format: %q. Available formats: %+v", formatAliases(format.ID()), formatAliases(attestFormats...))
} }
passFunc, err := selectPassFunc(appConfig.Attest.Key) passFunc, err := selectPassFunc(appConfig.Attest.Key)
@ -172,7 +180,7 @@ func attestExec(ctx context.Context, _ *cobra.Command, args []string) error {
defer sv.Close() defer sv.Close()
return eventLoop( return eventLoop(
attestationExecWorker(*si, output, predicateType, sv), attestationExecWorker(*si, format, predicateType, sv),
setupSignals(), setupSignals(),
eventSubscription, eventSubscription,
stereoscope.Cleanup, stereoscope.Cleanup,
@ -180,7 +188,7 @@ func attestExec(ctx context.Context, _ *cobra.Command, args []string) error {
) )
} }
func attestationExecWorker(sourceInput source.Input, output format.Option, predicateType string, sv *sign.SignerVerifier) <-chan error { func attestationExecWorker(sourceInput source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error {
errs := make(chan error) errs := make(chan error)
go func() { go func() {
defer close(errs) defer close(errs)
@ -200,7 +208,7 @@ func attestationExecWorker(sourceInput source.Input, output format.Option, predi
return return
} }
sbomBytes, err := syft.Encode(*s, output) sbomBytes, err := syft.Encode(*s, format)
if err != nil { if err != nil {
errs <- err errs <- err
return return
@ -215,14 +223,14 @@ func attestationExecWorker(sourceInput source.Input, output format.Option, predi
return errs return errs
} }
func assertPredicateType(output format.Option) string { func formatPredicateType(format sbom.Format) string {
switch output { switch format.ID() {
case format.SPDXJSONOption: case spdx22json.ID:
return in_toto.PredicateSPDX return in_toto.PredicateSPDX
// Tentative see https://github.com/in-toto/attestation/issues/82 case cyclonedx13json.ID:
case format.CycloneDxJSONOption: // Tentative see https://github.com/in-toto/attestation/issues/82
return "https://cyclonedx.org/bom" return "https://cyclonedx.org/bom"
case format.JSONOption: case syftjson.ID:
return "https://syft.dev/bom" return "https://syft.dev/bom"
default: default:
return "" return ""
@ -293,8 +301,8 @@ func setAttestFlags(flags *pflag.FlagSet) {
// in-toto attestations only support JSON predicates, so not all SBOM formats that syft can output are supported // in-toto attestations only support JSON predicates, so not all SBOM formats that syft can output are supported
flags.StringP( flags.StringP(
"output", "o", string(format.JSONOption), "output", "o", formatAliases(syftjson.ID)[0],
fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", attestFormats), fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", formatAliases(attestFormats...)),
) )
} }

30
cmd/format_aliases.go Normal file
View file

@ -0,0 +1,30 @@
package cmd
import (
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/sbom"
)
func formatAliases(ids ...sbom.FormatID) (aliases []string) {
for _, id := range ids {
switch id {
case syft.JSONFormatID:
aliases = append(aliases, "syft-json")
case syft.TextFormatID:
aliases = append(aliases, "text")
case syft.TableFormatID:
aliases = append(aliases, "table")
case syft.SPDXJSONFormatID:
aliases = append(aliases, "spdx-json")
case syft.SPDXTagValueFormatID:
aliases = append(aliases, "spdx-tag-value")
case syft.CycloneDxXMLFormatID:
aliases = append(aliases, "cyclonedx-xml")
case syft.CycloneDxJSONFormatID:
aliases = append(aliases, "cyclonedx-json")
default:
aliases = append(aliases, string(id))
}
}
return aliases
}

View file

@ -4,9 +4,10 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/anchore/syft/internal/formats" "github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/internal/output"
"github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
) )
@ -19,7 +20,7 @@ func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
return nil, err return nil, err
} }
writer, err := output.MakeWriter(outputOptions...) writer, err := sbom.NewWriter(outputOptions...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -28,10 +29,10 @@ func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
} }
// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file // parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
func parseOptions(outputs []string, defaultFile string) (out []output.WriterOption, errs error) { func parseOptions(outputs []string, defaultFile string) (out []sbom.WriterOption, errs error) {
// 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, string(format.TableOption)) outputs = append(outputs, string(table.ID))
} }
for _, name := range outputs { for _, name := range outputs {
@ -40,31 +41,25 @@ func parseOptions(outputs []string, defaultFile string) (out []output.WriterOpti
// split to at most two parts for <format>=<file> // split to at most two parts for <format>=<file>
parts := strings.SplitN(name, "=", 2) parts := strings.SplitN(name, "=", 2)
// the format option is the first part // the format name is the first part
name = parts[0] name = parts[0]
// default to the --file or empty string if not specified // default to the --file or empty string if not specified
file := defaultFile file := defaultFile
// If a file is specified as part of the output option, use that // If a file is specified as part of the output formatName, use that
if len(parts) > 1 { if len(parts) > 1 {
file = parts[1] file = parts[1]
} }
option := format.ParseOption(name) format := syft.FormatByName(name)
if option == format.UnknownFormatOption { if format == nil {
errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name)) errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name))
continue continue
} }
encoder := formats.ByOption(option) out = append(out, sbom.WriterOption{
if encoder == nil { Format: format,
errs = multierror.Append(errs, fmt.Errorf("unknown format: %s", outputFormat))
continue
}
out = append(out, output.WriterOption{
Format: *encoder,
Path: file, Path: file,
}) })
} }

View file

@ -6,6 +6,10 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/syft"
"github.com/anchore/stereoscope" "github.com/anchore/stereoscope"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore" "github.com/anchore/syft/internal/anchore"
@ -15,7 +19,6 @@ import (
"github.com/anchore/syft/internal/version" "github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/pkg/cataloger" "github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
@ -100,8 +103,8 @@ func setPackageFlags(flags *pflag.FlagSet) {
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes)) fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
flags.StringArrayP( flags.StringArrayP(
"output", "o", []string{string(format.TableOption)}, "output", "o", formatAliases(table.ID),
fmt.Sprintf("report output format, options=%v", format.AllOptions), fmt.Sprintf("report output format, options=%v", formatAliases(syft.FormatIDs()...)),
) )
flags.StringP( flags.StringP(

View file

@ -9,7 +9,6 @@ import (
"github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/formats/syftjson" "github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/output"
"github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version" "github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
@ -74,7 +73,7 @@ func powerUserExec(_ *cobra.Command, args []string) error {
// could be an image or a directory, with or without a scheme // could be an image or a directory, with or without a scheme
userInput := args[0] userInput := args[0]
writer, err := output.MakeWriter(output.WriterOption{ writer, err := sbom.NewWriter(sbom.WriterOption{
Format: syftjson.Format(), Format: syftjson.Format(),
Path: appConfig.File, Path: appConfig.File,
}) })

View file

@ -11,7 +11,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var outputFormat string var versionCmdOutputFormat string
var versionCmd = &cobra.Command{ var versionCmd = &cobra.Command{
Use: "version", Use: "version",
@ -20,14 +20,14 @@ var versionCmd = &cobra.Command{
} }
func init() { func init() {
versionCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "format to show version information (available=[text, json])") versionCmd.Flags().StringVarP(&versionCmdOutputFormat, "output", "o", "text", "format to show version information (available=[text, json])")
rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(versionCmd)
} }
func printVersion(_ *cobra.Command, _ []string) { func printVersion(_ *cobra.Command, _ []string) {
versionInfo := version.FromBuild() versionInfo := version.FromBuild()
switch outputFormat { switch versionCmdOutputFormat {
case "text": case "text":
fmt.Println("Application: ", internal.ApplicationName) fmt.Println("Application: ", internal.ApplicationName)
fmt.Println("Version: ", versionInfo.Version) fmt.Println("Version: ", versionInfo.Version)
@ -54,7 +54,7 @@ func printVersion(_ *cobra.Command, _ []string) {
os.Exit(1) os.Exit(1)
} }
default: default:
fmt.Printf("unsupported output format: %s\n", outputFormat) fmt.Printf("unsupported output format: %s\n", versionCmdOutputFormat)
os.Exit(1) os.Exit(1)
} }
} }

View file

@ -6,14 +6,13 @@ import (
"github.com/CycloneDX/cyclonedx-go" "github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/format"
"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"
) )
func GetValidator(format cyclonedx.BOMFileFormat) format.Validator { func GetValidator(format cyclonedx.BOMFileFormat) sbom.Validator {
return func(reader io.Reader) error { return func(reader io.Reader) error {
bom := &cyclonedx.BOM{} bom := &cyclonedx.BOM{}
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom) err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
@ -28,7 +27,7 @@ func GetValidator(format cyclonedx.BOMFileFormat) format.Validator {
} }
} }
func GetDecoder(format cyclonedx.BOMFileFormat) format.Decoder { func GetDecoder(format cyclonedx.BOMFileFormat) sbom.Decoder {
return func(reader io.Reader) (*sbom.SBOM, error) { return func(reader io.Reader) (*sbom.SBOM, error) {
bom := &cyclonedx.BOM{} bom := &cyclonedx.BOM{}
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom) err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)

View file

@ -9,7 +9,6 @@ import (
"github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/format"
"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"
@ -32,7 +31,7 @@ func FromSnapshot() ImageOption {
} }
} }
func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, redactors ...redactor) { func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer var buffer bytes.Buffer
// grab the latest image contents and persist // grab the latest image contents and persist
@ -66,7 +65,7 @@ func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format format.Format,
} }
} }
func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, updateSnapshot bool, redactors ...redactor) { func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer var buffer bytes.Buffer
err := format.Encode(&buffer, sbom) err := format.Encode(&buffer, sbom)

View file

@ -3,12 +3,14 @@ package cyclonedx13json
import ( import (
"github.com/CycloneDX/cyclonedx-go" "github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers" "github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
"github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/sbom"
) )
func Format() format.Format { const ID sbom.FormatID = "cyclonedx-1-json"
return format.NewFormat(
format.CycloneDxJSONOption, func Format() sbom.Format {
return sbom.NewFormat(
ID,
encoder, encoder,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),

View file

@ -3,12 +3,14 @@ package cyclonedx13xml
import ( import (
"github.com/CycloneDX/cyclonedx-go" "github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers" "github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
"github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/sbom"
) )
func Format() format.Format { const ID sbom.FormatID = "cyclonedx-1-xml"
return format.NewFormat(
format.CycloneDxXMLOption, func Format() sbom.Format {
return sbom.NewFormat(
ID,
encoder, encoder,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),

View file

@ -1,46 +0,0 @@
package formats
import (
"bytes"
"github.com/anchore/syft/internal/formats/cyclonedx13json"
"github.com/anchore/syft/internal/formats/cyclonedx13xml"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/internal/formats/text"
"github.com/anchore/syft/syft/format"
)
// TODO: eventually this is the source of truth for all formatters
func All() []format.Format {
return []format.Format{
syftjson.Format(),
table.Format(),
cyclonedx13xml.Format(),
cyclonedx13json.Format(),
spdx22json.Format(),
spdx22tagvalue.Format(),
text.Format(),
}
}
func Identify(by []byte) (*format.Format, error) {
for _, f := range All() {
if err := f.Validate(bytes.NewReader(by)); err != nil {
continue
}
return &f, nil
}
return nil, nil
}
func ByOption(option format.Option) *format.Format {
for _, f := range All() {
if f.Option == option {
return &f
}
}
return nil
}

View file

@ -1,34 +0,0 @@
package formats
import (
"io"
"os"
"testing"
"github.com/anchore/syft/syft/format"
"github.com/stretchr/testify/assert"
)
func TestIdentify(t *testing.T) {
tests := []struct {
fixture string
expected format.Option
}{
{
fixture: "test-fixtures/alpine-syft.json",
expected: format.JSONOption,
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
f, err := os.Open(test.fixture)
assert.NoError(t, err)
by, err := io.ReadAll(f)
assert.NoError(t, err)
frmt, err := Identify(by)
assert.NoError(t, err)
assert.NotNil(t, frmt)
assert.Equal(t, test.expected, frmt.Option)
})
}
}

View file

@ -1,11 +1,15 @@
package spdx22json package spdx22json
import "github.com/anchore/syft/syft/format" import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "spdx-2-json"
// note: this format is LOSSY relative to the syftjson format // note: this format is LOSSY relative to the syftjson format
func Format() format.Format { func Format() sbom.Format {
return format.NewFormat( return sbom.NewFormat(
format.SPDXJSONOption, ID,
encoder, encoder,
decoder, decoder,
validator, validator,

View file

@ -1,11 +1,15 @@
package spdx22tagvalue package spdx22tagvalue
import "github.com/anchore/syft/syft/format" import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "spdx-2-tag-value"
// note: this format is LOSSY relative to the syftjson formation, which means that decoding and validation is not supported at this time // note: this format is LOSSY relative to the syftjson formation, which means that decoding and validation is not supported at this time
func Format() format.Format { func Format() sbom.Format {
return format.NewFormat( return sbom.NewFormat(
format.SPDXTagValueOption, ID,
encoder, encoder,
decoder, decoder,
validator, validator,

View file

@ -221,7 +221,7 @@ func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2
// 3.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION" // 3.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one // Cardinality: mandatory, one
// Purpose: Identify the copyright holders of the package, as well as any dates present. This will be a free form text field extracted from package information files. The options to populate this field are limited to: // Purpose: IdentifyFormat the copyright holders of the package, as well as any dates present. This will be a free form text field extracted from package information files. The options to populate this field are limited to:
// //
// Any text related to a copyright notice, even if not complete; // Any text related to a copyright notice, even if not complete;
// NONE if the package contains no copyright information whatsoever; or // NONE if the package contains no copyright information whatsoever; or

View file

@ -1,10 +1,14 @@
package syftjson package syftjson
import "github.com/anchore/syft/syft/format" import (
"github.com/anchore/syft/syft/sbom"
)
func Format() format.Format { const ID sbom.FormatID = "syft-3-json"
return format.NewFormat(
format.JSONOption, func Format() sbom.Format {
return sbom.NewFormat(
ID,
encoder, encoder,
decoder, decoder,
validator, validator,

View file

@ -1,10 +1,14 @@
package table package table
import "github.com/anchore/syft/syft/format" import (
"github.com/anchore/syft/syft/sbom"
)
func Format() format.Format { const ID sbom.FormatID = "syft-table"
return format.NewFormat(
format.TableOption, func Format() sbom.Format {
return sbom.NewFormat(
ID,
encoder, encoder,
nil, nil,
nil, nil,

View file

@ -1,10 +1,14 @@
package text package text
import "github.com/anchore/syft/syft/format" import (
"github.com/anchore/syft/syft/sbom"
)
func Format() format.Format { const ID sbom.FormatID = "syft-text"
return format.NewFormat(
format.TextOption, func Format() sbom.Format {
return sbom.NewFormat(
ID,
encoder, encoder,
nil, nil,
nil, nil,

View file

@ -6,12 +6,7 @@ import "sort"
type StringSet map[string]struct{} type StringSet map[string]struct{}
// NewStringSet creates a new empty StringSet. // NewStringSet creates a new empty StringSet.
func NewStringSet() StringSet { func NewStringSet(start ...string) StringSet {
return make(StringSet)
}
// NewStringSetFromSlice creates a StringSet populated with values from the given slice.
func NewStringSetFromSlice(start []string) StringSet {
ret := make(StringSet) ret := make(StringSet)
for _, s := range start { for _, s := range start {
ret.Add(s) ret.Add(s)

View file

@ -6,18 +6,10 @@ import (
"io" "io"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/syft/format"
) )
// Encode takes all SBOM elements and a format option and encodes an SBOM document. // Encode takes all SBOM elements and a format option and encodes an SBOM document.
func Encode(s sbom.SBOM, option format.Option) ([]byte, error) { func Encode(s sbom.SBOM, f sbom.Format) ([]byte, error) {
f := formats.ByOption(option)
if f == nil {
return nil, fmt.Errorf("unsupported format: %+v", option)
}
buff := bytes.Buffer{} buff := bytes.Buffer{}
if err := f.Encode(&buff, s); err != nil { if err := f.Encode(&buff, s); err != nil {
@ -28,19 +20,17 @@ func Encode(s sbom.SBOM, option format.Option) ([]byte, error) {
} }
// Decode takes a reader for an SBOM and generates all internal SBOM elements. // Decode takes a reader for an SBOM and generates all internal SBOM elements.
func Decode(reader io.Reader) (*sbom.SBOM, format.Option, error) { func Decode(reader io.Reader) (*sbom.SBOM, sbom.Format, error) {
by, err := io.ReadAll(reader) by, err := io.ReadAll(reader)
if err != nil { if err != nil {
return nil, format.UnknownFormatOption, fmt.Errorf("unable to read sbom: %w", err) return nil, nil, fmt.Errorf("unable to read sbom: %w", err)
} }
f, err := formats.Identify(by) f := IdentifyFormat(by)
if err != nil {
return nil, format.UnknownFormatOption, fmt.Errorf("unable to detect format: %w", err)
}
if f == nil { if f == nil {
return nil, format.UnknownFormatOption, fmt.Errorf("unable to identify format") return nil, nil, fmt.Errorf("unable to identify format")
} }
s, err := f.Decode(bytes.NewReader(by)) s, err := f.Decode(bytes.NewReader(by))
return s, f.Option, err return s, f, err
} }

View file

@ -1,10 +0,0 @@
package format
import (
"io"
"github.com/anchore/syft/syft/sbom"
)
// 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.SBOM, error)

View file

@ -1,10 +0,0 @@
package format
import (
"io"
"github.com/anchore/syft/syft/sbom"
)
// Encoder is a function that can transform Syft native objects into an SBOM document of a specific format written to the given writer.
type Encoder func(io.Writer, sbom.SBOM) error

View file

@ -1,52 +0,0 @@
package format
import (
"errors"
"io"
"github.com/anchore/syft/syft/sbom"
)
var (
ErrEncodingNotSupported = errors.New("encoding not supported")
ErrDecodingNotSupported = errors.New("decoding not supported")
ErrValidationNotSupported = errors.New("validation not supported")
)
type Format struct {
Option Option
encoder Encoder
decoder Decoder
validator Validator
}
func NewFormat(option Option, encoder Encoder, decoder Decoder, validator Validator) Format {
return Format{
Option: option,
encoder: encoder,
decoder: decoder,
validator: validator,
}
}
func (f Format) Encode(output io.Writer, s sbom.SBOM) error {
if f.encoder == nil {
return ErrEncodingNotSupported
}
return f.encoder(output, s)
}
func (f Format) Decode(reader io.Reader) (*sbom.SBOM, error) {
if f.decoder == nil {
return nil, ErrDecodingNotSupported
}
return f.decoder(reader)
}
func (f Format) Validate(reader io.Reader) error {
if f.validator == nil {
return ErrValidationNotSupported
}
return f.validator(reader)
}

View file

@ -1,49 +0,0 @@
package format
import "strings"
const (
UnknownFormatOption Option = "UnknownFormatOption"
JSONOption Option = "json"
TextOption Option = "text"
TableOption Option = "table"
CycloneDxXMLOption Option = "cyclonedx"
CycloneDxJSONOption Option = "cyclonedx-json"
SPDXTagValueOption Option = "spdx-tag-value"
SPDXJSONOption Option = "spdx-json"
)
var AllOptions = []Option{
JSONOption,
TextOption,
TableOption,
CycloneDxXMLOption,
CycloneDxJSONOption,
SPDXTagValueOption,
SPDXJSONOption,
}
type Option string
func ParseOption(userStr string) Option {
switch strings.ToLower(userStr) {
case string(JSONOption):
return JSONOption
case string(TextOption):
return TextOption
case string(TableOption):
return TableOption
case string(CycloneDxXMLOption), "cyclone", "cyclone-dx", "cyclone-dx-xml", "cyclone-xml":
// NOTE(jonasagx): setting "cyclone" to XML by default for retro-compatibility.
// If we want to show no preference between XML and JSON please remove it.
return CycloneDxXMLOption
case string(CycloneDxJSONOption), "cyclone-json", "cyclone-dx-json":
return CycloneDxJSONOption
case string(SPDXTagValueOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv", "spdxtv":
return SPDXTagValueOption
case string(SPDXJSONOption), "spdxjson":
return SPDXJSONOption
default:
return UnknownFormatOption
}
}

View file

@ -1,12 +0,0 @@
package format
import "io"
// Validator reads the SBOM from the given reader and assesses whether the document conforms to the specific SBOM format.
// The validator should positively confirm if the SBOM is not only the format but also has the minimal set of values
// that the format requires. For example, all syftjson formatted documents have a schema section which should have
// "anchore/syft" within the version --if this isn't found then the validator should raise an error. These active
// assertions protect against "simple" format decoding validations that may lead to false positives (e.g. I decoded
// json successfully therefore this must be the target format, however, all values are their default zero-value and
// really represent a different format that also uses json)
type Validator func(reader io.Reader) error

100
syft/formats.go Normal file
View file

@ -0,0 +1,100 @@
package syft
import (
"bytes"
"strings"
"github.com/anchore/syft/internal/formats/cyclonedx13json"
"github.com/anchore/syft/internal/formats/cyclonedx13xml"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/internal/formats/text"
"github.com/anchore/syft/syft/sbom"
)
// these have been exported for the benefit of API users
const (
JSONFormatID = syftjson.ID
TextFormatID = text.ID
TableFormatID = table.ID
CycloneDxXMLFormatID = cyclonedx13xml.ID
CycloneDxJSONFormatID = cyclonedx13json.ID
SPDXTagValueFormatID = spdx22tagvalue.ID
SPDXJSONFormatID = spdx22json.ID
)
var formats []sbom.Format
func init() {
formats = []sbom.Format{
syftjson.Format(),
cyclonedx13xml.Format(),
cyclonedx13json.Format(),
spdx22tagvalue.Format(),
spdx22json.Format(),
table.Format(),
text.Format(),
}
}
func FormatIDs() (ids []sbom.FormatID) {
for _, f := range formats {
ids = append(ids, f.ID())
}
return ids
}
func FormatByID(id sbom.FormatID) sbom.Format {
for _, f := range formats {
if f.ID() == id {
return f
}
}
return nil
}
func FormatByName(name string) sbom.Format {
cleanName := cleanFormatName(name)
for _, f := range formats {
if cleanFormatName(string(f.ID())) == cleanName {
return f
}
}
// handle any aliases for any supported format
switch cleanName {
case "json", "syftjson":
return FormatByID(syftjson.ID)
case "cyclonedx", "cyclone", "cyclonedxxml":
return FormatByID(cyclonedx13xml.ID)
case "cyclonedxjson":
return FormatByID(cyclonedx13json.ID)
case "spdx", "spdxtv", "spdxtagvalue":
return FormatByID(spdx22tagvalue.ID)
case "spdxjson":
return FormatByID(spdx22json.ID)
case "table":
return FormatByID(table.ID)
case "text":
return FormatByID(text.ID)
}
return nil
}
func cleanFormatName(name string) string {
r := strings.NewReplacer("-", "", "_", "")
return strings.ToLower(r.Replace(name))
}
func IdentifyFormat(by []byte) sbom.Format {
for _, f := range formats {
if err := f.Validate(bytes.NewReader(by)); err != nil {
continue
}
return f
}
return nil
}

157
syft/formats_test.go Normal file
View file

@ -0,0 +1,157 @@
package syft
import (
"github.com/anchore/syft/internal/formats/cyclonedx13json"
"github.com/anchore/syft/internal/formats/cyclonedx13xml"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/internal/formats/text"
"github.com/anchore/syft/syft/sbom"
"github.com/stretchr/testify/require"
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIdentify(t *testing.T) {
tests := []struct {
fixture string
expected sbom.FormatID
}{
{
fixture: "test-fixtures/alpine-syft.json",
expected: syftjson.ID,
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
f, err := os.Open(test.fixture)
assert.NoError(t, err)
by, err := io.ReadAll(f)
assert.NoError(t, err)
frmt := IdentifyFormat(by)
assert.NotNil(t, frmt)
assert.Equal(t, test.expected, frmt.ID())
})
}
}
func TestFormatByName(t *testing.T) {
tests := []struct {
name string
want sbom.FormatID
}{
// SPDX Tag-Value
{
name: "spdx",
want: spdx22tagvalue.ID,
},
{
name: "spdx-tag-value",
want: spdx22tagvalue.ID,
},
{
name: "spdx-tv",
want: spdx22tagvalue.ID,
},
{
name: "spdxtv", // clean variant
want: spdx22tagvalue.ID,
},
{
name: "spdx-2-tag-value", // clean variant
want: spdx22tagvalue.ID,
},
{
name: "spdx-2-tagvalue", // clean variant
want: spdx22tagvalue.ID,
},
{
name: "spdx2-tagvalue", // clean variant
want: spdx22tagvalue.ID,
},
// SPDX JSON
{
name: "spdx-json",
want: spdx22json.ID,
},
{
name: "spdx-2-json",
want: spdx22json.ID,
},
// Cyclonedx JSON
{
name: "cyclonedx-json",
want: cyclonedx13json.ID,
},
{
name: "cyclonedx-1-json",
want: cyclonedx13json.ID,
},
// Cyclonedx XML
{
name: "cyclonedx",
want: cyclonedx13xml.ID,
},
{
name: "cyclonedx-xml",
want: cyclonedx13xml.ID,
},
{
name: "cyclonedx-1-xml",
want: cyclonedx13xml.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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := FormatByName(tt.name)
if tt.want == "" {
require.Nil(t, f)
return
}
require.NotNil(t, f)
assert.Equal(t, tt.want, f.ID())
})
}
}

View file

@ -14,7 +14,7 @@ import (
// integrity check // integrity check
var _ common.ParserFn = parseGemFileLockEntries var _ common.ParserFn = parseGemFileLockEntries
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"}) var sectionsOfInterest = internal.NewStringSet("GEM")
// parseGemFileLockEntries is a parser function for Gemfile.lock contents, returning all Gems discovered. // parseGemFileLockEntries is a parser function for Gemfile.lock contents, returning all Gems discovered.
func parseGemFileLockEntries(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) { func parseGemFileLockEntries(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {

78
syft/sbom/format.go Normal file
View file

@ -0,0 +1,78 @@
package sbom
import (
"errors"
"io"
)
var (
ErrEncodingNotSupported = errors.New("encoding not supported")
ErrDecodingNotSupported = errors.New("decoding not supported")
ErrValidationNotSupported = errors.New("validation not supported")
)
type FormatID string
type Format interface {
ID() FormatID
Encode(io.Writer, SBOM) error
Decode(io.Reader) (*SBOM, error)
Validate(io.Reader) error
}
type format struct {
id FormatID
encoder Encoder
decoder Decoder
validator Validator
}
// 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)
// Encoder is a function that can transform Syft native objects into an SBOM document of a specific format written to the given writer.
type Encoder func(io.Writer, SBOM) error
// Validator reads the SBOM from the given reader and assesses whether the document conforms to the specific SBOM format.
// The validator should positively confirm if the SBOM is not only the format but also has the minimal set of values
// that the format requires. For example, all syftjson formatted documents have a schema section which should have
// "anchore/syft" within the version --if this isn't found then the validator should raise an error. These active
// assertions protect against "simple" format decoding validations that may lead to false positives (e.g. I decoded
// json successfully therefore this must be the target format, however, all values are their default zero-value and
// 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,
encoder: encoder,
decoder: decoder,
validator: validator,
}
}
func (f format) ID() FormatID {
return f.id
}
func (f format) Encode(output io.Writer, s SBOM) error {
if f.encoder == nil {
return ErrEncodingNotSupported
}
return f.encoder(output, s)
}
func (f format) Decode(reader io.Reader) (*SBOM, error) {
if f.decoder == nil {
return nil, ErrDecodingNotSupported
}
return f.decoder(reader)
}
func (f format) Validate(reader io.Reader) error {
if f.validator == nil {
return ErrValidationNotSupported
}
return f.validator(reader)
}

View file

@ -1,71 +1,28 @@
package output package sbom
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"path" "path"
"github.com/anchore/syft/syft/format" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/sbom"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
) )
// streamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
type streamWriter struct {
format format.Format
out io.Writer
close func() error
}
// Write the provided SBOM to the data stream
func (w *streamWriter) Write(s sbom.SBOM) error {
return w.format.Encode(w.out, s)
}
// Close any resources, such as open files
func (w *streamWriter) Close() error {
if w.close != nil {
return w.close()
}
return nil
}
// multiWriter holds a list of child sbom.Writers to apply all Write and Close operations to // multiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
type multiWriter struct { type multiWriter struct {
writers []sbom.Writer writers []Writer
}
// Write writes the SBOM to all writers
func (m *multiWriter) Write(s sbom.SBOM) (errs error) {
for _, w := range m.writers {
err := w.Write(s)
if err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
}
// Close closes all writers
func (m *multiWriter) Close() (errs error) {
for _, w := range m.writers {
err := w.Close()
if err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
} }
// WriterOption Format and path strings used to create sbom.Writer // WriterOption Format and path strings used to create sbom.Writer
type WriterOption struct { type WriterOption struct {
Format format.Format Format Format
Path string Path string
} }
// MakeWriter create all report writers from input options; if a file is not specified, os.Stdout is used // NewWriter create all report writers from input options; if a file is not specified, os.Stdout is used
func MakeWriter(options ...WriterOption) (_ sbom.Writer, errs error) { func NewWriter(options ...WriterOption) (Writer, error) {
if len(options) == 0 { if len(options) == 0 {
return nil, fmt.Errorf("no output options provided") return nil, fmt.Errorf("no output options provided")
} }
@ -73,9 +30,9 @@ func MakeWriter(options ...WriterOption) (_ sbom.Writer, errs error) {
out := &multiWriter{} out := &multiWriter{}
defer func() { defer func() {
if errs != nil { // close any previously opened files; we can't really recover from any errors
// close any previously opened files; we can't really recover from any errors if err := out.Close(); err != nil {
_ = out.Close() log.Warnf("unable to close sbom writers: %+v", err)
} }
}() }()
@ -114,3 +71,25 @@ func MakeWriter(options ...WriterOption) (_ sbom.Writer, errs error) {
return out, nil return out, nil
} }
// Write writes the SBOM to all writers
func (m *multiWriter) Write(s SBOM) (errs error) {
for _, w := range m.writers {
err := w.Write(s)
if err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
}
// Close closes all writers
func (m *multiWriter) Close() (errs error) {
for _, w := range m.writers {
err := w.Close()
if err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
}

View file

@ -1,17 +1,21 @@
package output package sbom
import ( import (
"io"
"strings" "strings"
"testing" "testing"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/internal/formats/text"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func dummyEncoder(io.Writer, SBOM) error {
return nil
}
func dummyFormat(name string) Format {
return NewFormat(FormatID(name), dummyEncoder, nil, nil)
}
type writerConfig struct { type writerConfig struct {
format string format string
file string file string
@ -23,7 +27,7 @@ func TestOutputWriter(t *testing.T) {
testName := func(options []WriterOption, err bool) string { testName := func(options []WriterOption, err bool) string {
var out []string var out []string
for _, opt := range options { for _, opt := range options {
out = append(out, string(opt.Format.Option)+"="+opt.Path) out = append(out, string(opt.Format.ID())+"="+opt.Path)
} }
errs := "" errs := ""
if err { if err {
@ -44,7 +48,7 @@ func TestOutputWriter(t *testing.T) {
{ {
outputs: []WriterOption{ outputs: []WriterOption{
{ {
Format: table.Format(), Format: dummyFormat("table"),
Path: "", Path: "",
}, },
}, },
@ -57,7 +61,7 @@ func TestOutputWriter(t *testing.T) {
{ {
outputs: []WriterOption{ outputs: []WriterOption{
{ {
Format: syftjson.Format(), Format: dummyFormat("json"),
}, },
}, },
expected: []writerConfig{ expected: []writerConfig{
@ -69,7 +73,7 @@ func TestOutputWriter(t *testing.T) {
{ {
outputs: []WriterOption{ outputs: []WriterOption{
{ {
Format: syftjson.Format(), Format: dummyFormat("json"),
Path: "test-2.json", Path: "test-2.json",
}, },
}, },
@ -83,11 +87,11 @@ func TestOutputWriter(t *testing.T) {
{ {
outputs: []WriterOption{ outputs: []WriterOption{
{ {
Format: syftjson.Format(), Format: dummyFormat("json"),
Path: "test-3/1.json", Path: "test-3/1.json",
}, },
{ {
Format: spdx22json.Format(), Format: dummyFormat("spdx-json"),
Path: "test-3/2.json", Path: "test-3/2.json",
}, },
}, },
@ -105,10 +109,10 @@ func TestOutputWriter(t *testing.T) {
{ {
outputs: []WriterOption{ outputs: []WriterOption{
{ {
Format: text.Format(), Format: dummyFormat("text"),
}, },
{ {
Format: spdx22json.Format(), Format: dummyFormat("spdx-json"),
Path: "test-4.json", Path: "test-4.json",
}, },
}, },
@ -133,7 +137,7 @@ func TestOutputWriter(t *testing.T) {
} }
} }
writer, err := MakeWriter(outputs...) writer, err := NewWriter(outputs...)
if test.err { if test.err {
assert.Error(t, err) assert.Error(t, err)
@ -149,7 +153,7 @@ func TestOutputWriter(t *testing.T) {
for i, e := range test.expected { for i, e := range test.expected {
w := mw.writers[i].(*streamWriter) w := mw.writers[i].(*streamWriter)
assert.Equal(t, string(w.format.Option), e.format) assert.Equal(t, string(w.format.ID()), e.format)
if e.file != "" { if e.file != "" {
assert.FileExists(t, tmp+e.file) assert.FileExists(t, tmp+e.file)

View file

@ -0,0 +1,25 @@
package sbom
import (
"io"
)
// streamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
type streamWriter struct {
format Format
out io.Writer
close func() error
}
// Write the provided SBOM to the data stream
func (w *streamWriter) Write(s SBOM) error {
return w.format.Encode(w.out, s)
}
// Close any resources, such as open files
func (w *streamWriter) Close() error {
if w.close != nil {
return w.close()
}
return nil
}

View file

@ -2,10 +2,11 @@ package cli
import ( import (
"fmt" "fmt"
"github.com/stretchr/testify/require"
"strings" "strings"
"testing" "testing"
"github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft"
) )
func TestAllFormatsExpressible(t *testing.T) { func TestAllFormatsExpressible(t *testing.T) {
@ -18,8 +19,9 @@ func TestAllFormatsExpressible(t *testing.T) {
}, },
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
} }
formats := syft.FormatIDs()
for _, o := range format.AllOptions { require.NotEmpty(t, formats)
for _, o := range formats {
t.Run(fmt.Sprintf("format:%s", o), func(t *testing.T) { t.Run(fmt.Sprintf("format:%s", o), func(t *testing.T) {
cmd, stdout, stderr := runSyft(t, nil, "dir:./test-fixtures/image-pkg-coverage", "-o", string(o)) cmd, stdout, stderr := runSyft(t, nil, "dir:./test-fixtures/image-pkg-coverage", "-o", string(o))
for _, traitFn := range commonAssertions { for _, traitFn := range commonAssertions {

View file

@ -122,8 +122,6 @@ func TestLogFile(t *testing.T) {
request := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage") request := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage")
envLogFile := filepath.Join(os.TempDir(), "a-pretty-log-file.log") envLogFile := filepath.Join(os.TempDir(), "a-pretty-log-file.log")
t.Logf("log file path: %s", envLogFile)
tests := []struct { tests := []struct {
name string name string
args []string args []string

View file

@ -2,6 +2,11 @@ package integration
import ( import (
"bytes" "bytes"
"github.com/anchore/syft/internal/formats/cyclonedx13json"
"github.com/anchore/syft/internal/formats/cyclonedx13xml"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/syft/sbom"
"github.com/stretchr/testify/require"
"regexp" "regexp"
"testing" "testing"
@ -9,7 +14,6 @@ import (
"github.com/sergi/go-diff/diffmatchpatch" "github.com/sergi/go-diff/diffmatchpatch"
"github.com/anchore/syft/syft/format"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -21,16 +25,16 @@ import (
// encode-decode-encode loop which will detect lossy behavior in both directions. // encode-decode-encode loop which will detect lossy behavior in both directions.
func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
tests := []struct { tests := []struct {
format format.Option formatOption sbom.FormatID
redactor func(in []byte) []byte redactor func(in []byte) []byte
json bool json bool
}{ }{
{ {
format: format.JSONOption, formatOption: syftjson.ID,
json: true, json: true,
}, },
{ {
format: format.CycloneDxJSONOption, formatOption: cyclonedx13json.ID,
redactor: func(in []byte) []byte { redactor: func(in []byte) []byte {
in = regexp.MustCompile("\"(timestamp|serialNumber|bom-ref)\": \"[^\"]+\",").ReplaceAll(in, []byte{}) in = regexp.MustCompile("\"(timestamp|serialNumber|bom-ref)\": \"[^\"]+\",").ReplaceAll(in, []byte{})
return in return in
@ -38,7 +42,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
json: true, json: true,
}, },
{ {
format: format.CycloneDxXMLOption, formatOption: cyclonedx13xml.ID,
redactor: func(in []byte) []byte { redactor: func(in []byte) []byte {
in = regexp.MustCompile("(serialNumber|bom-ref)=\"[^\"]+\"").ReplaceAll(in, []byte{}) in = regexp.MustCompile("(serialNumber|bom-ref)=\"[^\"]+\"").ReplaceAll(in, []byte{})
in = regexp.MustCompile("<timestamp>[^<]+</timestamp>").ReplaceAll(in, []byte{}) in = regexp.MustCompile("<timestamp>[^<]+</timestamp>").ReplaceAll(in, []byte{})
@ -47,18 +51,21 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(string(test.format), func(t *testing.T) { t.Run(string(test.formatOption), func(t *testing.T) {
originalSBOM, _ := catalogFixtureImage(t, "image-pkg-coverage") originalSBOM, _ := catalogFixtureImage(t, "image-pkg-coverage")
by1, err := syft.Encode(originalSBOM, test.format) format := syft.FormatByID(test.formatOption)
require.NotNil(t, format)
by1, err := syft.Encode(originalSBOM, format)
assert.NoError(t, err) assert.NoError(t, err)
newSBOM, newFormat, err := syft.Decode(bytes.NewReader(by1)) newSBOM, newFormat, err := syft.Decode(bytes.NewReader(by1))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, test.format, newFormat) assert.Equal(t, format.ID(), newFormat.ID())
by2, err := syft.Encode(*newSBOM, test.format) by2, err := syft.Encode(*newSBOM, format)
assert.NoError(t, err) assert.NoError(t, err)
if test.redactor != nil { if test.redactor != nil {