8000 fix(sbom): improve logic for binding direct dependency to parent component by DmitriyLewen · Pull Request #8489 · aquasecurity/trivy · GitHub
[go: up one dir, main page]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Conversation

DmitriyLewen
Copy link
Contributor
@DmitriyLewen DmitriyLewen commented Mar 5, 2025

Description

This PR improves belongToParent logic for Direct dependencies:

Example:

{
  "ID": "django@5.1.6",
  "Name": "django",
  "Identifier": {
    "PURL": "pkg:pypi/django@5.1.6",
    "UID": "69691e87e187021d"
  },
  "Version": "5.1.6",
  "Relationship": "direct",
  "DependsOn": [
    "asgiref@3.8.1",
    "sqlparse@0.5.3",
    "tzdata@2025.1"
  ],
  "Layer": {}
},
{
  "ID": "sentry-sdk@2.22.0",
  "Name": "sentry-sdk",
  "Identifier": {
    "PURL": "pkg:pypi/sentry-sdk@2.22.0",
    "UID": "7e53a15e8bec68ad"
  },
  "Version": "2.22.0",
  "Relationship": "direct",
  "DependsOn": [
    "certifi@2025.1.31",
    "django@5.1.6",
    "urllib3@2.3.0"
  ],
  "Layer": {}
},

before:

{
  "ref": "59c637a8-4c7b-4c61-b68e-a8731ff3bb09", // Application component
  "dependsOn": [
    "pkg:pypi/sentry-sdk@2.22.0"
  ]
},
{
  "ref": "pkg:pypi/sentry-sdk@2.22.0",
  "dependsOn": [
    "pkg:pypi/certifi@2025.1.31",
    "pkg:pypi/django@5.1.6",
    "pkg:pypi/urllib3@2.3.0"
  ]
},

after:

{
  "ref": "285569c7-9e19-44e1-8bf0-906f4ef8d2a2",  // Application component
  "dependsOn": [
    "pkg:pypi/django@5.1.6",
    "pkg:pypi/sentry-sdk@2.22.0"
  ]
},
{
  "ref": "pkg:pypi/sentry-sdk@2.22.0",
  "dependsOn": [
    "pkg:pypi/certifi@2025.1.31",
    "pkg:pypi/django@5.1.6",
    "pkg:pypi/urllib3@2.3.0"
  ]
},

Related issues

Related PRs

Checklist

  • I've read the guidelines for contributing to this repository.
  • I've followed the conventions in the PR title.
  • I've added tests that prove my fix is effective or that my feature works.
  • I've updated the documentation with the relevant information (if needed).
  • I've added usage information (if the PR introduces new options)
  • I've included a "before" and "after" example to the description (if the PR is a user interface change).

@DmitriyLewen DmitriyLewen self-assigned this Mar 5, 2025
@DmitriyLewen DmitriyLewen marked this pull request as ready for review March 5, 2025 07:03
@DmitriyLewen DmitriyLewen requested a review from knqyf263 as a code owner March 5, 2025 07:03
Copy link
Collaborator
@knqyf263 knqyf263 left a comment

Choose a reason for hiding this comment

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

Does it work?

diff --git a/pkg/sbom/io/encode.go b/pkg/sbom/io/encode.go
index cf2c54648..8c9ea69a8 100644
--- a/pkg/sbom/io/encode.go
+++ b/pkg/sbom/io/encode.go
@@ -200,6 +200,7 @@ 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
@@ -219,6 +220,12 @@ func (e *Encoder) encodePackages(parent *core.Component, result types.Result) {
 		if vv := vulns[pkg.Identifier.UID]; vv != nil {
 			e.bom.AddVulnerabilities(c, vv)
 		}
+
+		// Check if the project has a root dependency
+		// TODO: Ideally, all projects should have a root dependency.
+		if pkg.Relationship == ftypes.RelationshipRoot {
+			hasRoot = true
+		}
 	}
 
 	// Build a dependency graph between packages
@@ -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,16 +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)
-	//             - There are parents, but they are not Root or Workspace: true (when indirect dependency was installed manually. cf. https://github.com/aquasecurity/trivy/issues/8488)
+	//             - 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
@@ -421,16 +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).
-	pkgParents := parents[pkg.ID]
-	if pkg.Relationship != ftypes.RelationshipDirect {
-		return len(pkgParents) == 0
+	if pkg.Relationship == ftypes.RelationshipDirect {
+		return !hasRoot
 	}
-
-	_, found := lo.Find(pkgParents, func(pkg ftypes.Package) bool {
-		return pkg.Relationship == ftypes.RelationshipRoot || pkg.Relationship == ftypes.RelationshipWorkspace
-	})
-
-	return !found
+	return len(parents[pkg.ID]) == 0
 }
 
 func filterProperties(props []core.Property) []core.Property {

@knqyf263 knqyf263 mentioned this pull request Mar 5, 2025
@DmitriyLewen
Copy link
Contributor Author
DmitriyLewen commented Mar 5, 2025

This seems better to me than my solution (we don't need to iterate over parents map for each package). Thanks!

@DmitriyLewen DmitriyLewen added this pull request to the merge queue Mar 5, 2025
Merged via the queue into aquasecurity:main with commit 85cca8c Mar 5, 2025
12 checks passed
@DmitriyLewen DmitriyLewen deleted the fix-sbom/handle-direct-indirect-dep branch March 5, 2025 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bug(sbom): in some cases, an application component doesn't contain a direct dependency
3 participants
0