-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
parse.go
231 lines (202 loc) · 7.57 KB
/
parse.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package binary
import (
"cmp"
"debug/buildinfo"
"runtime/debug"
"slices"
"sort"
"strings"
"github.com/spf13/pflag"
"golang.org/x/mod/semver"
"golang.org/x/xerrors"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
)
var (
ErrUnrecognizedExe = xerrors.New("unrecognized executable format")
ErrNonGoBinary = xerrors.New("non go binary")
)
// convertError detects buildinfo.errUnrecognizedFormat and convert to
// ErrUnrecognizedExe and convert buildinfo.errNotGoExe to ErrNonGoBinary
func convertError(err error) error {
errText := err.Error()
if strings.HasSuffix(errText, "unrecognized file format") {
return ErrUnrecognizedExe
}
if strings.HasSuffix(errText, "not a Go executable") {
return ErrNonGoBinary
}
return err
}
type Parser struct {
logger *log.Logger
}
func NewParser() *Parser {
return &Parser{
logger: log.WithPrefix("gobinary"),
}
}
// Parse scans file to try to report the Go and module versions.
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
info, err := buildinfo.Read(r)
if err != nil {
return nil, nil, convertError(err)
}
// Ex: "go1.22.3 X:boringcrypto"
stdlibVersion := strings.TrimPrefix(info.GoVersion, "go")
stdlibVersion, _, _ = strings.Cut(stdlibVersion, " ")
ldflags := p.ldFlags(info.Settings)
pkgs := make(ftypes.Packages, 0, len(info.Deps)+2)
pkgs = append(pkgs, ftypes.Package{
// Add the Go version used to build this binary.
Name: "stdlib",
Version: stdlibVersion,
Relationship: ftypes.RelationshipDirect, // Considered a direct dependency as the main module depends on the standard packages.
})
// There are times when gobinaries don't contain Main information.
// e.g. `Go` binaries (e.g. `go`, `gofmt`, etc.)
if info.Main.Path != "" {
pkgs = append(pkgs, ftypes.Package{
// 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, 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: cmp.Or(p.checkVersion(info.Main.Path, info.Main.Version), p.ParseLDFlags(info.Main.Path, ldflags)),
Relationship: ftypes.RelationshipRoot,
})
}
for _, dep := range info.Deps {
// binaries with old go version may incorrectly add module in Deps
// In this case Path == "", Version == "Devel"
// we need to skip this
if dep.Path == "" {
continue
}
mod := dep
if dep.Replace != nil {
mod = dep.Replace
}
pkgs = append(pkgs, ftypes.Package{
Name: mod.Path,
Version: p.checkVersion(mod.Path, mod.Version),
})
}
sort.Sort(pkgs)
return pkgs, nil, nil
}
// 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 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 ""
}
// foundVersions contains discovered versions by type.
// foundVersions doesn't contain duplicates. Versions are filled into first corresponding category.
// Possible elements(categories):
// [0]: Versions using format `github.com/<module_owner>/<module_name>/cmd/**/*.<version>=x.x.x`
// [1]: Versions that use prefixes from `defaultPrefixes`
// [2]: Other versions
var foundVersions = make([][]string, 3)
defaultPrefixes := []string{"main", "common", "version", "cmd"}
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 isVersionXKey(key) && isValidSemVer(val) {
switch {
case strings.HasPrefix(key, name+"/cmd/"):
foundVersions[0] = append(foundVersions[0], val)
case slices.Contains(defaultPrefixes, strings.ToLower(versionPrefix(key))):
foundVersions[1] = append(foundVersions[1], val)
default:
foundVersions[2] = append(foundVersions[2], val)
}
}
}
return p.chooseVersion(name, foundVersions)
}
// chooseVersion chooses version from found versions
// Categories order:
// module name with `cmd` => versions with default prefixes => other versions
// See more in https://github.com/aquasecurity/trivy/issues/6702#issuecomment-2122271427
func (p *Parser) chooseVersion(moduleName string, vers [][]string) string {
for _, versions := range vers {
// Versions for this category was not found
if len(versions) == 0 {
continue
}
// More than 1 version for one category.
// Use empty version.
if len(versions) > 1 {
p.logger.Debug("Unable to detect dependency version. `-ldflags` build info settings contain more than one version. Empty version used.", log.String("dependency", moduleName))
return ""
}
return versions[0]
}
p.logger.Debug("Unable to detect dependency version. `-ldflags` build info settings don't contain version flag. Empty version used.", log.String("dependency", moduleName))
return ""
}
func isVersionXKey(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)
}
// versionPrefix returns version prefix from `-ldflags` flag key
// e.g.
// - `github.com/aquasecurity/trivy/pkg/version/app.ver` => `version`
// - `github.com/google/go-containerregistry/cmd/crane/common.ver` => `common`
func versionPrefix(s string) string {
// Trim module part.
// e.g. `github.com/aquasecurity/trivy/pkg/Version.version` => `Version.version`
if lastIndex := strings.LastIndex(s, "/"); lastIndex > 0 {
s = s[lastIndex+1:]
}
s, _, _ = strings.Cut(s, ".")
return strings.ToLower(s)
}