feat: add cyclonedx schema version selection (#2123)

---------

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
This commit is contained in:
Christopher Angelo Phillips 2023-09-13 14:50:22 -04:00 committed by GitHub
parent 5035d9ca1a
commit 3e16c6813f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 274 additions and 24 deletions

2
go.mod
View File

@ -3,7 +3,7 @@ module github.com/anchore/syft
go 1.21.0 go 1.21.0
require ( require (
github.com/CycloneDX/cyclonedx-go v0.7.1 github.com/CycloneDX/cyclonedx-go v0.7.2
github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver v1.5.0
github.com/Masterminds/sprig/v3 v3.2.3 github.com/Masterminds/sprig/v3 v3.2.3
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d

6
go.sum
View File

@ -57,8 +57,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CycloneDX/cyclonedx-go v0.7.1 h1:5w1SxjGm9MTMNTuRbEPyw21ObdbaagTWF/KfF0qHTRE= github.com/CycloneDX/cyclonedx-go v0.7.2 h1:kKQ0t1dPOlugSIYVOMiMtFqeXI2wp/f5DBIdfux8gnQ=
github.com/CycloneDX/cyclonedx-go v0.7.1/go.mod h1:N/nrdWQI2SIjaACyyDs/u7+ddCkyl/zkNs8xFsHF2Ps= github.com/CycloneDX/cyclonedx-go v0.7.2/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
@ -685,6 +685,8 @@ github.com/sylabs/sif/v2 v2.11.5 h1:7ssPH3epSonsTrzbS1YxeJ9KuqAN7ISlSM61a7j/mQM=
github.com/sylabs/sif/v2 v2.11.5/go.mod h1:GBoZs9LU3e4yJH1dcZ3Akf/jsqYgy5SeguJQC+zd75Y= github.com/sylabs/sif/v2 v2.11.5/go.mod h1:GBoZs9LU3e4yJH1dcZ3Akf/jsqYgy5SeguJQC+zd75Y=
github.com/sylabs/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ= github.com/sylabs/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ=
github.com/sylabs/squashfs v0.6.1/go.mod h1:ZwpbPCj0ocIvMy2br6KZmix6Gzh6fsGQcCnydMF+Kx8= github.com/sylabs/squashfs v0.6.1/go.mod h1:ZwpbPCj0ocIvMy2br6KZmix6Gzh6fsGQcCnydMF+Kx8=
github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo=
github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=

View File

@ -1,6 +1,6 @@
# CycloneDX Schemas # CycloneDX Schemas
`syft` generates a CycloneDX BOm output. We want to be able to validate the CycloneDX schemas `syft` generates a CycloneDX Bom output. We want to be able to validate the CycloneDX schemas
(and dependent schemas) against generated syft output. The best way to do this is with `xmllint`, (and dependent schemas) against generated syft output. The best way to do this is with `xmllint`,
however, this tool does not know how to deal with references from HTTP, only the local filesystem. however, this tool does not know how to deal with references from HTTP, only the local filesystem.
For this reason we've included a copy of all schemas needed to validate `syft` output, modified For this reason we've included a copy of all schemas needed to validate `syft` output, modified

View File

@ -9,11 +9,40 @@ import (
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
) )
func encoder(output io.Writer, s sbom.SBOM) error { func encoderV1_0(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_0)
}
func encoderV1_1(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_1)
}
func encoderV1_2(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_2)
}
func encoderV1_3(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_3)
}
func encoderV1_4(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_4)
}
func encoderV1_5(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_5)
}
func buildEncoder(output io.Writer, s sbom.SBOM) (cyclonedx.BOMEncoder, *cyclonedx.BOM) {
bom := cyclonedxhelpers.ToFormatModel(s) bom := cyclonedxhelpers.ToFormatModel(s)
enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatJSON) enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatJSON)
enc.SetPretty(true) enc.SetPretty(true)
enc.SetEscapeHTML(false) enc.SetEscapeHTML(false)
err := enc.Encode(bom) return enc, bom
return err
} }

View File

@ -9,10 +9,62 @@ import (
const ID sbom.FormatID = "cyclonedx-json" const ID sbom.FormatID = "cyclonedx-json"
func Format() sbom.Format { var Format = Format1_4
func Format1_0() sbom.Format {
return sbom.NewFormat( return sbom.NewFormat(
sbom.AnyVersion, cyclonedx.SpecVersion1_0.String(),
encoder, encoderV1_0,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
ID,
)
}
func Format1_1() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_1.String(),
encoderV1_1,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
ID,
)
}
func Format1_2() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_2.String(),
encoderV1_2,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
ID,
)
}
func Format1_3() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_3.String(),
encoderV1_3,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
ID,
)
}
func Format1_4() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_4.String(),
encoderV1_4,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
ID,
)
}
func Format1_5() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_5.String(),
encoderV1_5,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
ID, ID,

View File

@ -0,0 +1,34 @@
package cyclonedxjson
import (
"testing"
"github.com/CycloneDX/cyclonedx-go"
)
func TestFormatVersions(t *testing.T) {
tests := []struct {
name string
expectedVersion string
}{
{
"cyclonedx-json should default to v1.4",
cyclonedx.SpecVersion1_4.String(),
},
}
for _, c := range tests {
c := c
t.Run(c.name, func(t *testing.T) {
sbomFormat := Format()
if sbomFormat.ID() != ID {
t.Errorf("expected ID %q, got %q", ID, sbomFormat.ID())
}
if sbomFormat.Version() != c.expectedVersion {
t.Errorf("expected version %q, got %q", c.expectedVersion, sbomFormat.Version())
}
})
}
}

View File

