mirror of
https://github.com/anchore/syft
synced 2024-11-13 23:57:07 +00:00
Migrate format definitions to sbom package (#864)
This commit is contained in:
parent
640099ce2e
commit
4af32c5bee
36 changed files with 580 additions and 401 deletions
|
@ -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
|
||||||
|
case cyclonedx13json.ID:
|
||||||
// Tentative see https://github.com/in-toto/attestation/issues/82
|
// Tentative see https://github.com/in-toto/attestation/issues/82
|
||||||
case format.CycloneDxJSONOption:
|
|
||||||
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
30
cmd/format_aliases.go
Normal 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
|
||||||
|
}
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
100
syft/formats.go
Normal 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
157
syft/formats_test.go
Normal 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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
78
syft/sbom/format.go
Normal 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)
|
||||||
|
}
|
|
@ -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
|
||||||
_ = out.Close()
|
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
|
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
|
||||||
|
}
|
|
@ -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)
|
25
syft/sbom/stream_writer.go
Normal file
25
syft/sbom/stream_writer.go
Normal 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
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue