binary.go (7531B)
1 // Copyright 2021 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package vulncheck 6 7 import ( 8 "context" 9 "fmt" 10 11 "golang.org/x/tools/go/packages" 12 "golang.org/x/vuln/internal" 13 "golang.org/x/vuln/internal/buildinfo" 14 "golang.org/x/vuln/internal/client" 15 "golang.org/x/vuln/internal/govulncheck" 16 "golang.org/x/vuln/internal/semver" 17 ) 18 19 // Bin is an abstraction of Go binary containing 20 // minimal information needed by govulncheck. 21 type Bin struct { 22 // Path of the main package. 23 Path string `json:"path,omitempty"` 24 // Main module. When present, it never has empty information. 25 Main *packages.Module `json:"main,omitempty"` 26 Modules []*packages.Module `json:"modules,omitempty"` 27 PkgSymbols []buildinfo.Symbol `json:"pkgSymbols,omitempty"` 28 GoVersion string `json:"goVersion,omitempty"` 29 GOOS string `json:"goos,omitempty"` 30 GOARCH string `json:"goarch,omitempty"` 31 } 32 33 // Binary detects presence of vulnerable symbols in bin and 34 // emits findings to handler. 35 func Binary(ctx context.Context, handler govulncheck.Handler, bin *Bin, cfg *govulncheck.Config, client *client.Client) error { 36 vr, err := binary(ctx, handler, bin, cfg, client) 37 if err != nil { 38 return err 39 } 40 if cfg.ScanLevel.WantSymbols() { 41 return emitCallFindings(handler, binaryCallstacks(vr)) 42 } 43 return nil 44 } 45 46 // binary detects presence of vulnerable symbols in bin. 47 // It does not compute call graphs so the corresponding 48 // info in Result will be empty. 49 func binary(ctx context.Context, handler govulncheck.Handler, bin *Bin, cfg *govulncheck.Config, client *client.Client) (*Result, error) { 50 graph := NewPackageGraph(bin.GoVersion) 51 mods := append(bin.Modules, graph.GetModule(internal.GoStdModulePath)) 52 53 if bin.Main != nil { 54 mods = append(mods, bin.Main) 55 } 56 57 graph.AddModules(mods...) 58 59 if err := handler.SBOM(bin.SBOM()); err != nil { 60 return nil, err 61 } 62 63 if err := handler.Progress(&govulncheck.Progress{Message: fetchingVulnsMessage}); err != nil { 64 return nil, err 65 } 66 67 mv, err := FetchVulnerabilities(ctx, client, mods) 68 if err != nil { 69 return nil, err 70 } 71 72 // Emit OSV entries immediately in their raw unfiltered form. 73 if err := emitOSVs(handler, mv); err != nil { 74 return nil, err 75 } 76 77 if err := handler.Progress(&govulncheck.Progress{Message: checkingBinVulnsMessage}); err != nil { 78 return nil, err 79 } 80 81 // Emit warning message for ancient Go binaries, defined as binaries 82 // built with Go version without support for debug.BuildInfo (< go1.18). 83 if semver.Valid(bin.GoVersion) && semver.Less(bin.GoVersion, "go1.18") { 84 p := &govulncheck.Progress{Message: fmt.Sprintf("warning: binary built with Go version %s, only standard library vulnerabilities will be checked", bin.GoVersion)} 85 if err := handler.Progress(p); err != nil { 86 return nil, err 87 } 88 } 89 90 if bin.GOOS == "" || bin.GOARCH == "" { 91 p := &govulncheck.Progress{Message: fmt.Sprintf("warning: failed to extract build system specification GOOS: %s GOARCH: %s\n", bin.GOOS, bin.GOARCH)} 92 if err := handler.Progress(p); err != nil { 93 return nil, err 94 } 95 } 96 affVulns := affectingVulnerabilities(mv, bin.GOOS, bin.GOARCH) 97 if err := emitModuleFindings(handler, affVulns); err != nil { 98 return nil, err 99 } 100 101 if !cfg.ScanLevel.WantPackages() || len(affVulns) == 0 { 102 return &Result{}, nil 103 } 104 105 // Group symbols per package to avoid querying affVulns all over again. 106 var pkgSymbols map[string][]string 107 if len(bin.PkgSymbols) == 0 { 108 // The binary exe is stripped. We currently cannot detect inlined 109 // symbols for stripped binaries (see #57764), so we report 110 // vulnerabilities at the go.mod-level precision. 111 pkgSymbols = allKnownVulnerableSymbols(affVulns) 112 } else { 113 pkgSymbols = packagesAndSymbols(bin) 114 } 115 116 impVulns := binImportedVulnPackages(graph, pkgSymbols, affVulns) 117 // Emit information on imported vulnerable packages now to 118 // mimic behavior of source. 119 if err := emitPackageFindings(handler, impVulns); err != nil { 120 return nil, err 121 } 122 123 // Return result immediately if not in symbol mode to mimic the 124 // behavior of source. 125 if !cfg.ScanLevel.WantSymbols() || len(impVulns) == 0 { 126 return &Result{Vulns: impVulns}, nil 127 } 128 129 symVulns := binVulnSymbols(graph, pkgSymbols, affVulns) 130 return &Result{Vulns: symVulns}, nil 131 } 132 133 func packagesAndSymbols(bin *Bin) map[string][]string { 134 pkgSymbols := make(map[string][]string) 135 for _, sym := range bin.PkgSymbols { 136 // If the name of the package is main, we need to expand 137 // it to its full path as that is what vuln db uses. 138 if sym.Pkg == "main" && bin.Path != "" { 139 pkgSymbols[bin.Path] = append(pkgSymbols[bin.Path], sym.Name) 140 } else { 141 pkgSymbols[sym.Pkg] = append(pkgSymbols[sym.Pkg], sym.Name) 142 } 143 } 144 return pkgSymbols 145 } 146 147 func binImportedVulnPackages(graph *PackageGraph, pkgSymbols map[string][]string, affVulns affectingVulns) []*Vuln { 148 var vulns []*Vuln 149 for pkg := range pkgSymbols { 150 for _, osv := range affVulns.ForPackage(internal.UnknownModulePath, pkg) { 151 vuln := &Vuln{ 152 OSV: osv, 153 Package: graph.GetPackage(pkg), 154 } 155 vulns = append(vulns, vuln) 156 } 157 } 158 return vulns 159 } 160 161 func binVulnSymbols(graph *PackageGraph, pkgSymbols map[string][]string, affVulns affectingVulns) []*Vuln { 162 var vulns []*Vuln 163 for pkg, symbols := range pkgSymbols { 164 for _, symbol := range symbols { 165 for _, osv := range affVulns.ForSymbol(internal.UnknownModulePath, pkg, symbol) { 166 vuln := &Vuln{ 167 OSV: osv, 168 Symbol: symbol, 169 Package: graph.GetPackage(pkg), 170 } 171 vulns = append(vulns, vuln) 172 } 173 } 174 } 175 return vulns 176 } 177 178 // allKnownVulnerableSymbols returns all known vulnerable symbols for packages in graph. 179 // If all symbols of a package are vulnerable, that is modeled as a wild car symbol "<pkg-path>/*". 180 func allKnownVulnerableSymbols(affVulns affectingVulns) map[string][]string { 181 pkgSymbols := make(map[string][]string) 182 for _, mv := range affVulns { 183 for _, osv := range mv.Vulns { 184 for _, affected := range osv.Affected { 185 for _, p := range affected.EcosystemSpecific.Packages { 186 syms := p.Symbols 187 if len(syms) == 0 { 188 // If every symbol of pkg is vulnerable, we would ideally 189 // compute every symbol mentioned in the pkg and then add 190 // Vuln entry for it, just as we do in Source. However, 191 // we don't have code of pkg here and we don't even have 192 // pkg symbols used in stripped binary, so we add a placeholder 193 // symbol. 194 // 195 // Note: this should not affect output of govulncheck since 196 // in binary mode no symbol/call stack information is 197 // communicated back to the user. 198 syms = []string{fmt.Sprintf("%s/*", p.Path)} 199 } 200 201 pkgSymbols[p.Path] = append(pkgSymbols[p.Path], syms...) 202 } 203 } 204 } 205 } 206 return pkgSymbols 207 } 208 209 func (bin *Bin) SBOM() (sbom *govulncheck.SBOM) { 210 sbom = &govulncheck.SBOM{} 211 if bin.Main != nil { 212 sbom.Roots = []string{bin.Main.Path} 213 sbom.Modules = append(sbom.Modules, &govulncheck.Module{ 214 Path: bin.Main.Path, 215 Version: bin.Main.Version, 216 }) 217 } 218 219 sbom.GoVersion = bin.GoVersion 220 for _, mod := range bin.Modules { 221 if mod.Replace != nil { 222 mod = mod.Replace 223 } 224 sbom.Modules = append(sbom.Modules, &govulncheck.Module{ 225 Path: mod.Path, 226 Version: mod.Version, 227 }) 228 } 229 230 // add stdlib to mirror source mode output 231 sbom.Modules = append(sbom.Modules, &govulncheck.Module{ 232 Path: internal.GoStdModulePath, 233 Version: bin.GoVersion, 234 }) 235 236 return sbom 237 }