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
parent 3252b6f806
commit 1dcac54b0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 5414 additions and 45 deletions

View File

@ -198,7 +198,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings).
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
).
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)),
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)).
WithCaptureSymbols(cfg.Golang.CaptureSymbols),
JavaScript: javascript.DefaultCatalogerConfig().
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).

View File

@ -4,6 +4,7 @@ import (
"strings"
"github.com/anchore/clio"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
)
@ -17,10 +18,12 @@ type golangConfig struct {
NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"`
MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"`
UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
CaptureSymbols cataloging.SymbolScope `json:"capture-symbols" yaml:"capture-symbols" mapstructure:"capture-symbols"`
}
var _ interface {
clio.FieldDescriber
clio.PostLoader
} = (*golangConfig)(nil)
func (o *golangConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
@ -39,12 +42,19 @@ if unset this defaults to $GONOPROXY`)
always show (devel) as the version. Use these options to control heuristics to guess
a more accurate version from the binary.`)
descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`)
descriptions.Add(&o.CaptureSymbols, `capture function symbols from the binary symbol table (pclntab). valid values are:
"none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)`)
descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`)
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
(e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`)
descriptions.Add(&o.MainModuleVersion.FromContents, `search for semver-like strings in the binary contents`)
}
func (o *golangConfig) PostLoad() error {
o.CaptureSymbols = o.CaptureSymbols.Parse()
return nil
}
type golangMainModuleVersionConfig struct {
FromLDFlags bool `json:"from-ld-flags" yaml:"from-ld-flags" mapstructure:"from-ld-flags"`
FromContents bool `json:"from-contents" yaml:"from-contents" mapstructure:"from-contents"`
@ -67,5 +77,6 @@ func defaultGolangConfig() golangConfig {
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
},
UsePackagesLib: nil, // this defaults to true, which is the API default
CaptureSymbols: def.CaptureSymbols,
}
}

View File

@ -0,0 +1,57 @@
package options
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/cataloging"
)
func Test_golangConfig_PostLoad(t *testing.T) {
tests := []struct {
name string
cfg golangConfig
expected cataloging.SymbolScope
wantErr assert.ErrorAssertionFunc
}{
{
name: "normalize all",
cfg: golangConfig{CaptureSymbols: "all"},
expected: cataloging.SymbolScopeAll,
},
{
name: "normalize stdlib",
cfg: golangConfig{CaptureSymbols: "stdlib"},
expected: cataloging.SymbolScopeStdlib,
},
{
name: "empty defaults to none",
cfg: golangConfig{CaptureSymbols: ""},
expected: cataloging.SymbolScopeNone,
},
{
name: "invalid value defaults to none",
cfg: golangConfig{CaptureSymbols: "bogus"},
expected: cataloging.SymbolScopeNone,
},
{
name: "boolean spellings default to none",
cfg: golangConfig{CaptureSymbols: "true"},
expected: cataloging.SymbolScopeNone,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = assert.NoError
}
err := tt.cfg.PostLoad()
tt.wantErr(t, err)
if err != nil {
return
}
assert.Equal(t, tt.expected, tt.cfg.CaptureSymbols)
})
}
}

View File

@ -14,6 +14,8 @@ application: # AUTO-GENERATED - application-level config keys
description: treat DLL claims or on-disk evidence for child packages as DLL claims or on-disk evidence for any parent package
- key: dotnet.relax-dll-claims-when-bundling-detected
description: show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)
- key: golang.capture-symbols
description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)'
- key: golang.local-mod-cache-dir
description: specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod
- key: golang.local-vendor-dir

View File

