Skip to content

Commit b5d3d92

Browse files
committed
fix: vulnerability scans with extensions
When adding extensions grype was unable to match the suppressions due to the way we were generating sboms. So let's add a root identifier and put all others as a reference. Signed-off-by: Noel Georgi <git@frezbo.dev>
1 parent 916bcf6 commit b5d3d92

3 files changed

Lines changed: 192 additions & 10 deletions

File tree

enterprise/spdx/builder/spdx.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,14 @@ import (
1818
spdxjson "github.com/spdx/tools-golang/json"
1919
"github.com/spdx/tools-golang/spdx"
2020
"github.com/spdx/tools-golang/spdx/v2/common"
21+
22+
ifconstants "github.com/siderolabs/image-factory/pkg/constants"
2123
)
2224

25+
// talosRootSPDXID is the SPDXIdentifier of the synthetic root package added
26+
// to every merged bundle.
27+
const talosRootSPDXID = common.ElementID("DocumentRoot-Directory-talos")
28+
2329
// File represents an extracted SPDX file.
2430
type File struct {
2531
// Filename is the original filename (e.g., "extension-name.spdx.json").
@@ -76,6 +82,25 @@ func BundleToJSON(bundle *Bundle) (io.Reader, int64, error) {
7682
{CreatorType: "Tool", Creator: "image-factory"},
7783
},
7884
},
85+
Packages: []*spdx.Package{
86+
{
87+
PackageSPDXIdentifier: talosRootSPDXID,
88+
PackageName: ifconstants.TalosPackageName,
89+
PackageVersion: bundle.TalosVersion,
90+
PrimaryPackagePurpose: "FILE",
91+
PackageDownloadLocation: "NOASSERTION",
92+
// PackageSupplier must be non-nil — syft's fileSource
93+
// dereferences it unconditionally when classifying the root.
94+
PackageSupplier: &common.Supplier{Supplier: "NOASSERTION"},
95+
},
96+
},
97+
Relationships: []*spdx.Relationship{
98+
{
99+
RefA: common.DocElementID{ElementRefID: common.ElementID("DOCUMENT")},
100+
RefB: common.DocElementID{ElementRefID: talosRootSPDXID},
101+
Relationship: common.TypeRelationshipDescribe,
102+
},
103+
},
79104
}
80105

