Skip to content

Commit

Permalink
feat(go): parse main mod version from build info settings (#6564)
Browse files Browse the repository at this point in the history
Co-authored-by: Teppei Fukuda <knqyf263@gmail.com>
  • Loading branch information
oatovar and knqyf263 committed May 2, 2024
1 parent f0961d5 commit 419e3d2
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 6 deletions.
8 changes: 5 additions & 3 deletions docs/docs/coverage/language/golang.md
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
@@ -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
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.

0 comments on commit 419e3d2

Please sign in to comment.