@ -3,7 +3,7 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.1.8"
JSONSchemaVersion = "16.1.9"
// Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
@ -15,4 +15,5 @@ const (
// 16.1.6 - add Dependencies to ElixirMixLockEntry metadata
// 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger
// 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support
// 16.1.9 - add Symbols to GolangBinaryBuildinfoEntry metadata
)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.1.8/document",
"$id": "anchore.io/schema/syft/json/16.1.9/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -1653,6 +1653,13 @@
},
"type": "array",
"description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")."
},
"symbols": {
"items": {
"type": "string"
},
"type": "array",
"description": "Symbols are the fully qualified function symbols from this module that are compiled into the binary\n(e.g., \"github.com/foo/bar.(*Type).Method\"), extracted from the binary symbol table (pclntab).\nPopulated only when the golang cataloger's capture-symbols scope covers this package: the \"all\" scope\npopulates every module package plus the synthetic stdlib package, while the \"stdlib\" scope populates\nonly the stdlib package."
}
},
"type": "object",
@ -4394,39 +4401,47 @@
"items": {
"type": "string"
},
"type": "array"
"type": "array",
"description": "Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest)."
},
"documentation": {
"type": "string"
"type": "string",
"description": "Documentation is the URL to the package's documentation."
},
"full-version": {
"type": "string"
"type": "string",
"description": "FullVersion is the complete version string including the port-version suffix (e.g. \"1.2.3#2\")."
},
"version": {
"type": "string"
"type": "string",
"description": "Version is the upstream package version without the port-version suffix (e.g. \"1.2.3\")."
},
"port-version": {
"type": "integer"
"type": "integer",
"description": "PortVersion is the vcpkg-specific packaging revision for a given upstream version."
},
"maintainers": {
"items": {
"type": "string"
},
"type": "array"
"type": "array",
"description": "Maintainers are the people responsible for maintaining the vcpkg port."
},
"name": {
"type": "string"
"type": "string",
"description": "Name is the package name as declared in the manifest."
},
"supports": {
"type": "string"
"type": "string",
"description": "Supports is the platform expression describing which triplets the package can be built for (e.g. \"!windows\")."
},
"registry": {
"$ref": "#/$defs/VcpkgRegistryEntry",
"description": "to show where it came from"
"description": "Registry indicates where the package definition came from."
},
"triplet": {
"type": "string",
"description": "found by looking at build folder to find target. ex. \"x64-linux\""
"description": "Triplet is the build target discovered from the build folder (e.g. \"x64-linux\")."
}
},
"type": "object",
@ -4436,7 +4451,7 @@
"port-version",
"name"
],
"description": "used for metadata."
"description": "VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package."
},
"VcpkgRegistryEntry": {
"properties": {

View File

@ -0,0 +1,28 @@
package cataloging
import "strings"
// SymbolScope controls which packages get function symbols (from a binary's symbol table) attached to their metadata.
type SymbolScope string
const (
// SymbolScopeNone disables symbol capture entirely.
SymbolScopeNone SymbolScope = "none"
// SymbolScopeStdlib captures symbols only for the synthetic "stdlib" package, leaving module packages without symbols.
SymbolScopeStdlib SymbolScope = "stdlib"
// SymbolScopeAll captures symbols for all module packages as well as the synthetic "stdlib" package.
SymbolScopeAll SymbolScope = "all"
)
// Parse normalizes a SymbolScope, treating empty (unset) and unrecognized values as SymbolScopeNone.
func (s SymbolScope) Parse() SymbolScope {
switch strings.ToLower(strings.TrimSpace(string(s))) {
case string(SymbolScopeAll):
return SymbolScopeAll
case string(SymbolScopeStdlib):
return SymbolScopeStdlib
}
return SymbolScopeNone
}

View File

@ -0,0 +1,30 @@
package cataloging
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_SymbolScope_Parse(t *testing.T) {
tests := []struct {
input string
expected SymbolScope
}{
{"all", SymbolScopeAll},
{"ALL", SymbolScopeAll},
{" all ", SymbolScopeAll},
{"stdlib", SymbolScopeStdlib},
{"Stdlib", SymbolScopeStdlib},
{"none", SymbolScopeNone},
{"", SymbolScopeNone},
{"true", SymbolScopeNone},
{"false", SymbolScopeNone},
{"bogus", SymbolScopeNone},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
assert.Equal(t, test.expected, SymbolScope(test.input).Parse())
})
}
}

View File

@ -24,6 +24,9 @@ configs: # AUTO-GENERATED - config structs and their fields
- key: NoProxy
description: NoProxy is a list of glob patterns that match go module names that should not be fetched from the go proxy. When not set, syft will use the GOPRIVATE and GONOPROXY env vars.
app_key: golang.no-proxy
- key: CaptureSymbols
description: CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib).
app_key: golang.capture-symbols
catalogers:
- ecosystem: go # MANUAL
name: go-module-binary-cataloger # AUTO-GENERATED

View File

