diff --git a/docs/docs/coverage/language/golang.md b/docs/docs/coverage/language/golang.md index 4127d288a8b8..892746ecef54 100644 --- a/docs/docs/coverage/language/golang.md +++ b/docs/docs/coverage/language/golang.md @@ -75,16 +75,18 @@ $ trivy rootfs ./your_binary It doesn't work with UPX-compressed binaries. #### Empty versions -There are times when Go uses the `(devel)` version for modules/dependencies and Trivy can't resolve them: +There are times when Go uses the `(devel)` version for modules/dependencies. - Only Go binaries installed using the `go install` command contain correct (semver) version for the main module. In other cases, Go uses the `(devel)` version[^3]. - Dependencies replaced with local ones use the `(devel)` versions. -In these cases, the version of such packages is empty. +In the first case, Trivy will attempt to parse any `-ldflags` as a secondary source, and will leave the version +empty if it cannot do so[^4]. For the second case, the version of such packages is empty. [^1]: It doesn't require the Internet access. [^2]: Need to download modules to local cache beforehand [^3]: See https://github.com/aquasecurity/trivy/issues/1837#issuecomment-1832523477 +[^4]: See https://github.com/golang/go/issues/63432#issuecomment-1751610604 -[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies \ No newline at end of file +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies diff --git a/pkg/dependency/parser/golang/binary/parse.go b/pkg/dependency/parser/golang/binary/parse.go index 94fe3900b006..ae7d4d81adae 100644 --- a/pkg/dependency/parser/golang/binary/parse.go +++ b/pkg/dependency/parser/golang/binary/parse.go @@ -1,10 +1,14 @@ package binary import ( + "cmp" "debug/buildinfo" + "runtime/debug" "sort" "strings" + "github.com/spf13/pflag" + "golang.org/x/mod/semver" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/dependency/types" @@ -48,15 +52,18 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, return nil, nil, convertError(err) } + ldflags := p.ldFlags(info.Settings) libs := make([]types.Library, 0, len(info.Deps)+2) libs = append(libs, []types.Library{ { // Add main module Name: info.Main.Path, // Only binaries installed with `go install` contain semver version of the main module. - // Other binaries use the `(devel)` version. + // Other binaries use the `(devel)` version, but still may contain a stamped version + // set via `go build -ldflags='-X main.version='`, so we fallback to this as. + // as a secondary source. // See https://github.com/aquasecurity/trivy/issues/1837#issuecomment-1832523477. - Version: p.checkVersion(info.Main.Path, info.Main.Version), + Version: cmp.Or(p.checkVersion(info.Main.Path, info.Main.Version), p.ParseLDFlags(info.Main.Path, ldflags)), Relationship: types.RelationshipRoot, }, { @@ -93,8 +100,71 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, // checkVersion detects `(devel)` versions, removes them and adds a debug message about it. func (p *Parser) checkVersion(name, version string) string { if version == "(devel)" { - p.logger.Debug("Unable to detect dependency version (`(devel)` is used). Version will be empty.", log.String("dependency", name)) + p.logger.Debug("Unable to detect main module's dependency version - `(devel)` is used", log.String("dependency", name)) return "" } return version } + +func (p *Parser) ldFlags(settings []debug.BuildSetting) []string { + for _, setting := range settings { + if setting.Key != "-ldflags" { + continue + } + + return strings.Fields(setting.Value) + } + return nil +} + +// ParseLDFlags attempts to parse the binary's version from any `-ldflags` passed to `go build` at build time. +func (p *Parser) ParseLDFlags(name string, flags []string) string { + p.logger.Debug("Parsing dependency's build info settings", "dependency", name, "-ldflags", flags) + fset := pflag.NewFlagSet("ldflags", pflag.ContinueOnError) + // This prevents the flag set from erroring out if other flags were provided. + // This helps keep the implementation small, so that only the -X flag is needed. + fset.ParseErrorsWhitelist.UnknownFlags = true + // The shorthand name is needed here because setting the full name + // to `X` will cause the flag set to look for `--X` instead of `-X`. + // The flag can also be set multiple times, so a string slice is needed + // to handle that edge case. + var x map[string]string + fset.StringToStringVarP(&x, "", "X", nil, "") + if err := fset.Parse(flags); err != nil { + p.logger.Error("Could not parse -ldflags found in build info", log.Err(err)) + return "" + } + + for key, val := range x { + // It's valid to set the -X flags with quotes so we trim any that might + // have been provided: Ex: + // + // -X main.version=1.0.0 + // -X=main.version=1.0.0 + // -X 'main.version=1.0.0' + // -X='main.version=1.0.0' + // -X="main.version=1.0.0" + // -X "main.version=1.0.0" + key = strings.TrimLeft(key, `'`) + val = strings.TrimRight(val, `'`) + if isValidXKey(key) && isValidSemVer(val) { + return val + } + } + + p.logger.Debug("Unable to detect dependency version used in `-ldflags` build info settings. Empty version used.", log.String("dependency", name)) + return "" +} + +func isValidXKey(key string) bool { + key = strings.ToLower(key) + // The check for a 'ver' prefix enables the parser to pick up Trivy's own version value that's set. + return strings.HasSuffix(key, "version") || strings.HasSuffix(key, "ver") +} + +func isValidSemVer(ver string) bool { + // semver.IsValid strictly checks for the v prefix so prepending 'v' + // here and checking validity again increases the chances that we + // parse a valid semver version. + return semver.IsValid(ver) || semver.IsValid("v"+ver) +} diff --git a/pkg/dependency/parser/golang/binary/parse_test.go b/pkg/dependency/parser/golang/binary/parse_test.go index 14038194b2c4..96dc7213311c 100644 --- a/pkg/dependency/parser/golang/binary/parse_test.go +++ b/pkg/dependency/parser/golang/binary/parse_test.go @@ -98,6 +98,22 @@ func TestParse(t *testing.T) { }, }, }, + { + name: "with -ldflags=\"-X main.version=v1.0.0\"", + inputFile: "testdata/main-version-via-ldflags.elf", + want: []types.Library{ + { + Name: "github.com/aquasecurity/test", + Version: "v1.0.0", + Relationship: types.RelationshipRoot, + }, + { + Name: "stdlib", + Version: "1.22.1", + Relationship: types.RelationshipDirect, + }, + }, + }, { name: "sad path", inputFile: "testdata/dummy", @@ -122,3 +138,108 @@ func TestParse(t *testing.T) { }) } } + +func TestParser_ParseLDFlags(t *testing.T) { + type args struct { + name string + flags []string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "with version suffix", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version.version=v0.50.1'", + }, + }, + want: "v0.50.1", + }, + { + name: "with version suffix titlecased", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version.Version=v0.50.1'", + }, + }, + want: "v0.50.1", + }, + { + name: "with ver suffix", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version.ver=v0.50.1'", + }, + }, + want: "v0.50.1", + }, + { + name: "with ver suffix titlecased", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version.Ver=v0.50.1'", + }, + }, + want: "v0.50.1", + }, + { + name: "with double quoted flag", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X=\"github.com/aquasecurity/trivy/pkg/version.Ver=0.50.1\"", + }, + }, + want: "0.50.1", + }, + { + name: "with semver version without v prefix", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version.Ver=0.50.1'", + }, + }, + want: "0.50.1", + }, + { + name: "with no flags", + args: args{ + name: "github.com/aquasecurity/test", + flags: []string{}, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := binary.NewParser().(*binary.Parser) + assert.Equal(t, tt.want, p.ParseLDFlags(tt.args.name, tt.args.flags)) + }) + } +} diff --git a/pkg/dependency/parser/golang/binary/testdata/main-version-via-ldflags.elf b/pkg/dependency/parser/golang/binary/testdata/main-version-via-ldflags.elf new file mode 100755 index 000000000000..8271b6872e12 Binary files /dev/null and b/pkg/dependency/parser/golang/binary/testdata/main-version-via-ldflags.elf differ