src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }