mirror of
https://github.com/anchore/syft.git
synced 2026-08-19 16:48:27 +02:00
fix: consider vendored golang packages in module attribution (#5093)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
parent
1286689419
commit
16223e6dd7
@ -16,6 +16,10 @@ import (
|
|||||||
// mainPackage is the import path the linker assigns to the binary's main package.
|
// mainPackage is the import path the linker assigns to the binary's main package.
|
||||||
const mainPackage = "main"
|
const mainPackage = "main"
|
||||||
|
|
||||||
|
// vendorPrefix is the import-path prefix carried by vendored packages (e.g. from `go mod vendor`, or the
|
||||||
|
// standard library's own vendored dependencies such as "vendor/golang.org/x/net/http2").
|
||||||
|
const vendorPrefix = "vendor/"
|
||||||
|
|
||||||
// binarySymbol represents a single function symbol extracted from a go binary's pclntab.
|
// binarySymbol represents a single function symbol extracted from a go binary's pclntab.
|
||||||
type binarySymbol struct {
|
type binarySymbol struct {
|
||||||
// packagePath is the import path of the package that owns the symbol (e.g. "github.com/foo/bar/internal/baz")
|
// packagePath is the import path of the package that owns the symbol (e.g. "github.com/foo/bar/internal/baz")
|
||||||
@ -319,8 +323,13 @@ func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) {
|
|||||||
// "(*T).M"). Symbols from the "main" package are attributed to the main module and keyed by the "main"
|
// "(*T).M"). Symbols from the "main" package are attributed to the main module and keyed by the "main"
|
||||||
// import path the linker assigns. Standard-library symbols (which belong to no module) are collected
|
// import path the linker assigns. Standard-library symbols (which belong to no module) are collected
|
||||||
// separately and returned as the second value, grouped by import path, so they can be attached to the
|
// separately and returned as the second value, grouped by import path, so they can be attached to the
|
||||||
// synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a
|
// synthetic "stdlib" package. Vendored packages carry a "vendor/" import-path prefix: such symbols match
|
||||||
// recognizable stdlib import path are dropped.
|
// both modules whose own path carries the prefix and modules without it (matched with the prefix trimmed),
|
||||||
|
// and the prefix is retained in the group key only when the owning module itself is named "vendor/...".
|
||||||
|
// Module-less vendored packages (the stdlib's own vendored dependencies, e.g.
|
||||||
|
// "vendor/golang.org/x/net/http2") are dropped: stdlib vulnerabilities seem to be reported against the public
|
||||||
|
// packages (e.g. "crypto/x509"), not the vendored internal copies. Compiler/runtime-internal symbols that
|
||||||
|
// are neither module-owned nor a recognizable stdlib import path are likewise dropped.
|
||||||
func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string]map[string][]string, stdlib map[string][]string) {
|
func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string]map[string][]string, stdlib map[string][]string) {
|
||||||
if len(symbols) == 0 {
|
if len(symbols) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -348,16 +357,17 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
|
|||||||
attrPath = main.Path
|
attrPath = main.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
var best string
|
best := findBestMatch(modulePaths, attrPath)
|
||||||
for _, modPath := range modulePaths {
|
|
||||||
if len(modPath) > len(best) && (attrPath == modPath || strings.HasPrefix(attrPath, modPath+"/")) {
|
// the vendor/ prefix is only retained when the owning module itself is named "vendor/...";
|
||||||
best = modPath
|
// in all other cases (non-vendored modules and vendored stdlib) the recorded import path is trimmed
|
||||||
}
|
if !strings.HasPrefix(best, vendorPrefix) {
|
||||||
|
importPath = strings.TrimPrefix(importPath, vendorPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
local := localSymbolName(sym.name, importPath)
|
local := localSymbolName(sym.name, importPath)
|
||||||
if best == "" {
|
if best == "" {
|
||||||
if importPath != mainPackage && isStandardImportPath(importPath) {
|
if importPath != mainPackage && isStandardImportPath(importPath) { // drop stdlib vendored packages
|
||||||
stdlib[importPath] = append(stdlib[importPath], local)
|
stdlib[importPath] = append(stdlib[importPath], local)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@ -379,11 +389,36 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
|
|||||||
return results, stdlib
|
return results, stdlib
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findBestMatch returns the module path that owns the given package path taking into account vendor/ prefixes: a vendor/ import path
|
||||||
|
// will take precedence and continue to match, non-vendored imports will match against their vendored equivalent
|
||||||
|
func findBestMatch(modulePaths []string, importPath string) string {
|
||||||
|
trimmedPath, trimmed := strings.CutPrefix(importPath, vendorPrefix)
|
||||||
|
candidatePaths := []string{importPath, trimmedPath}
|
||||||
|
if !trimmed {
|
||||||
|
candidatePaths = candidatePaths[:1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var best string
|
||||||
|
for _, candidate := range candidatePaths {
|
||||||
|
for _, modPath := range modulePaths {
|
||||||
|
// the prefix must end at a path-segment boundary in candidate, so that e.g. the module
|
||||||
|
// "github.com/foo/bar" matches the package "github.com/foo/bar/baz" but not "github.com/foo/barbaz"
|
||||||
|
if len(modPath) > len(best) && strings.HasPrefix(candidate, modPath) && (candidate == modPath || candidate[len(modPath)] == '/') {
|
||||||
|
best = modPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
// localSymbolName strips the owning package's import path prefix from a fully qualified symbol name, e.g.
|
// localSymbolName strips the owning package's import path prefix from a fully qualified symbol name, e.g.
|
||||||
// "github.com/foo/bar.(*T).M" with import path "github.com/foo/bar" becomes "(*T).M". The name is returned
|
// "github.com/foo/bar.(*T).M" with import path "github.com/foo/bar" becomes "(*T).M". The name is returned
|
||||||
// unchanged when it does not carry the expected prefix.
|
// unchanged when it does not carry the expected prefix.
|
||||||
func localSymbolName(name, importPath string) string {
|
func localSymbolName(name, importPath string) string {
|
||||||
if importPath != "" && strings.HasPrefix(name, importPath+".") {
|
if !strings.HasPrefix(importPath, vendorPrefix) {
|
||||||
|
name = strings.TrimPrefix(name, vendorPrefix)
|
||||||
|
}
|
||||||
|
if len(importPath) < len(name) && strings.HasPrefix(name, importPath) && name[len(importPath)] == '.' {
|
||||||
return name[len(importPath)+1:]
|
return name[len(importPath)+1:]
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
|
|||||||
@ -16,6 +16,8 @@ func Test_moduleSymbols(t *testing.T) {
|
|||||||
deps := []*debug.Module{
|
deps := []*debug.Module{
|
||||||
{Path: "github.com/foo/bar"},
|
{Path: "github.com/foo/bar"},
|
||||||
{Path: "github.com/foo/bar/v2"},
|
{Path: "github.com/foo/bar/v2"},
|
||||||
|
{Path: "vendor/github.com/vendored/mod"},
|
||||||
|
{Path: "github.com/vendored/mod"},
|
||||||
nil,
|
nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,6 +76,63 @@ func Test_moduleSymbols(t *testing.T) {
|
|||||||
"runtime": {"main"},
|
"runtime": {"main"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "vendored packages match non-vendored modules and are recorded under the canonical import path",
|
||||||
|
symbols: []binarySymbol{
|
||||||
|
{packagePath: "vendor/github.com/foo/bar", name: "vendor/github.com/foo/bar.Parse"},
|
||||||
|
{packagePath: "vendor/github.com/foo/bar/internal/util", name: "vendor/github.com/foo/bar/internal/util.(*Helper).Do"},
|
||||||
|
},
|
||||||
|
expected: map[string]map[string][]string{
|
||||||
|
"github.com/foo/bar": {
|
||||||
|
"github.com/foo/bar": {"Parse"},
|
||||||
|
"github.com/foo/bar/internal/util": {"(*Helper).Do"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stdlib-vendored packages are dropped",
|
||||||
|
symbols: []binarySymbol{
|
||||||
|
{packagePath: "vendor/golang.org/x/net/http2", name: "vendor/golang.org/x/net/http2.(*Framer).ReadFrame"},
|
||||||
|
},
|
||||||
|
expected: map[string]map[string][]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "modules whose own path carries the vendor/ prefix match exactly and win over the trimmed match",
|
||||||
|
symbols: []binarySymbol{
|
||||||
|
{packagePath: "vendor/github.com/vendored/mod", name: "vendor/github.com/vendored/mod.Run"},
|
||||||
|
{packagePath: "github.com/vendored/mod", name: "github.com/vendored/mod.Run"},
|
||||||
|
},
|
||||||
|
expected: map[string]map[string][]string{
|
||||||
|
"vendor/github.com/vendored/mod": {
|
||||||
|
"vendor/github.com/vendored/mod": {"Run"},
|
||||||
|
},
|
||||||
|
"github.com/vendored/mod": {
|
||||||
|
"github.com/vendored/mod": {"Run"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "vendored and unvendored symbols for the same package are merged and deduplicated under the canonical import path",
|
||||||
|
symbols: []binarySymbol{
|
||||||
|
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
|
||||||
|
{packagePath: "vendor/github.com/foo/bar", name: "vendor/github.com/foo/bar.Parse"},
|
||||||
|
},
|
||||||
|
expected: map[string]map[string][]string{
|
||||||
|
"github.com/foo/bar": {
|
||||||
|
"github.com/foo/bar": {"Parse"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// "github.com/foo/barbaz" shares a string prefix with the "github.com/foo/bar" module but not a
|
||||||
|
// path-segment boundary, so it must not be attributed to it (and, having a dotted first element,
|
||||||
|
// it is not a stdlib path either — it is dropped)
|
||||||
|
name: "module paths only match the package path at a path-segment boundary",
|
||||||
|
symbols: []binarySymbol{
|
||||||
|
{packagePath: "github.com/foo/barbaz", name: "github.com/foo/barbaz.Parse"},
|
||||||
|
},
|
||||||
|
expected: map[string]map[string][]string{},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "duplicate symbols are deduplicated",
|
name: "duplicate symbols are deduplicated",
|
||||||
symbols: []binarySymbol{
|
symbols: []binarySymbol{
|
||||||
@ -291,6 +350,9 @@ func Test_packagePathFromSymbolName(t *testing.T) {
|
|||||||
// module paths that begin with "go." are not compiler-generated
|
// module paths that begin with "go." are not compiler-generated
|
||||||
{"go.uber.org/zap.(*Logger).Info", "go.uber.org/zap"},
|
{"go.uber.org/zap.(*Logger).Info", "go.uber.org/zap"},
|
||||||
{"go.opentelemetry.io/otel.Tracer", "go.opentelemetry.io/otel"},
|
{"go.opentelemetry.io/otel.Tracer", "go.opentelemetry.io/otel"},
|
||||||
|
// vendored packages retain their "vendor/" import-path prefix in symbol names
|
||||||
|
{"vendor/golang.org/x/net/http2.(*Framer).ReadFrame", "vendor/golang.org/x/net/http2"},
|
||||||
|
{"vendor/github.com/foo/bar.Parse", "vendor/github.com/foo/bar"},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user