@ -26,10 +26,11 @@ func NewGoModuleFileCataloger(opts CatalogerConfig) pkg.Cataloger {
// NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler.
func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger {
c := newGoBinaryCataloger(opts)
return generic.NewCataloger(binaryCatalogerName).
WithParserByMimeTypes(
newGoBinaryCataloger(opts).parseGoBinary,
c.parseGoBinary,
mimetype.ExecutableMIMETypeSet.List()...,
).
WithResolvingProcessors(stdlibProcessor)
WithResolvingProcessors(c.stdlibProcessor)
}

View File

@ -7,6 +7,7 @@ import (
"github.com/anchore/go-homedir"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloging"
)
const (
@ -49,6 +50,11 @@ type CatalogerConfig struct {
MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"`
// CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are
// "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib).
// app-config: golang.capture-symbols
CaptureSymbols cataloging.SymbolScope `yaml:"capture-symbols" json:"capture-symbols" mapstructure:"capture-symbols"`
// Whether to use the golang.org/x/tools/go/packages, which executes golang tooling found on the path in addition to potential network access
UsePackagesLib bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
}
@ -76,6 +82,7 @@ func DefaultCatalogerConfig() CatalogerConfig {
UsePackagesLib: true,
MainModuleVersion: DefaultMainModuleVersionConfig(),
LocalModCacheDir: defaultGoModDir(),
CaptureSymbols: cataloging.SymbolScopeNone,
}
// first process the proxy settings
@ -184,6 +191,11 @@ func (g CatalogerConfig) WithMainModuleVersion(input MainModuleVersionConfig) Ca
return g
}
func (g CatalogerConfig) WithCaptureSymbols(input cataloging.SymbolScope) CatalogerConfig {
g.CaptureSymbols = input
return g
}
func (g CatalogerConfig) WithUsePackagesLib(useLib bool) CatalogerConfig {
g.UsePackagesLib = useLib
return g

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/go-homedir"
"github.com/anchore/syft/syft/cataloging"
)
func Test_Config(t *testing.T) {
@ -58,6 +59,7 @@ func Test_Config(t *testing.T) {
NoProxy: []string{"my.private", "no.proxy"},
MainModuleVersion: DefaultMainModuleVersionConfig(),
UsePackagesLib: true,
CaptureSymbols: cataloging.SymbolScopeNone,
},
},
{
@ -86,6 +88,7 @@ func Test_Config(t *testing.T) {
NoProxy: []string{"alt.no.proxy"},
MainModuleVersion: DefaultMainModuleVersionConfig(),
UsePackagesLib: true,
CaptureSymbols: cataloging.SymbolScopeNone,
},
},
}

View File

@ -45,7 +45,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBi
return p
}
func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments []string) pkg.GolangBinaryBuildinfoEntry {
func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments, symbols []string) pkg.GolangBinaryBuildinfoEntry {
if dep.Replace != nil {
dep = dep.Replace
}
@ -58,6 +58,7 @@ func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture st
MainModule: mainModule,
GoCryptoSettings: cryptoSettings,
GoExperiments: experiments,
Symbols: symbols,
}
}

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, ",")...)

View File

@ -18,6 +18,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
"github.com/anchore/syft/syft/internal/unionreader"
@ -1422,3 +1423,71 @@ type alwaysErrorReader struct{}
func (alwaysErrorReader) Read(_ []byte) (int, error) {
return 0, errors.New("read from always error reader")
}
func Test_buildGoPkgInfo_symbolScope(t *testing.T) {
location := file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/a-path", FileSystemID: "layer-id"})
// the symbols a binary would carry once scanFile has extracted them: one main-package symbol, one
// dependency symbol, and one standard-library symbol. For the "none" scope scanFile never runs, so the
// build info carries no symbols at all.
populatedSymbols := []binarySymbol{
{packagePath: "main", name: "main.main"},
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
{packagePath: "net/http", name: "net/http.(*Client).Do"},
}
tests := []struct {
name string
scope cataloging.SymbolScope
symbols []binarySymbol
wantMainSyms []string
wantDepSyms []string
wantStdlibSyms []string
}{
{
name: "none captures nothing",
scope: cataloging.SymbolScopeNone,
symbols: nil,
},
{
name: "stdlib captures only the stdlib package",
scope: cataloging.SymbolScopeStdlib,
symbols: populatedSymbols,
wantStdlibSyms: []string{"net/http.(*Client).Do"},
},
{
name: "all captures module and stdlib packages",
scope: cataloging.SymbolScopeAll,
symbols: populatedSymbols,
wantMainSyms: []string{"main.main"},
wantDepSyms: []string{"github.com/foo/bar.Parse"},
wantStdlibSyms: []string{"net/http.(*Client).Do"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mod := &extendedBuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: "go1.22.0",
Main: debug.Module{Path: "github.com/anchore/syft", Version: "v1.0.0"},
Deps: []*debug.Module{{Path: "github.com/foo/bar", Version: "v1.2.3"}},
},
arch: "amd64",
symbols: tt.symbols,
}
c := newGoBinaryCataloger(CatalogerConfig{CaptureSymbols: tt.scope})
reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader("")))
require.NoError(t, err)
mainPkg, pkgs := c.buildGoPkgInfo(context.Background(), fileresolver.Empty{}, location, mod, mod.arch, reader)
require.NotNil(t, mainPkg)
require.Len(t, pkgs, 1)
assert.Equal(t, tt.wantMainSyms, mainPkg.Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "main module symbols")
assert.Equal(t, tt.wantDepSyms, pkgs[0].Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "dependency symbols")
assert.Equal(t, tt.wantStdlibSyms, c.stdlibSymbolsFor(location.Coordinates), "recorded stdlib symbols")
})
}
}

