feat: optionally capture golang binary symbols (#4988)

---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Christopher Angelo Phillips
2026-07-13 10:30:23 -04:00
committed by GitHub
co-authored by Alex Goodman
parent 3252b6f806
commit 1dcac54b0e
23 changed files with 5414 additions and 45 deletions
+47 -4
View File
@@ -13,6 +13,7 @@ import (
"runtime/debug"
"slices"
"strings"
"sync"
"time"
"golang.org/x/mod/module"
@@ -20,6 +21,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
@@ -49,15 +51,45 @@ const devel = "(devel)"
type goBinaryCataloger struct {
licenseResolver goLicenseResolver
mainModuleVersion MainModuleVersionConfig
symbolScope cataloging.SymbolScope
// stdlibSymbols holds the standard-library function symbols discovered per binary (keyed by the
// binary's location), populated during parsing and consumed by stdlibProcessor when it builds the
// synthetic "stdlib" package. Guarded by stdlibSymbolsMu because parsers run concurrently.
stdlibSymbols map[file.Coordinates][]string
stdlibSymbolsMu sync.Mutex
}
func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger {
return &goBinaryCataloger{
licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts),
mainModuleVersion: opts.MainModuleVersion,
symbolScope: opts.CaptureSymbols,
stdlibSymbols: make(map[file.Coordinates][]string),
}
}
// recordStdlibSymbols merges the standard-library symbols discovered for a binary location so the
// stdlib processor can attach them to the synthetic stdlib package.
func (c *goBinaryCataloger) recordStdlibSymbols(coord file.Coordinates, symbols []string) {
if len(symbols) == 0 {
return
}
c.stdlibSymbolsMu.Lock()
defer c.stdlibSymbolsMu.Unlock()
merged := slices.Concat(c.stdlibSymbols[coord], symbols)
slices.Sort(merged)
c.stdlibSymbols[coord] = slices.Compact(merged)
}
// stdlibSymbolsFor returns the standard-library symbols recorded for a binary location. It returns a copy
// so callers cannot alias (and later mutate or race on) the map's internal slice.
func (c *goBinaryCataloger) stdlibSymbolsFor(coord file.Coordinates) []string {
c.stdlibSymbolsMu.Lock()
defer c.stdlibSymbolsMu.Unlock()
return slices.Clone(c.stdlibSymbols[coord])
}
// parseGoBinary catalogs packages found in the "buildinfo" section of a binary built by the go compiler.
func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
@@ -68,7 +100,7 @@ func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Res
}
defer internal.CloseAndLogError(reader.ReadCloser, reader.RealPath)
mods, errs := scanFile(reader.Location, unionReader)
mods, errs := scanFile(reader.Location, unionReader, c.symbolScope != cataloging.SymbolScopeNone)
var rels []artifact.Relationship
for _, mod := range mods {
@@ -128,6 +160,15 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
mod.Main = createMainModuleFromPath(mod)
}
symbolsByModule, stdlibSymbols := moduleSymbols(mod.symbols, &mod.Main, mod.Deps)
c.recordStdlibSymbols(location.Coordinates, stdlibSymbols)
if c.symbolScope != cataloging.SymbolScopeAll {
// only the "all" scope attaches per-module symbols; for the "stdlib" scope we keep just the
// recorded stdlib symbols. nil map lookups below then yield nil symbol lists for each module.
symbolsByModule = nil
}
var pkgs []pkg.Package
for _, dep := range mod.Deps {
if dep == nil {
@@ -147,6 +188,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
nil,
mod.cryptoSettings,
experiments,
symbolsByModule[dep.Path],
)
p := c.newGoBinaryPackage(
@@ -164,7 +206,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
return nil, pkgs
}
main := c.makeGoMainPackage(ctx, resolver, mod, arch, location, reader)
main := c.makeGoMainPackage(ctx, resolver, mod, arch, location, reader, symbolsByModule[mod.Main.Path])
return &main, pkgs
}
@@ -179,7 +221,7 @@ func missingMainModule(mod *extendedBuildInfo) bool {
return mod.Main == moduleFromPartialPackageBuild
}
func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package {
func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser, symbols []string) pkg.Package {
gbs := getBuildSettings(mod.Settings)
lics := c.licenseResolver.getLicenses(ctx, resolver, mod.Main.Path, mod.Main.Version)
gover, experiments := getExperimentsFromVersion(mod.GoVersion)
@@ -192,6 +234,7 @@ func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file
gbs,
mod.cryptoSettings,
experiments,
symbols,
)
if mod.Main.Version == devel {
@@ -387,7 +430,7 @@ func getExperimentsFromVersion(version string) (string, []string) {
version, rest, ok := strings.Cut(version, " ")
if ok {
// Assume they may add more non-version chunks in the future, so only look for "X:".
for _, chunk := range strings.Split(rest, " ") {
for chunk := range strings.SplitSeq(rest, " ") {
if strings.HasPrefix(rest, "X:") {
csv := strings.TrimPrefix(chunk, "X:")
experiments = append(experiments, strings.Split(csv, ",")...)