diff --git a/go.mod b/go.mod index 18980352fd3..b6cc8b52d69 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/anchore/syft go 1.21.0 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/sprig/v3 v3.2.3 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d diff --git a/go.sum b/go.sum index 4bc3adbd2c5..b75b602406d 100644 --- a/go.sum +++ b/go.sum @@ -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 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/CycloneDX/cyclonedx-go v0.7.1 h1:5w1SxjGm9MTMNTuRbEPyw21ObdbaagTWF/KfF0qHTRE= -github.com/CycloneDX/cyclonedx-go v0.7.1/go.mod h1:N/nrdWQI2SIjaACyyDs/u7+ddCkyl/zkNs8xFsHF2Ps= +github.com/CycloneDX/cyclonedx-go v0.7.2 h1:kKQ0t1dPOlugSIYVOMiMtFqeXI2wp/f5DBIdfux8gnQ= +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/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= @@ -683,6 +683,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/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ= 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/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= diff --git a/schema/cyclonedx/README.md b/schema/cyclonedx/README.md index 200d6393e30..b9278db66c4 100644 --- a/schema/cyclonedx/README.md +++ b/schema/cyclonedx/README.md @@ -1,6 +1,6 @@ # 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`, 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 diff --git a/syft/formats/cyclonedxjson/encoder.go b/syft/formats/cyclonedxjson/encoder.go index 13ad32f682e..297b8026580 100644 --- a/syft/formats/cyclonedxjson/encoder.go +++ b/syft/formats/cyclonedxjson/encoder.go @@ -9,11 +9,40 @@ import ( "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) enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatJSON) enc.SetPretty(true) enc.SetEscapeHTML(false) - err := enc.Encode(bom) - return err + return enc, bom } diff --git a/syft/formats/cyclonedxjson/format.go b/syft/formats/cyclonedxjson/format.go index 8c1e2e01643..97a088aaea7 100644 --- a/syft/formats/cyclonedxjson/format.go +++ b/syft/formats/cyclonedxjson/format.go @@ -9,10 +9,62 @@ import ( const ID sbom.FormatID = "cyclonedx-json" -func Format() sbom.Format { +var Format = Format1_4 + +func Format1_0() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_0.String(), + 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( - sbom.AnyVersion, - encoder, + cyclonedx.SpecVersion1_5.String(), + encoderV1_5, cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), ID, diff --git a/syft/formats/cyclonedxjson/format_test.go b/syft/formats/cyclonedxjson/format_test.go new file mode 100644 index 00000000000..2cfd5e54504 --- /dev/null +++ b/syft/formats/cyclonedxjson/format_test.go @@ -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()) + } + }) + } +} diff --git a/syft/formats/cyclonedxxml/encoder.go b/syft/formats/cyclonedxxml/encoder.go index b8abdf81a15..3941feca00b 100644 --- a/syft/formats/cyclonedxxml/encoder.go +++ b/syft/formats/cyclonedxxml/encoder.go @@ -9,11 +9,40 @@ import ( "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) enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatXML) enc.SetPretty(true) - - err := enc.Encode(bom) - return err + enc.SetEscapeHTML(false) + return enc, bom } diff --git a/syft/formats/cyclonedxxml/format.go b/syft/formats/cyclonedxxml/format.go index 7fe53c4f7f1..1b22cee1476 100644 --- a/syft/formats/cyclonedxxml/format.go +++ b/syft/formats/cyclonedxxml/format.go @@ -9,10 +9,62 @@ import ( const ID sbom.FormatID = "cyclonedx-xml" -func Format() sbom.Format { +var Format = Format1_4 + +func Format1_0() sbom.Format { + return sbom.NewFormat( + cyclonedx.SpecVersion1_0.String(), + 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( - sbom.AnyVersion, - encoder, + cyclonedx.SpecVersion1_5.String(), + encoderV1_5, cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), ID, "cyclonedx", "cyclone", diff --git a/syft/formats/formats.go b/syft/formats/formats.go index 13af6cf058f..b61027ed4d4 100644 --- a/syft/formats/formats.go +++ b/syft/formats/formats.go @@ -26,17 +26,27 @@ import ( func Formats() []sbom.Format { return []sbom.Format{ syftjson.Format(), - cyclonedxxml.Format(), - cyclonedxjson.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_2(), spdxtagvalue.Format2_3(), spdxjson.Format2_2(), 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: // -// spdx-json@2.1 or cyclonedx@2 +// spdx-json@2.1 or cyclonedx@1.5 func ByName(name string) sbom.Format { parts := strings.SplitN(name, "@", 2) version := sbom.AnyVersion @@ -71,6 +81,16 @@ func ByNameAndVersion(name string, version string) sbom.Format { for _, f := range Formats() { for _, n := range f.IDs() { 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() { mostRecentFormat = f } diff --git a/syft/formats/formats_test.go b/syft/formats/formats_test.go index 60dae72c9b3..2cdc99b8a52 100644 --- a/syft/formats/formats_test.go +++ b/syft/formats/formats_test.go @@ -70,7 +70,6 @@ func TestFormats_EmptyInput(t *testing.T) { } func TestByName(t *testing.T) { - tests := []struct { name string want sbom.FormatID diff --git a/syft/formats/syftjson/format_test.go b/syft/formats/syftjson/format_test.go new file mode 100644 index 00000000000..4ef7a21a167 --- /dev/null +++ b/syft/formats/syftjson/format_test.go @@ -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()) + } + }) + } +}