View File

@ -18,10 +18,11 @@ type extendedBuildInfo struct {
*debug.BuildInfo
cryptoSettings []string
arch string
symbols []binarySymbol
}
// scanFile scans file to try to report the Go and module versions.
func scanFile(location file.Location, reader unionreader.UnionReader) ([]*extendedBuildInfo, error) {
func scanFile(location file.Location, reader unionreader.UnionReader, captureSymbols bool) ([]*extendedBuildInfo, error) {
// NOTE: multiple readers are returned to cover universal binaries, which are files
// with more than one binary
readers, errs := unionreader.GetReaders(reader)
@ -61,7 +62,18 @@ func scanFile(location file.Location, reader unionreader.UnionReader) ([]*extend
}
}
builds = append(builds, &extendedBuildInfo{BuildInfo: bi, cryptoSettings: v, arch: arch})
var symbols []binarySymbol
if captureSymbols {
symbols, err = getSymbols(r)
if err != nil {
log.WithFields("file", location.RealPath, "error", err).Trace("unable to read golang symbol info")
// don't skip this build info.
// we can still catalog packages, even if we can't get the symbol information
errs = unknown.Appendf(errs, location, "unable to read golang symbol info: %w", err)
}
}
builds = append(builds, &extendedBuildInfo{BuildInfo: bi, cryptoSettings: v, arch: arch, symbols: symbols})
}
return builds, errs
}

View File

@ -12,12 +12,12 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func stdlibProcessor(ctx context.Context, _ file.Resolver, pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
compilerPkgs, newRelationships := stdlibPackageAndRelationships(ctx, pkgs)
func (c *goBinaryCataloger) stdlibProcessor(ctx context.Context, _ file.Resolver, pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
compilerPkgs, newRelationships := c.stdlibPackageAndRelationships(ctx, pkgs)
return append(pkgs, compilerPkgs...), append(relationships, newRelationships...), err
}
func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) {
func (c *goBinaryCataloger) stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) {
var goCompilerPkgs []pkg.Package
var relationships []artifact.Relationship
totalLocations := file.NewLocationSet()
@ -33,7 +33,7 @@ func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]p
continue
}
stdLibPkg := newGoStdLib(ctx, mValue.GoCompiledVersion, goPkg.Locations)
stdLibPkg := newGoStdLib(ctx, mValue.GoCompiledVersion, goPkg.Locations, c.stdlibSymbolsFor(location.Coordinates))
if stdLibPkg == nil {
continue
}
@ -50,7 +50,7 @@ func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]p
return goCompilerPkgs, relationships
}
func newGoStdLib(ctx context.Context, version string, location file.LocationSet) *pkg.Package {
func newGoStdLib(ctx context.Context, version string, location file.LocationSet, symbols []string) *pkg.Package {
stdlibCpe, err := generateStdlibCpe(version)
if err != nil {
return nil
@ -66,6 +66,7 @@ func newGoStdLib(ctx context.Context, version string, location file.LocationSet)
Type: pkg.GoModulePkg,
Metadata: pkg.GolangBinaryBuildinfoEntry{
GoCompiledVersion: version,
Symbols: symbols,
},
}
goCompilerPkg.SetID()
@ -79,10 +80,10 @@ func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) {
// we also need to trim starting from the first +<metadata> to
// correctly extract potential rc candidate information for cpe generation
// ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found then + is returned
after, _, found := strings.Cut("+", version)
// ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found version is unchanged
before, _, found := strings.Cut(version, "+")
if found {
version = after
version = before
}
// extracting <version> and <candidate>

View File

@ -88,7 +88,8 @@ func Test_stdlibPackageAndRelationships(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPkgs, gotRels := stdlibPackageAndRelationships(ctx, tt.pkgs)
c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)}
gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, tt.pkgs)
assert.Len(t, gotPkgs, tt.wantPkgs)
assert.Len(t, gotRels, tt.wantRels)
})
@ -137,7 +138,8 @@ func Test_stdlibPackageAndRelationships_values(t *testing.T) {
Type: artifact.DependencyOfRelationship,
}
gotPkgs, gotRels := stdlibPackageAndRelationships(ctx, []pkg.Package{p})
c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)}
gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, []pkg.Package{p})
require.Len(t, gotPkgs, 1)
gotPkg := gotPkgs[0]

View File

