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
26 changed files with 349 additions and 187 deletions
+2 -2
View File
@@ -30,12 +30,12 @@ const (
// TODO: deprecated, moved to syft/formats/formats.go. will be removed in v1.0.0
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
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
+3 -2
View File
@@ -7,13 +7,14 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "cyclonedx-1-json"
const ID sbom.FormatID = "cyclonedx-json"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
encoder,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
ID,
)
}
+3 -2
View File
@@ -7,13 +7,14 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "cyclonedx-1-xml"
const ID sbom.FormatID = "cyclonedx-xml"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
encoder,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
ID, "cyclonedx", "cyclone",
)
}
+56 -42
View File
@@ -5,8 +5,11 @@ import (
"errors"
"fmt"
"io"
"regexp"
"strings"
"golang.org/x/exp/slices"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/cyclonedxxml"
@@ -26,8 +29,11 @@ func Formats() []sbom.Format {
cyclonedxxml.Format(),
cyclonedxjson.Format(),
github.Format(),
spdxtagvalue.Format(),
spdxjson.Format(),
spdxtagvalue.Format2_1(),
spdxtagvalue.Format2_2(),
spdxtagvalue.Format2_3(),
spdxjson.Format2_2(),
spdxjson.Format2_3(),
table.Format(),
text.Format(),
template.Format(),
@@ -47,53 +53,51 @@ func Identify(by []byte) sbom.Format {
return nil
}
// ByName accepts a name@version string, such as:
//
// spdx-json@2.1 or cyclonedx@2
func ByName(name string) sbom.Format {
cleanName := cleanFormatName(name)
for _, f := range Formats() {
if cleanFormatName(string(f.ID())) == cleanName {
return f
}
parts := strings.SplitN(name, "@", 2)
version := sbom.AnyVersion
if len(parts) > 1 {
version = parts[1]
}
// 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
return ByNameAndVersion(parts[0], version)
}
func IDs() (ids []sbom.FormatID) {
func ByNameAndVersion(name string, version string) sbom.Format {
name = cleanFormatName(name)
var mostRecentFormat sbom.Format
for _, f := range Formats() {
ids = append(ids, f.ID())
}
return ids
}
func ByID(id sbom.FormatID) sbom.Format {
for _, f := range Formats() {
if f.ID() == id {
return f
for _, n := range f.IDs() {
if cleanFormatName(string(n)) == name && versionMatches(f.Version(), version) {
if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
mostRecentFormat = 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 {
@@ -127,3 +131,13 @@ func Decode(reader io.Reader) (*sbom.SBOM, sbom.Format, error) {
s, err := f.Decode(bytes.NewReader(by))
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
}
+61 -19
View File
@@ -92,18 +92,6 @@ func TestByName(t *testing.T) {
name: "spdxtv", // clean variant
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
{
@@ -111,7 +99,7 @@ func TestByName(t *testing.T) {
want: spdxjson.ID,
},
{
name: "spdx-2-json",
name: "spdxjson", // clean variant
want: spdxjson.ID,
},
@@ -121,7 +109,7 @@ func TestByName(t *testing.T) {
want: cyclonedxjson.ID,
},
{
name: "cyclonedx-1-json",
name: "cyclonedxjson", // clean variant
want: cyclonedxjson.ID,
},
@@ -135,7 +123,7 @@ func TestByName(t *testing.T) {
want: cyclonedxxml.ID,
},
{
name: "cyclonedx-1-xml",
name: "cyclonedxxml", // clean variant
want: cyclonedxxml.ID,
},
@@ -144,7 +132,6 @@ func TestByName(t *testing.T) {
name: "table",
want: table.ID,
},
{
name: "syft-table",
want: table.ID,
@@ -155,7 +142,6 @@ func TestByName(t *testing.T) {
name: "text",
want: text.ID,
},
{
name: "syft-text",
want: text.ID,
@@ -166,23 +152,26 @@ func TestByName(t *testing.T) {
name: "json",
want: syftjson.ID,
},
{
name: "syft-json",
want: syftjson.ID,
},
{
name: "syftjson", // clean variant
want: syftjson.ID,
},
// GitHub JSON
{
name: "github",
want: github.ID,
},
{
name: "github-json",
want: github.ID,
},
// Syft template
{
name: "template",
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)
})
}
}
+3 -2
View File
@@ -7,11 +7,11 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "github-0-json"
const ID sbom.FormatID = "github-json"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
func(writer io.Writer, sbom sbom.SBOM) error {
bom := toGithubModel(&sbom)
@@ -25,5 +25,6 @@ func Format() sbom.Format {
},
nil,
nil,
ID, "github",
)
}
+19 -1
View File
@@ -4,13 +4,31 @@ import (
"encoding/json"
"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/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)
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)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
+20 -4
View File
@@ -4,14 +4,30 @@ import (
"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
func Format() sbom.Format {
func Format2_2() sbom.Format {
return sbom.NewFormat(
ID,
encoder,
"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
+24 -1
View File
@@ -3,13 +3,36 @@ package spdxtagvalue
import (
"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/anchore/syft/syft/formats/common/spdxhelpers"
"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)
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)
}
+30 -5
View File
@@ -4,14 +4,39 @@ import (
"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
func Format() sbom.Format {
var IDs = []sbom.FormatID{ID, "spdx", "spdx-tv"}
// note: this format is LOSSY relative to the syftjson format
func Format2_1() sbom.Format {
return sbom.NewFormat(
ID,
encoder,
"2.1",
encoder2_1,
decoder,
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
+3 -2
View File
@@ -4,13 +4,14 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "syft-6-json"
const ID sbom.FormatID = "syft-json"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
"6",
encoder,
decoder,
validator,
ID, "json", "syft",
)
}
@@ -15,8 +15,7 @@ import (
func Test_SyftJsonID_Compatibility(t *testing.T) {
jsonMajorVersion := strings.Split(internal.JSONSchemaVersion, ".")[0]
syftJsonIDVersion := strings.Split(string(ID), "-")[1]
assert.Equal(t, jsonMajorVersion, syftJsonIDVersion)
assert.Equal(t, jsonMajorVersion, string(Format().Version()))
}
func Test_toSourceModel(t *testing.T) {
+2 -1
View File
@@ -8,9 +8,10 @@ const ID sbom.FormatID = "syft-table"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
encoder,
nil,
nil,
ID, "table",
)
}
+17 -2
View File
@@ -1,6 +1,7 @@
package template
import (
"fmt"
"io"
"github.com/anchore/syft/syft/formats/syftjson"
@@ -23,7 +24,19 @@ func (f OutputFormat) ID() sbom.FormatID {
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
}
@@ -37,7 +50,7 @@ func (f OutputFormat) Encode(output io.Writer, s sbom.SBOM) error {
return tmpl.Execute(output, doc)
}
func (f OutputFormat) Validate(reader io.Reader) error {
func (f OutputFormat) Validate(_ io.Reader) error {
return sbom.ErrValidationNotSupported
}
@@ -45,3 +58,5 @@ func (f OutputFormat) Validate(reader io.Reader) error {
func (f *OutputFormat) SetTemplatePath(filePath string) {
f.templateFilePath = filePath
}
var _ sbom.Format = (*OutputFormat)(nil)
+2 -1
View File
@@ -8,9 +8,10 @@ const ID sbom.FormatID = "syft-text"
func Format() sbom.Format {
return sbom.NewFormat(
ID,
sbom.AnyVersion,
encoder,
nil,
nil,
ID, "text",
)
}
+30 -5
View File
@@ -2,6 +2,7 @@ package sbom
import (
"errors"
"fmt"
"io"
)
@@ -18,20 +19,41 @@ func (f FormatID) String() string {
return string(f)
}
const AnyVersion = ""
type Format interface {
ID() FormatID
IDs() []FormatID
Version() string
Encode(io.Writer, SBOM) error
Decode(io.Reader) (*SBOM, error)
Validate(io.Reader) error
fmt.Stringer
}
type format struct {
id FormatID
ids []FormatID
version string
encoder Encoder
decoder Decoder
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.
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)
type Validator func(reader io.Reader) error
func NewFormat(id FormatID, encoder Encoder, decoder Decoder, validator Validator) Format {
return &format{
id: id,
func NewFormat(version string, encoder Encoder, decoder Decoder, validator Validator, ids ...FormatID) Format {
return format{
ids: ids,
version: version,
encoder: encoder,
decoder: decoder,
validator: validator,
@@ -57,7 +80,7 @@ func NewFormat(id FormatID, encoder Encoder, decoder Decoder, validator Validato
}
func (f format) ID() FormatID {
return f.id
return f.ids[0]
}
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)
}
var _ Format = (*format)(nil)
+1 -1
View File
@@ -15,7 +15,7 @@ func dummyEncoder(io.Writer, SBOM) error {
}
func dummyFormat(name string) Format {
return NewFormat(FormatID(name), dummyEncoder, nil, nil)
return NewFormat(AnyVersion, dummyEncoder, nil, nil, FormatID(name))
}
type writerConfig struct {