Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions pkg/sbom/io/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,19 @@ func (m *Decoder) addOrphanPkgs(sbom *types.SBOM) error {
for _, pkgs := range osPkgMap {
// TODO: mismatch between the OS and the packages should be rejected.
// e.g. OS: debian, Packages: rpm
sort.Sort(pkgs)
sbom.Packages = append(sbom.Packages, ftypes.PackageInfo{Packages: pkgs})

// Find existing PackageInfo with empty FilePath to merge with
if _, idx, found := lo.FindIndexOf(sbom.Packages, func(pkg ftypes.PackageInfo) bool {
return pkg.FilePath == ""
}); found {
// Merge with existing PackageInfo
sbom.Packages[idx].Packages = append(sbom.Packages[idx].Packages, pkgs...)
sort.Sort(sbom.Packages[idx].Packages)
} else {
// Create new PackageInfo
sort.Sort(pkgs)
sbom.Packages = append(sbom.Packages, ftypes.PackageInfo{Packages: pkgs})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to use this difficalt logic?

I understand you idea - detected OS packages don't have filepath (i mean PackageInfo struct):

trivy/pkg/sbom/io/decode.go

Lines 332 to 338 in e76c7ee

func (m *Decoder) addOSPkgs(sbom *types.SBOM) {
pkgs := m.traverseDependencies(m.osID)
if len(pkgs) == 0 {
return
}
sbom.Packages = []ftypes.PackageInfo{{Packages: pkgs}}
}

But in addOrphanPkgs function sbom.Packages always contains 0 or 1 element.
And we check only first iteration of the osPkgMap loop.

break // Just take the first element

It means that we don't need to check filePath and we can just add orphan packages into sbom.Packages[0].

e.g.:

Suggested change
if _, idx, found := lo.FindIndexOf(sbom.Packages, func(pkg ftypes.PackageInfo) bool {
return pkg.FilePath == ""
}); found {
// Merge with existing PackageInfo
sbom.Packages[idx].Packages = append(sbom.Packages[idx].Packages, pkgs...)
sort.Sort(sbom.Packages[idx].Packages)
} else {
// Create new PackageInfo
sort.Sort(pkgs)
sbom.Packages = append(sbom.Packages, ftypes.PackageInfo{Packages: pkgs})
}
if len(sbom.Packages) == 0 {
sbom.Packages = []ftypes.PackageInfo{{}}
}
sbom.Packages[0].Packages = append(sbom.Packages[0].Packages, pkgs...)
sort.Sort(sbom.Packages[0].Packages)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a possibility that OS package file paths may be filled in the future, and I'm concerned that writing logic that's easily broken when other parts are changed could lead to bugs. However, as things stand now, you're right, so I simplified it.
1f0e04e


break // Just take the first element
}
Expand Down
97 changes: 94 additions & 3 deletions pkg/sbom/io/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ var (
},
Licenses: []string{"GPL-2.0"},
}

orphanedApkComponent = &core.Component{
Type: core.TypeLibrary,
Name: "orphaned-package",
Version: "1.0.0",
PkgIdentifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeApk,
Name: "orphaned-package",
Version: "1.0.0",
Qualifiers: packageurl.Qualifiers{
{Key: "arch", Value: "aarch64"},
{Key: "distro", Value: "wolfi-20230201"},
},
},
},
Licenses: []string{"MIT"},
}
)

func TestDecoder_Decode_OSPackages(t *testing.T) {
Expand Down Expand Up @@ -141,16 +159,17 @@ func TestDecoder_Decode_OSPackages(t *testing.T) {
},
},
{
name: "multiple OS packages without OS metadata should be included",
name: "multiple OS packages without OS metadata should be included and merged",
setupBOM: func() *core.BOM {
bom := core.NewBOM(core.Options{})
bom.SerialNumber = "test-multiple-no-os"
bom.Version = 1

// Add multiple APK packages
// Add multiple APK packages including orphaned package
bom.AddComponent(apkToolsComponent)
bom.AddComponent(busyboxComponent)
bom.AddComponent(caCertificatesComponent)
bom.AddComponent(orphanedApkComponent)

return bom
},
Expand Down Expand Up @@ -197,6 +216,18 @@ func TestDecoder_Decode_OSPackages(t *testing.T) {
PURL: caCertificatesComponent.PkgIdentifier.PURL,
},
},
{
ID: "[email protected]",
Name: "orphaned-package",
Version: "1.0.0",
Arch: "aarch64",
SrcName: "orphaned-package",
SrcVersion: "1.0.0",
Licenses: []string{"MIT"},
Identifier: ftypes.PkgIdentifier{
PURL: orphanedApkComponent.PkgIdentifier.PURL,
},
},
},
},
},
Expand Down Expand Up @@ -233,6 +264,66 @@ func TestDecoder_Decode_OSPackages(t *testing.T) {
},
},
},
{
name: "mixed OS packages (in-graph and out-of-graph) should be merged",
setupBOM: func() *core.BOM {
bom := core.NewBOM(core.Options{})
bom.SerialNumber = "test-mixed-os-packages"
bom.Version = 1

// Add OS component
bom.AddComponent(wolfiOSComponent)

// Add OS packages - busybox is connected to OS (in-graph)
bom.AddComponent(busyboxComponent)
// Add orphaned package not connected to OS (out-of-graph)
bom.AddComponent(orphanedApkComponent)

// Create relationship between OS and busybox only
bom.AddRelationship(wolfiOSComponent, busyboxComponent, core.RelationshipContains)
// orphanedApkComponent is not connected to OS component

return bom
},
wantSBOM: types.SBOM{
Metadata: types.Metadata{
OS: &ftypes.OS{
Family: ftypes.Wolfi,
Name: "20230201",
},
},
Packages: []ftypes.PackageInfo{
{
Packages: ftypes.Packages{
{
ID: "[email protected]",
Name: "busybox",
Version: "1.37.0-r42",
Arch: "aarch64",
SrcName: "busybox",
SrcVersion: "1.37.0-r42",
Licenses: []string{"GPL-2.0-only"},
Identifier: ftypes.PkgIdentifier{
PURL: busyboxComponent.PkgIdentifier.PURL,
},
},
{
ID: "[email protected]",
Name: "orphaned-package",
Version: "1.0.0",
Arch: "aarch64",
SrcName: "orphaned-package",
SrcVersion: "1.0.0",
Licenses: []string{"MIT"},
Identifier: ftypes.PkgIdentifier{
PURL: orphanedApkComponent.PkgIdentifier.PURL,
},
},
},
},
},
},
},
}

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

// Compare the entire SBOM structure
assert.Equal(t, tt.wantSBOM, gotSBOM)
assert.EqualExportedValues(t, tt.wantSBOM, gotSBOM)
})
}
}
Loading