From 582d1c0f3378be6382ae3f101a06f017b6d3385d Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 27 Aug 2026 17:11:37 -0500 Subject: [PATCH] sbom: make internal SPDX relationships reproducible Sort the top-level elements copied from internal APK SBOMs before adding their relationships to the image SBOM. This prevents equivalent builds from producing different predicates due to Go map iteration order. --- pkg/sbom/generator/spdx/spdx.go | 13 +++++-- pkg/sbom/generator/spdx/spdx_test.go | 54 ++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/pkg/sbom/generator/spdx/spdx.go b/pkg/sbom/generator/spdx/spdx.go index 0f8968478..ff1297de4 100644 --- a/pkg/sbom/generator/spdx/spdx.go +++ b/pkg/sbom/generator/spdx/spdx.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "regexp" + "sort" "strings" "time" "unicode/utf8" @@ -254,9 +255,15 @@ func (sx *SPDX) ProcessInternalApkSBOM(ctx context.Context, opts *options.Option } } - // Copy the targetElementIDs - todo := make(map[string]struct{}, len(apkSBOMDoc.Relationships)) + sortedTargetElementIDs := make([]string, 0, len(targetElementIDs)) for id := range targetElementIDs { + sortedTargetElementIDs = append(sortedTargetElementIDs, id) + } + // Sort the element IDs so repeated builds produce the same relationship order. + sort.Strings(sortedTargetElementIDs) + + todo := make(map[string]struct{}, len(apkSBOMDoc.Relationships)) + for _, id := range sortedTargetElementIDs { todo[id] = struct{}{} } @@ -270,7 +277,7 @@ func (sx *SPDX) ProcessInternalApkSBOM(ctx context.Context, opts *options.Option // This ensures they are reachable from the document root for tools that traverse the SBOM graph. if len(doc.DocumentDescribes) > 0 { rootPkgID := doc.DocumentDescribes[0] - for elementID := range targetElementIDs { + for _, elementID := range sortedTargetElementIDs { doc.Relationships = append(doc.Relationships, Relationship{ Element: rootPkgID, Type: "CONTAINS", diff --git a/pkg/sbom/generator/spdx/spdx_test.go b/pkg/sbom/generator/spdx/spdx_test.go index 9a7f6c02f..e157e5416 100644 --- a/pkg/sbom/generator/spdx/spdx_test.go +++ b/pkg/sbom/generator/spdx/spdx_test.go @@ -271,23 +271,63 @@ func TestSPDX_Generate(t *testing.T) { } func TestReproducible(t *testing.T) { - // Create two sboms based on the same input and ensure - // they are identical + // Create SBOMs based on the same input and ensure they are identical. + const ( + packageID = "SPDXRef-Package-glibc-2.40-r0" + documentRootID = "SPDXRef-DocumentRoot-Directory-glibc" + internalSBOMPath = "/var/lib/db/sbom/glibc-2.40-r0.spdx.json" + ) + dir := t.TempDir() fsys := apkfs.NewMemFS() opts := testOpts(fsys) + opts.Packages = []*apk.InstalledPackage{{Name: "glibc", Version: "2.40-r0"}} + + internalSBOM := Document{ + DocumentDescribes: []string{ + packageID, + documentRootID, + }, + Packages: []Package{ + {ID: packageID, Name: "glibc"}, + {ID: documentRootID, Name: "/"}, + }, + } + data, err := json.Marshal(internalSBOM) + require.NoError(t, err) + require.NoError(t, fsys.MkdirAll("/var/lib/db/sbom", 0750)) + require.NoError(t, fsys.WriteFile(internalSBOMPath, data, 0644)) + sx := New() - d := make([][]byte, 0, 2) - for i := range 2 { + generate := func(i int) []byte { path := filepath.Join(dir, fmt.Sprintf("sbom%d.%s", i, sx.Ext())) require.NoError(t, sx.Generate(t.Context(), opts, path)) require.FileExists(t, path) data, err := os.ReadFile(path) require.NoError(t, err) - d = append(d, data) + return data + } + + expected := generate(0) + var doc Document + require.NoError(t, json.Unmarshal(expected, &doc)) + require.Contains(t, doc.Relationships, Relationship{ + Element: doc.DocumentDescribes[0], + Type: "CONTAINS", + Related: packageID, + }) + require.Contains(t, doc.Relationships, Relationship{ + Element: doc.DocumentDescribes[0], + Type: "CONTAINS", + Related: documentRootID, + }) + + // Exercise enough new maps to detect unstable iteration order if the relationships are not sorted. + for i := 1; i < 100; i++ { + if diff := cmp.Diff(expected, generate(i)); diff != "" { + t.Fatalf("SBOM differs from first generation (-want +got):\n%s", diff) + } } - diff := cmp.Diff(d[0], d[1]) - require.Empty(t, diff, fmt.Sprintf("difference in expected output %s", diff)) } // To run TestValidateSPDX, point SPDX_TOOLS_JAR to the SPDX tools