From 4af32c5beee1bac8960ca7488afc24f19b341a63 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 4 Mar 2022 17:22:40 -0500 Subject: [PATCH] Migrate format definitions to sbom package (#864) --- cmd/attest.go | 40 +++-- cmd/format_aliases.go | 30 ++++ cmd/output_writer.go | 31 ++-- cmd/packages.go | 9 +- cmd/power_user.go | 3 +- cmd/version.go | 8 +- .../common/cyclonedxhelpers/decoder.go | 5 +- internal/formats/common/testutils/utils.go | 5 +- internal/formats/cyclonedx13json/format.go | 10 +- internal/formats/cyclonedx13xml/format.go | 10 +- internal/formats/formats.go | 46 ----- internal/formats/formats_test.go | 34 ---- internal/formats/spdx22json/format.go | 12 +- internal/formats/spdx22tagvalue/format.go | 12 +- .../formats/spdx22tagvalue/to_format_model.go | 2 +- internal/formats/syftjson/format.go | 12 +- internal/formats/table/format.go | 12 +- internal/formats/text/format.go | 12 +- internal/stringset.go | 7 +- syft/encode_decode.go | 24 +-- syft/format/decoder.go | 10 -- syft/format/encoder.go | 10 -- syft/format/format.go | 52 ------ syft/format/option.go | 49 ------ syft/format/validator.go | 12 -- syft/formats.go | 100 +++++++++++ syft/formats_test.go | 157 ++++++++++++++++++ syft/pkg/cataloger/ruby/parse_gemfile_lock.go | 2 +- syft/sbom/format.go | 78 +++++++++ .../writer.go => syft/sbom/multi_writer.go | 85 ++++------ .../sbom/multi_writer_test.go | 36 ++-- syft/sbom/stream_writer.go | 25 +++ .../test-fixtures/alpine-syft.json | 0 test/cli/all_formats_expressible_test.go | 8 +- test/cli/root_cmd_test.go | 2 - test/integration/encode_decode_cycle_test.go | 31 ++-- 36 files changed, 580 insertions(+), 401 deletions(-) create mode 100644 cmd/format_aliases.go delete mode 100644 internal/formats/formats.go delete mode 100644 internal/formats/formats_test.go delete mode 100644 syft/format/decoder.go delete mode 100644 syft/format/encoder.go delete mode 100644 syft/format/format.go delete mode 100644 syft/format/option.go delete mode 100644 syft/format/validator.go create mode 100644 syft/formats.go create mode 100644 syft/formats_test.go create mode 100644 syft/sbom/format.go rename internal/output/writer.go => syft/sbom/multi_writer.go (63%) rename internal/output/writer_test.go => syft/sbom/multi_writer_test.go (79%) create mode 100644 syft/sbom/stream_writer.go rename {internal/formats => syft}/test-fixtures/alpine-syft.json (100%) diff --git a/cmd/attest.go b/cmd/attest.go index 9be6b1d5c..1a7b430aa 100644 --- a/cmd/attest.go +++ b/cmd/attest.go @@ -9,6 +9,10 @@ import ( "os" "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/pkg/image" "github.com/anchore/syft/internal" @@ -17,7 +21,7 @@ import ( "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/syft" "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/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" @@ -49,7 +53,11 @@ const ( 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 ( 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") } - output := format.ParseOption(appConfig.Output[0]) - predicateType := assertPredicateType(output) + format := syft.FormatByName(appConfig.Output[0]) + predicateType := formatPredicateType(format) 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) @@ -172,7 +180,7 @@ func attestExec(ctx context.Context, _ *cobra.Command, args []string) error { defer sv.Close() return eventLoop( - attestationExecWorker(*si, output, predicateType, sv), + attestationExecWorker(*si, format, predicateType, sv), setupSignals(), eventSubscription, 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) go func() { defer close(errs) @@ -200,7 +208,7 @@ func attestationExecWorker(sourceInput source.Input, output format.Option, predi return } - sbomBytes, err := syft.Encode(*s, output) + sbomBytes, err := syft.Encode(*s, format) if err != nil { errs <- err return @@ -215,14 +223,14 @@ func attestationExecWorker(sourceInput source.Input, output format.Option, predi return errs } -func assertPredicateType(output format.Option) string { - switch output { - case format.SPDXJSONOption: +func formatPredicateType(format sbom.Format) string { + switch format.ID() { + case spdx22json.ID: return in_toto.PredicateSPDX - // Tentative see https://github.com/in-toto/attestation/issues/82 - case format.CycloneDxJSONOption: + case cyclonedx13json.ID: + // Tentative see https://github.com/in-toto/attestation/issues/82 return "https://cyclonedx.org/bom" - case format.JSONOption: + case syftjson.ID: return "https://syft.dev/bom" default: 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 flags.StringP( - "output", "o", string(format.JSONOption), - fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", attestFormats), + "output", "o", formatAliases(syftjson.ID)[0], + fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", formatAliases(attestFormats...)), ) } diff --git a/cmd/format_aliases.go b/cmd/format_aliases.go new file mode 100644 index 000000000..558d13f24 --- /dev/null +++ b/cmd/format_aliases.go @@ -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 +} diff --git a/cmd/output_writer.go b/cmd/output_writer.go index c444cb236..1e0628f45 100644 --- a/cmd/output_writer.go +++ b/cmd/output_writer.go @@ -4,9 +4,10 @@ import ( "fmt" "strings" - "github.com/anchore/syft/internal/formats" - "github.com/anchore/syft/internal/output" - "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/internal/formats/table" + + "github.com/anchore/syft/syft" + "github.com/anchore/syft/syft/sbom" "github.com/hashicorp/go-multierror" ) @@ -19,7 +20,7 @@ func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) { return nil, err } - writer, err := output.MakeWriter(outputOptions...) + writer, err := sbom.NewWriter(outputOptions...) if err != nil { 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 -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 if len(outputs) == 0 { - outputs = append(outputs, string(format.TableOption)) + outputs = append(outputs, string(table.ID)) } for _, name := range outputs { @@ -40,31 +41,25 @@ func parseOptions(outputs []string, defaultFile string) (out []output.WriterOpti // split to at most two parts for = parts := strings.SplitN(name, "=", 2) - // the format option is the first part + // the format name is the first part name = parts[0] // default to the --file or empty string if not specified 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 { file = parts[1] } - option := format.ParseOption(name) - if option == format.UnknownFormatOption { + format := syft.FormatByName(name) + if format == nil { errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name)) continue } - encoder := formats.ByOption(option) - if encoder == nil { - errs = multierror.Append(errs, fmt.Errorf("unknown format: %s", outputFormat)) - continue - } - - out = append(out, output.WriterOption{ - Format: *encoder, + out = append(out, sbom.WriterOption{ + Format: format, Path: file, }) } diff --git a/cmd/packages.go b/cmd/packages.go index d79ac4081..b14331833 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -6,6 +6,10 @@ import ( "io/ioutil" "os" + "github.com/anchore/syft/internal/formats/table" + + "github.com/anchore/syft/syft" + "github.com/anchore/stereoscope" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/anchore" @@ -15,7 +19,6 @@ import ( "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/event" - "github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/pkg/cataloger" "github.com/anchore/syft/syft/sbom" "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)) flags.StringArrayP( - "output", "o", []string{string(format.TableOption)}, - fmt.Sprintf("report output format, options=%v", format.AllOptions), + "output", "o", formatAliases(table.ID), + fmt.Sprintf("report output format, options=%v", formatAliases(syft.FormatIDs()...)), ) flags.StringP( diff --git a/cmd/power_user.go b/cmd/power_user.go index e5c7c0041..59c52b262 100644 --- a/cmd/power_user.go +++ b/cmd/power_user.go @@ -9,7 +9,6 @@ import ( "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/formats/syftjson" "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/internal/output" "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/version" "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 userInput := args[0] - writer, err := output.MakeWriter(output.WriterOption{ + writer, err := sbom.NewWriter(sbom.WriterOption{ Format: syftjson.Format(), Path: appConfig.File, }) diff --git a/cmd/version.go b/cmd/version.go index 020aef889..af312c1c6 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -var outputFormat string +var versionCmdOutputFormat string var versionCmd = &cobra.Command{ Use: "version", @@ -20,14 +20,14 @@ var versionCmd = &cobra.Command{ } 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) } func printVersion(_ *cobra.Command, _ []string) { versionInfo := version.FromBuild() - switch outputFormat { + switch versionCmdOutputFormat { case "text": fmt.Println("Application: ", internal.ApplicationName) fmt.Println("Version: ", versionInfo.Version) @@ -54,7 +54,7 @@ func printVersion(_ *cobra.Command, _ []string) { os.Exit(1) } default: - fmt.Printf("unsupported output format: %s\n", outputFormat) + fmt.Printf("unsupported output format: %s\n", versionCmdOutputFormat) os.Exit(1) } } diff --git a/internal/formats/common/cyclonedxhelpers/decoder.go b/internal/formats/common/cyclonedxhelpers/decoder.go index b45187aca..2c2a0e684 100644 --- a/internal/formats/common/cyclonedxhelpers/decoder.go +++ b/internal/formats/common/cyclonedxhelpers/decoder.go @@ -6,14 +6,13 @@ import ( "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "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 { bom := &cyclonedx.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) { bom := &cyclonedx.BOM{} err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom) diff --git a/internal/formats/common/testutils/utils.go b/internal/formats/common/testutils/utils.go index ba56d0a82..6b5b5f670 100644 --- a/internal/formats/common/testutils/utils.go +++ b/internal/formats/common/testutils/utils.go @@ -9,7 +9,6 @@ import ( "github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/imagetest" - "github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "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 // 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 err := format.Encode(&buffer, sbom) diff --git a/internal/formats/cyclonedx13json/format.go b/internal/formats/cyclonedx13json/format.go index 0f3aa494c..d33c703dc 100644 --- a/internal/formats/cyclonedx13json/format.go +++ b/internal/formats/cyclonedx13json/format.go @@ -3,12 +3,14 @@ package cyclonedx13json import ( "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/internal/formats/common/cyclonedxhelpers" - "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft/sbom" ) -func Format() format.Format { - return format.NewFormat( - format.CycloneDxJSONOption, +const ID sbom.FormatID = "cyclonedx-1-json" + +func Format() sbom.Format { + return sbom.NewFormat( + ID, encoder, cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), diff --git a/internal/formats/cyclonedx13xml/format.go b/internal/formats/cyclonedx13xml/format.go index 9ba8d9401..24a8fe3dc 100644 --- a/internal/formats/cyclonedx13xml/format.go +++ b/internal/formats/cyclonedx13xml/format.go @@ -3,12 +3,14 @@ package cyclonedx13xml import ( "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/internal/formats/common/cyclonedxhelpers" - "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft/sbom" ) -func Format() format.Format { - return format.NewFormat( - format.CycloneDxXMLOption, +const ID sbom.FormatID = "cyclonedx-1-xml" + +func Format() sbom.Format { + return sbom.NewFormat( + ID, encoder, cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), diff --git a/internal/formats/formats.go b/internal/formats/formats.go deleted file mode 100644 index a964d4ac8..000000000 --- a/internal/formats/formats.go +++ /dev/null @@ -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 -} diff --git a/internal/formats/formats_test.go b/internal/formats/formats_test.go deleted file mode 100644 index 3df97558c..000000000 --- a/internal/formats/formats_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/internal/formats/spdx22json/format.go b/internal/formats/spdx22json/format.go index 468698ce4..ae4016910 100644 --- a/internal/formats/spdx22json/format.go +++ b/internal/formats/spdx22json/format.go @@ -1,11 +1,15 @@ 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 -func Format() format.Format { - return format.NewFormat( - format.SPDXJSONOption, +func Format() sbom.Format { + return sbom.NewFormat( + ID, encoder, decoder, validator, diff --git a/internal/formats/spdx22tagvalue/format.go b/internal/formats/spdx22tagvalue/format.go index 4e9a83bee..8bd5856c5 100644 --- a/internal/formats/spdx22tagvalue/format.go +++ b/internal/formats/spdx22tagvalue/format.go @@ -1,11 +1,15 @@ 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 -func Format() format.Format { - return format.NewFormat( - format.SPDXTagValueOption, +func Format() sbom.Format { + return sbom.NewFormat( + ID, encoder, decoder, validator, diff --git a/internal/formats/spdx22tagvalue/to_format_model.go b/internal/formats/spdx22tagvalue/to_format_model.go index ef17dc54a..a52aba13b 100644 --- a/internal/formats/spdx22tagvalue/to_format_model.go +++ b/internal/formats/spdx22tagvalue/to_format_model.go @@ -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" // 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; // NONE if the package contains no copyright information whatsoever; or diff --git a/internal/formats/syftjson/format.go b/internal/formats/syftjson/format.go index bc3f51642..c595699b7 100644 --- a/internal/formats/syftjson/format.go +++ b/internal/formats/syftjson/format.go @@ -1,10 +1,14 @@ package syftjson -import "github.com/anchore/syft/syft/format" +import ( + "github.com/anchore/syft/syft/sbom" +) -func Format() format.Format { - return format.NewFormat( - format.JSONOption, +const ID sbom.FormatID = "syft-3-json" + +func Format() sbom.Format { + return sbom.NewFormat( + ID, encoder, decoder, validator, diff --git a/internal/formats/table/format.go b/internal/formats/table/format.go index 750b13031..66e60e0ec 100644 --- a/internal/formats/table/format.go +++ b/internal/formats/table/format.go @@ -1,10 +1,14 @@ package table -import "github.com/anchore/syft/syft/format" +import ( + "github.com/anchore/syft/syft/sbom" +) -func Format() format.Format { - return format.NewFormat( - format.TableOption, +const ID sbom.FormatID = "syft-table" + +func Format() sbom.Format { + return sbom.NewFormat( + ID, encoder, nil, nil, diff --git a/internal/formats/text/format.go b/internal/formats/text/format.go index a33dab291..e8e5e98b5 100644 --- a/internal/formats/text/format.go +++ b/internal/formats/text/format.go @@ -1,10 +1,14 @@ package text -import "github.com/anchore/syft/syft/format" +import ( + "github.com/anchore/syft/syft/sbom" +) -func Format() format.Format { - return format.NewFormat( - format.TextOption, +const ID sbom.FormatID = "syft-text" + +func Format() sbom.Format { + return sbom.NewFormat( + ID, encoder, nil, nil, diff --git a/internal/stringset.go b/internal/stringset.go index 2bd68a4b1..536e0a8a7 100644 --- a/internal/stringset.go +++ b/internal/stringset.go @@ -6,12 +6,7 @@ import "sort" type StringSet map[string]struct{} // NewStringSet creates a new empty StringSet. -func NewStringSet() StringSet { - return make(StringSet) -} - -// NewStringSetFromSlice creates a StringSet populated with values from the given slice. -func NewStringSetFromSlice(start []string) StringSet { +func NewStringSet(start ...string) StringSet { ret := make(StringSet) for _, s := range start { ret.Add(s) diff --git a/syft/encode_decode.go b/syft/encode_decode.go index 80314bee4..0b54a5d56 100644 --- a/syft/encode_decode.go +++ b/syft/encode_decode.go @@ -6,18 +6,10 @@ import ( "io" "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. -func Encode(s sbom.SBOM, option format.Option) ([]byte, error) { - f := formats.ByOption(option) - if f == nil { - return nil, fmt.Errorf("unsupported format: %+v", option) - } - +func Encode(s sbom.SBOM, f sbom.Format) ([]byte, error) { buff := bytes.Buffer{} 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. -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) 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) - if err != nil { - return nil, format.UnknownFormatOption, fmt.Errorf("unable to detect format: %w", err) - } + f := IdentifyFormat(by) 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)) - return s, f.Option, err + return s, f, err } diff --git a/syft/format/decoder.go b/syft/format/decoder.go deleted file mode 100644 index bdeb0afbf..000000000 --- a/syft/format/decoder.go +++ /dev/null @@ -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) diff --git a/syft/format/encoder.go b/syft/format/encoder.go deleted file mode 100644 index 9c1c874de..000000000 --- a/syft/format/encoder.go +++ /dev/null @@ -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 diff --git a/syft/format/format.go b/syft/format/format.go deleted file mode 100644 index cc66b0632..000000000 --- a/syft/format/format.go +++ /dev/null @@ -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) -} diff --git a/syft/format/option.go b/syft/format/option.go deleted file mode 100644 index 8d85b1386..000000000 --- a/syft/format/option.go +++ /dev/null @@ -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 - } -} diff --git a/syft/format/validator.go b/syft/format/validator.go deleted file mode 100644 index 6559186b0..000000000 --- a/syft/format/validator.go +++ /dev/null @@ -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 diff --git a/syft/formats.go b/syft/formats.go new file mode 100644 index 000000000..b425ef68a --- /dev/null +++ b/syft/formats.go @@ -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 +} diff --git a/syft/formats_test.go b/syft/formats_test.go new file mode 100644 index 000000000..76cf52586 --- /dev/null +++ b/syft/formats_test.go @@ -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()) + }) + } +} diff --git a/syft/pkg/cataloger/ruby/parse_gemfile_lock.go b/syft/pkg/cataloger/ruby/parse_gemfile_lock.go index 6cf96676d..6bf0e6334 100644 --- a/syft/pkg/cataloger/ruby/parse_gemfile_lock.go +++ b/syft/pkg/cataloger/ruby/parse_gemfile_lock.go @@ -14,7 +14,7 @@ import ( // integrity check 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. func parseGemFileLockEntries(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) { diff --git a/syft/sbom/format.go b/syft/sbom/format.go new file mode 100644 index 000000000..abdf7b971 --- /dev/null +++ b/syft/sbom/format.go @@ -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) +} diff --git a/internal/output/writer.go b/syft/sbom/multi_writer.go similarity index 63% rename from internal/output/writer.go rename to syft/sbom/multi_writer.go index a41f0745e..da379d9f6 100644 --- a/internal/output/writer.go +++ b/syft/sbom/multi_writer.go @@ -1,71 +1,28 @@ -package output +package sbom import ( "fmt" - "io" "os" "path" - "github.com/anchore/syft/syft/format" - "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/internal/log" + "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 type multiWriter struct { - writers []sbom.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 + writers []Writer } // WriterOption Format and path strings used to create sbom.Writer type WriterOption struct { - Format format.Format + Format Format Path string } -// MakeWriter 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) { +// NewWriter create all report writers from input options; if a file is not specified, os.Stdout is used +func NewWriter(options ...WriterOption) (Writer, error) { if len(options) == 0 { return nil, fmt.Errorf("no output options provided") } @@ -73,9 +30,9 @@ func MakeWriter(options ...WriterOption) (_ sbom.Writer, errs error) { out := &multiWriter{} defer func() { - if errs != nil { - // close any previously opened files; we can't really recover from any errors - _ = out.Close() + // close any previously opened files; we can't really recover from any errors + if err := out.Close(); err != nil { + log.Warnf("unable to close sbom writers: %+v", err) } }() @@ -114,3 +71,25 @@ func MakeWriter(options ...WriterOption) (_ sbom.Writer, errs error) { 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 +} diff --git a/internal/output/writer_test.go b/syft/sbom/multi_writer_test.go similarity index 79% rename from internal/output/writer_test.go rename to syft/sbom/multi_writer_test.go index 8bc9d285f..d9c46cc14 100644 --- a/internal/output/writer_test.go +++ b/syft/sbom/multi_writer_test.go @@ -1,17 +1,21 @@ -package output +package sbom import ( + "io" "strings" "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" ) +func dummyEncoder(io.Writer, SBOM) error { + return nil +} + +func dummyFormat(name string) Format { + return NewFormat(FormatID(name), dummyEncoder, nil, nil) +} + type writerConfig struct { format string file string @@ -23,7 +27,7 @@ func TestOutputWriter(t *testing.T) { testName := func(options []WriterOption, err bool) string { var out []string for _, opt := range options { - out = append(out, string(opt.Format.Option)+"="+opt.Path) + out = append(out, string(opt.Format.ID())+"="+opt.Path) } errs := "" if err { @@ -44,7 +48,7 @@ func TestOutputWriter(t *testing.T) { { outputs: []WriterOption{ { - Format: table.Format(), + Format: dummyFormat("table"), Path: "", }, }, @@ -57,7 +61,7 @@ func TestOutputWriter(t *testing.T) { { outputs: []WriterOption{ { - Format: syftjson.Format(), + Format: dummyFormat("json"), }, }, expected: []writerConfig{ @@ -69,7 +73,7 @@ func TestOutputWriter(t *testing.T) { { outputs: []WriterOption{ { - Format: syftjson.Format(), + Format: dummyFormat("json"), Path: "test-2.json", }, }, @@ -83,11 +87,11 @@ func TestOutputWriter(t *testing.T) { { outputs: []WriterOption{ { - Format: syftjson.Format(), + Format: dummyFormat("json"), Path: "test-3/1.json", }, { - Format: spdx22json.Format(), + Format: dummyFormat("spdx-json"), Path: "test-3/2.json", }, }, @@ -105,10 +109,10 @@ func TestOutputWriter(t *testing.T) { { outputs: []WriterOption{ { - Format: text.Format(), + Format: dummyFormat("text"), }, { - Format: spdx22json.Format(), + Format: dummyFormat("spdx-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 { assert.Error(t, err) @@ -149,7 +153,7 @@ func TestOutputWriter(t *testing.T) { for i, e := range test.expected { 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 != "" { assert.FileExists(t, tmp+e.file) diff --git a/syft/sbom/stream_writer.go b/syft/sbom/stream_writer.go new file mode 100644 index 000000000..a66b81fac --- /dev/null +++ b/syft/sbom/stream_writer.go @@ -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 +} diff --git a/internal/formats/test-fixtures/alpine-syft.json b/syft/test-fixtures/alpine-syft.json similarity index 100% rename from internal/formats/test-fixtures/alpine-syft.json rename to syft/test-fixtures/alpine-syft.json diff --git a/test/cli/all_formats_expressible_test.go b/test/cli/all_formats_expressible_test.go index 924c2a0cb..b769aaac5 100644 --- a/test/cli/all_formats_expressible_test.go +++ b/test/cli/all_formats_expressible_test.go @@ -2,10 +2,11 @@ package cli import ( "fmt" + "github.com/stretchr/testify/require" "strings" "testing" - "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft" ) func TestAllFormatsExpressible(t *testing.T) { @@ -18,8 +19,9 @@ func TestAllFormatsExpressible(t *testing.T) { }, assertSuccessfulReturnCode, } - - for _, o := range format.AllOptions { + formats := syft.FormatIDs() + require.NotEmpty(t, formats) + for _, o := range formats { t.Run(fmt.Sprintf("format:%s", o), func(t *testing.T) { cmd, stdout, stderr := runSyft(t, nil, "dir:./test-fixtures/image-pkg-coverage", "-o", string(o)) for _, traitFn := range commonAssertions { diff --git a/test/cli/root_cmd_test.go b/test/cli/root_cmd_test.go index 4efa26d66..468fc927c 100644 --- a/test/cli/root_cmd_test.go +++ b/test/cli/root_cmd_test.go @@ -122,8 +122,6 @@ func TestLogFile(t *testing.T) { request := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage") envLogFile := filepath.Join(os.TempDir(), "a-pretty-log-file.log") - t.Logf("log file path: %s", envLogFile) - tests := []struct { name string args []string diff --git a/test/integration/encode_decode_cycle_test.go b/test/integration/encode_decode_cycle_test.go index 56788cd4f..baffd1847 100644 --- a/test/integration/encode_decode_cycle_test.go +++ b/test/integration/encode_decode_cycle_test.go @@ -2,6 +2,11 @@ package integration import ( "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" "testing" @@ -9,7 +14,6 @@ import ( "github.com/sergi/go-diff/diffmatchpatch" - "github.com/anchore/syft/syft/format" "github.com/stretchr/testify/assert" ) @@ -21,16 +25,16 @@ import ( // encode-decode-encode loop which will detect lossy behavior in both directions. func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { tests := []struct { - format format.Option - redactor func(in []byte) []byte - json bool + formatOption sbom.FormatID + redactor func(in []byte) []byte + json bool }{ { - format: format.JSONOption, - json: true, + formatOption: syftjson.ID, + json: true, }, { - format: format.CycloneDxJSONOption, + formatOption: cyclonedx13json.ID, redactor: func(in []byte) []byte { in = regexp.MustCompile("\"(timestamp|serialNumber|bom-ref)\": \"[^\"]+\",").ReplaceAll(in, []byte{}) return in @@ -38,7 +42,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { json: true, }, { - format: format.CycloneDxXMLOption, + formatOption: cyclonedx13xml.ID, redactor: func(in []byte) []byte { in = regexp.MustCompile("(serialNumber|bom-ref)=\"[^\"]+\"").ReplaceAll(in, []byte{}) in = regexp.MustCompile("[^<]+").ReplaceAll(in, []byte{}) @@ -47,18 +51,21 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { }, } 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") - 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) newSBOM, newFormat, err := syft.Decode(bytes.NewReader(by1)) 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) if test.redactor != nil {