fix(sbom): improve logic for binding direct dependency to parent component (#8489)

This commit is contained in:
DmitriyLewen
2025-03-05 15:08:46 +06:00
committed by GitHub
parent 9892d040bc
commit 85cca8c07a
2 changed files with 172 additions and 4 deletions

View File

@@ -200,10 +200,17 @@ func (e *Encoder) encodePackages(parent *core.Component, result types.Result) {
components := make(map[string]*core.Component, len(result.Packages))
// PkgID => Package Component
dependencies := make(map[string]*core.Component, len(result.Packages))
var hasRoot bool
for i, pkg := range result.Packages {
pkgID := lo.Ternary(pkg.ID == "", fmt.Sprintf("%s@%s", pkg.Name, pkg.Version), pkg.ID)
result.Packages[i].ID = pkgID
// Check if the project has a root dependency
// TODO: Ideally, all projects should have a root dependency.
if pkg.Relationship == ftypes.RelationshipRoot {
hasRoot = true
}
// Convert packages to components
c := e.component(result, pkg)
components[pkg.Identifier.UID] = c
@@ -226,7 +233,7 @@ func (e *Encoder) encodePackages(parent *core.Component, result types.Result) {
c := components[pkg.Identifier.UID]
// Add a relationship between the parent and the package if needed
if e.belongToParent(pkg, parents) {
if e.belongToParent(pkg, parents, hasRoot) {
e.bom.AddRelationship(parent, c, core.RelationshipContains)
}
@@ -403,15 +410,15 @@ func (*Encoder) vulnerability(vuln types.DetectedVulnerability) core.Vulnerabili
}
// belongToParent determines if a package should be directly included in the parent based on its relationship and dependencies.
func (*Encoder) belongToParent(pkg ftypes.Package, parents map[string]ftypes.Packages) bool {
func (*Encoder) belongToParent(pkg ftypes.Package, parents map[string]ftypes.Packages, hasRoot bool) bool {
// Case 1: Relationship: known , DependsOn: known
// Packages with no parent are included in the parent
// - Relationship:
// - Root: true (it doesn't have a parent)
// - Workspace: false (it always has a parent)
// - Direct:
// - Under Root or Workspace: false (it always has a parent)
// - No parents: true (e.g., package-lock.json)
// - No root dependency in the project: true (e.g., poetry.lock)
// - Otherwise: false (Direct dependencies should belong to the root/workspace)
// - Indirect: false (it always has a parent)
// Case 2: Relationship: unknown, DependsOn: unknown (e.g., conan lockfile v2)
// All packages are included in the parent
@@ -420,6 +427,10 @@ func (*Encoder) belongToParent(pkg ftypes.Package, parents map[string]ftypes.Pac
// Case 4: Relationship: unknown, DependsOn: known (e.g., GoBinaries, OS packages)
// - Packages with parents: false. These packages are included in the packages from `parents` (e.g. GoBinaries deps and root package).
// - Packages without parents: true. These packages are included in the parent (e.g. OS packages without parents).
if pkg.Relationship == ftypes.RelationshipDirect {
return !hasRoot
}
return len(parents[pkg.ID]) == 0
}

View File

@@ -838,6 +838,163 @@ func TestEncoder_Encode(t *testing.T) {
},
wantVulns: make(map[uuid.UUID][]core.Vulnerability),
},
{
name: "direct package is also dependency",
report: types.Report{
SchemaVersion: 2,
ArtifactName: "test",
ArtifactType: artifact.TypeFilesystem,
Results: []types.Result{
{
Target: "poetry.lock",
Type: ftypes.Poetry,
Class: types.ClassLangPkg,
Packages: []ftypes.Package{
{
ID: "django@5.1.6",
Name: "django",
Version: "5.1.6",
Identifier: ftypes.PkgIdentifier{
UID: "69691e87e187021d",
PURL: &packageurl.PackageURL{
Type: packageurl.TypePyPi,
Name: "django",
Version: "5.1.6",
},
},
Relationship: ftypes.RelationshipDirect,
},
{
ID: "sentry-sdk@2.22.0",
Name: "sentry-sdk",
Version: "2.22.0",
Identifier: ftypes.PkgIdentifier{
UID: "7e53a15e8bec68ad",
PURL: &packageurl.PackageURL{
Type: packageurl.TypePyPi,
Name: "sentry-sdk",
Version: "2.22.0",
},
},
Relationship: ftypes.RelationshipDirect,
DependsOn: []string{
"django@5.1.6",
},
},
},
},
},
},
wantComponents: map[uuid.UUID]*core.Component{
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): {
Type: core.TypeFilesystem,
Name: "test",
Root: true,
Properties: []core.Property{
{
Name: core.PropertySchemaVersion,
Value: "2",
},
},
PkgIdentifier: ftypes.PkgIdentifier{
BOMRef: "3ff14136-e09f-4df9-80ea-000000000001",
},
},
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): {
Type: core.TypeApplication,
Name: "poetry.lock",
Properties: []core.Property{
{
Name: core.PropertyClass,
Value: "lang-pkgs",
},
{
Name: core.PropertyType,
Value: "poetry",
},
},
PkgIdentifier: ftypes.PkgIdentifier{
BOMRef: "3ff14136-e09f-4df9-80ea-000000000002",
},
},
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): {
Type: core.TypeLibrary,
Name: "django",
Version: "5.1.6",
SrcFile: "poetry.lock",
Properties: []core.Property{
{
Name: core.PropertyPkgID,
Value: "django@5.1.6",
},
{
Name: core.PropertyPkgType,
Value: "poetry",
},
},
PkgIdentifier: ftypes.PkgIdentifier{
UID: "69691e87e187021d",
PURL: &packageurl.PackageURL{
Type: packageurl.TypePyPi,
Name: "django",
Version: "5.1.6",
},
BOMRef: "pkg:pypi/django@5.1.6",
},
},
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): {
Type: core.TypeLibrary,
Name: "sentry-sdk",
Version: "2.22.0",
SrcFile: "poetry.lock",
Properties: []core.Property{
{
Name: core.PropertyPkgID,
Value: "sentry-sdk@2.22.0",
},
{
Name: core.PropertyPkgType,
Value: "poetry",
},
},
PkgIdentifier: ftypes.PkgIdentifier{
UID: "7e53a15e8bec68ad",
PURL: &packageurl.PackageURL{
Type: packageurl.TypePyPi,
Name: "sentry-sdk",
Version: "2.22.0",
},
BOMRef: "pkg:pypi/sentry-sdk@2.22.0",
},
},
},
wantRels: map[uuid.UUID][]core.Relationship{
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): {
{
Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"),
Type: core.RelationshipContains,
},
},
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): {
{
Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"),
Type: core.RelationshipContains,
},
{
Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"),
Type: core.RelationshipContains,
},
},
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): nil,
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): {
{
Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"),
Type: core.RelationshipDependsOn,
},
},
},
wantVulns: make(map[uuid.UUID][]core.Vulnerability),
},
{
name: "SBOM file",
report: types.Report{