Skip to content

Commit aa944cc

Browse files
authored
fix(sbom): merge in-graph and out-of-graph OS packages in scan results (#9194)
1 parent adfa879 commit aa944cc

2 files changed

Lines changed: 102 additions & 5 deletions

File tree

pkg/sbom/io/decode.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,14 @@ func (m *Decoder) addOrphanPkgs(sbom *types.SBOM) error {
391391
for _, pkgs := range osPkgMap {
392392
// TODO: mismatch between the OS and the packages should be rejected.
393393
// e.g. OS: debian, Packages: rpm
394-
sort.Sort(pkgs)
395-
sbom.Packages = append(sbom.Packages, ftypes.PackageInfo{Packages: pkgs})
394+
395+
// addOSPkgs creates exactly one PackageInfo with empty FilePath, so we can merge directly
396+
if len(sbom.Packages) == 0 {
397+
sbom.Packages = append(sbom.Packages, ftypes.PackageInfo{})
398+
}
399+
// Merge with existing PackageInfo (always sbom.Packages[0])
400+
sbom.Packages[0].Packages = append(sbom.Packages[0].Packages, pkgs...)
401+
sort.Sort(sbom.Packages[0].Packages)
396402

397403
break // Just take the first element
398404
}

pkg/sbom/io/decode_test.go

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,24 @@ var (
8787
},
8888
Licenses: []string{"GPL-2.0"},
8989
}
90+
91+
orphanedApkComponent = &core.Component{
92+
Type: core.TypeLibrary,
93+
Name: "orphaned-package",
94+
Version: "1.0.0",
95+
PkgIdentifier: ftypes.PkgIdentifier{
96+
PURL: &packageurl.PackageURL{
97+
Type: packageurl.TypeApk,
98+
Name: "orphaned-package",
99+
Version: "1.0.0",
100+
Qualifiers: packageurl.Qualifiers{
101+
{Key: "arch", Value: "aarch64"},
102+
{Key: "distro", Value: "wolfi-20230201"},
103+
},
104+
},
105+
},
106+
Licenses: []string{"MIT"},
107+
}
90108
)
91109

92110
func TestDecoder_Decode_OSPackages(t *testing.T) {
@@ -141,16 +159,17 @@ func TestDecoder_Decode_OSPackages(t *testing.T) {
141159
},
142160
},
143161
{
144-
name: "multiple OS packages without OS metadata should be included",
162+
name: "multiple OS packages without OS metadata should be included and merged",
145163
setupBOM: func() *core.BOM {
146164
bom := core.NewBOM(core.Options{})
147165
bom.SerialNumber = "test-multiple-no-os"
148166
bom.Version = 1
149167

150-
// Add multiple APK packages
168+
// Add multiple APK packages including orphaned package
151169
bom.AddComponent(apkToolsComponent)
152170
bom.AddComponent(busyboxComponent)
153171
bom.AddComponent(caCertificatesComponent)
172+
bom.AddComponent(orphanedApkComponent)
154173

155174
return bom
156175
},
@@ -197,6 +216,18 @@ func TestDecoder_Decode_OSPackages(t *testing.T) {
197216
PURL: caCertificatesComponent.PkgIdentifier.PURL,
198217
},
199218
},
219+
{
220+
ID: "orphaned-package@1.0.0",
221+
Name: "orphaned-package",
222+
Version: "1.0.0",
223+
Arch: "aarch64",
224+
SrcName: "orphaned-package",
225+
SrcVersion: "1.0.0",
226+
Licenses: []string{"MIT"},
227+
Identifier: ftypes.PkgIdentifier{
228+
PURL: orphanedApkComponent.PkgIdentifier.PURL,
229+
},
230+
},
200231
},
201232
},
202233
},
@@ -233,6 +264,66 @@ func TestDecoder_Decode_OSPackages(t *testing.T) {
233264
},
234265
},
235266
},
267+
{
268+
name: "mixed OS packages (in-graph and out-of-graph) should be merged",
269+
setupBOM: func() *core.BOM {
270+
bom := core.NewBOM(core.Options{})
271+
bom.SerialNumber = "test-mixed-os-packages"
272+
bom.Version = 1
273+
274+
// Add OS component
275+
bom.AddComponent(wolfiOSComponent)
276+
277+
// Add OS packages - busybox is connected to OS (in-graph)
278+
bom.AddComponent(busyboxComponent)
279+
// Add orphaned package not connected to OS (out-of-graph)
280+
bom.AddComponent(orphanedApkComponent)
281+
282+
// Create relationship between OS and busybox only
283+
bom.AddRelationship(wolfiOSComponent, busyboxComponent, core.RelationshipContains)
284+
// orphanedApkComponent is not connected to OS component
285+
286+
return bom
287+
},
288+
wantSBOM: types.SBOM{
289+
Metadata: types.Metadata{
290+
OS: &ftypes.OS{
291+
Family: ftypes.Wolfi,
292+
Name: "20230201",
293+
},
294+
},
295+
Packages: []ftypes.PackageInfo{
296+
{
297+
Packages: ftypes.Packages{
298+
{
299+
ID: "busybox@1.37.0-r42",
300+
Name: "busybox",
301+
Version: "1.37.0-r42",
302+
Arch: "aarch64",
303+
SrcName: "busybox",
304+
SrcVersion: "1.37.0-r42",
305+
Licenses: []string{"GPL-2.0-only"},
306+
Identifier: ftypes.PkgIdentifier{
307+
PURL: busyboxComponent.PkgIdentifier.PURL,
308+
},
309+
},
310+
{
311+
ID: "orphaned-package@1.0.0",
312+
Name: "orphaned-package",
313+
Version: "1.0.0",
314+
Arch: "aarch64",
315+
SrcName: "orphaned-package",
316+
SrcVersion: "1.0.0",
317+
Licenses: []string{"MIT"},
318+
Identifier: ftypes.PkgIdentifier{
319+
PURL: orphanedApkComponent.PkgIdentifier.PURL,
320+
},
321+
},
322+
},
323+
},
324+
},
325+
},
326+
},
236327
}
237328

238329
for _, tt := range tests {
@@ -252,7 +343,7 @@ func TestDecoder_Decode_OSPackages(t *testing.T) {
252343
tt.wantSBOM.BOM = gotSBOM.BOM
253344

254345
// Compare the entire SBOM structure
255-
assert.Equal(t, tt.wantSBOM, gotSBOM)
346+
assert.EqualExportedValues(t, tt.wantSBOM, gotSBOM)
256347
})
257348
}
258349
}

0 commit comments

Comments
 (0)