Skip to content

Commit 6a96a3e

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 6a96a3e

3 files changed

Lines changed: 184 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: 143 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,142 @@ 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.TalosPURL (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+
sourceDoc := minimalSourceSPDX(t, "talos-amd64", "talos", "v1.13.3")
127+
128+
bundle := &builder.Bundle{
129+
SchematicID: "sch",
130+
TalosVersion: "v1.13.3",
131+
Arch: "amd64",
132+
ExternalURL: "https://factory.sidero.dev",
133+
Files: []builder.File{
134+
{
135+
Filename: "talos-amd64.spdx.json",
136+
Source: "talos-amd64",
137+
Content: sourceDoc,
138+
},
139+
},
140+
}
141+
142+
r, _, err := builder.BundleToJSON(bundle)
143+
require.NoError(t, err)
144+
145+
data, err := io.ReadAll(r)
146+
require.NoError(t, err)
147+
148+
doc, err := spdxjson.Read(bytes.NewReader(data))
149+
require.NoError(t, err)
150+
151+
var roots []common.ElementID
152+
153+
for _, rel := range doc.Relationships {
154+
if rel.RefA.ElementRefID == common.ElementID("DOCUMENT") &&
155+
rel.Relationship == common.TypeRelationshipDescribe {
156+
roots = append(roots, rel.RefB.ElementRefID)
157+
}
158+
}
159+
160+
require.Len(t, roots, 1, "merged document must have exactly one DOCUMENT-DESCRIBES root")
161+
162+
var rootPkg *spdx.Package
163+
164+
for _, p := range doc.Packages {
165+
if p.PackageSPDXIdentifier == roots[0] {
166+
rootPkg = p
167+
168+
break
169+
}
170+
}
171+
172+
require.NotNil(t, rootPkg, "DESCRIBES target %q must exist in doc.Packages", roots[0])
173+
174+
wantName := strings.TrimPrefix(ifconstants.TalosPURL, "pkg:generic/")
175+
assert.Equal(t, wantName, rootPkg.PackageName,
176+
"root PackageName must match the short name in constants.TalosPURL so grype's derived pkg:generic/<name>@<version> equals the VEX product")
177+
assert.Equal(t, "v1.13.3", rootPkg.PackageVersion, "root package version feeds syft's Source.Version")
178+
assert.Equal(t, "FILE", rootPkg.PrimaryPackagePurpose, "FILE purpose routes syft through fileSource()")
179+
assert.True(t,
180+
strings.HasPrefix(string(rootPkg.PackageSPDXIdentifier), "DocumentRoot-"),
181+
"SPDXIdentifier prefix is what triggers syft's directory/file source classification")
182+
183+
// Per-source root must be re-parented under the new talos root via
184+
// CONTAINS, not via another DOCUMENT-DESCRIBES (which would create a
185+
// second root and break syft's single-root detection).
186+
var perSourceContained bool
187+
188+
for _, rel := range doc.Relationships {
189+
if rel.RefA.ElementRefID == roots[0] &&
190+
rel.Relationship == common.TypeRelationshipContains &&
191+
rel.RefB.ElementRefID == common.ElementID("talos-amd64-DocumentRoot-Directory-talos") {
192+
perSourceContained = true
193+
194+
break
195+
}
196+
}
197+
198+
assert.True(t, perSourceContained,
199+
"per-source root must be re-parented under the talos root via CONTAINS")
200+
}
201+
202+
// minimalSourceSPDX produces a syft-shaped SPDX 2.3 JSON document with one
203+
// DocumentRoot-Directory-<name> root, mirroring what hack/sbom.sh embeds
204+
// into the Talos initramfs.
205+
func minimalSourceSPDX(t *testing.T, sourceID, name, version string) []byte {
206+
t.Helper()
207+
208+
rootID := common.ElementID("DocumentRoot-Directory-" + name)
209+
210+
doc := &spdx.Document{
211+
SPDXVersion: spdx.Version,
212+
DataLicense: spdx.DataLicense,
213+
SPDXIdentifier: common.ElementID("DOCUMENT"),
214+
DocumentName: sourceID,
215+
DocumentNamespace: "https://anchore.com/syft/dir/" + sourceID,
216+
CreationInfo: &spdx.CreationInfo{
217+
Created: "2026-05-30T00:00:00Z",
218+
Creators: []common.Creator{
219+
{CreatorType: "Tool", Creator: "syft-test"},
220+
},
221+
},
222+
Packages: []*spdx.Package{
223+
{
224+
PackageSPDXIdentifier: rootID,
225+
PackageName: name,
226+
PackageVersion: version,
227+
PrimaryPackagePurpose: "FILE",
228+
PackageDownloadLocation: "NOASSERTION",
229+
},
230+
},
231+
Relationships: []*spdx.Relationship{
232+
{
233+
RefA: common.DocElementID{ElementRefID: common.ElementID("DOCUMENT")},
234+
RefB: common.DocElementID{ElementRefID: rootID},
235+
Relationship: common.TypeRelationshipDescribe,
236+
},
237+
},
238+
}
239+
240+
var buf bytes.Buffer
241+
242+
require.NoError(t, spdxjson.Write(doc, &buf))
243+
244+
return buf.Bytes()
245+
}

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)