diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index 88600355c..4b4962830 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -11,6 +11,7 @@ import ( "io" "regexp" "runtime/debug" + "slices" "strings" "time" @@ -97,17 +98,19 @@ func createModuleRelationships(main pkg.Package, deps []pkg.Package) []artifact. return relationships } +var emptyModule debug.Module +var moduleFromPartialPackageBuild = debug.Module{Path: "command-line-arguments"} + func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string, reader io.ReadSeekCloser) (*pkg.Package, []pkg.Package) { - var pkgs []pkg.Package if mod == nil { - return nil, pkgs + return nil, nil } - var empty debug.Module - if mod.Main == empty && mod.Path != "" { - mod.Main = createMainModuleFromPath(mod.Path) + if missingMainModule(mod) { + mod.Main = createMainModuleFromPath(mod) } + var pkgs []pkg.Package for _, dep := range mod.Deps { if dep == nil { continue @@ -130,7 +133,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file } } - if mod.Main == empty { + if mod.Main == emptyModule { return nil, pkgs } @@ -139,6 +142,16 @@ func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file return &main, pkgs } +func missingMainModule(mod *extendedBuildInfo) bool { + if mod.Main == emptyModule && mod.Path != "" { + return true + } + // special case: when invoking go build with a source file and not a package (directory) then you will + // see "command-line-arguments" as the main module path... even though that's not the main module. In this + // circumstance, we should treat the main module as missing and search for it within the dependencies. + return mod.Main == moduleFromPartialPackageBuild +} + func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package { gbs := getBuildSettings(mod.Settings) gover, experiments := getExperimentsFromVersion(mod.GoVersion) @@ -358,8 +371,29 @@ func getExperimentsFromVersion(version string) (string, []string) { return version, experiments } -func createMainModuleFromPath(path string) (mod debug.Module) { - mod.Path = path - mod.Version = devel - return +func createMainModuleFromPath(existing *extendedBuildInfo) debug.Module { + // search for a main module candidate within the dependencies + var mainModuleCandidates []debug.Module + var usedIndex int + for i, dep := range existing.Deps { + if dep == nil { + continue + } + + if dep.Version == devel { + usedIndex = i + mainModuleCandidates = append(mainModuleCandidates, *dep) + } + } + if len(mainModuleCandidates) == 1 { + // we need to prune the dependency from module list + existing.Deps = slices.Delete(existing.Deps, usedIndex, usedIndex+1) + return mainModuleCandidates[0] + } + + // otherwise craft a main module from the path (a bit of a cop out, but allows us to have a main module) + return debug.Module{ + Path: existing.Path, + Version: devel, + } } diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index 0346324fe..8b88b9e12 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -210,7 +210,7 @@ func TestBuildGoPkgInfo(t *testing.T) { }, { name: "buildGoPkgInfo parses a blank mod and returns no packages", - mod: &extendedBuildInfo{&debug.BuildInfo{}, nil, ""}, + mod: &extendedBuildInfo{BuildInfo: &debug.BuildInfo{}, cryptoSettings: nil, arch: ""}, expected: []pkg.Package(nil), }, { @@ -946,6 +946,95 @@ func TestBuildGoPkgInfo(t *testing.T) { }, }}, }, + { + name: "parse a mod from path (partial build of package)", + mod: &extendedBuildInfo{ + BuildInfo: &debug.BuildInfo{ + GoVersion: "go1.22.2", + Main: debug.Module{Path: "command-line-arguments"}, + Settings: []debug.BuildSetting{ + { + Key: "-ldflags", + Value: `build -ldflags="-w -s -X github.com/kuskoman/logstash-exporter/config.Version=v1.7.0 -X github.com/kuskoman/logstash-exporter/config.GitCommit=db696dbcfe5a91d288d5ad44ce8ccbea97e65978 -X github.com/kuskoman/logstash-exporter/config.BuildDate=2024-07-17T08:12:17Z"`, + }, + {Key: "GOARCH", Value: archDetails}, + {Key: "GOOS", Value: "darwin"}, + {Key: "GOAMD64", Value: "v1"}, + }, + Deps: []*debug.Module{ + { + Path: "github.com/kuskoman/something-else", + Version: "v1.2.3", + }, + { + Path: "github.com/kuskoman/logstash-exporter", + Version: "(devel)", + }, + }, + }, + arch: archDetails, + }, + expected: []pkg.Package{ + { + Name: "github.com/kuskoman/something-else", + Language: pkg.Go, + Type: pkg.GoModulePkg, + Version: "v1.2.3", + PURL: "pkg:golang/github.com/kuskoman/something-else@v1.2.3", + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates( + file.Coordinates{ + RealPath: "/a-path", + FileSystemID: "layer-id", + }, + ).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + Metadata: pkg.GolangBinaryBuildinfoEntry{ + GoCompiledVersion: "go1.22.2", + Architecture: archDetails, + MainModule: "github.com/kuskoman/logstash-exporter", // correctly attached the main module + }, + }, + { + Name: "github.com/kuskoman/logstash-exporter", + Language: pkg.Go, + Type: pkg.GoModulePkg, + Version: "v1.7.0", + PURL: "pkg:golang/github.com/kuskoman/logstash-exporter@v1.7.0", + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates( + file.Coordinates{ + RealPath: "/a-path", + FileSystemID: "layer-id", + }, + ).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + Metadata: pkg.GolangBinaryBuildinfoEntry{ + GoCompiledVersion: "go1.22.2", + BuildSettings: []pkg.KeyValue{ + { + Key: "-ldflags", + Value: `build -ldflags="-w -s -X github.com/kuskoman/logstash-exporter/config.Version=v1.7.0 -X github.com/kuskoman/logstash-exporter/config.GitCommit=db696dbcfe5a91d288d5ad44ce8ccbea97e65978 -X github.com/kuskoman/logstash-exporter/config.BuildDate=2024-07-17T08:12:17Z"`, + }, + { + Key: "GOARCH", + Value: "amd64", + }, + { + Key: "GOOS", + Value: "darwin", + }, + { + Key: "GOAMD64", + Value: "v1", + }, + }, + Architecture: archDetails, + MainModule: "github.com/kuskoman/logstash-exporter", + }, + }, + }, + }, } for _, test := range tests { @@ -1092,6 +1181,12 @@ func Test_extractVersionFromLDFlags(t *testing.T) { wantMajorVersion: "6", wantFullVersion: "v6.1.7", }, + { + name: "logstash-exporter", + ldflags: `build -ldflags="-w -s -X github.com/kuskoman/logstash-exporter/config.Version=v1.7.0 -X github.com/kuskoman/logstash-exporter/config.GitCommit=db696dbcfe5a91d288d5ad44ce8ccbea97e65978 -X github.com/kuskoman/logstash-exporter/config.BuildDate=2024-07-17T08:12:17Z"`, + wantMajorVersion: "1", + wantFullVersion: "v1.7.0", + }, ////////////////////////////////////////////////////////////////// // negative cases { diff --git a/syft/pkg/cataloger/golang/scan_binary.go b/syft/pkg/cataloger/golang/scan_binary.go index 846e34a11..720008045 100644 --- a/syft/pkg/cataloger/golang/scan_binary.go +++ b/syft/pkg/cataloger/golang/scan_binary.go @@ -56,7 +56,7 @@ func scanFile(reader unionreader.UnionReader, filename string) []*extendedBuildI } } - builds = append(builds, &extendedBuildInfo{bi, v, arch}) + builds = append(builds, &extendedBuildInfo{BuildInfo: bi, cryptoSettings: v, arch: arch}) } return builds }