diff --git a/pkg/sbom/cyclonedx/testdata/happy/nested-packages-bom.json b/pkg/sbom/cyclonedx/testdata/happy/nested-packages-bom.json new file mode 100644 index 000000000000..fd7623c73028 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/nested-packages-bom.json @@ -0,0 +1,172 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:12346a7a-5819-43bf-9411-5c146304f023", + "version": 1, + "metadata": { + "timestamp": "2024-12-20T10:57:13+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.38.7-764-g30c7cb137" + } + ] + }, + "component": { + "bom-ref": "1cb40520-a22c-481f-ad77-6bc6960430c5", + "type": "application", + "name": "/test", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "4021d631-e242-4e69-8a93-928665810a27", + "type": "application", + "name": "foo/bar/test.elf", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "type": "library", + "name": "github.com/aquasecurity/go-pep440-version", + "version": "v0.0.0-20210121094942-22b2f8951d46", + "purl": "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "type": "library", + "name": "github.com/aquasecurity/go-version", + "version": "v0.0.0-20210121072130-637058cfe492", + "purl": "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/aquasecurity/test", + "type": "library", + "name": "github.com/aquasecurity/test", + "purl": "pkg:golang/github.com/aquasecurity/test", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "github.com/aquasecurity/test" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "type": "library", + "name": "golang.org/x/xerrors", + "version": "v0.0.0-20200804184101-5ec99f83aff1", + "purl": "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/stdlib@v1.15.2", + "type": "library", + "name": "stdlib", + "version": "v1.15.2", + "purl": "pkg:golang/stdlib@v1.15.2", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.15.2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + } + ], + "dependencies": [ + { + "ref": "1cb40520-a22c-481f-ad77-6bc6960430c5", + "dependsOn": [ + "4021d631-e242-4e69-8a93-928665810a27" + ] + }, + { + "ref": "4021d631-e242-4e69-8a93-928665810a27", + "dependsOn": [ + "pkg:golang/github.com/aquasecurity/test" + ] + }, + { + "ref": "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "dependsOn": [] + }, + { + "ref": "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "dependsOn": [] + }, + { + "ref": "pkg:golang/github.com/aquasecurity/test", + "dependsOn": [ + "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "pkg:golang/stdlib@v1.15.2" + ] + }, + { + "ref": "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "dependsOn": [] + }, + { + "ref": "pkg:golang/stdlib@v1.15.2", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/pkg/sbom/cyclonedx/unmarshal_test.go b/pkg/sbom/cyclonedx/unmarshal_test.go index 7008efb8e9eb..27a116a8e42c 100644 --- a/pkg/sbom/cyclonedx/unmarshal_test.go +++ b/pkg/sbom/cyclonedx/unmarshal_test.go @@ -668,6 +668,93 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { }, }, }, + { + name: "happy path for BOM with nested packages", + inputFile: "testdata/happy/nested-packages-bom.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: "gobinary", + FilePath: "foo/bar/test.elf", + Packages: ftypes.Packages{ + { + ID: "github.com/aquasecurity/test", + Name: "github.com/aquasecurity/test", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "test", + }, + BOMRef: "pkg:golang/github.com/aquasecurity/test", + }, + DependsOn: []string{ + "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "stdlib@v1.15.2", + }, + }, + { + ID: "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + Name: "github.com/aquasecurity/go-pep440-version", + Version: "v0.0.0-20210121094942-22b2f8951d46", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-pep440-version", + Version: "v0.0.0-20210121094942-22b2f8951d46", + }, + BOMRef: "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + }, + }, + { + ID: "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + Name: "github.com/aquasecurity/go-version", + Version: "v0.0.0-20210121072130-637058cfe492", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-version", + Version: "v0.0.0-20210121072130-637058cfe492", + }, + BOMRef: "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "golang.org/x", + Name: "xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + }, + BOMRef: "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + }, + }, + { + ID: "stdlib@v1.15.2", + Name: "stdlib", + Version: "v1.15.2", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Name: "stdlib", + Version: "v1.15.2", + }, + BOMRef: "pkg:golang/stdlib@v1.15.2", + }, + }, + }, + }, + }, + }, + }, { name: "happy path only os component", inputFile: "testdata/happy/os-only-bom.json", diff --git a/pkg/sbom/io/decode.go b/pkg/sbom/io/decode.go index 6cfc79a0871c..4920f08b7323 100644 --- a/pkg/sbom/io/decode.go +++ b/pkg/sbom/io/decode.go @@ -330,15 +330,7 @@ func (m *Decoder) parseSrcVersion(ctx context.Context, pkg *ftypes.Package, ver // addOSPkgs traverses relationships and adds OS packages func (m *Decoder) addOSPkgs(sbom *types.SBOM) { - var pkgs []ftypes.Package - for _, rel := range m.bom.Relationships()[m.osID] { - pkg, ok := m.pkgs[rel.Dependency] - if !ok { - continue - } - pkgs = append(pkgs, *pkg) - delete(m.pkgs, rel.Dependency) // Delete the added package - } + pkgs := m.traverseDependencies(m.osID) if len(pkgs) == 0 { return } @@ -348,18 +340,33 @@ func (m *Decoder) addOSPkgs(sbom *types.SBOM) { // addLangPkgs traverses relationships and adds language-specific packages func (m *Decoder) addLangPkgs(sbom *types.SBOM) { for id, app := range m.apps { - for _, rel := range m.bom.Relationships()[id] { - pkg, ok := m.pkgs[rel.Dependency] - if !ok { - continue - } - app.Packages = append(app.Packages, *pkg) - delete(m.pkgs, rel.Dependency) // Delete the added package - } + app.Packages = append(app.Packages, m.traverseDependencies(id)...) sbom.Applications = append(sbom.Applications, *app) } } +// traverseDependencies recursively retrieves all packages that the specified component depends on. +// It starts from the given component ID and traverses the dependency tree, collecting all +// dependent packages. The collected packages are removed from m.pkgs to prevent duplicate +// processing. This ensures that all dependencies, including transitive ones, are properly +// captured and associated with their parent component. +func (m *Decoder) traverseDependencies(id uuid.UUID) ftypes.Packages { + var pkgs ftypes.Packages + for _, rel := range m.bom.Relationships()[id] { + pkg, ok := m.pkgs[rel.Dependency] + if !ok { + continue + } + // Add the current package + pkgs = append(pkgs, *pkg) + delete(m.pkgs, rel.Dependency) // Delete the added package + + // Add the nested packages + pkgs = append(pkgs, m.traverseDependencies(rel.Dependency)...) + } + return pkgs +} + // addOrphanPkgs adds orphan packages. // Orphan packages are packages that are not related to any components. func (m *Decoder) addOrphanPkgs(ctx context.Context, sbom *types.SBOM) error {