Skip to content

Commit 419e3d2

Browse files
oatovarknqyf263
andauthored
feat(go): parse main mod version from build info settings (aquasecurity#6564)
Co-authored-by: Teppei Fukuda <[email protected]>
1 parent f0961d5 commit 419e3d2

File tree

4 files changed

+199
-6
lines changed

4 files changed

+199
-6
lines changed

docs/docs/coverage/language/golang.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,18 @@ $ trivy rootfs ./your_binary
7575
It doesn't work with UPX-compressed binaries.
7676

7777
#### Empty versions
78-
There are times when Go uses the `(devel)` version for modules/dependencies and Trivy can't resolve them:
78+
There are times when Go uses the `(devel)` version for modules/dependencies.
7979

8080
- Only Go binaries installed using the `go install` command contain correct (semver) version for the main module.
8181
In other cases, Go uses the `(devel)` version[^3].
8282
- Dependencies replaced with local ones use the `(devel)` versions.
8383

84-
In these cases, the version of such packages is empty.
84+
In the first case, Trivy will attempt to parse any `-ldflags` as a secondary source, and will leave the version
85+
empty if it cannot do so[^4]. For the second case, the version of such packages is empty.
8586

8687
[^1]: It doesn't require the Internet access.
8788
[^2]: Need to download modules to local cache beforehand
8889
[^3]: See https://github.com/aquasecurity/trivy/issues/1837#issuecomment-1832523477
90+
[^4]: See https://github.com/golang/go/issues/63432#issuecomment-1751610604
8991

90-
[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies
92+
[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies

pkg/dependency/parser/golang/binary/parse.go

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package binary
22

33
import (
4+
"cmp"
45
"debug/buildinfo"
6+
"runtime/debug"
57
"sort"
68
"strings"
79

10+
"github.com/spf13/pflag"
11+
"golang.org/x/mod/semver"
812
"golang.org/x/xerrors"
913

1014
"github.com/aquasecurity/trivy/pkg/dependency/types"
@@ -48,15 +52,18 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency,
4852
return nil, nil, convertError(err)
4953
}
5054

55+
ldflags := p.ldFlags(info.Settings)
5156
libs := make([]types.Library, 0, len(info.Deps)+2)
5257
libs = append(libs, []types.Library{
5358
{
5459
// Add main module
5560
Name: info.Main.Path,
5661
// Only binaries installed with `go install` contain semver version of the main module.
57-
// Other binaries use the `(devel)` version.
62+
// Other binaries use the `(devel)` version, but still may contain a stamped version
63+
// set via `go build -ldflags='-X main.version=<semver>'`, so we fallback to this as.
64+
// as a secondary source.
5865
// See https://github.com/aquasecurity/trivy/issues/1837#issuecomment-1832523477.
59-
Version: p.checkVersion(info.Main.Path, info.Main.Version),
66+
Version: cmp.Or(p.checkVersion(info.Main.Path, info.Main.Version), p.ParseLDFlags(info.Main.Path, ldflags)),
6067
Relationship: types.RelationshipRoot,
6168
},
6269
{
@@ -93,8 +100,71 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency,
93100
// checkVersion detects `(devel)` versions, removes them and adds a debug message about it.
94101
func (p *Parser) checkVersion(name, version string) string {
95102
if version == "(devel)" {
96-
p.logger.Debug("Unable to detect dependency version (`(devel)` is used). Version will be empty.", log.String("dependency", name))
103+
p.logger.Debug("Unable to detect main module's dependency version - `(devel)` is used", log.String("dependency", name))
97104
return ""
98105
}
99106
return version
100107
}
108+
109+
func (p *Parser) ldFlags(settings []debug.BuildSetting) []string {
110+
for _, setting := range settings {
111+
if setting.Key != "-ldflags" {
112+
continue
113+
}
114+
115+
return strings.Fields(setting.Value)
116+
}
117+
return nil
118+
}
119+
120+
// ParseLDFlags attempts to parse the binary's version from any `-ldflags` passed to `go build` at build time.
121+
func (p *Parser) ParseLDFlags(name string, flags []string) string {
122+
p.logger.Debug("Parsing dependency's build info settings", "dependency", name, "-ldflags", flags)
123+
fset := pflag.NewFlagSet("ldflags", pflag.ContinueOnError)
124+
// This prevents the flag set from erroring out if other flags were provided.
125+
// This helps keep the implementation small, so that only the -X flag is needed.
126+
fset.ParseErrorsWhitelist.UnknownFlags = true
127+
// The shorthand name is needed here because setting the full name
128+
// to `X` will cause the flag set to look for `--X` instead of `-X`.
129+
// The flag can also be set multiple times, so a string slice is needed
130+
// to handle that edge case.
131+
var x map[string]string
132+
fset.StringToStringVarP(&x, "", "X", nil, "")
133+
if err := fset.Parse(flags); err != nil {
134+
p.logger.Error("Could not parse -ldflags found in build info", log.Err(err))
135+
return ""
136+
}
137+
138+
for key, val := range x {
139+
// It's valid to set the -X flags with quotes so we trim any that might
140+
// have been provided: Ex:
141+
//
142+
// -X main.version=1.0.0
143+
// -X=main.version=1.0.0
144+
// -X 'main.version=1.0.0'
145+
// -X='main.version=1.0.0'
146+
// -X="main.version=1.0.0"
147+
// -X "main.version=1.0.0"
148+
key = strings.TrimLeft(key, `'`)
149+
val = strings.TrimRight(val, `'`)
150+
if isValidXKey(key) && isValidSemVer(val) {
151+
return val
152+
}
153+
}
154+
155+
p.logger.Debug("Unable to detect dependency version used in `-ldflags` build info settings. Empty version used.", log.String("dependency", name))
156+
return ""
157+
}
158+
159+
func isValidXKey(key string) bool {
160+
key = strings.ToLower(key)
161+
// The check for a 'ver' prefix enables the parser to pick up Trivy's own version value that's set.
162+
return strings.HasSuffix(key, "version") || strings.HasSuffix(key, "ver")
163+
}
164+
165+
func isValidSemVer(ver string) bool {
166+
// semver.IsValid strictly checks for the v prefix so prepending 'v'
167+
// here and checking validity again increases the chances that we
168+
// parse a valid semver version.
169+
return semver.IsValid(ver) || semver.IsValid("v"+ver)
170+
}

pkg/dependency/parser/golang/binary/parse_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,22 @@ func TestParse(t *testing.T) {
9898
},
9999
},
100100
},
101+
{
102+
name: "with -ldflags=\"-X main.version=v1.0.0\"",
103+
inputFile: "testdata/main-version-via-ldflags.elf",
104+
want: []types.Library{
105+
{
106+
Name: "github.com/aquasecurity/test",
107+
Version: "v1.0.0",
108+
Relationship: types.RelationshipRoot,
109+
},
110+
{
111+
Name: "stdlib",
112+
Version: "1.22.1",
113+
Relationship: types.RelationshipDirect,
114+
},
115+
},
116+
},
101117
{
102118
name: "sad path",
103119
inputFile: "testdata/dummy",
@@ -122,3 +138,108 @@ func TestParse(t *testing.T) {
122138
})
123139
}
124140
}
141+
142+
func TestParser_ParseLDFlags(t *testing.T) {
143+
type args struct {
144+
name string
145+
flags []string
146+
}
147+
tests := []struct {
148+
name string
149+
args args
150+
want string
151+
}{
152+
{
153+
name: "with version suffix",
154+
args: args{
155+
name: "github.com/aquasecurity/trivy",
156+
flags: []string{
157+
"-s",
158+
"-w",
159+
"-X=foo=bar",
160+
"-X='github.com/aquasecurity/trivy/pkg/version.version=v0.50.1'",
161+
},
162+
},
163+
want: "v0.50.1",
164+
},
165+
{
166+
name: "with version suffix titlecased",
167+
args: args{
168+
name: "github.com/aquasecurity/trivy",
169+
flags: []string{
170+
"-s",
171+
"-w",
172+
"-X=foo=bar",
173+
"-X='github.com/aquasecurity/trivy/pkg/version.Version=v0.50.1'",
174+
},
175+
},
176+
want: "v0.50.1",
177+
},
178+
{
179+
name: "with ver suffix",
180+
args: args{
181+
name: "github.com/aquasecurity/trivy",
182+
flags: []string{
183+
"-s",
184+
"-w",
185+
"-X=foo=bar",
186+
"-X='github.com/aquasecurity/trivy/pkg/version.ver=v0.50.1'",
187+
},
188+
},
189+
want: "v0.50.1",
190+
},
191+
{
192+
name: "with ver suffix titlecased",
193+
args: args{
194+
name: "github.com/aquasecurity/trivy",
195+
flags: []string{
196+
"-s",
197+
"-w",
198+
"-X=foo=bar",
199+
"-X='github.com/aquasecurity/trivy/pkg/version.Ver=v0.50.1'",
200+
},
201+
},
202+
want: "v0.50.1",
203+
},
204+
{
205+
name: "with double quoted flag",
206+
args: args{
207+
name: "github.com/aquasecurity/trivy",
208+
flags: []string{
209+
"-s",
210+
"-w",
211+
"-X=foo=bar",
212+
"-X=\"github.com/aquasecurity/trivy/pkg/version.Ver=0.50.1\"",
213+
},
214+
},
215+
want: "0.50.1",
216+
},
217+
{
218+
name: "with semver version without v prefix",
219+
args: args{
220+
name: "github.com/aquasecurity/trivy",
221+
flags: []string{
222+
"-s",
223+
"-w",
224+
"-X=foo=bar",
225+
"-X='github.com/aquasecurity/trivy/pkg/version.Ver=0.50.1'",
226+
},
227+
},
228+
want: "0.50.1",
229+
},
230+
{
231+
name: "with no flags",
232+
args: args{
233+
name: "github.com/aquasecurity/test",
234+
flags: []string{},
235+
},
236+
want: "",
237+
},
238+
}
239+
for _, tt := range tests {
240+
t.Run(tt.name, func(t *testing.T) {
241+
p := binary.NewParser().(*binary.Parser)
242+
assert.Equal(t, tt.want, p.ParseLDFlags(tt.args.name, tt.args.flags))
243+
})
244+
}
245+
}
Binary file not shown.

0 commit comments

Comments
 (0)