@ -0,0 +1,332 @@
package golang
import (
"bytes"
"debug/elf"
"debug/gosym"
"debug/macho"
"encoding/binary"
"fmt"
"io"
"runtime/debug"
"slices"
"strings"
)
// mainPackage is the import path the linker assigns to the binary's main package.
const mainPackage = "main"
// 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")
packagePath string
// name is the fully qualified symbol name (e.g. "github.com/foo/bar/internal/baz.(*Type).Method")
name string
}
// getSymbols extracts all function symbols from the pclntab of a go binary. The pclntab is required by the
// go runtime (for panic tracebacks and GC), so it is present even in binaries built with -ldflags="-s -w".
func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) {
defer func() {
if r := recover(); r != nil {
// the gosym package can panic on malformed pclntab data
syms = nil
err = fmt.Errorf("recovered from panic while reading pclntab: %v", r)
}
}()
pclntab, textStart, err := readPclntab(r)
if err != nil {
return nil, err
}
table, err := gosym.NewTable(nil, gosym.NewLineTable(pclntab, textStart))
if err != nil {
return nil, fmt.Errorf("unable to parse pclntab: %w", err)
}
seen := make(map[string]struct{})
for _, fn := range table.Funcs {
if fn.Sym == nil || isCompilerGeneratedName(fn.Name) {
continue
}
seen[fn.Name] = struct{}{}
syms = append(syms, binarySymbol{
packagePath: fn.PackageName(),
name: fn.Name,
})
}
// debug/gosym only exposes top-level functions; functions that the compiler inlined into their
// callers are absent from table.Funcs even though their names are recorded in the pclntab funcname
// table (used to reconstruct inlined frames in tracebacks). Recover those names so that a
// vulnerable-but-inlined function (e.g. a small stdlib wrapper) is still reported as present.
for _, name := range funcNameTable(pclntab) {
if _, ok := seen[name]; ok {
continue
}
pkgPath := packagePathFromSymbolName(name)
if pkgPath == "" {
continue
}
seen[name] = struct{}{}
syms = append(syms, binarySymbol{packagePath: pkgPath, name: name})
}
return syms, nil
}
// packagePathFromSymbolName derives the owning package import path from a fully qualified symbol name.
// The package path is everything up to the first "." that follows the final "/" — e.g.
// "path/filepath.IsLocal" -> "path/filepath" and "golang.org/x/net/html.(*Tokenizer).Next" ->
// "golang.org/x/net/html". Returns "" when the name has no package-qualifying dot or is compiler-generated.
func packagePathFromSymbolName(name string) string {
if isCompilerGeneratedName(name) {
return ""
}
name = nameWithoutTypeArgs(name)
slash := strings.LastIndex(name, "/")
dot := strings.IndexByte(name[slash+1:], '.')
if dot < 0 {
return ""
}
return name[:slash+1+dot]
}
// nameWithoutTypeArgs strips the type-argument portion from an instantiated generic symbol name, e.g.
// "foo/bar.Do[net/url.Values]" -> "foo/bar.Do". The slashes and dots inside the brackets would otherwise
// corrupt package-path derivation (yielding "foo/bar.Do[net" for the example above). Mirrors
// debug/gosym's (*Sym).nameWithoutInst.
func nameWithoutTypeArgs(name string) string {
start := strings.IndexByte(name, '[')
if start < 0 {
return name
}
end := strings.LastIndexByte(name, ']')
if end < 0 {
// malformed: an opening bracket should always have a closing one
return name
}
return name[:start] + name[end+1:]
}
// oldStyleCompilerGeneratedPrefixes match compiler/linker-generated symbols from toolchains older than
// go1.20, which used "." where newer toolchains use ":" (e.g. "go.buildid" is now "go:buildid",
// "go.type.*" is now "go:type.*"). These must be prefix (not substring) matches, and a bare "go." prefix
// is not enough: legitimate module paths such as "go.uber.org/zap" also start with "go.".
var oldStyleCompilerGeneratedPrefixes = []string{
"go.buildid",
"go.builtin.",
"go.constinfo.",
"go.cuinfo.",
"go.func.",
"go.importpath.",
"go.info.",
"go.interface.",
"go.itab.",
"go.itablink.",
"go.map.",
"go.shape.",
"go.string.",
"go.type.",
"go.typelink.",
"type.",
}
// isCompilerGeneratedName reports whether a symbol name was synthesized by the compiler or linker rather
// than declared in Go source. Since go1.20 these names contain ':' or '..' (e.g. "type:.eq.*",
// "go:string.*") — byte sequences that never appear in a real Go import path or identifier. Older
// toolchains used '.' as the separator (e.g. "go.type.*", "type..hash.*"), which is matched against the
// known reserved prefixes. Such names belong to no package and are dropped rather than mis-attributed
// (e.g. bucketed under a bogus "type" stdlib package).
func isCompilerGeneratedName(name string) bool {
if strings.Contains(name, ":") || strings.Contains(name, "..") {
return true
}
for _, prefix := range oldStyleCompilerGeneratedPrefixes {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}
// funcNameTable returns every function name recorded in the pclntab's funcname table, including the
// names of inlined functions that debug/gosym does not expose. It parses the pclntab header for the
// Go 1.16+ layouts; on any unrecognized layout or out-of-bounds offset it returns nil (fail-soft), so
// callers fall back to the debug/gosym function set. See the runtime's pcHeader / moduledata layout.
func funcNameTable(pclntab []byte) []string {
if len(pclntab) < 8 {
return nil
}
magic := binary.LittleEndian.Uint32(pclntab[0:4])
// the field before funcnameOffset is textStart, which exists in the 1.18+ headers but not 1.16/1.17
var hasTextStart bool
switch magic {
case 0xfffffff1, 0xfffffff0: // go1.20+, go1.18/1.19
hasTextStart = true
case 0xfffffffa: // go1.16/1.17
hasTextStart = false
default:
return nil
}
ptrSize := int(pclntab[7])
if ptrSize != 4 && ptrSize != 8 {
return nil
}
readWord := func(idx int) (uint64, bool) {
off := 8 + idx*ptrSize
if off+ptrSize > len(pclntab) {
return 0, false
}
if ptrSize == 8 {
return binary.LittleEndian.Uint64(pclntab[off : off+8]), true
}
return uint64(binary.LittleEndian.Uint32(pclntab[off : off+4])), true
}
// header words after (nfunc, nfiles): [textStart,] funcnameOffset, cuOffset, ...
funcnameIdx := 2
if hasTextStart {
funcnameIdx = 3
}
funcnameOffset, ok1 := readWord(funcnameIdx)
cuOffset, ok2 := readWord(funcnameIdx + 1)
if !ok1 || !ok2 {
return nil
}
start, end := int(funcnameOffset), int(cuOffset)
if start < 0 || end > len(pclntab) || start >= end {
return nil
}
var names []string
for raw := range bytes.SplitSeq(pclntab[start:end], []byte{0}) {
if len(raw) == 0 {
continue
}
names = append(names, string(raw))
}
return names
}
// readPclntab locates the pclntab and the start address of the text segment within the binary.
func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) {
ident := make([]byte, 16)
if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil {
return nil, 0, errUnrecognizedFormat
}
switch {
case strings.HasPrefix(string(ident), "\x7FELF"):
f, err := elf.NewFile(r)
if err != nil {
return nil, 0, fmt.Errorf("unable to parse ELF binary: %w", err)
}
sect := f.Section(".gopclntab")
if sect == nil {
return nil, 0, fmt.Errorf("no .gopclntab section found")
}
pclntab, err := sect.Data()
if err != nil {
return nil, 0, fmt.Errorf("unable to read .gopclntab section: %w", err)
}
text := f.Section(".text")
if text == nil {
return nil, 0, fmt.Errorf("no .text section found")
}
return pclntab, text.Addr, nil
case strings.HasPrefix(string(ident), "\xFE\xED\xFA") || strings.HasPrefix(string(ident[1:]), "\xFA\xED\xFE"):
f, err := macho.NewFile(r)
if err != nil {
return nil, 0, fmt.Errorf("unable to parse Mach-O binary: %w", err)
}
sect := f.Section("__gopclntab")
if sect == nil {
return nil, 0, fmt.Errorf("no __gopclntab section found")
}
pclntab, err := sect.Data()
if err != nil {
return nil, 0, fmt.Errorf("unable to read __gopclntab section: %w", err)
}
text := f.Section("__text")
if text == nil {
return nil, 0, fmt.Errorf("no __text section found")
}
return pclntab, text.Addr, nil
}
// note: PE and XCOFF binaries do not place the pclntab in a dedicated section; locating it requires
// walking the symbol table for runtime.pclntab markers, which is not yet supported here
return nil, 0, errUnrecognizedFormat
}
// moduleSymbols attributes each extracted symbol to the module that owns it (by longest module path prefix
// of the symbol's package path) and returns a sorted, deduplicated list of symbol names per module path.
// Symbols from the "main" package are attributed to the main module. Standard-library symbols (which belong
// to no module) are collected separately and returned as the second value 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.
func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string][]string, stdlib []string) {
if len(symbols) == 0 {
return nil, nil
}
var modulePaths []string
if main != nil && main.Path != "" {
modulePaths = append(modulePaths, main.Path)
}
for _, dep := range deps {
if dep != nil && dep.Path != "" {
modulePaths = append(modulePaths, dep.Path)
}
}
results := make(map[string][]string)
for _, sym := range symbols {
pkgPath := sym.packagePath
if pkgPath == mainPackage && main != nil {
// the linker renames the main package's import path to "main"
pkgPath = main.Path
}
var best string
for _, modPath := range modulePaths {
if len(modPath) > len(best) && (pkgPath == modPath || strings.HasPrefix(pkgPath, modPath+"/")) {
best = modPath
}
}
if best == "" {
if pkgPath != mainPackage && isStandardImportPath(pkgPath) {
stdlib = append(stdlib, sym.name)
}
continue
}
results[best] = append(results[best], sym.name)
}
for modPath, names := range results {
slices.Sort(names)
results[modPath] = slices.Compact(names)
}
if len(stdlib) > 0 {
slices.Sort(stdlib)
stdlib = slices.Compact(stdlib)
}
return results, stdlib
}
// isStandardImportPath reports whether path is a Go standard-library import path. This mirrors the rule
// the Go toolchain uses: a path is standard if the element before its first slash contains no dot (e.g.
// "net/http", "runtime", "internal/abi"), which distinguishes it from module paths like
// "github.com/foo/bar" whose leading element is a domain name.
func isStandardImportPath(path string) bool {
first, _, _ := strings.Cut(path, "/")
return first != "" && !strings.Contains(first, ".")
}