@ -9,11 +9,40 @@ import (
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
) )
func encoder(output io.Writer, s sbom.SBOM) error { func encoderV1_0(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_0)
}
func encoderV1_1(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_1)
}
func encoderV1_2(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_2)
}
func encoderV1_3(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_3)
}
func encoderV1_4(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_4)
}
func encoderV1_5(output io.Writer, s sbom.SBOM) error {
enc, bom := buildEncoder(output, s)
return enc.EncodeVersion(bom, cyclonedx.SpecVersion1_5)
}
func buildEncoder(output io.Writer, s sbom.SBOM) (cyclonedx.BOMEncoder, *cyclonedx.BOM) {
bom := cyclonedxhelpers.ToFormatModel(s) bom := cyclonedxhelpers.ToFormatModel(s)
enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatXML) enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatXML)
enc.SetPretty(true) enc.SetPretty(true)
enc.SetEscapeHTML(false)
err := enc.Encode(bom) return enc, bom
return err
} }

View File

@ -9,10 +9,62 @@ import (
const ID sbom.FormatID = "cyclonedx-xml" const ID sbom.FormatID = "cyclonedx-xml"
func Format() sbom.Format { var Format = Format1_4
func Format1_0() sbom.Format {
return sbom.NewFormat( return sbom.NewFormat(
sbom.AnyVersion, cyclonedx.SpecVersion1_0.String(),
encoder, encoderV1_0,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
ID, "cyclonedx", "cyclone",
)
}
func Format1_1() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_1.String(),
encoderV1_1,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
ID, "cyclonedx", "cyclone",
)
}
func Format1_2() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_2.String(),
encoderV1_2,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
ID, "cyclonedx", "cyclone",
)
}
func Format1_3() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_3.String(),
encoderV1_3,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
ID, "cyclonedx", "cyclone",
)
}
func Format1_4() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_4.String(),
encoderV1_4,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
ID, "cyclonedx", "cyclone",
)
}
func Format1_5() sbom.Format {
return sbom.NewFormat(
cyclonedx.SpecVersion1_5.String(),
encoderV1_5,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
ID, "cyclonedx", "cyclone", ID, "cyclonedx", "cyclone",

View File

@ -26,17 +26,27 @@ import (
func Formats() []sbom.Format { func Formats() []sbom.Format {
return []sbom.Format{ return []sbom.Format{
syftjson.Format(), syftjson.Format(),
cyclonedxxml.Format(),
cyclonedxjson.Format(),
github.Format(), github.Format(),
table.Format(),
text.Format(),
template.Format(),
cyclonedxxml.Format1_0(),
cyclonedxxml.Format1_1(),
cyclonedxxml.Format1_2(),
cyclonedxxml.Format1_3(),
cyclonedxxml.Format1_4(),
cyclonedxxml.Format1_5(),
cyclonedxjson.Format1_0(),
cyclonedxjson.Format1_1(),
cyclonedxjson.Format1_2(),
cyclonedxjson.Format1_3(),
cyclonedxjson.Format1_4(),
cyclonedxjson.Format1_5(),
spdxtagvalue.Format2_1(), spdxtagvalue.Format2_1(),
spdxtagvalue.Format2_2(), spdxtagvalue.Format2_2(),
spdxtagvalue.Format2_3(), spdxtagvalue.Format2_3(),
spdxjson.Format2_2(), spdxjson.Format2_2(),
spdxjson.Format2_3(), spdxjson.Format2_3(),
table.Format(),
text.Format(),
template.Format(),
} }
} }
@ -55,7 +65,7 @@ func Identify(by []byte) sbom.Format {
// ByName accepts a name@version string, such as: // ByName accepts a name@version string, such as:
// //
// spdx-json@2.1 or cyclonedx@2 // spdx-json@2.1 or cyclonedx@1.5
func ByName(name string) sbom.Format { func ByName(name string) sbom.Format {
parts := strings.SplitN(name, "@", 2) parts := strings.SplitN(name, "@", 2)
version := sbom.AnyVersion version := sbom.AnyVersion
@ -71,6 +81,16 @@ func ByNameAndVersion(name string, version string) sbom.Format {
for _, f := range Formats() { for _, f := range Formats() {
for _, n := range f.IDs() { for _, n := range f.IDs() {
if cleanFormatName(string(n)) == name && versionMatches(f.Version(), version) { if cleanFormatName(string(n)) == name && versionMatches(f.Version(), version) {
// if the version is not specified and the format is cyclonedx, then we want to return the most recent version up to 1.4
// If more aliases like cdx are added this will not catch those - we want to eventually provide a way for
// formats to inform this function what their default version is
// TODO: remove this check when 1.5 is stable or default formats are designed. PR below should be merged.
// https://github.com/CycloneDX/cyclonedx-go/pull/90
if version == sbom.AnyVersion && strings.Contains(string(n), "cyclone") {
if f.Version() == "1.5" {
continue
}
}
if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() { if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
mostRecentFormat = f mostRecentFormat = f
} }

View File

@ -70,7 +70,6 @@ func TestFormats_EmptyInput(t *testing.T) {
} }
func TestByName(t *testing.T) { func TestByName(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
want sbom.FormatID want sbom.FormatID

View File

@ -0,0 +1,33 @@
package syftjson
import (
"testing"
"github.com/anchore/syft/internal"
)
func TestFormat(t *testing.T) {
tests := []struct {
name string
version string
}{
{
name: "default version should use latest internal version",
version: "",
},
}
for _, c := range tests {
c := c
t.Run(c.name, func(t *testing.T) {
sbomFormat := Format()
if sbomFormat.ID() != ID {
t.Errorf("expected ID %q, got %q", ID, sbomFormat.ID())
}
if sbomFormat.Version() != internal.JSONSchemaVersion {
t.Errorf("expected version %q, got %q", c.version, sbomFormat.Version())
}
})
}
}