Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(go): parse main mod version from build info settings #6564

Merged
8 changes: 5 additions & 3 deletions docs/docs/coverage/language/golang.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies
76 changes: 73 additions & 3 deletions pkg/dependency/parser/golang/binary/parse.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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=<semver>'`, 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,
},
{
Expand Down Expand Up @@ -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)
}
121 changes: 121 additions & 0 deletions pkg/dependency/parser/golang/binary/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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))
})
}
}
Binary file not shown.