github.com/quay/claircore@v1.5.28/gobin/exe.go (about) 1 package gobin 2 3 import ( 4 "context" 5 "debug/buildinfo" 6 "errors" 7 "fmt" 8 "io" 9 "regexp" 10 "strconv" 11 "strings" 12 _ "unsafe" // for error linkname tricks 13 14 "github.com/quay/zlog" 15 16 "github.com/quay/claircore" 17 ) 18 19 //go:linkname errNotGoExe debug/buildinfo.errNotGoExe 20 var errNotGoExe error 21 22 // It's frustrating that there's no good way to check the error returned from 23 // [buildinfo.Read]. It's either doing a string compare, which will break 24 // silently if the error's contents are changed, or the linker tricks done here, 25 // which will break loudly if the error is renamed or built differently. 26 27 func toPackages(ctx context.Context, out *[]*claircore.Package, p string, r io.ReaderAt) error { 28 bi, err := buildinfo.Read(r) 29 switch { 30 case errors.Is(err, nil): 31 case errors.Is(err, errNotGoExe): 32 return nil 33 default: 34 zlog.Debug(ctx). 35 Err(err). 36 Msg("unable to open executable") 37 return nil 38 } 39 ctx = zlog.ContextWithValues(ctx, "exe", p) 40 pkgdb := "go:" + p 41 badVers := make(map[string]string) 42 defer func() { 43 if len(badVers) == 0 { 44 return 45 } 46 zlog.Warn(ctx). 47 Interface("module_versions", badVers). 48 Msg("invalid semantic versions found in binary") 49 }() 50 51 // TODO(hank) This package could use canonical versions, but the 52 // [claircore.Version] type is lossy for pre-release versions (I'm sorry). 53 54 // TODO(hank) The "go version" is documented as the toolchain that produced 55 // the binary, which may be distinct from the version of the stdlib used? 56 // Need to investigate. 57 runtimeVer, err := ParseVersion(strings.TrimPrefix(bi.GoVersion, "go")) 58 switch { 59 case errors.Is(err, nil): 60 case errors.Is(err, ErrInvalidSemVer): 61 badVers["stdlib"] = bi.GoVersion 62 default: 63 return fmt.Errorf("error parsing runtime version: %q: %w", bi.GoVersion, err) 64 } 65 66 *out = append(*out, &claircore.Package{ 67 Kind: claircore.BINARY, 68 Name: "stdlib", 69 Version: bi.GoVersion, 70 PackageDB: pkgdb, 71 Filepath: p, 72 NormalizedVersion: runtimeVer, 73 }) 74 75 ev := zlog.Debug(ctx) 76 vs := map[string]string{ 77 "stdlib": bi.GoVersion, 78 } 79 var mmv string 80 mainVer, err := ParseVersion(bi.Main.Version) 81 switch { 82 case errors.Is(err, nil): 83 case bi.Main.Version == `(devel)`, bi.Main.Version == ``: 84 // This is currently the state of any main module built from source; see 85 // the package documentation. Don't record it as a "bad" version and 86 // pull out any vcs metadata that's been stamped in. 87 mmv = bi.Main.Version 88 var v []string 89 for _, s := range bi.Settings { 90 switch s.Key { 91 case "vcs": 92 v = append(v, s.Value) 93 case "vcs.revision": 94 switch len(s.Value) { 95 case 40, 64: 96 v = append(v, "commit "+s.Value) 97 default: 98 v = append(v, "rev "+s.Value) 99 } 100 case "vcs.time": 101 v = append(v, "built at "+s.Value) 102 case "vcs.modified": 103 if s.Value == "true" { 104 v = append(v, "dirty") 105 } 106 default: 107 } 108 } 109 switch { 110 case len(v) != 0: 111 mmv = fmt.Sprintf("(devel) (%s)", strings.Join(v, ", ")) 112 case mmv == ``: 113 mmv = `(devel)` // Not totally sure what else to put here. 114 } 115 case errors.Is(err, ErrInvalidSemVer): 116 badVers[bi.Main.Path] = bi.Main.Version 117 mmv = bi.Main.Version 118 default: 119 return fmt.Errorf("error parsing main version: %q: %w", bi.Main.Version, err) 120 } 121 122 // This substitution makes the results look like `go version -m` output. 123 name := bi.Main.Path 124 if name == "" { 125 name = "command-line-arguments" 126 } 127 *out = append(*out, &claircore.Package{ 128 Kind: claircore.BINARY, 129 PackageDB: pkgdb, 130 Name: name, 131 Version: mmv, 132 Filepath: p, 133 NormalizedVersion: mainVer, 134 }) 135 136 if ev.Enabled() { 137 vs[bi.Main.Path] = bi.Main.Version 138 } 139 for _, d := range bi.Deps { 140 // Replacements are only evaluated for the main module and seem to only 141 // be evaluated once, so this shouldn't be recursive. 142 if r := d.Replace; r != nil { 143 d = r 144 } 145 nv, err := ParseVersion(d.Version) 146 switch { 147 case errors.Is(err, nil): 148 case errors.Is(err, ErrInvalidSemVer): 149 badVers[d.Path] = d.Version 150 default: 151 return fmt.Errorf("error parsing dep version: %q: %w", d.Version, err) 152 } 153 154 *out = append(*out, &claircore.Package{ 155 Kind: claircore.BINARY, 156 PackageDB: pkgdb, 157 Name: d.Path, 158 Version: d.Version, 159 Filepath: p, 160 NormalizedVersion: nv, 161 }) 162 163 if ev.Enabled() { 164 vs[d.Path] = d.Version 165 } 166 } 167 ev. 168 Interface("versions", vs). 169 Msg("analyzed exe") 170 return nil 171 } 172 173 var versionRegex = regexp.MustCompile(`^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?$`) 174 var ErrInvalidSemVer = errors.New("invalid semantic version") 175 176 // ParseVersion will return a claircore.Version of type semver given 177 // a valid semantic version. If the string is not a valid semver it 178 // will return an ErrInvalidSemVer. 179 func ParseVersion(ver string) (c claircore.Version, err error) { 180 m := versionRegex.FindStringSubmatch(ver) 181 if m == nil { 182 err = ErrInvalidSemVer 183 return 184 } 185 if c.V[1], err = fitInt32(m[1]); err != nil { 186 return 187 } 188 if c.V[2], err = fitInt32(strings.TrimPrefix(m[2], ".")); err != nil { 189 return 190 } 191 if c.V[3], err = fitInt32(strings.TrimPrefix(m[3], ".")); err != nil { 192 return 193 } 194 c.Kind = "semver" 195 return 196 } 197 198 func fitInt32(seg string) (int32, error) { 199 if len(seg) > 9 { 200 // Technically 2147483647 is possible so this should be well within bounds. 201 // Slicing here to avoid any big.Int allocations at the expense of a little 202 // more accuracy. 203 seg = seg[:9] 204 } 205 if seg == "" { 206 return 0, nil 207 } 208 i, err := strconv.ParseInt(seg, 10, 32) 209 if err != nil { 210 return 0, err 211 } 212 return int32(i), nil 213 }