From 16223e6dd7893fe578787658ceb876257483d404 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 27 Jul 2026 15:20:49 -0400 Subject: [PATCH] fix: consider vendored golang packages in module attribution (#5093) Signed-off-by: Keith Zantow --- syft/pkg/cataloger/golang/symbols.go | 53 +++++++++++++++---- syft/pkg/cataloger/golang/symbols_test.go | 62 +++++++++++++++++++++++ 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/syft/pkg/cataloger/golang/symbols.go b/syft/pkg/cataloger/golang/symbols.go index 3b0987f2e..cd6b8bdd3 100644 --- a/syft/pkg/cataloger/golang/symbols.go +++ b/syft/pkg/cataloger/golang/symbols.go @@ -16,6 +16,10 @@ import ( // mainPackage is the import path the linker assigns to the binary's main package. 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. type binarySymbol struct { // 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" // 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 -// synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a -// recognizable stdlib import path are dropped. +// synthetic "stdlib" package. Vendored packages carry a "vendor/" import-path prefix: such symbols match +// 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) { if len(symbols) == 0 { return nil, nil @@ -348,16 +357,17 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod attrPath = main.Path } - var best string - for _, modPath := range modulePaths { - if len(modPath) > len(best) && (attrPath == modPath || strings.HasPrefix(attrPath, modPath+"/")) { - best = modPath - } + best := findBestMatch(modulePaths, attrPath) + + // the vendor/ prefix is only retained when the owning module itself is named "vendor/..."; + // 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) if best == "" { - if importPath != mainPackage && isStandardImportPath(importPath) { + if importPath != mainPackage && isStandardImportPath(importPath) { // drop stdlib vendored packages stdlib[importPath] = append(stdlib[importPath], local) } continue @@ -379,11 +389,36 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod 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. // "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. 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 diff --git a/syft/pkg/cataloger/golang/symbols_test.go b/syft/pkg/cataloger/golang/symbols_test.go index 5d140a431..bff5a778c 100644 --- a/syft/pkg/cataloger/golang/symbols_test.go +++ b/syft/pkg/cataloger/golang/symbols_test.go @@ -16,6 +16,8 @@ func Test_moduleSymbols(t *testing.T) { deps := []*debug.Module{ {Path: "github.com/foo/bar"}, {Path: "github.com/foo/bar/v2"}, + {Path: "vendor/github.com/vendored/mod"}, + {Path: "github.com/vendored/mod"}, nil, } @@ -74,6 +76,63 @@ func Test_moduleSymbols(t *testing.T) { "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", symbols: []binarySymbol{ @@ -291,6 +350,9 @@ func Test_packagePathFromSymbolName(t *testing.T) { // module paths that begin with "go." are not compiler-generated {"go.uber.org/zap.(*Logger).Info", "go.uber.org/zap"}, {"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 { t.Run(test.name, func(t *testing.T) {