View File

@ -0,0 +1,201 @@
package golang
import (
"debug/gosym"
"os"
"runtime"
"runtime/debug"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_moduleSymbols(t *testing.T) {
mainModule := &debug.Module{Path: "github.com/someorg/somecli"}
deps := []*debug.Module{
{Path: "github.com/foo/bar"},
{Path: "github.com/foo/bar/v2"},
nil,
}
tests := []struct {
name string
symbols []binarySymbol
expected map[string][]string
expectedStdlib []string
}{
{
name: "no symbols",
symbols: nil,
expected: nil,
},
{
name: "attribute symbols by longest module path prefix",
symbols: []binarySymbol{
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
{packagePath: "github.com/foo/bar/internal/util", name: "github.com/foo/bar/internal/util.(*Helper).Do"},
{packagePath: "github.com/foo/bar/v2", name: "github.com/foo/bar/v2.Parse"},
},
expected: map[string][]string{
"github.com/foo/bar": {
"github.com/foo/bar.Parse",
"github.com/foo/bar/internal/util.(*Helper).Do",
},
"github.com/foo/bar/v2": {
"github.com/foo/bar/v2.Parse",
},
},
},
{
name: "main package symbols are attributed to the main module",
symbols: []binarySymbol{
{packagePath: "main", name: "main.main"},
{packagePath: "github.com/someorg/somecli/cmd", name: "github.com/someorg/somecli/cmd.Execute"},
},
expected: map[string][]string{
"github.com/someorg/somecli": {
"github.com/someorg/somecli/cmd.Execute",
"main.main",
},
},
},
{
name: "stdlib and runtime symbols are collected separately",
symbols: []binarySymbol{
{packagePath: "runtime", name: "runtime.main"},
{packagePath: "net/http", name: "net/http.(*Client).Do"},
{packagePath: "internal/abi", name: "internal/abi.(*Type).Kind"},
},
expected: map[string][]string{},
expectedStdlib: []string{
"internal/abi.(*Type).Kind",
"net/http.(*Client).Do",
"runtime.main",
},
},
{
name: "duplicate symbols are deduplicated",
symbols: []binarySymbol{
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
},
expected: map[string][]string{
"github.com/foo/bar": {
"github.com/foo/bar.Parse",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotByModule, gotStdlib := moduleSymbols(tt.symbols, mainModule, deps)
assert.Equal(t, tt.expected, gotByModule)
assert.Equal(t, tt.expectedStdlib, gotStdlib)
})
}
}
func Test_getSymbols(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("PE binaries are not supported for symbol extraction")
}
// the test executable is itself a go binary with a pclntab, which makes for a hermetic fixture
exe, err := os.Executable()
require.NoError(t, err)
f, err := os.Open(exe)
require.NoError(t, err)
defer f.Close()
symbols, err := getSymbols(f)
require.NoError(t, err)
require.NotEmpty(t, symbols)
var foundRuntime, foundTesting bool
for _, sym := range symbols {
switch {
case sym.packagePath == "runtime" && sym.name == "runtime.main":
foundRuntime = true
case sym.packagePath == "testing" && sym.name == "testing.tRunner":
foundTesting = true
}
}
assert.True(t, foundRuntime, "expected to find runtime.main symbol")
assert.True(t, foundTesting, "expected to find testing.tRunner symbol")
// the recovery loop relies on packagePathFromSymbolName, so confirm at least one recovered name is
// present that debug/gosym's table.Funcs does not surface directly (i.e. an inlined function).
require.True(t, hasInlinedOnlySymbol(t, exe, symbols), "expected to recover at least one inlined-only symbol")
}
// hasInlinedOnlySymbol reports whether syms contains a function name that is absent from the raw
// debug/gosym function table for the same binary — i.e. a name that could only have come from the
// funcname-table recovery path.
func hasInlinedOnlySymbol(t *testing.T, exe string, syms []binarySymbol) bool {
t.Helper()
f, err := os.Open(exe)
require.NoError(t, err)
defer f.Close()
pclntab, textStart, err := readPclntab(f)
require.NoError(t, err)
table, err := gosym.NewTable(nil, gosym.NewLineTable(pclntab, textStart))
require.NoError(t, err)
gosymNames := make(map[string]struct{}, len(table.Funcs))
for _, fn := range table.Funcs {
gosymNames[fn.Name] = struct{}{}
}
for _, sym := range syms {
if _, ok := gosymNames[sym.name]; !ok {
return true
}
}
return false
}
func Test_packagePathFromSymbolName(t *testing.T) {
tests := []struct {
name string
expected string
}{
{"path/filepath.IsLocal", "path/filepath"},
// pointer-receiver method
{"golang.org/x/net/html.(*Tokenizer).Next", "golang.org/x/net/html"},
// versioned (major-version-suffixed) module path
{"github.com/foo/bar/v2.Parse", "github.com/foo/bar/v2"},
{"github.com/foo/bar/v2.(*Client).Do", "github.com/foo/bar/v2"},
{"github.com/foo/bar.Parse.func1", "github.com/foo/bar"},
{"main.main", "main"},
{"runtime.gcBgMarkWorker", "runtime"},
// generic instantiations: type arguments must not corrupt the package path
{"foo/bar.Do[net/url.Values]", "foo/bar"},
{"github.com/foo/bar.Map[go.shape.int,go.shape.string]", "github.com/foo/bar"},
{"main.Do[go.shape.int]", "main"},
// no package-qualifying dot
{"runtime", ""},
// compiler/linker-generated symbols belong to no package
{"type:.eq.[]string", ""},
{"type..hash.runtime._type", ""},
{"go:string.\"foo\"", ""},
// pre-go1.20 toolchains generated symbols with "." where newer ones use ":"
{"go.buildid", ""},
{"go.type.*runtime._type", ""},
{"go.itab.*os.File,io.Reader", ""},
{"go.string.\"foo\"", ""},
// 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"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, packagePathFromSymbolName(test.name))
})
}
}

