fix(vex): use a separate visited set for each DFS path (#9760)

This commit is contained in:
DmitriyLewen
2025-12-01 14:02:39 +06:00
committed by GitHub
parent 15a5465ad3
commit c274f5b986
2 changed files with 79 additions and 10 deletions

View File

@@ -10,6 +10,7 @@ import (
"github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/sbom/core" "github.com/aquasecurity/trivy/pkg/sbom/core"
sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" sbomio "github.com/aquasecurity/trivy/pkg/sbom/io"
"github.com/aquasecurity/trivy/pkg/set"
"github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/uuid" "github.com/aquasecurity/trivy/pkg/uuid"
) )
@@ -181,35 +182,38 @@ func reachRoot(leaf *core.Component, components map[uuid.UUID]*core.Component, p
return false return false
} }
visited := make(map[uuid.UUID]bool)
// Use Depth First Search (DFS) // Use Depth First Search (DFS)
var dfs func(c *core.Component) bool var dfs func(c *core.Component, visited set.Set[uuid.UUID]) bool
dfs = func(c *core.Component) bool { dfs = func(c *core.Component, visited set.Set[uuid.UUID]) bool {
// Call the function with the current component and the leaf component // Call the function with the current component and the leaf component
switch { switch {
case notAffected(c, leaf): case notAffected(c, leaf):
return false return false
case c.Root: case c.Root:
return true return true
case lo.Every(lo.Keys(visited), parents[c.ID()]): case set.New[uuid.UUID](parents[c.ID()]...).Difference(visited).Size() == 0:
// Should never go here, since all components except the root must have at least one parent and be related to the root component. // Should never go here, since all components except the root must have at least one parent and be related to the root component.
// If it does, it means the component tree is not connected due to a bug in the SBOM generation. // If it does, it means the component tree is not connected due to a bug in the SBOM generation.
// In this case, so as not to filter out all the vulnerabilities accidentally, return true for fail-safe. // In this case, so as not to filter out all the vulnerabilities accidentally, return true for fail-safe.
return true return true
} }
visited[c.ID()] = true visited.Append(c.ID())
for _, parent := range parents[c.ID()] { for _, parent := range parents[c.ID()] {
if visited[parent] { if visited.Contains(parent) {
continue continue
} }
if dfs(components[parent]) {
// Each DFS path needs its own visited set,
// to avoid false positives in other paths
newVisited := visited.Clone()
if dfs(components[parent], newVisited) {
return true return true
} }
} }
return false return false
} }
return dfs(leaf) return dfs(leaf, set.New[uuid.UUID]())
} }

View File

@@ -70,6 +70,20 @@ var (
}, },
}, },
} }
baseFiles2Package = ftypes.Package{
ID: "base-files2@5.3",
Name: "base-files2",
Version: "5.3",
Identifier: ftypes.PkgIdentifier{
UID: "08",
PURL: &packageurl.PackageURL{
Type: packageurl.TypeDebian,
Namespace: "debian",
Name: "base-files2",
Version: "5.3",
},
},
}
goModulePackage = ftypes.Package{ goModulePackage = ftypes.Package{
ID: "github.com/aquasecurity/go-module@v1.0.0", ID: "github.com/aquasecurity/go-module@v1.0.0",
Name: "github.com/aquasecurity/go-module", Name: "github.com/aquasecurity/go-module",
@@ -572,7 +586,7 @@ repositories:
Sources: []vex.Source{ Sources: []vex.Source{
{ {
Type: vex.TypeFile, Type: vex.TypeFile,
FilePath: "testdata/openvex-multiple.json", FilePath: "testdata/openvex-oci.json",
}, },
}, },
}, },
@@ -585,6 +599,37 @@ repositories:
}), }),
}), }),
}, },
{
name: "check one parent from multiple dependency paths",
args: args{
// - oci:debian?tag=12
// - pkg:deb/debian/base-files@5.3
// - pkg:deb/debian/bash@5.3
// - pkg:deb/debian/base-files2@5.3
// - pkg:deb/debian/bash@5.3
report: imageReport([]types.Result{
bashPackagesResult(types.Result{
Vulnerabilities: []types.DetectedVulnerability{
vuln3,
},
}),
}),
opts: vex.Options{
Sources: []vex.Source{
{
Type: vex.TypeFile,
FilePath: "testdata/openvex-oci.json",
},
},
},
},
want: imageReport([]types.Result{
bashPackagesResult(types.Result{
Vulnerabilities: []types.DetectedVulnerability{},
ModifiedFindings: []types.ModifiedFinding{modifiedFinding(vuln3, vulnerableCodeNotInExecutePath, "testdata/openvex-oci.json")},
}),
}),
},
{ {
name: "unknown format", name: "unknown format",
args: args{ args: args{
@@ -705,6 +750,26 @@ func bashResult(result types.Result) types.Result {
return result return result
} }
func bashPackagesResult(result types.Result) types.Result {
result.Type = ftypes.Debian
result.Class = types.ClassOSPkg
bashPkg := clonePackage(bashPackage)
baseFilesPkg := clonePackage(baseFilesPackage)
baseFiles2Pkg := clonePackage(baseFiles2Package)
baseFilesPkg.DependsOn = []string{bashPkg.ID}
baseFiles2Pkg.DependsOn = []string{bashPkg.ID}
result.Packages = []ftypes.Package{
bashPkg,
baseFilesPkg,
baseFiles2Pkg,
}
return result
}
func infinityLoopOSPackagesResult(result types.Result) types.Result { func infinityLoopOSPackagesResult(result types.Result) types.Result {
result.Type = ftypes.Debian result.Type = ftypes.Debian
result.Class = types.ClassOSPkg result.Class = types.ClassOSPkg