fix: support CycloneDX 1.7 (#4967)

Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Keith Zantow
2026-06-11 09:40:42 -04:00
committed by GitHub
parent b08d3c2970
commit 89773c0a12
13 changed files with 4520 additions and 799 deletions
+59
View File
@@ -0,0 +1,59 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"serialNumber": "urn:uuid:5208fea9-73dd-4624-b596-69fddccdb9e7",
"version": 1,
"metadata": {
"timestamp": "2023-09-29T12:02:02-04:00",
"tools": [
{
"vendor": "anchore",
"name": "syft",
"version": "[not provided]"
}
],
"component": {
"bom-ref": "a0ff99a6af10f11f",
"type": "file",
"name": "go.mod",
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
}
},
"components": [
{
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
"type": "library",
"name": "github.com/wagoodman/go-partybus",
"version": "v0.0.0-20230516145632-8ccac152c651",
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651",
"properties": [
{
"name": "syft:package:foundBy",
"value": "go-module-file-cataloger"
},
{
"name": "syft:package:language",
"value": "go"
},
{
"name": "syft:package:metadataType",
"value": "GolangModMetadata"
},
{
"name": "syft:package:type",
"value": "go-module"
},
{
"name": "syft:cpe23",
"value": "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*"
},
{
"name": "syft:location:0:path",
"value": "/go.mod"
}
]
}
]
}
@@ -1,7 +1,7 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"specVersion": "1.7",
"serialNumber": "urn:uuid:redacted",
"version": 1,
"metadata": {
@@ -1,7 +1,7 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"specVersion": "1.7",
"serialNumber": "urn:uuid:redacted",
"version": 1,
"metadata": {
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" serialNumber="urn:uuid:098e8516-ecd5-4130-9d5f-c32ba1ddb0dd" version="1">
<metadata>
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
<name>syft</name>
<version>[not provided]</version>
</tool>
</tools>
<component bom-ref="a0ff99a6af10f11f" type="file">
<name>go.mod</name>
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
</component>
</metadata>
<components>
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
<name>github.com/wagoodman/go-partybus</name>
<version>v0.0.0-20230516145632-8ccac152c651</version>
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
<properties>
<property name="syft:package:foundBy">go-module-file-cataloger</property>
<property name="syft:package:language">go</property>
<property name="syft:package:metadataType">GolangModMetadata</property>
<property name="syft:package:type">go-module</property>
<property name="syft:cpe23">cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</property>
<property name="syft:location:0:path">/go.mod</property>
</properties>
</component>
</components>
</bom>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.6" serialNumber="redacted" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" serialNumber="redacted" version="1">
<metadata>
<timestamp>redacted</timestamp>
<tools>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.6" serialNumber="redacted" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" serialNumber="redacted" version="1">
<metadata>
<timestamp>redacted</timestamp>
<tools>
@@ -9,8 +9,6 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const DefaultVersion = "1.6"
type Encoder struct {
version cyclonedx.SpecVersion
format cyclonedx.BOMFileFormat
+67 -41
View File
@@ -2,6 +2,10 @@ package cyclonedxutil
import (
"fmt"
"maps"
"slices"
"strconv"
"strings"
"github.com/CycloneDX/cyclonedx-go"
@@ -13,59 +17,81 @@ const (
JSONFormatID sbom.FormatID = "cyclonedx-json"
)
const DefaultVersion = "1.7"
var commonVersions = map[string]cyclonedx.SpecVersion{
"1.2": cyclonedx.SpecVersion1_2,
"1.3": cyclonedx.SpecVersion1_3,
"1.4": cyclonedx.SpecVersion1_4,
"1.5": cyclonedx.SpecVersion1_5,
"1.6": cyclonedx.SpecVersion1_6,
DefaultVersion: cyclonedx.SpecVersion1_7,
}
var allVersions = func() map[string]cyclonedx.SpecVersion {
out := map[string]cyclonedx.SpecVersion{
"1.0": cyclonedx.SpecVersion1_0,
"1.1": cyclonedx.SpecVersion1_1,
}
maps.Copy(out, commonVersions)
return out
}()
func SupportedVersions(id sbom.FormatID) []string {
versions := []string{
"1.2",
"1.3",
"1.4",
"1.5",
"1.6",
versionSet := commonVersions
if id == XMLFormatID {
versionSet = allVersions
}
if id != JSONFormatID {
// JSON format not supported for version < 1.2
versions = append([]string{"1.0", "1.1"}, versions...)
versions := make([]string, 0, len(versionSet))
for _, v := range versionSet {
versions = append(versions, v.String())
}
slices.SortFunc(versions, versionSort)
return versions
}
func SpecVersionFromString(v string) (cyclonedx.SpecVersion, error) {
switch v {
case "1.0":
return cyclonedx.SpecVersion1_0, nil
case "1.1":
return cyclonedx.SpecVersion1_1, nil
case "1.2":
return cyclonedx.SpecVersion1_2, nil
case "1.3":
return cyclonedx.SpecVersion1_3, nil
case "1.4":
return cyclonedx.SpecVersion1_4, nil
case "1.5":
return cyclonedx.SpecVersion1_5, nil
case "1.6":
return cyclonedx.SpecVersion1_6, nil
if specVersion, ok := allVersions[v]; ok {
return specVersion, nil
}
return -1, fmt.Errorf("unsupported CycloneDX version %q", v)
}
func VersionFromSpecVersion(spec cyclonedx.SpecVersion) string {
switch spec {
case cyclonedx.SpecVersion1_0:
return "1.0"
case cyclonedx.SpecVersion1_1:
return "1.1"
case cyclonedx.SpecVersion1_2:
return "1.2"
case cyclonedx.SpecVersion1_3:
return "1.3"
case cyclonedx.SpecVersion1_4:
return "1.4"
case cyclonedx.SpecVersion1_5:
return "1.5"
case cyclonedx.SpecVersion1_6:
return "1.6"
for version, specVersion := range allVersions {
if specVersion == spec {
return version
}
}
return ""
}
func versionSort(a string, b string) int {
partsA := strings.Split(a, ".")
partsB := strings.Split(b, ".")
lenA := len(partsA)
lenB := len(partsB)
for i := range max(lenA, lenB) {
if i >= lenA {
return -1 // 1 < 1.x
}
if i >= lenB {
return 1 // 1.x > 1
}
partA, errA := strconv.ParseInt(partsA[i], 10, 64)
partB, errB := strconv.ParseInt(partsB[i], 10, 64)
if errA != nil || errB != nil {
// string compare if we can't parse one of the sides
strcmp := strings.Compare(partsA[i], partsB[i])
if strcmp == 0 {
continue
}
return strcmp // not equal
}
if partA == partB {
continue
}
return int(partA - partB)
}
return 0
}
@@ -0,0 +1,107 @@
package cyclonedxutil
import (
"go/constant"
"go/types"
"regexp"
"slices"
"testing"
"github.com/CycloneDX/cyclonedx-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/tools/go/packages"
)
// Test_allSpecVersionsMapped loads the cyclonedx-go package's type information and enumerates every
// SpecVersion* constant it declares, asserting each one is present in our version maps. Upgrading
// cyclonedx-go to a release that adds a new spec version will fail this test until versions.go is updated.
func Test_allSpecVersionsMapped(t *testing.T) {
const pkgPath = "github.com/CycloneDX/cyclonedx-go"
pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName | packages.NeedTypes}, pkgPath)
require.NoError(t, err)
require.Len(t, pkgs, 1)
pkg := pkgs[0]
require.Empty(t, pkg.Errors, "errors loading %s", pkgPath)
require.NotNil(t, pkg.Types, "no type info loaded for %s", pkgPath)
// allVersions is the superset (json/common versions merged in), so it covers every spec version we map.
mapped := make(map[cyclonedx.SpecVersion]struct{})
for syftVersion, sv := range allVersions {
// make sure the key matches the version it's mapped to
require.Contains(t, sv.String(), syftVersion, "version %q is not correctly mapped to cyclonedx SpecVersion %q", syftVersion, sv.String())
mapped[sv] = struct{}{}
}
specVersionConst := regexp.MustCompile(`^SpecVersion\d`)
scope := pkg.Types.Scope()
found := 0
for _, name := range scope.Names() {
if !specVersionConst.MatchString(name) {
continue
}
c, ok := scope.Lookup(name).(*types.Const)
if !ok {
continue
}
found++
val, ok := constant.Int64Val(c.Val())
require.Truef(t, ok, "could not read int value of cyclonedx.%s", name)
_, ok = mapped[cyclonedx.SpecVersion(val)]
assert.Truef(t, ok, "cyclonedx.%s is not mapped in versions.go; add it when upgrading cyclonedx-go", name)
}
require.NotZero(t, found, "no SpecVersion* constants found in %s", pkgPath)
}
func Test_versionSort(t *testing.T) {
// versionSort returns <0 when a<b, 0 when a==b, >0 when a>b; assert on sign rather than exact value.
sign := func(n int) int {
switch {
case n < 0:
return -1
case n > 0:
return 1
default:
return 0
}
}
tests := []struct {
name string
a string
b string
want int
}{
{name: "equal single part", a: "1", b: "1", want: 0},
{name: "equal two parts", a: "1.4", b: "1.4", want: 0},
{name: "minor less than", a: "1.3", b: "1.4", want: -1},
{name: "minor greater than", a: "1.6", b: "1.5", want: 1},
{name: "major less than", a: "1.7", b: "2.0", want: -1},
{name: "major greater than", a: "2.0", b: "1.7", want: 1},
{name: "fewer parts sorts first", a: "1", b: "1.0", want: -1},
{name: "more parts sorts last", a: "1.0", b: "1", want: 1},
{name: "numeric not lexical (10 > 9)", a: "1.10", b: "1.9", want: 1},
{name: "multi-digit major", a: "10.0", b: "9.0", want: 1},
{name: "three parts equal", a: "1.4.0", b: "1.4.0", want: 0},
{name: "three parts patch differs", a: "1.4.1", b: "1.4.0", want: 1},
{name: "non-numeric equal falls through", a: "1.x", b: "1.x", want: 0},
{name: "non-numeric string compare", a: "1.a", b: "1.b", want: -1},
{name: "non-numeric side string compare", a: "1.2", b: "1.x", want: -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, sign(versionSort(tt.a, tt.b)))
// comparator must be antisymmetric: swapping operands negates the sign.
assert.Equal(t, -tt.want, sign(versionSort(tt.b, tt.a)))
})
}
}
func Test_versionSort_sortsSlice(t *testing.T) {
versions := []string{"1.10", "1.2", "1", "2.0", "1.9", "1.0"}
slices.SortFunc(versions, versionSort)
assert.Equal(t, []string{"1", "1.0", "1.2", "1.9", "1.10", "2.0"}, versions)
}