From b71e66137d3734b8f12969b08fd526d487b6c8a3 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 8 Feb 2024 09:40:31 -0500 Subject: [PATCH 1/6] guess go main module version based on binary contents Signed-off-by: Alex Goodman --- syft/file/cataloger/executable/elf.go | 2 +- syft/pkg/cataloger/golang/parse_go_binary.go | 146 +++++++++++------- .../cataloger/golang/parse_go_binary_test.go | 82 +++++++++- 3 files changed, 168 insertions(+), 62 deletions(-) diff --git a/syft/file/cataloger/executable/elf.go b/syft/file/cataloger/executable/elf.go index 451aa14209d..faebbfb84f2 100644 --- a/syft/file/cataloger/executable/elf.go +++ b/syft/file/cataloger/executable/elf.go @@ -45,7 +45,7 @@ func hasAnyDynamicSymbols(file *elf.File, symbolNames ...string) *bool { dynSyms, err := file.DynamicSymbols() if err != nil { // TODO: known-unknowns - log.WithFields("error", err).Warn("unable to read dynamic symbols from elf file") + log.WithFields("error", err).Trace("unable to read dynamic symbols from elf file") return nil } diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index a1a119700bb..020d05952ce 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -17,6 +17,7 @@ import ( "golang.org/x/mod/module" "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/unionreader" @@ -61,13 +62,53 @@ func (c *goBinaryCataloger) parseGoBinary(_ context.Context, resolver file.Resol internal.CloseAndLogError(reader.ReadCloser, reader.RealPath) 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 } -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) main := c.newGoBinaryPackage( resolver, @@ -81,14 +122,35 @@ func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *exten ) if main.Version != devel { + // found a full package with a non-development version... return it as is... 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 + var metadata *pkg.GolangBinaryBuildinfoEntry + if v, ok := main.Metadata.(pkg.GolangBinaryBuildinfoEntry); ok { + metadata = &v + } + version := findMainModuleVersion(metadata, gbs, reader) + + if version != "" { + main.Version = version + main.PURL = packageURL(main.Name, main.Version) + + main.SetID() + } + + return main +} + +var semverPattern = regexp.MustCompile(`\x00(?Pv?(\d+\.\d+\.\d+[-\w]*[+\w]*))\x00`) + +func 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 string - if metadata, ok := main.Metadata.(pkg.GolangBinaryBuildinfoEntry); ok { + if 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 @@ -100,26 +162,38 @@ func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *exten majorVersion, fullVersion := extractVersionFromLDFlags(ldflags) if fullVersion != "" { - version = fullVersion - } else if hasVersion && hasTimestamp { + return fullVersion + } + + // guess the version from pattern matching in the binary (can result in false positives) + _, _ = reader.Seek(0, io.SeekStart) + 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 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(version) >= 12 { - version = version[:12] + if len(vcsVersion) >= 12 { + version = vcsVersion[:12] } - version = module.PseudoVersion(majorVersion, fullVersion, ts, version) - } - if version != "" { - main.Version = version - main.PURL = packageURL(main.Name, main.Version) - - main.SetID() + return module.PseudoVersion(majorVersion, fullVersion, ts, version) } - return main + return "" } func extractVersionFromLDFlags(ldflags string) (majorVersion string, fullVersion string) { @@ -223,43 +297,3 @@ func createMainModuleFromPath(path string) (mod debug.Module) { mod.Version = devel 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 -} diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index a59758b7e5b..cd7243b2b20 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "runtime/debug" "strconv" + "strings" "syscall" "testing" @@ -16,7 +17,9 @@ import ( "github.com/anchore/syft/syft/file" "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/cataloger/internal/pkgtest" ) // make will run the default make target for the given test fixture path @@ -163,9 +166,10 @@ func TestBuildGoPkgInfo(t *testing.T) { } tests := []struct { - name string - mod *extendedBuildInfo - expected []pkg.Package + name string + mod *extendedBuildInfo + expected []pkg.Package + binaryContent string }{ { name: "package without name", @@ -839,6 +843,69 @@ func TestBuildGoPkgInfo(t *testing.T) { 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 { @@ -855,8 +922,13 @@ func TestBuildGoPkgInfo(t *testing.T) { ) c := goBinaryCataloger{} - pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch) - assert.Equal(t, test.expected, pkgs) + reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(test.binaryContent))) + 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) + } }) } } From 3763a8a0fc8765b72c01192c1a2c7b82041ac98d Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 8 Feb 2024 11:37:57 -0500 Subject: [PATCH 2/6] add configuration options for golang main module version heuristics Signed-off-by: Alex Goodman --- README.md | 18 +++++++ cmd/syft/internal/options/catalog.go | 9 +++- cmd/syft/internal/options/golang.go | 39 ++++++++++++++-- syft/pkg/cataloger/golang/cataloger.go | 3 +- syft/pkg/cataloger/golang/config.go | 49 +++++++++++++++++--- syft/pkg/cataloger/golang/parse_go_binary.go | 45 +++++++++--------- 6 files changed, 129 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 4897b631252..6b56b606721 100644 --- a/README.md +++ b/README.md @@ -688,6 +688,24 @@ golang: # SYFT_GOLANG_NOPROXY env var 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: maven-url: "https://repo1.maven.org/maven2" max-parent-recursive-depth: 5 diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index 73cda77e721..11dcbac19df 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -62,6 +62,7 @@ func DefaultCatalog() Catalog { Scope: source.SquashedScope.String(), Package: defaultPackageConfig(), LinuxKernel: defaultLinuxKernelConfig(), + Golang: defaultGolangConfig(), File: defaultFileConfig(), Relationships: defaultRelationshipsConfig(), Source: defaultSourceConfig(), @@ -131,7 +132,13 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { WithLocalModCacheDir(cfg.Golang.LocalModCacheDir). WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses). 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(). WithSearchRemoteLicenses(cfg.JavaScript.SearchRemoteLicenses). WithNpmBaseURL(cfg.JavaScript.NpmBaseURL), diff --git a/cmd/syft/internal/options/golang.go b/cmd/syft/internal/options/golang.go index c8884602b88..a4121eda08a 100644 --- a/cmd/syft/internal/options/golang.go +++ b/cmd/syft/internal/options/golang.go @@ -1,9 +1,38 @@ package options +import ( + "strings" + + "github.com/anchore/syft/syft/pkg/cataloger/golang" +) + type golangConfig struct { - 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"` - SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"` - Proxy string `json:"proxy" yaml:"proxy" mapstructure:"proxy"` - NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"` + 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"` + SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"` + Proxy string `json:"proxy" yaml:"proxy" mapstructure:"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, + }, + } } diff --git a/syft/pkg/cataloger/golang/cataloger.go b/syft/pkg/cataloger/golang/cataloger.go index 1835942567d..8a26179d3f7 100644 --- a/syft/pkg/cataloger/golang/cataloger.go +++ b/syft/pkg/cataloger/golang/cataloger.go @@ -39,7 +39,8 @@ func NewGoModuleFileCataloger(opts CatalogerConfig) pkg.Cataloger { // NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler. func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger { c := goBinaryCataloger{ - licenses: newGoLicenses(binaryCatalogerName, opts), + licenses: newGoLicenses(binaryCatalogerName, opts), + mainModuleVersion: opts.MainModuleVersion, } return &progressingCataloger{ cataloger: generic.NewCataloger(binaryCatalogerName). diff --git a/syft/pkg/cataloger/golang/config.go b/syft/pkg/cataloger/golang/config.go index 78aef78e549..b8c7ece5a87 100644 --- a/syft/pkg/cataloger/golang/config.go +++ b/syft/pkg/cataloger/golang/config.go @@ -20,11 +20,18 @@ var ( ) type CatalogerConfig struct { - 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"` - SearchRemoteLicenses bool `yaml:"search-remote-licenses" json:"search-remote-licenses" mapstructure:"search-remote-licenses"` - Proxies []string `yaml:"proxies,omitempty" json:"proxies,omitempty" mapstructure:"proxies"` - NoProxy []string `yaml:"no-proxy,omitempty" json:"no-proxy,omitempty" mapstructure:"no-proxy"` + 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"` + SearchRemoteLicenses bool `yaml:"search-remote-licenses" json:"search-remote-licenses" mapstructure:"search-remote-licenses"` + Proxies []string `yaml:"proxies,omitempty" json:"proxies,omitempty" mapstructure:"proxies"` + 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: @@ -32,7 +39,9 @@ type CatalogerConfig struct { // - setting the default no proxy if none is provided // - setting the default local module cache dir if none is provided func DefaultCatalogerConfig() CatalogerConfig { - g := CatalogerConfig{} + g := CatalogerConfig{ + MainModuleVersion: DefaultMainModuleVersionConfig(), + } // first process the proxy settings if len(g.Proxies) == 0 { @@ -76,6 +85,14 @@ func DefaultCatalogerConfig() CatalogerConfig { return g } +func DefaultMainModuleVersionConfig() MainModuleVersionConfig { + return MainModuleVersionConfig{ + FromLDFlags: true, + FromContents: true, + FromBuildSettings: true, + } +} + func (g CatalogerConfig) WithSearchLocalModCacheLicenses(input bool) CatalogerConfig { g.SearchLocalModCacheLicenses = input return g @@ -112,3 +129,23 @@ func (g CatalogerConfig) WithNoProxy(input string) CatalogerConfig { g.NoProxy = strings.Split(input, ",") 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 +} diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index 020d05952ce..98ce20b5ab2 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -38,7 +38,7 @@ var ( // inject the correct version into the main module of the build process knownBuildFlagPatterns = []*regexp.Regexp{ - regexp.MustCompile(`(?m)\.([gG]it)?([bB]uild)?[vV]ersion=(\S+/)*(?Pv?\d+.\d+.\d+[-\w]*)`), + regexp.MustCompile(`(?m)\.([gG]it)?([bB]uild)?[vV]er(sion)?=(\S+/)*(?Pv?\d+.\d+.\d+[-\w]*)`), regexp.MustCompile(`(?m)\.([tT]ag)=(\S+/)*(?Pv?\d+.\d+.\d+[-\w]*)`), } ) @@ -46,7 +46,8 @@ var ( const devel = "(devel)" type goBinaryCataloger struct { - licenses goLicenses + licenses goLicenses + mainModuleVersion MainModuleVersionConfig } // parseGoBinary catalogs packages found in the "buildinfo" section of a binary built by the go compiler. @@ -131,7 +132,7 @@ func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *exten if v, ok := main.Metadata.(pkg.GolangBinaryBuildinfoEntry); ok { metadata = &v } - version := findMainModuleVersion(metadata, gbs, reader) + version := c.findMainModuleVersion(metadata, gbs, reader) if version != "" { main.Version = version @@ -145,12 +146,12 @@ func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *exten var semverPattern = regexp.MustCompile(`\x00(?Pv?(\d+\.\d+\.\d+[-\w]*[+\w]*))\x00`) -func findMainModuleVersion(metadata *pkg.GolangBinaryBuildinfoEntry, gbs pkg.KeyValues, reader io.ReadSeekCloser) string { +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 string - if metadata != nil { + 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 @@ -158,29 +159,31 @@ func findMainModuleVersion(metadata *pkg.GolangBinaryBuildinfoEntry, gbs pkg.Key // 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 + majorVersion, fullVersion = extractVersionFromLDFlags(ldflags) + if fullVersion != "" { + return fullVersion + } } // guess the version from pattern matching in the binary (can result in false positives) - _, _ = reader.Seek(0, io.SeekStart) - 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 + if c.mainModuleVersion.FromContents { + _, _ = reader.Seek(0, io.SeekStart) + 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 hasVersion && hasTimestamp { + 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 From be0059434f42977878fa32cec8b0a8fb64044b45 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 9 Feb 2024 09:10:40 -0500 Subject: [PATCH 3/6] fix test setup for go bin cataloger Signed-off-by: Alex Goodman --- syft/pkg/cataloger/golang/cataloger.go | 9 ++++----- syft/pkg/cataloger/golang/parse_go_binary.go | 7 +++++++ syft/pkg/cataloger/golang/parse_go_binary_test.go | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/syft/pkg/cataloger/golang/cataloger.go b/syft/pkg/cataloger/golang/cataloger.go index 8a26179d3f7..985376d6cca 100644 --- a/syft/pkg/cataloger/golang/cataloger.go +++ b/syft/pkg/cataloger/golang/cataloger.go @@ -38,13 +38,12 @@ func NewGoModuleFileCataloger(opts CatalogerConfig) pkg.Cataloger { // NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler. func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger { - c := goBinaryCataloger{ - licenses: newGoLicenses(binaryCatalogerName, opts), - mainModuleVersion: opts.MainModuleVersion, - } return &progressingCataloger{ cataloger: generic.NewCataloger(binaryCatalogerName). - WithParserByMimeTypes(c.parseGoBinary, mimetype.ExecutableMIMETypeSet.List()...), + WithParserByMimeTypes( + newGoBinaryCataloger(opts).parseGoBinary, + mimetype.ExecutableMIMETypeSet.List()..., + ), } } diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index 98ce20b5ab2..0a94d3eb38d 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -50,6 +50,13 @@ type goBinaryCataloger struct { 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. func (c *goBinaryCataloger) parseGoBinary(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index cd7243b2b20..f4fc84fc5e9 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -921,7 +921,7 @@ func TestBuildGoPkgInfo(t *testing.T) { }, ) - c := goBinaryCataloger{} + c := newGoBinaryCataloger(DefaultCatalogerConfig()) reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(test.binaryContent))) require.NoError(t, err) pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch, reader) From 47f7498f5c120962898aa4c1b09a302587deed05 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 9 Feb 2024 11:36:02 -0500 Subject: [PATCH 4/6] fix unit test Signed-off-by: Alex Goodman --- syft/pkg/cataloger/golang/config_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/syft/pkg/cataloger/golang/config_test.go b/syft/pkg/cataloger/golang/config_test.go index 8c27379bc1c..5719b312fae 100644 --- a/syft/pkg/cataloger/golang/config_test.go +++ b/syft/pkg/cataloger/golang/config_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_Options(t *testing.T) { +func Test_Config(t *testing.T) { type opts struct { local bool cacheDir string @@ -51,6 +51,7 @@ func Test_Options(t *testing.T) { SearchRemoteLicenses: false, Proxies: []string{"https://my.proxy"}, NoProxy: []string{"my.private", "no.proxy"}, + MainModuleVersion: DefaultMainModuleVersionConfig(), }, }, { @@ -74,6 +75,7 @@ func Test_Options(t *testing.T) { SearchRemoteLicenses: true, Proxies: []string{"https://alt.proxy", "direct"}, NoProxy: []string{"alt.no.proxy"}, + MainModuleVersion: DefaultMainModuleVersionConfig(), }, }, } From 8259d6cb349a555fed69982828fd53e1137a8d55 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 9 Feb 2024 11:57:50 -0500 Subject: [PATCH 5/6] fix incorrect test assert ordering Signed-off-by: Alex Goodman --- .../test/integration/package_cataloger_convention_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/syft/internal/test/integration/package_cataloger_convention_test.go b/cmd/syft/internal/test/integration/package_cataloger_convention_test.go index a119e51bded..4040dbf6833 100644 --- a/cmd/syft/internal/test/integration/package_cataloger_convention_test.go +++ b/cmd/syft/internal/test/integration/package_cataloger_convention_test.go @@ -31,7 +31,9 @@ func Test_packageCatalogerExports(t *testing.T) { for pkg, expected := range expectAtLeast { actual, ok := exports[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())) + } } } From 8b8e4c233a8b73dec096c2ef198ee9e391128f10 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 9 Feb 2024 14:42:53 -0500 Subject: [PATCH 6/6] handle error from seek Signed-off-by: Alex Goodman --- syft/pkg/cataloger/golang/parse_go_binary.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index 0a94d3eb38d..eaef4909503 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -175,16 +175,20 @@ func (c *goBinaryCataloger) findMainModuleVersion(metadata *pkg.GolangBinaryBuil // guess the version from pattern matching in the binary (can result in false positives) if c.mainModuleVersion.FromContents { - _, _ = reader.Seek(0, io.SeekStart) - contents, err := io.ReadAll(reader) + _, err := reader.Seek(0, io.SeekStart) if err != nil { - log.WithFields("error", err).Trace("unable to read from go binary reader") + log.WithFields("error", err).Trace("unable to seek to start of go binary reader") } else { - matchMetadata := internal.MatchNamedCaptureGroups(semverPattern, string(contents)) - - version, ok := matchMetadata["version"] - if ok { - return version + 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 + } } } }