|
1 | 1 | package binary |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "cmp" |
4 | 5 | "debug/buildinfo" |
| 6 | + "runtime/debug" |
5 | 7 | "sort" |
6 | 8 | "strings" |
7 | 9 |
|
| 10 | + "github.com/spf13/pflag" |
| 11 | + "golang.org/x/mod/semver" |
8 | 12 | "golang.org/x/xerrors" |
9 | 13 |
|
10 | 14 | "github.com/aquasecurity/trivy/pkg/dependency/types" |
@@ -48,15 +52,18 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, |
48 | 52 | return nil, nil, convertError(err) |
49 | 53 | } |
50 | 54 |
|
| 55 | + ldflags := p.ldFlags(info.Settings) |
51 | 56 | libs := make([]types.Library, 0, len(info.Deps)+2) |
52 | 57 | libs = append(libs, []types.Library{ |
53 | 58 | { |
54 | 59 | // Add main module |
55 | 60 | Name: info.Main.Path, |
56 | 61 | // 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. |
58 | 65 | // 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)), |
60 | 67 | Relationship: types.RelationshipRoot, |
61 | 68 | }, |
62 | 69 | { |
@@ -93,8 +100,71 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, |
93 | 100 | // checkVersion detects `(devel)` versions, removes them and adds a debug message about it. |
94 | 101 | func (p *Parser) checkVersion(name, version string) string { |
95 | 102 | 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)) |
97 | 104 | return "" |
98 | 105 | } |
99 | 106 | return version |
100 | 107 | } |
| 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 | +} |
0 commit comments