View File

@ -22,6 +22,13 @@ type GolangBinaryBuildinfoEntry struct {
// GoExperiments lists experimental Go features enabled during compilation (e.g., "arenas", "cgocheck2").
GoExperiments []string `json:"goExperiments,omitempty" cyclonedx:"goExperiments"`
// Symbols are the fully qualified function symbols from this module that are compiled into the binary
// (e.g., "github.com/foo/bar.(*Type).Method"), extracted from the binary symbol table (pclntab).
// Populated only when the golang cataloger's capture-symbols scope covers this package: the "all" scope
// populates every module package plus the synthetic stdlib package, while the "stdlib" scope populates
// only the stdlib package.
Symbols []string `json:"symbols,omitempty"`
}
// GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum

View File

@ -8,19 +8,36 @@ const (
Builtin VcpkgRegistryKind = "builtin"
)
// used for metadata. Includes summary of data found in manifest (vcpkg.json file) relevant to just that vcpkg
// VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package.
type VcpkgManifest struct {
Description []string `json:"description,omitempty"`
Documentation string `json:"documentation,omitempty"`
FullVersion string `json:"full-version"`
Version string `json:"version"`
PortVersion int `json:"port-version"`
Maintainers []string `json:"maintainers,omitempty"`
Name string `json:"name"`
Supports string `json:"supports,omitempty"`
// to show where it came from
// Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest).
Description []string `json:"description,omitempty"`
// Documentation is the URL to the package's documentation.
Documentation string `json:"documentation,omitempty"`
// FullVersion is the complete version string including the port-version suffix (e.g. "1.2.3#2").
FullVersion string `json:"full-version"`
// Version is the upstream package version without the port-version suffix (e.g. "1.2.3").
Version string `json:"version"`
// PortVersion is the vcpkg-specific packaging revision for a given upstream version.
PortVersion int `json:"port-version"`
// Maintainers are the people responsible for maintaining the vcpkg port.
Maintainers []string `json:"maintainers,omitempty"`
// Name is the package name as declared in the manifest.
Name string `json:"name"`
// Supports is the platform expression describing which triplets the package can be built for (e.g. "!windows").
Supports string `json:"supports,omitempty"`
// Registry indicates where the package definition came from.
Registry *VcpkgRegistryEntry `json:"registry,omitempty"`
// found by looking at build folder to find target. ex. "x64-linux"
// Triplet is the build target discovered from the build folder (e.g. "x64-linux").
Triplet string `json:"triplet,omitempty"`
}