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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -92,18 +92,6 @@ func TestByName(t *testing.T) {
name: "spdxtv", // clean variant name: "spdxtv", // clean variant
want: spdxtagvalue.ID, want: spdxtagvalue.ID,
}, },
{
name: "spdx-2-tag-value", // clean variant
want: spdxtagvalue.ID,
},
{
name: "spdx-2-tagvalue", // clean variant
want: spdxtagvalue.ID,
},
{
name: "spdx2-tagvalue", // clean variant
want: spdxtagvalue.ID,
},
// SPDX JSON // SPDX JSON
{ {
@ -111,7 +99,7 @@ func TestByName(t *testing.T) {
want: spdxjson.ID, want: spdxjson.ID,
}, },
{ {
name: "spdx-2-json", name: "spdxjson", // clean variant
want: spdxjson.ID, want: spdxjson.ID,
}, },
@ -121,7 +109,7 @@ func TestByName(t *testing.T) {
want: cyclonedxjson.ID, want: cyclonedxjson.ID,
}, },
{ {
name: "cyclonedx-1-json", name: "cyclonedxjson", // clean variant
want: cyclonedxjson.ID, want: cyclonedxjson.ID,
}, },
@ -135,7 +123,7 @@ func TestByName(t *testing.T) {
want: cyclonedxxml.ID, want: cyclonedxxml.ID,
}, },
{ {
name: "cyclonedx-1-xml", name: "cyclonedxxml", // clean variant
want: cyclonedxxml.ID, want: cyclonedxxml.ID,
}, },
@ -144,7 +132,6 @@ func TestByName(t *testing.T) {
name: "table", name: "table",
want: table.ID, want: table.ID,
}, },
{ {
name: "syft-table", name: "syft-table",
want: table.ID, want: table.ID,
@ -155,7 +142,6 @@ func TestByName(t *testing.T) {
name: "text", name: "text",
want: text.ID, want: text.ID,
}, },
{ {
name: "syft-text", name: "syft-text",
want: text.ID, want: text.ID,
@ -166,23 +152,26 @@ func TestByName(t *testing.T) {
name: "json", name: "json",
want: syftjson.ID, want: syftjson.ID,
}, },
{ {
name: "syft-json", name: "syft-json",
want: syftjson.ID, want: syftjson.ID,
}, },
{
name: "syftjson", // clean variant
want: syftjson.ID,
},
// GitHub JSON // GitHub JSON
{ {
name: "github", name: "github",
want: github.ID, want: github.ID,
}, },
{ {
name: "github-json", name: "github-json",
want: github.ID, want: github.ID,
}, },
// Syft template
{ {
name: "template", name: "template",
want: template.ID, want: template.ID,
@ -200,3 +189,56 @@ func TestByName(t *testing.T) {
}) })
} }
} }
func Test_versionMatches(t *testing.T) {
tests := []struct {
name string
version string
match string
matches bool
}{
{
name: "any version matches number",
version: string(sbom.AnyVersion),
match: "6",
matches: true,
},
{
name: "number matches any version",
version: "6",
match: string(sbom.AnyVersion),
matches: true,
},
{
name: "same number matches",
version: "3",
match: "3",
matches: true,
},
{
name: "same major number matches",
version: "3.1",
match: "3",
matches: true,
},
{
name: "same minor number matches",
version: "3.1",
match: "3.1",
matches: true,
},
{
name: "different number does not match",
version: "3",
match: "4",
matches: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matches := versionMatches(test.version, test.match)
assert.Equal(t, test.matches, matches)
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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