mirror of
https://github.com/anchore/syft.git
synced 2026-08-19 16:48:27 +02:00
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:
parent
3252b6f806
commit
1dcac54b0e
@ -198,7 +198,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
|
|||||||
WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings).
|
WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings).
|
||||||
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
|
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().
|
JavaScript: javascript.DefaultCatalogerConfig().
|
||||||
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
|
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
|
||||||
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).
|
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/clio"
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/syft/cataloging"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
"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"`
|
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"`
|
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"`
|
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 {
|
var _ interface {
|
||||||
clio.FieldDescriber
|
clio.FieldDescriber
|
||||||
|
clio.PostLoader
|
||||||
} = (*golangConfig)(nil)
|
} = (*golangConfig)(nil)
|
||||||
|
|
||||||
func (o *golangConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
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
|
always show (devel) as the version. Use these options to control heuristics to guess
|
||||||
a more accurate version from the binary.`)
|
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.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.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`)
|
(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`)
|
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 {
|
type golangMainModuleVersionConfig struct {
|
||||||
FromLDFlags bool `json:"from-ld-flags" yaml:"from-ld-flags" mapstructure:"from-ld-flags"`
|
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"`
|
FromContents bool `json:"from-contents" yaml:"from-contents" mapstructure:"from-contents"`
|
||||||
@ -67,5 +77,6 @@ func defaultGolangConfig() golangConfig {
|
|||||||
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
|
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
|
||||||
},
|
},
|
||||||
UsePackagesLib: nil, // this defaults to true, which is the API default
|
UsePackagesLib: nil, // this defaults to true, which is the API default
|
||||||
|
CaptureSymbols: def.CaptureSymbols,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
cmd/syft/internal/options/golang_test.go
Normal file
57
cmd/syft/internal/options/golang_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
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
|
- 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)
|
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
|
- 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
|
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
|
- key: golang.local-vendor-dir
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package internal
|
|||||||
const (
|
const (
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
// 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.
|
// 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
|
// Changelog
|
||||||
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
// 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.6 - add Dependencies to ElixirMixLockEntry metadata
|
||||||
// 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger
|
// 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.8 - add VcpkgManifest metadata type for vcpkg manifest support
|
||||||
|
// 16.1.9 - add Symbols to GolangBinaryBuildinfoEntry metadata
|
||||||
)
|
)
|
||||||
|
|||||||
4520
schema/json/schema-16.1.9.json
Normal file
4520
schema/json/schema-16.1.9.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$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",
|
"$ref": "#/$defs/Document",
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"AlpmDbEntry": {
|
"AlpmDbEntry": {
|
||||||
@ -1653,6 +1653,13 @@
|
|||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")."
|
"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",
|
"type": "object",
|
||||||
@ -4394,39 +4401,47 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"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": {
|
"documentation": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Documentation is the URL to the package's documentation."
|
||||||
},
|
},
|
||||||
"full-version": {
|
"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": {
|
"version": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Version is the upstream package version without the port-version suffix (e.g. \"1.2.3\")."
|
||||||
},
|
},
|
||||||
"port-version": {
|
"port-version": {
|
||||||
"type": "integer"
|
"type": "integer",
|
||||||
|
"description": "PortVersion is the vcpkg-specific packaging revision for a given upstream version."
|
||||||
},
|
},
|
||||||
"maintainers": {
|
"maintainers": {
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array",
|
||||||
|
"description": "Maintainers are the people responsible for maintaining the vcpkg port."
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Name is the package name as declared in the manifest."
|
||||||
},
|
},
|
||||||
"supports": {
|
"supports": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Supports is the platform expression describing which triplets the package can be built for (e.g. \"!windows\")."
|
||||||
},
|
},
|
||||||
"registry": {
|
"registry": {
|
||||||
"$ref": "#/$defs/VcpkgRegistryEntry",
|
"$ref": "#/$defs/VcpkgRegistryEntry",
|
||||||
"description": "to show where it came from"
|
"description": "Registry indicates where the package definition came from."
|
||||||
},
|
},
|
||||||
"triplet": {
|
"triplet": {
|
||||||
"type": "string",
|
"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",
|
"type": "object",
|
||||||
@ -4436,7 +4451,7 @@
|
|||||||
"port-version",
|
"port-version",
|
||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
"description": "used for metadata."
|
"description": "VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package."
|
||||||
},
|
},
|
||||||
"VcpkgRegistryEntry": {
|
"VcpkgRegistryEntry": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
28
syft/cataloging/symbols.go
Normal file
28
syft/cataloging/symbols.go
Normal 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
|
||||||
|
}
|
||||||
30
syft/cataloging/symbols_test.go
Normal file
30
syft/cataloging/symbols_test.go
Normal 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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,6 +24,9 @@ configs: # AUTO-GENERATED - config structs and their fields
|
|||||||
- key: NoProxy
|
- 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.
|
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
|
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:
|
catalogers:
|
||||||
- ecosystem: go # MANUAL
|
- ecosystem: go # MANUAL
|
||||||
name: go-module-binary-cataloger # AUTO-GENERATED
|
name: go-module-binary-cataloger # AUTO-GENERATED
|
||||||
|
|||||||
@ -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.
|
// NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler.
|
||||||
func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger {
|
func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger {
|
||||||
|
c := newGoBinaryCataloger(opts)
|
||||||
return generic.NewCataloger(binaryCatalogerName).
|
return generic.NewCataloger(binaryCatalogerName).
|
||||||
WithParserByMimeTypes(
|
WithParserByMimeTypes(
|
||||||
newGoBinaryCataloger(opts).parseGoBinary,
|
c.parseGoBinary,
|
||||||
mimetype.ExecutableMIMETypeSet.List()...,
|
mimetype.ExecutableMIMETypeSet.List()...,
|
||||||
).
|
).
|
||||||
WithResolvingProcessors(stdlibProcessor)
|
WithResolvingProcessors(c.stdlibProcessor)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/anchore/go-homedir"
|
"github.com/anchore/go-homedir"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/cataloging"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -49,6 +50,11 @@ type CatalogerConfig struct {
|
|||||||
|
|
||||||
MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"`
|
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
|
// 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"`
|
UsePackagesLib bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
|
||||||
}
|
}
|
||||||
@ -76,6 +82,7 @@ func DefaultCatalogerConfig() CatalogerConfig {
|
|||||||
UsePackagesLib: true,
|
UsePackagesLib: true,
|
||||||
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
||||||
LocalModCacheDir: defaultGoModDir(),
|
LocalModCacheDir: defaultGoModDir(),
|
||||||
|
CaptureSymbols: cataloging.SymbolScopeNone,
|
||||||
}
|
}
|
||||||
|
|
||||||
// first process the proxy settings
|
// first process the proxy settings
|
||||||
@ -184,6 +191,11 @@ func (g CatalogerConfig) WithMainModuleVersion(input MainModuleVersionConfig) Ca
|
|||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g CatalogerConfig) WithCaptureSymbols(input cataloging.SymbolScope) CatalogerConfig {
|
||||||
|
g.CaptureSymbols = input
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
func (g CatalogerConfig) WithUsePackagesLib(useLib bool) CatalogerConfig {
|
func (g CatalogerConfig) WithUsePackagesLib(useLib bool) CatalogerConfig {
|
||||||
g.UsePackagesLib = useLib
|
g.UsePackagesLib = useLib
|
||||||
return g
|
return g
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/anchore/go-homedir"
|
"github.com/anchore/go-homedir"
|
||||||
|
"github.com/anchore/syft/syft/cataloging"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Config(t *testing.T) {
|
func Test_Config(t *testing.T) {
|
||||||
@ -58,6 +59,7 @@ func Test_Config(t *testing.T) {
|
|||||||
NoProxy: []string{"my.private", "no.proxy"},
|
NoProxy: []string{"my.private", "no.proxy"},
|
||||||
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
||||||
UsePackagesLib: true,
|
UsePackagesLib: true,
|
||||||
|
CaptureSymbols: cataloging.SymbolScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -86,6 +88,7 @@ func Test_Config(t *testing.T) {
|
|||||||
NoProxy: []string{"alt.no.proxy"},
|
NoProxy: []string{"alt.no.proxy"},
|
||||||
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
||||||
UsePackagesLib: true,
|
UsePackagesLib: true,
|
||||||
|
CaptureSymbols: cataloging.SymbolScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBi
|
|||||||
return p
|
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 {
|
if dep.Replace != nil {
|
||||||
dep = dep.Replace
|
dep = dep.Replace
|
||||||
}
|
}
|
||||||
@ -58,6 +58,7 @@ func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture st
|
|||||||
MainModule: mainModule,
|
MainModule: mainModule,
|
||||||
GoCryptoSettings: cryptoSettings,
|
GoCryptoSettings: cryptoSettings,
|
||||||
GoExperiments: experiments,
|
GoExperiments: experiments,
|
||||||
|
Symbols: symbols,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import (
|
|||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/mod/module"
|
"golang.org/x/mod/module"
|
||||||
@ -20,6 +21,7 @@ import (
|
|||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/cataloging"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/internal/unionreader"
|
"github.com/anchore/syft/syft/internal/unionreader"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
@ -49,15 +51,45 @@ const devel = "(devel)"
|
|||||||
type goBinaryCataloger struct {
|
type goBinaryCataloger struct {
|
||||||
licenseResolver goLicenseResolver
|
licenseResolver goLicenseResolver
|
||||||
mainModuleVersion MainModuleVersionConfig
|
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 {
|
func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger {
|
||||||
return &goBinaryCataloger{
|
return &goBinaryCataloger{
|
||||||
licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts),
|
licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts),
|
||||||
mainModuleVersion: opts.MainModuleVersion,
|
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.
|
// 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) {
|
func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
var pkgs []pkg.Package
|
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)
|
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
|
var rels []artifact.Relationship
|
||||||
for _, mod := range mods {
|
for _, mod := range mods {
|
||||||
@ -128,6 +160,15 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
|
|||||||
mod.Main = createMainModuleFromPath(mod)
|
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
|
var pkgs []pkg.Package
|
||||||
for _, dep := range mod.Deps {
|
for _, dep := range mod.Deps {
|
||||||
if dep == nil {
|
if dep == nil {
|
||||||
@ -147,6 +188,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
|
|||||||
nil,
|
nil,
|
||||||
mod.cryptoSettings,
|
mod.cryptoSettings,
|
||||||
experiments,
|
experiments,
|
||||||
|
symbolsByModule[dep.Path],
|
||||||
)
|
)
|
||||||
|
|
||||||
p := c.newGoBinaryPackage(
|
p := c.newGoBinaryPackage(
|
||||||
@ -164,7 +206,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
|
|||||||
return nil, pkgs
|
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
|
return &main, pkgs
|
||||||
}
|
}
|
||||||
@ -179,7 +221,7 @@ func missingMainModule(mod *extendedBuildInfo) bool {
|
|||||||
return mod.Main == moduleFromPartialPackageBuild
|
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)
|
gbs := getBuildSettings(mod.Settings)
|
||||||
lics := c.licenseResolver.getLicenses(ctx, resolver, mod.Main.Path, mod.Main.Version)
|
lics := c.licenseResolver.getLicenses(ctx, resolver, mod.Main.Path, mod.Main.Version)
|
||||||
gover, experiments := getExperimentsFromVersion(mod.GoVersion)
|
gover, experiments := getExperimentsFromVersion(mod.GoVersion)
|
||||||
@ -192,6 +234,7 @@ func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file
|
|||||||
gbs,
|
gbs,
|
||||||
mod.cryptoSettings,
|
mod.cryptoSettings,
|
||||||
experiments,
|
experiments,
|
||||||
|
symbols,
|
||||||
)
|
)
|
||||||
|
|
||||||
if mod.Main.Version == devel {
|
if mod.Main.Version == devel {
|
||||||
@ -387,7 +430,7 @@ func getExperimentsFromVersion(version string) (string, []string) {
|
|||||||
version, rest, ok := strings.Cut(version, " ")
|
version, rest, ok := strings.Cut(version, " ")
|
||||||
if ok {
|
if ok {
|
||||||
// Assume they may add more non-version chunks in the future, so only look for "X:".
|
// 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:") {
|
if strings.HasPrefix(rest, "X:") {
|
||||||
csv := strings.TrimPrefix(chunk, "X:")
|
csv := strings.TrimPrefix(chunk, "X:")
|
||||||
experiments = append(experiments, strings.Split(csv, ",")...)
|
experiments = append(experiments, strings.Split(csv, ",")...)
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/cataloging"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||||
"github.com/anchore/syft/syft/internal/unionreader"
|
"github.com/anchore/syft/syft/internal/unionreader"
|
||||||
@ -1422,3 +1423,71 @@ type alwaysErrorReader struct{}
|
|||||||
func (alwaysErrorReader) Read(_ []byte) (int, error) {
|
func (alwaysErrorReader) Read(_ []byte) (int, error) {
|
||||||
return 0, errors.New("read from always error reader")
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -18,10 +18,11 @@ type extendedBuildInfo struct {
|
|||||||
*debug.BuildInfo
|
*debug.BuildInfo
|
||||||
cryptoSettings []string
|
cryptoSettings []string
|
||||||
arch string
|
arch string
|
||||||
|
symbols []binarySymbol
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanFile scans file to try to report the Go and module versions.
|
// 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
|
// NOTE: multiple readers are returned to cover universal binaries, which are files
|
||||||
// with more than one binary
|
// with more than one binary
|
||||||
readers, errs := unionreader.GetReaders(reader)
|
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
|
return builds, errs
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,12 +12,12 @@ import (
|
|||||||
"github.com/anchore/syft/syft/pkg"
|
"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) {
|
func (c *goBinaryCataloger) stdlibProcessor(ctx context.Context, _ file.Resolver, pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
compilerPkgs, newRelationships := stdlibPackageAndRelationships(ctx, pkgs)
|
compilerPkgs, newRelationships := c.stdlibPackageAndRelationships(ctx, pkgs)
|
||||||
return append(pkgs, compilerPkgs...), append(relationships, newRelationships...), err
|
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 goCompilerPkgs []pkg.Package
|
||||||
var relationships []artifact.Relationship
|
var relationships []artifact.Relationship
|
||||||
totalLocations := file.NewLocationSet()
|
totalLocations := file.NewLocationSet()
|
||||||
@ -33,7 +33,7 @@ func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]p
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
stdLibPkg := newGoStdLib(ctx, mValue.GoCompiledVersion, goPkg.Locations)
|
stdLibPkg := newGoStdLib(ctx, mValue.GoCompiledVersion, goPkg.Locations, c.stdlibSymbolsFor(location.Coordinates))
|
||||||
if stdLibPkg == nil {
|
if stdLibPkg == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]p
|
|||||||
return goCompilerPkgs, relationships
|
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)
|
stdlibCpe, err := generateStdlibCpe(version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
@ -66,6 +66,7 @@ func newGoStdLib(ctx context.Context, version string, location file.LocationSet)
|
|||||||
Type: pkg.GoModulePkg,
|
Type: pkg.GoModulePkg,
|
||||||
Metadata: pkg.GolangBinaryBuildinfoEntry{
|
Metadata: pkg.GolangBinaryBuildinfoEntry{
|
||||||
GoCompiledVersion: version,
|
GoCompiledVersion: version,
|
||||||
|
Symbols: symbols,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
goCompilerPkg.SetID()
|
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
|
// we also need to trim starting from the first +<metadata> to
|
||||||
// correctly extract potential rc candidate information for cpe generation
|
// 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
|
// ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found version is unchanged
|
||||||
after, _, found := strings.Cut("+", version)
|
before, _, found := strings.Cut(version, "+")
|
||||||
if found {
|
if found {
|
||||||
version = after
|
version = before
|
||||||
}
|
}
|
||||||
|
|
||||||
// extracting <version> and <candidate>
|
// extracting <version> and <candidate>
|
||||||
|
|||||||
@ -88,7 +88,8 @@ func Test_stdlibPackageAndRelationships(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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, gotPkgs, tt.wantPkgs)
|
||||||
assert.Len(t, gotRels, tt.wantRels)
|
assert.Len(t, gotRels, tt.wantRels)
|
||||||
})
|
})
|
||||||
@ -137,7 +138,8 @@ func Test_stdlibPackageAndRelationships_values(t *testing.T) {
|
|||||||
Type: artifact.DependencyOfRelationship,
|
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)
|
require.Len(t, gotPkgs, 1)
|
||||||
|
|
||||||
gotPkg := gotPkgs[0]
|
gotPkg := gotPkgs[0]
|
||||||
|
|||||||
332
syft/pkg/cataloger/golang/symbols.go
Normal file
332
syft/pkg/cataloger/golang/symbols.go
Normal 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, ".")
|
||||||
|
}
|
||||||
201
syft/pkg/cataloger/golang/symbols_test.go
Normal file
201
syft/pkg/cataloger/golang/symbols_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,13 @@ type GolangBinaryBuildinfoEntry struct {
|
|||||||
|
|
||||||
// GoExperiments lists experimental Go features enabled during compilation (e.g., "arenas", "cgocheck2").
|
// GoExperiments lists experimental Go features enabled during compilation (e.g., "arenas", "cgocheck2").
|
||||||
GoExperiments []string `json:"goExperiments,omitempty" cyclonedx:"goExperiments"`
|
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
|
// GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum
|
||||||
|
|||||||
@ -8,19 +8,36 @@ const (
|
|||||||
Builtin VcpkgRegistryKind = "builtin"
|
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 {
|
type VcpkgManifest struct {
|
||||||
Description []string `json:"description,omitempty"`
|
// Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest).
|
||||||
Documentation string `json:"documentation,omitempty"`
|
Description []string `json:"description,omitempty"`
|
||||||
FullVersion string `json:"full-version"`
|
|
||||||
Version string `json:"version"`
|
// Documentation is the URL to the package's documentation.
|
||||||
PortVersion int `json:"port-version"`
|
Documentation string `json:"documentation,omitempty"`
|
||||||
Maintainers []string `json:"maintainers,omitempty"`
|
|
||||||
Name string `json:"name"`
|
// FullVersion is the complete version string including the port-version suffix (e.g. "1.2.3#2").
|
||||||
Supports string `json:"supports,omitempty"`
|
FullVersion string `json:"full-version"`
|
||||||
// to show where it came from
|
|
||||||
|
// 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"`
|
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"`
|
Triplet string `json:"triplet,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user