Skip to content

Commit

Permalink
feat(go): parse main mod version from build info settings
Browse files Browse the repository at this point in the history
* Parse the build info for -ldflags. If included, the flags will be
  further parsed, and searched for -X sub-flags. These sub-flags are
  commonly used to set the binary version during build time. A common
  pattern is to run `go build -ldflags='-X main.version=<semver>'` so
  that the main package's version variable is replaced with the latest
  tag. It's not guaranteed that this flag will propogate into the
  binary. See golang/go#63432 for more info.

* Flag parsing reuses the pflags library that's used by the main Trivy
  binary. This keeps the implementation concise, and a bit more robust
  than creating a custom flag parser for -X flag commonly passed to
  -ldflags.

* Add simple binary to test for validity of ldflags parsing. The flag

* The documentation has been updated to reflect the improved accuracy of
  the Go binary parser.
  • Loading branch information
oatovar committed Apr 26, 2024
1 parent 7811ad0 commit 0a8aff3
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 5 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
64 changes: 62 additions & 2 deletions pkg/dependency/parser/golang/binary/parse.go
@@ -1,7 +1,9 @@
package binary

import (
"cmp"
"debug/buildinfo"
"runtime/debug"
"sort"
"strings"

Expand All @@ -10,6 +12,7 @@ import (
"github.com/aquasecurity/trivy/pkg/dependency/types"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
"github.com/spf13/pflag"
)

var (
Expand Down Expand Up @@ -48,6 +51,7 @@ 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{
{
Expand All @@ -59,9 +63,11 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency,
// 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(ldflags)),
},
}...)

Expand Down Expand Up @@ -96,3 +102,57 @@ func (p *Parser) checkVersion(name, version string) string {
}
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(flags []string) string {
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 []string
fset.StringSliceVarP(&x, "", "X", nil, `Set the value of the string variable in importpath named name to value.
This is only effective if the variable is declared in the source code either uninitialized
or initialized to a constant string expression. -X will not work if the initializer makes
a function call or refers to other variables.
Note that before Go 1.5 this option took two separate arguments.`)
fset.Parse(flags)

for _, xx := 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"
xx = strings.Trim(xx, `'"`)
key, val, found := strings.Cut(xx, "=")
if !found {
p.logger.Debug("Unable to parse an -ldflags -X flag value", "got", xx)
continue
}

if strings.EqualFold(key, "main.version") {
return val
}
}

return ""
}
14 changes: 14 additions & 0 deletions pkg/dependency/parser/golang/binary/parse_test.go
Expand Up @@ -132,6 +132,20 @@ 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",
},
{
Name: "stdlib",
Version: "1.22.1",
},
},
},
{
name: "sad path",
inputFile: "testdata/dummy",
Expand Down
Binary file not shown.

0 comments on commit 0a8aff3

Please sign in to comment.