mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
fix(sbom): improve logic for binding direct dependency to parent component (#8489)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user