mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 10:36:45 +01:00
Guess go main module version based on binary contents (#2608)
* guess go main module version based on binary contents Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add configuration options for golang main module version heuristics Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix test setup for go bin cataloger Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix unit test Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix incorrect test assert ordering Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * handle error from seek Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
737c4e44c5
commit
84576b93e1
18
README.md
18
README.md
@ -691,6 +691,24 @@ golang:
|
|||||||
# SYFT_GOLANG_NOPROXY env var
|
# SYFT_GOLANG_NOPROXY env var
|
||||||
no-proxy: ""
|
no-proxy: ""
|
||||||
|
|
||||||
|
# the go main module version discovered from binaries built with the go compiler will
|
||||||
|
# always show (devel) as the version. Use these options to control heuristics to guess
|
||||||
|
# a more accurate version from the binary.
|
||||||
|
main-module-version:
|
||||||
|
|
||||||
|
# look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)
|
||||||
|
# SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_LD_FLAGS env var
|
||||||
|
from-ld-flags: true
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_BUILD_SETTINGS env var
|
||||||
|
from-build-settings: true
|
||||||
|
|
||||||
|
# search for semver-like strings in the binary contents.
|
||||||
|
# SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_CONTENTS env var
|
||||||
|
from-contents: true
|
||||||
|
|
||||||
java:
|
java:
|
||||||
maven-url: "https://repo1.maven.org/maven2"
|
maven-url: "https://repo1.maven.org/maven2"
|
||||||
max-parent-recursive-depth: 5
|
max-parent-recursive-depth: 5
|
||||||
|
|||||||
@ -62,6 +62,7 @@ func DefaultCatalog() Catalog {
|
|||||||
Scope: source.SquashedScope.String(),
|
Scope: source.SquashedScope.String(),
|
||||||
Package: defaultPackageConfig(),
|
Package: defaultPackageConfig(),
|
||||||
LinuxKernel: defaultLinuxKernelConfig(),
|
LinuxKernel: defaultLinuxKernelConfig(),
|
||||||
|
Golang: defaultGolangConfig(),
|
||||||
File: defaultFileConfig(),
|
File: defaultFileConfig(),
|
||||||
Relationships: defaultRelationshipsConfig(),
|
Relationships: defaultRelationshipsConfig(),
|
||||||
Source: defaultSourceConfig(),
|
Source: defaultSourceConfig(),
|
||||||
@ -131,7 +132,13 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
|
|||||||
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
|
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
|
||||||
WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
|
WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
|
||||||
WithProxy(cfg.Golang.Proxy).
|
WithProxy(cfg.Golang.Proxy).
|
||||||
WithNoProxy(cfg.Golang.NoProxy),
|
WithNoProxy(cfg.Golang.NoProxy).
|
||||||
|
WithMainModuleVersion(
|
||||||
|
golang.DefaultMainModuleVersionConfig().
|
||||||
|
WithFromContents(cfg.Golang.MainModuleVersion.FromContents).
|
||||||
|
WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings).
|
||||||
|
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
|
||||||
|
),
|
||||||
JavaScript: javascript.DefaultCatalogerConfig().
|
JavaScript: javascript.DefaultCatalogerConfig().
|
||||||
WithSearchRemoteLicenses(cfg.JavaScript.SearchRemoteLicenses).
|
WithSearchRemoteLicenses(cfg.JavaScript.SearchRemoteLicenses).
|
||||||
WithNpmBaseURL(cfg.JavaScript.NpmBaseURL),
|
WithNpmBaseURL(cfg.JavaScript.NpmBaseURL),
|
||||||
|
|||||||
@ -1,9 +1,38 @@
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||||
|
)
|
||||||
|
|
||||||
type golangConfig struct {
|
type golangConfig struct {
|
||||||
SearchLocalModCacheLicenses bool `json:"search-local-mod-cache-licenses" yaml:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
|
SearchLocalModCacheLicenses bool `json:"search-local-mod-cache-licenses" yaml:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
|
||||||
LocalModCacheDir string `json:"local-mod-cache-dir" yaml:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`
|
LocalModCacheDir string `json:"local-mod-cache-dir" yaml:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`
|
||||||
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||||
Proxy string `json:"proxy" yaml:"proxy" mapstructure:"proxy"`
|
Proxy string `json:"proxy" yaml:"proxy" mapstructure:"proxy"`
|
||||||
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
FromBuildSettings bool `json:"from-build-settings" yaml:"from-build-settings" mapstructure:"from-build-settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultGolangConfig() golangConfig {
|
||||||
|
def := golang.DefaultCatalogerConfig()
|
||||||
|
return golangConfig{
|
||||||
|
SearchLocalModCacheLicenses: def.SearchLocalModCacheLicenses,
|
||||||
|
LocalModCacheDir: def.LocalModCacheDir,
|
||||||
|
SearchRemoteLicenses: def.SearchRemoteLicenses,
|
||||||
|
Proxy: strings.Join(def.Proxies, ","),
|
||||||
|
NoProxy: strings.Join(def.NoProxy, ","),
|
||||||
|
MainModuleVersion: golangMainModuleVersionConfig{
|
||||||
|
FromLDFlags: def.MainModuleVersion.FromLDFlags,
|
||||||
|
FromContents: def.MainModuleVersion.FromContents,
|
||||||
|
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,9 @@ func Test_packageCatalogerExports(t *testing.T) {
|
|||||||
for pkg, expected := range expectAtLeast {
|
for pkg, expected := range expectAtLeast {
|
||||||
actual, ok := exports[pkg]
|
actual, ok := exports[pkg]
|
||||||
require.True(t, ok, pkg)
|
require.True(t, ok, pkg)
|
||||||
require.True(t, expected.IsSubset(actual.Names()), pkg)
|
if !assert.True(t, actual.Names().IsSubset(expected), pkg) {
|
||||||
|
t.Logf("missing: %s", strset.SymmetricDifference(expected, actual.Names()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,12 +38,12 @@ 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 := goBinaryCataloger{
|
|
||||||
licenses: newGoLicenses(binaryCatalogerName, opts),
|
|
||||||
}
|
|
||||||
return &progressingCataloger{
|
return &progressingCataloger{
|
||||||
cataloger: generic.NewCataloger(binaryCatalogerName).
|
cataloger: generic.NewCataloger(binaryCatalogerName).
|
||||||
WithParserByMimeTypes(c.parseGoBinary, mimetype.ExecutableMIMETypeSet.List()...),
|
WithParserByMimeTypes(
|
||||||
|
newGoBinaryCataloger(opts).parseGoBinary,
|
||||||
|
mimetype.ExecutableMIMETypeSet.List()...,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,11 +20,18 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CatalogerConfig struct {
|
type CatalogerConfig struct {
|
||||||
SearchLocalModCacheLicenses bool `yaml:"search-local-mod-cache-licenses" json:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
|
SearchLocalModCacheLicenses bool `yaml:"search-local-mod-cache-licenses" json:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
|
||||||
LocalModCacheDir string `yaml:"local-mod-cache-dir" json:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`
|
LocalModCacheDir string `yaml:"local-mod-cache-dir" json:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`
|
||||||
SearchRemoteLicenses bool `yaml:"search-remote-licenses" json:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
SearchRemoteLicenses bool `yaml:"search-remote-licenses" json:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||||
Proxies []string `yaml:"proxies,omitempty" json:"proxies,omitempty" mapstructure:"proxies"`
|
Proxies []string `yaml:"proxies,omitempty" json:"proxies,omitempty" mapstructure:"proxies"`
|
||||||
NoProxy []string `yaml:"no-proxy,omitempty" json:"no-proxy,omitempty" mapstructure:"no-proxy"`
|
NoProxy []string `yaml:"no-proxy,omitempty" json:"no-proxy,omitempty" mapstructure:"no-proxy"`
|
||||||
|
MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MainModuleVersionConfig struct {
|
||||||
|
FromLDFlags bool `yaml:"from-ld-flags" json:"from-ld-flags" mapstructure:"from-ld-flags"`
|
||||||
|
FromContents bool `yaml:"from-contents" json:"from-contents" mapstructure:"from-contents"`
|
||||||
|
FromBuildSettings bool `yaml:"from-build-settings" json:"from-build-settings" mapstructure:"from-build-settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultCatalogerConfig create a CatalogerConfig with default options, which includes:
|
// DefaultCatalogerConfig create a CatalogerConfig with default options, which includes:
|
||||||
@ -32,7 +39,9 @@ type CatalogerConfig struct {
|
|||||||
// - setting the default no proxy if none is provided
|
// - setting the default no proxy if none is provided
|
||||||
// - setting the default local module cache dir if none is provided
|
// - setting the default local module cache dir if none is provided
|
||||||
func DefaultCatalogerConfig() CatalogerConfig {
|
func DefaultCatalogerConfig() CatalogerConfig {
|
||||||
g := CatalogerConfig{}
|
g := CatalogerConfig{
|
||||||
|
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
||||||
|
}
|
||||||
|
|
||||||
// first process the proxy settings
|
// first process the proxy settings
|
||||||
if len(g.Proxies) == 0 {
|
if len(g.Proxies) == 0 {
|
||||||
@ -76,6 +85,14 @@ func DefaultCatalogerConfig() CatalogerConfig {
|
|||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DefaultMainModuleVersionConfig() MainModuleVersionConfig {
|
||||||
|
return MainModuleVersionConfig{
|
||||||
|
FromLDFlags: true,
|
||||||
|
FromContents: true,
|
||||||
|
FromBuildSettings: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (g CatalogerConfig) WithSearchLocalModCacheLicenses(input bool) CatalogerConfig {
|
func (g CatalogerConfig) WithSearchLocalModCacheLicenses(input bool) CatalogerConfig {
|
||||||
g.SearchLocalModCacheLicenses = input
|
g.SearchLocalModCacheLicenses = input
|
||||||
return g
|
return g
|
||||||
@ -112,3 +129,23 @@ func (g CatalogerConfig) WithNoProxy(input string) CatalogerConfig {
|
|||||||
g.NoProxy = strings.Split(input, ",")
|
g.NoProxy = strings.Split(input, ",")
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g CatalogerConfig) WithMainModuleVersion(input MainModuleVersionConfig) CatalogerConfig {
|
||||||
|
g.MainModuleVersion = input
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g MainModuleVersionConfig) WithFromLDFlags(input bool) MainModuleVersionConfig {
|
||||||
|
g.FromLDFlags = input
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g MainModuleVersionConfig) WithFromContents(input bool) MainModuleVersionConfig {
|
||||||
|
g.FromContents = input
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g MainModuleVersionConfig) WithFromBuildSettings(input bool) MainModuleVersionConfig {
|
||||||
|
g.FromBuildSettings = input
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Options(t *testing.T) {
|
func Test_Config(t *testing.T) {
|
||||||
type opts struct {
|
type opts struct {
|
||||||
local bool
|
local bool
|
||||||
cacheDir string
|
cacheDir string
|
||||||
@ -51,6 +51,7 @@ func Test_Options(t *testing.T) {
|
|||||||
SearchRemoteLicenses: false,
|
SearchRemoteLicenses: false,
|
||||||
Proxies: []string{"https://my.proxy"},
|
Proxies: []string{"https://my.proxy"},
|
||||||
NoProxy: []string{"my.private", "no.proxy"},
|
NoProxy: []string{"my.private", "no.proxy"},
|
||||||
|
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -74,6 +75,7 @@ func Test_Options(t *testing.T) {
|
|||||||
SearchRemoteLicenses: true,
|
SearchRemoteLicenses: true,
|
||||||
Proxies: []string{"https://alt.proxy", "direct"},
|
Proxies: []string{"https://alt.proxy", "direct"},
|
||||||
NoProxy: []string{"alt.no.proxy"},
|
NoProxy: []string{"alt.no.proxy"},
|
||||||
|
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import (
|
|||||||
"golang.org/x/mod/module"
|
"golang.org/x/mod/module"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"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"
|
||||||
@ -37,7 +38,7 @@ var (
|
|||||||
// inject the correct version into the main module of the build process
|
// inject the correct version into the main module of the build process
|
||||||
|
|
||||||
knownBuildFlagPatterns = []*regexp.Regexp{
|
knownBuildFlagPatterns = []*regexp.Regexp{
|
||||||
regexp.MustCompile(`(?m)\.([gG]it)?([bB]uild)?[vV]ersion=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
regexp.MustCompile(`(?m)\.([gG]it)?([bB]uild)?[vV]er(sion)?=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
||||||
regexp.MustCompile(`(?m)\.([tT]ag)=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
regexp.MustCompile(`(?m)\.([tT]ag)=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -45,7 +46,15 @@ var (
|
|||||||
const devel = "(devel)"
|
const devel = "(devel)"
|
||||||
|
|
||||||
type goBinaryCataloger struct {
|
type goBinaryCataloger struct {
|
||||||
licenses goLicenses
|
licenses goLicenses
|
||||||
|
mainModuleVersion MainModuleVersionConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger {
|
||||||
|
return &goBinaryCataloger{
|
||||||
|
licenses: newGoLicenses(binaryCatalogerName, opts),
|
||||||
|
mainModuleVersion: opts.MainModuleVersion,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@ -61,13 +70,53 @@ func (c *goBinaryCataloger) parseGoBinary(_ context.Context, resolver file.Resol
|
|||||||
internal.CloseAndLogError(reader.ReadCloser, reader.RealPath)
|
internal.CloseAndLogError(reader.ReadCloser, reader.RealPath)
|
||||||
|
|
||||||
for _, mod := range mods {
|
for _, mod := range mods {
|
||||||
pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch)...)
|
pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch, unionReader)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pkgs, nil, nil
|
return pkgs, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location) pkg.Package {
|
func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string, reader io.ReadSeekCloser) []pkg.Package {
|
||||||
|
var pkgs []pkg.Package
|
||||||
|
if mod == nil {
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
var empty debug.Module
|
||||||
|
if mod.Main == empty && mod.Path != "" {
|
||||||
|
mod.Main = createMainModuleFromPath(mod.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dep := range mod.Deps {
|
||||||
|
if dep == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p := c.newGoBinaryPackage(
|
||||||
|
resolver,
|
||||||
|
dep,
|
||||||
|
mod.Main.Path,
|
||||||
|
mod.GoVersion,
|
||||||
|
arch,
|
||||||
|
nil,
|
||||||
|
mod.cryptoSettings,
|
||||||
|
location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||||
|
)
|
||||||
|
if pkg.IsValid(&p) {
|
||||||
|
pkgs = append(pkgs, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mod.Main == empty {
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
main := c.makeGoMainPackage(resolver, mod, arch, location, reader)
|
||||||
|
pkgs = append(pkgs, main)
|
||||||
|
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package {
|
||||||
gbs := getBuildSettings(mod.Settings)
|
gbs := getBuildSettings(mod.Settings)
|
||||||
main := c.newGoBinaryPackage(
|
main := c.newGoBinaryPackage(
|
||||||
resolver,
|
resolver,
|
||||||
@ -81,37 +130,17 @@ func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *exten
|
|||||||
)
|
)
|
||||||
|
|
||||||
if main.Version != devel {
|
if main.Version != devel {
|
||||||
|
// found a full package with a non-development version... return it as is...
|
||||||
return main
|
return main
|
||||||
}
|
}
|
||||||
|
|
||||||
version, hasVersion := gbs.Get("vcs.revision")
|
// we have a package, but the version is "devel"... let's try and find a better answer
|
||||||
timestamp, hasTimestamp := gbs.Get("vcs.time")
|
var metadata *pkg.GolangBinaryBuildinfoEntry
|
||||||
|
if v, ok := main.Metadata.(pkg.GolangBinaryBuildinfoEntry); ok {
|
||||||
var ldflags string
|
metadata = &v
|
||||||
if metadata, ok := main.Metadata.(pkg.GolangBinaryBuildinfoEntry); ok {
|
|
||||||
// we've found a specific version from the ldflags! use it as the version.
|
|
||||||
// why not combine that with the pseudo version (e.g. v1.2.3-0.20210101000000-abcdef123456)?
|
|
||||||
// short answer: we're assuming that if a specific semver was provided in the ldflags that
|
|
||||||
// there is a matching vcs tag to match that could be referenced. This assumption could
|
|
||||||
// be incorrect in terms of the go.mod contents, but is not incorrect in terms of the logical
|
|
||||||
// version of the package.
|
|
||||||
ldflags, _ = metadata.BuildSettings.Get("-ldflags")
|
|
||||||
}
|
}
|
||||||
|
version := c.findMainModuleVersion(metadata, gbs, reader)
|
||||||
|
|
||||||
majorVersion, fullVersion := extractVersionFromLDFlags(ldflags)
|
|
||||||
if fullVersion != "" {
|
|
||||||
version = fullVersion
|
|
||||||
} else if hasVersion && hasTimestamp {
|
|
||||||
//NOTE: err is ignored, because if parsing fails
|
|
||||||
// we still use the empty Time{} struct to generate an empty date, like 00010101000000
|
|
||||||
// for consistency with the pseudo-version format: https://go.dev/ref/mod#pseudo-versions
|
|
||||||
ts, _ := time.Parse(time.RFC3339, timestamp)
|
|
||||||
if len(version) >= 12 {
|
|
||||||
version = version[:12]
|
|
||||||
}
|
|
||||||
|
|
||||||
version = module.PseudoVersion(majorVersion, fullVersion, ts, version)
|
|
||||||
}
|
|
||||||
if version != "" {
|
if version != "" {
|
||||||
main.Version = version
|
main.Version = version
|
||||||
main.PURL = packageURL(main.Name, main.Version)
|
main.PURL = packageURL(main.Name, main.Version)
|
||||||
@ -122,6 +151,65 @@ func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *exten
|
|||||||
return main
|
return main
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var semverPattern = regexp.MustCompile(`\x00(?P<version>v?(\d+\.\d+\.\d+[-\w]*[+\w]*))\x00`)
|
||||||
|
|
||||||
|
func (c *goBinaryCataloger) findMainModuleVersion(metadata *pkg.GolangBinaryBuildinfoEntry, gbs pkg.KeyValues, reader io.ReadSeekCloser) string {
|
||||||
|
vcsVersion, hasVersion := gbs.Get("vcs.revision")
|
||||||
|
timestamp, hasTimestamp := gbs.Get("vcs.time")
|
||||||
|
|
||||||
|
var ldflags, majorVersion, fullVersion string
|
||||||
|
if c.mainModuleVersion.FromLDFlags && metadata != nil {
|
||||||
|
// we've found a specific version from the ldflags! use it as the version.
|
||||||
|
// why not combine that with the pseudo version (e.g. v1.2.3-0.20210101000000-abcdef123456)?
|
||||||
|
// short answer: we're assuming that if a specific semver was provided in the ldflags that
|
||||||
|
// there is a matching vcs tag to match that could be referenced. This assumption could
|
||||||
|
// be incorrect in terms of the go.mod contents, but is not incorrect in terms of the logical
|
||||||
|
// version of the package.
|
||||||
|
ldflags, _ = metadata.BuildSettings.Get("-ldflags")
|
||||||
|
|
||||||
|
majorVersion, fullVersion = extractVersionFromLDFlags(ldflags)
|
||||||
|
if fullVersion != "" {
|
||||||
|
return fullVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// guess the version from pattern matching in the binary (can result in false positives)
|
||||||
|
if c.mainModuleVersion.FromContents {
|
||||||
|
_, err := reader.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Trace("unable to seek to start of go binary reader")
|
||||||
|
} else {
|
||||||
|
contents, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Trace("unable to read from go binary reader")
|
||||||
|
} else {
|
||||||
|
matchMetadata := internal.MatchNamedCaptureGroups(semverPattern, string(contents))
|
||||||
|
|
||||||
|
version, ok := matchMetadata["version"]
|
||||||
|
if ok {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to using the go standard pseudo v0.0.0 version
|
||||||
|
if c.mainModuleVersion.FromBuildSettings && hasVersion && hasTimestamp {
|
||||||
|
version := vcsVersion
|
||||||
|
//NOTE: err is ignored, because if parsing fails
|
||||||
|
// we still use the empty Time{} struct to generate an empty date, like 00010101000000
|
||||||
|
// for consistency with the pseudo-version format: https://go.dev/ref/mod#pseudo-versions
|
||||||
|
ts, _ := time.Parse(time.RFC3339, timestamp)
|
||||||
|
if len(vcsVersion) >= 12 {
|
||||||
|
version = vcsVersion[:12]
|
||||||
|
}
|
||||||
|
|
||||||
|
return module.PseudoVersion(majorVersion, fullVersion, ts, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func extractVersionFromLDFlags(ldflags string) (majorVersion string, fullVersion string) {
|
func extractVersionFromLDFlags(ldflags string) (majorVersion string, fullVersion string) {
|
||||||
if ldflags == "" {
|
if ldflags == "" {
|
||||||
return "", ""
|
return "", ""
|
||||||
@ -223,43 +311,3 @@ func createMainModuleFromPath(path string) (mod debug.Module) {
|
|||||||
mod.Version = devel
|
mod.Version = devel
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string) []pkg.Package {
|
|
||||||
var pkgs []pkg.Package
|
|
||||||
if mod == nil {
|
|
||||||
return pkgs
|
|
||||||
}
|
|
||||||
|
|
||||||
var empty debug.Module
|
|
||||||
if mod.Main == empty && mod.Path != "" {
|
|
||||||
mod.Main = createMainModuleFromPath(mod.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dep := range mod.Deps {
|
|
||||||
if dep == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p := c.newGoBinaryPackage(
|
|
||||||
resolver,
|
|
||||||
dep,
|
|
||||||
mod.Main.Path,
|
|
||||||
mod.GoVersion,
|
|
||||||
arch,
|
|
||||||
nil,
|
|
||||||
mod.cryptoSettings,
|
|
||||||
location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
|
||||||
)
|
|
||||||
if pkg.IsValid(&p) {
|
|
||||||
pkgs = append(pkgs, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mod.Main == empty {
|
|
||||||
return pkgs
|
|
||||||
}
|
|
||||||
|
|
||||||
main := c.makeGoMainPackage(resolver, mod, arch, location)
|
|
||||||
pkgs = append(pkgs, main)
|
|
||||||
|
|
||||||
return pkgs
|
|
||||||
}
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -16,7 +17,9 @@ import (
|
|||||||
|
|
||||||
"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/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// make will run the default make target for the given test fixture path
|
// make will run the default make target for the given test fixture path
|
||||||
@ -163,9 +166,10 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
mod *extendedBuildInfo
|
mod *extendedBuildInfo
|
||||||
expected []pkg.Package
|
expected []pkg.Package
|
||||||
|
binaryContent string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "package without name",
|
name: "package without name",
|
||||||
@ -839,6 +843,69 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
|||||||
unmodifiedMain,
|
unmodifiedMain,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "parse main mod and replace devel with pattern from binary contents",
|
||||||
|
mod: &extendedBuildInfo{
|
||||||
|
BuildInfo: &debug.BuildInfo{
|
||||||
|
GoVersion: goCompiledVersion,
|
||||||
|
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
|
||||||
|
Settings: []debug.BuildSetting{
|
||||||
|
{Key: "GOARCH", Value: archDetails},
|
||||||
|
{Key: "GOOS", Value: "darwin"},
|
||||||
|
{Key: "GOAMD64", Value: "v1"},
|
||||||
|
{Key: "vcs.time", Value: "2022-10-14T19:54:57Z"}, // important! missing revision
|
||||||
|
{Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X blah=foobar`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cryptoSettings: nil,
|
||||||
|
arch: archDetails,
|
||||||
|
},
|
||||||
|
binaryContent: "\x00v1.0.0-somethingelse+incompatible\x00",
|
||||||
|
expected: []pkg.Package{
|
||||||
|
{
|
||||||
|
Name: "github.com/anchore/syft",
|
||||||
|
Language: pkg.Go,
|
||||||
|
Type: pkg.GoModulePkg,
|
||||||
|
Version: "v1.0.0-somethingelse+incompatible",
|
||||||
|
PURL: "pkg:golang/github.com/anchore/syft@v1.0.0-somethingelse+incompatible",
|
||||||
|
Locations: file.NewLocationSet(
|
||||||
|
file.NewLocationFromCoordinates(
|
||||||
|
file.Coordinates{
|
||||||
|
RealPath: "/a-path",
|
||||||
|
FileSystemID: "layer-id",
|
||||||
|
},
|
||||||
|
).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||||
|
),
|
||||||
|
Metadata: pkg.GolangBinaryBuildinfoEntry{
|
||||||
|
GoCompiledVersion: goCompiledVersion,
|
||||||
|
Architecture: archDetails,
|
||||||
|
BuildSettings: []pkg.KeyValue{
|
||||||
|
{
|
||||||
|
Key: "GOARCH",
|
||||||
|
Value: archDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "GOOS",
|
||||||
|
Value: "darwin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "GOAMD64",
|
||||||
|
Value: "v1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "vcs.time",
|
||||||
|
Value: "2022-10-14T19:54:57Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "-ldflags",
|
||||||
|
Value: `build -ldflags="-w -s -extldflags '-static' -X blah=foobar`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MainModule: "github.com/anchore/syft",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@ -854,9 +921,14 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
c := goBinaryCataloger{}
|
c := newGoBinaryCataloger(DefaultCatalogerConfig())
|
||||||
pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch)
|
reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(test.binaryContent)))
|
||||||
assert.Equal(t, test.expected, pkgs)
|
require.NoError(t, err)
|
||||||
|
pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch, reader)
|
||||||
|
require.Len(t, pkgs, len(test.expected))
|
||||||
|
for i, p := range pkgs {
|
||||||
|
pkgtest.AssertPackagesEqual(t, test.expected[i], p)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user