mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +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"
|
||||
"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...)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
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"
|
||||
"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 <format>=<file>
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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) {
|
||||
|
|
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 (
|
||||
"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
|
||||
}
|
|
@ -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)
|
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 (
|
||||
"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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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("<timestamp>[^<]+</timestamp>").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 {
|
||||
|
|
Loading…
Reference in a new issue