81106
// Sort files for deterministic merge output. Without this, map-derived
@@ -143,9 +168,13 @@ func mergeDocument(merged, source *spdx.Document, sourceID string) {
143168
rel.Relationship == common.TypeRelationshipDescribe
144169

145170
if isDocDescribes {
146-
// Point DESCRIBES from the merged document to the prefixed target.
171+
// Re-parent each source's root package under the synthetic talos
172+
// root via CONTAINS so the merged document has exactly one
173+
// DOCUMENT-DESCRIBES root — required by syft's source detection
174+
// (see talosRootSPDXID).
175+
newRel.Relationship = common.TypeRelationshipContains
147176
newRel.RefA = common.DocElementID{
148-
ElementRefID: merged.SPDXIdentifier,
177+
ElementRefID: talosRootSPDXID,
149178
}
150179
newRel.RefB = prefixDocElementID(prefix, rel.RefB)
151180
} else {

enterprise/spdx/builder/spdx_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@
88
package builder_test
99

1010
import (
11+
"bytes"
1112
"io"
1213
"strings"
1314
"testing"
1415

1516
spdxjson "github.com/spdx/tools-golang/json"
17+
"github.com/spdx/tools-golang/spdx"
18+
"github.com/spdx/tools-golang/spdx/v2/common"
1619
"github.com/stretchr/testify/assert"
1720
"github.com/stretchr/testify/require"
1821

1922
"github.com/siderolabs/image-factory/enterprise/spdx/builder"
23+
ifconstants "github.com/siderolabs/image-factory/pkg/constants"
2024
)
2125

2226
func TestCacheTag(t *testing.T) {
@@ -100,3 +104,150 @@ func TestBundleToJSON_DocumentNamespace(t *testing.T) {
100104
})
101105
}
102106
}
107+
108+
// TestBundleToJSON_SingleTalosRoot guards the source-name/version pathway
109+
// that grype's OpenVEX matcher relies on. Two invariants the test enforces:
110+
//
111+
// 1. Exactly one package is the target of a DOCUMENT-DESCRIBES relationship.
112+
// syft's SPDX importer returns its derived source only when there is a
113+
// single root (see findRootPackages + extractSource in syft).
114+
// 2. That root package is named after constants.TalosPackageName (always
115+
// "talos-enterprise" in this enterprise-only package), carries the
116+
// bundle's TalosVersion, and is marked with PrimaryPackagePurpose=FILE
117+
// plus the "DocumentRoot-Directory-..." SPDXIdentifier prefix syft uses
118+
// to map the root onto a syft Source.Description with Name and Version set.
119+
//
120+
// Without both, grype's productIdentifiersFromContext returns an empty list,
121+
// the VEX product `pkg:generic/talos-enterprise@<ver>` never matches any
122+
// artifact, and the talos-vex statements don't suppress matches.
123+
func TestBundleToJSON_SingleTalosRoot(t *testing.T) {
124+
t.Parallel()
125+
126+
const (
127+
sourceID = "talos-amd64"
128+
sourceName = "talos"
129+
)
130+
131+
sourceDoc := minimalSourceSPDX(t, sourceID, sourceName, "v1.13.3")
132+
133+
bundle := &builder.Bundle{
134+
SchematicID: "sch",
135+
TalosVersion: "v1.13.3",
136+
Arch: "amd64",
137+
ExternalURL: "https://factory.sidero.dev",
138+
Files: []builder.File{
139+
{
140+
Filename: sourceID + ".spdx.json",
141+
Source: sourceID,
142+
Content: sourceDoc,
143+
},
144+
},
145+
}
146+
147+
r, _, err := builder.BundleToJSON(bundle)
148+
require.NoError(t, err)
149+
150+
data, err := io.ReadAll(r)
151+
require.NoError(t, err)
152+
153+
doc, err := spdxjson.Read(bytes.NewReader(data))
154+
require.NoError(t, err)
155+
156+
var roots []common.ElementID
157+
158+
for _, rel := range doc.Relationships {
159+
if rel.RefA.ElementRefID == common.ElementID("DOCUMENT") &&
160+
rel.Relationship == common.TypeRelationshipDescribe {
161+
roots = append(roots, rel.RefB.ElementRefID)
162+
}
163+
}
164+
165+
require.Len(t, roots, 1, "merged document must have exactly one DOCUMENT-DESCRIBES root")
166+
167+
var rootPkg *spdx.Package
168+
169+
for _, p := range doc.Packages {
170+
if p.PackageSPDXIdentifier == roots[0] {
171+
rootPkg = p
172+
173+
break
174+
}
175+
}
176+
177+
require.NotNil(t, rootPkg, "DESCRIBES target %q must exist in doc.Packages", roots[0])
178+
179+
assert.Equal(t, ifconstants.TalosPackageName, rootPkg.PackageName,
180+
"root PackageName must equal constants.TalosPackageName so grype's derived pkg:generic/<name>@<version> equals the VEX product")
181+
assert.Equal(t, "v1.13.3", rootPkg.PackageVersion, "root package version feeds syft's Source.Version")
182+
assert.Equal(t, "FILE", rootPkg.PrimaryPackagePurpose, "FILE purpose routes syft through fileSource()")
183+
assert.True(t,
184+
strings.HasPrefix(string(rootPkg.PackageSPDXIdentifier), "DocumentRoot-"),
185+
"SPDXIdentifier prefix is what triggers syft's directory/file source classification")
186+
187+
// Per-source root must be re-parented under the new talos root via
188+
// CONTAINS, not via another DOCUMENT-DESCRIBES (which would create a
189+
// second root and break syft's single-root detection).
190+
// The merge prefixes every source element ID with "<source>-", so the
191+
// per-source root is "<sourceID>-DocumentRoot-Directory-<sourceName>".
192+
perSourceRoot := common.ElementID(sourceID + "-DocumentRoot-Directory-" + sourceName)
193+
194+
var perSourceContained bool
195+
196+
for _, rel := range doc.Relationships {
197+
if rel.RefA.ElementRefID == roots[0] &&
198+
rel.Relationship == common.TypeRelationshipContains &&
199+
rel.RefB.ElementRefID == perSourceRoot {
200+
perSourceContained = true
201+
202+
break
203+
}
204+
}
205+
206+
assert.True(t, perSourceContained,
207+
"per-source root must be re-parented under the talos root via CONTAINS")
208+
}
209+
210+
// minimalSourceSPDX produces a syft-shaped SPDX 2.3 JSON document with one
211+
// DocumentRoot-Directory-<name> root, mirroring what hack/sbom.sh embeds
212+
// into the Talos initramfs.
213+
func minimalSourceSPDX(t *testing.T, sourceID, name, version string) []byte {
214+
t.Helper()
215+
216+
rootID := common.ElementID("DocumentRoot-Directory-" + name)
217+
218+
doc := &spdx.Document{
219+
SPDXVersion: spdx.Version,
220+
DataLicense: spdx.DataLicense,
221+
SPDXIdentifier: common.ElementID("DOCUMENT"),
222+
DocumentName: sourceID,
223+
DocumentNamespace: "https://anchore.com/syft/dir/" + sourceID,
224+
CreationInfo: &spdx.CreationInfo{
225+
Created: "2026-05-30T00:00:00Z",
226+
Creators: []common.Creator{
227+
{CreatorType: "Tool", Creator: "syft-test"},
228+
},
229+
},
230+
Packages: []*spdx.Package{
231+
{
232+
PackageSPDXIdentifier: rootID,
233+
PackageName: name,
234+
PackageVersion: version,
235+
PrimaryPackagePurpose: "FILE",
236+
PackageDownloadLocation: "NOASSERTION",
237+
},
238+
},
239+
Relationships: []*spdx.Relationship{
240+
{
241+
RefA: common.DocElementID{ElementRefID: common.ElementID("DOCUMENT")},
242+
RefB: common.DocElementID{ElementRefID: rootID},
243+
Relationship: common.TypeRelationshipDescribe,
244+
},
245+
},
246+
}
247+
248+
var buf bytes.Buffer
249+
250+
require.NoError(t, spdxjson.Write(doc, &buf))
251+
252+
return buf.Bytes()
253+
}

pkg/constants/name_ent_on.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
package constants
88

9-
// TalosName is the name in the profile.
10-
const TalosName = "Talos Enterprise"
11-
12-
// ImageFactoryName is the name of the image factory.
13-
const ImageFactoryName = "Image Factory Enterprise"
14-
15-
// TalosPURL is the purl for Talos Enterprise.
16-
const TalosPURL = "pkg:generic/talos-enterprise"
9+
const (
10+
// TalosName is the name in the profile.
11+
TalosName = "Talos Enterprise"
12+
// ImageFactoryName is the name of the image factory.
13+
ImageFactoryName = "Image Factory Enterprise"
14+
// TalosPackageName is the name of the Talos package in SPDX documents.
15+
TalosPackageName = "talos-enterprise"
16+
// TalosPURL is the purl for Talos Enterprise.
17+
TalosPURL = "pkg:generic/" + TalosPackageName
18+
)

0 commit comments

Comments
 (0)