src

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

source.go (8753B)


      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 	"sync"
     10 
     11 	"golang.org/x/tools/go/callgraph"
     12 	"golang.org/x/tools/go/packages"
     13 	"golang.org/x/tools/go/ssa"
     14 	"golang.org/x/vuln/internal/client"
     15 	"golang.org/x/vuln/internal/govulncheck"
     16 	"golang.org/x/vuln/internal/osv"
     17 )
     18 
     19 // Source detects vulnerabilities in pkgs and emits the findings to handler.
     20 func Source(ctx context.Context, handler govulncheck.Handler, cfg *govulncheck.Config, client *client.Client, graph *PackageGraph) error {
     21 	vr, err := source(ctx, handler, cfg, client, graph)
     22 	if err != nil {
     23 		return err
     24 	}
     25 
     26 	if cfg.ScanLevel.WantSymbols() {
     27 		return emitCallFindings(handler, sourceCallstacks(vr))
     28 	}
     29 	return nil
     30 }
     31 
     32 // source detects vulnerabilities in packages. It emits findings to handler
     33 // and produces a Result that contains info on detected vulnerabilities.
     34 //
     35 // Assumes that pkgs are non-empty and belong to the same program.
     36 func source(ctx context.Context, handler govulncheck.Handler, cfg *govulncheck.Config, client *client.Client, graph *PackageGraph) (*Result, error) {
     37 	ctx, cancel := context.WithCancel(ctx)
     38 	defer cancel()
     39 
     40 	// If we are building the callgraph, build ssa and the callgraph in parallel
     41 	// with fetching vulnerabilities. If the vulns set is empty, return without
     42 	// waiting for SSA construction or callgraph to finish.
     43 	var (
     44 		wg       sync.WaitGroup // guards entries, cg, and buildErr
     45 		entries  []*ssa.Function
     46 		cg       *callgraph.Graph
     47 		buildErr error
     48 	)
     49 	if cfg.ScanLevel.WantSymbols() {
     50 		fset := graph.TopPkgs()[0].Fset
     51 		wg.Add(1)
     52 		go func() {
     53 			defer wg.Done()
     54 			prog, ssaPkgs := buildSSA(graph.TopPkgs(), fset)
     55 			entries = entryPoints(ssaPkgs)
     56 			cg, buildErr = callGraph(ctx, prog, entries)
     57 		}()
     58 	}
     59 
     60 	if err := handler.SBOM(graph.SBOM()); err != nil {
     61 		return nil, err
     62 	}
     63 
     64 	if err := handler.Progress(&govulncheck.Progress{Message: fetchingVulnsMessage}); err != nil {
     65 		return nil, err
     66 	}
     67 
     68 	mv, err := FetchVulnerabilities(ctx, client, graph.Modules())
     69 	if err != nil {
     70 		return nil, err
     71 	}
     72 
     73 	// Emit OSV entries immediately in their raw unfiltered form.
     74 	if err := emitOSVs(handler, mv); err != nil {
     75 		return nil, err
     76 	}
     77 
     78 	if err := handler.Progress(&govulncheck.Progress{Message: checkingSrcVulnsMessage}); err != nil {
     79 		return nil, err
     80 	}
     81 
     82 	affVulns := affectingVulnerabilities(mv, "", "")
     83 	if err := emitModuleFindings(handler, affVulns); err != nil {
     84 		return nil, err
     85 	}
     86 
     87 	if !cfg.ScanLevel.WantPackages() || len(affVulns) == 0 {
     88 		return &Result{}, nil
     89 	}
     90 
     91 	impVulns := importedVulnPackages(affVulns, graph)
     92 	// Emit information on imported vulnerable packages now as
     93 	// call graph computation might take a while.
     94 	if err := emitPackageFindings(handler, impVulns); err != nil {
     95 		return nil, err
     96 	}
     97 
     98 	// Return result immediately if not in symbol mode or
     99 	// if there are no vulnerabilities imported.
    100 	if !cfg.ScanLevel.WantSymbols() || len(impVulns) == 0 {
    101 		return &Result{Vulns: impVulns}, nil
    102 	}
    103 
    104 	wg.Wait() // wait for build to finish
    105 	if buildErr != nil {
    106 		return nil, err
    107 	}
    108 
    109 	entryFuncs, callVulns := calledVulnSymbols(entries, affVulns, cg, graph)
    110 	return &Result{EntryFunctions: entryFuncs, Vulns: callVulns}, nil
    111 }
    112 
    113 // importedVulnPackages detects imported vulnerable packages.
    114 func importedVulnPackages(affVulns affectingVulns, graph *PackageGraph) []*Vuln {
    115 	var vulns []*Vuln
    116 	analyzed := make(map[*packages.Package]bool) // skip analyzing the same package multiple times
    117 	var vulnImports func(pkg *packages.Package)
    118 	vulnImports = func(pkg *packages.Package) {
    119 		if analyzed[pkg] {
    120 			return
    121 		}
    122 
    123 		osvs := affVulns.ForPackage(pkgModPath(pkg), pkg.PkgPath)
    124 		// Create Vuln entry for each OSV entry for pkg.
    125 		for _, osv := range osvs {
    126 			vuln := &Vuln{
    127 				OSV:     osv,
    128 				Package: graph.GetPackage(pkg.PkgPath),
    129 			}
    130 			vulns = append(vulns, vuln)
    131 		}
    132 
    133 		analyzed[pkg] = true
    134 		for _, imp := range pkg.Imports {
    135 			vulnImports(imp)
    136 		}
    137 	}
    138 
    139 	for _, pkg := range graph.TopPkgs() {
    140 		vulnImports(pkg)
    141 	}
    142 	return vulns
    143 }
    144 
    145 // calledVulnSymbols detects vuln symbols transitively reachable from sources
    146 // via call graph cg.
    147 //
    148 // A slice of call graph is computed related to the reachable vulnerabilities. Each
    149 // reachable Vuln has attached FuncNode that can be upward traversed to the entry points.
    150 // Entry points that reach the vulnerable symbols are also returned.
    151 func calledVulnSymbols(sources []*ssa.Function, affVulns affectingVulns, cg *callgraph.Graph, graph *PackageGraph) ([]*FuncNode, []*Vuln) {
    152 	sinksWithVulns := vulnFuncs(cg, affVulns, graph)
    153 
    154 	// Compute call graph backwards reachable
    155 	// from vulnerable functions and methods.
    156 	var sinks []*callgraph.Node
    157 	for n := range sinksWithVulns {
    158 		sinks = append(sinks, n)
    159 	}
    160 	bcg := callGraphSlice(sinks, false)
    161 
    162 	// Interesect backwards call graph with forward
    163 	// reachable graph to remove redundant edges.
    164 	var filteredSources []*callgraph.Node
    165 	for _, e := range sources {
    166 		if n, ok := bcg.Nodes[e]; ok {
    167 			filteredSources = append(filteredSources, n)
    168 		}
    169 	}
    170 	fcg := callGraphSlice(filteredSources, true)
    171 
    172 	// Get the sinks that are in fact reachable from entry points.
    173 	filteredSinks := make(map[*callgraph.Node][]*osv.Entry)
    174 	for n, vs := range sinksWithVulns {
    175 		if fn, ok := fcg.Nodes[n.Func]; ok {
    176 			filteredSinks[fn] = vs
    177 		}
    178 	}
    179 
    180 	// Transform the resulting call graph slice into
    181 	// vulncheck representation.
    182 	return vulnCallGraph(filteredSources, filteredSinks, graph)
    183 }
    184 
    185 // callGraphSlice computes a slice of callgraph beginning at starts
    186 // in the direction (forward/backward) controlled by forward flag.
    187 func callGraphSlice(starts []*callgraph.Node, forward bool) *callgraph.Graph {
    188 	g := &callgraph.Graph{Nodes: make(map[*ssa.Function]*callgraph.Node)}
    189 
    190 	visited := make(map[*callgraph.Node]bool)
    191 	var visit func(*callgraph.Node)
    192 	visit = func(n *callgraph.Node) {
    193 		if visited[n] {
    194 			return
    195 		}
    196 		visited[n] = true
    197 
    198 		var edges []*callgraph.Edge
    199 		if forward {
    200 			edges = n.Out
    201 		} else {
    202 			edges = n.In
    203 		}
    204 
    205 		for _, edge := range edges {
    206 			nCallee := g.CreateNode(edge.Callee.Func)
    207 			nCaller := g.CreateNode(edge.Caller.Func)
    208 			callgraph.AddEdge(nCaller, edge.Site, nCallee)
    209 
    210 			if forward {
    211 				visit(edge.Callee)
    212 			} else {
    213 				visit(edge.Caller)
    214 			}
    215 		}
    216 	}
    217 
    218 	for _, s := range starts {
    219 		visit(s)
    220 	}
    221 	return g
    222 }
    223 
    224 // vulnCallGraph creates vulnerability call graph in terms of sources and sinks.
    225 func vulnCallGraph(sources []*callgraph.Node, sinks map[*callgraph.Node][]*osv.Entry, graph *PackageGraph) ([]*FuncNode, []*Vuln) {
    226 	var entries []*FuncNode
    227 	var vulns []*Vuln
    228 	nodes := make(map[*ssa.Function]*FuncNode)
    229 
    230 	// First create entries and sinks and store relevant information.
    231 	for _, s := range sources {
    232 		fn := createNode(nodes, s.Func, graph)
    233 		entries = append(entries, fn)
    234 	}
    235 
    236 	for s, osvs := range sinks {
    237 		f := s.Func
    238 		funNode := createNode(nodes, s.Func, graph)
    239 
    240 		// Populate CallSink field for each detected vuln symbol.
    241 		for _, osv := range osvs {
    242 			vulns = append(vulns, calledVuln(funNode, osv, dbFuncName(f), funNode.Package))
    243 		}
    244 	}
    245 
    246 	visited := make(map[*callgraph.Node]bool)
    247 	var visit func(*callgraph.Node)
    248 	visit = func(n *callgraph.Node) {
    249 		if visited[n] {
    250 			return
    251 		}
    252 		visited[n] = true
    253 
    254 		for _, edge := range n.In {
    255 			nCallee := createNode(nodes, edge.Callee.Func, graph)
    256 			nCaller := createNode(nodes, edge.Caller.Func, graph)
    257 
    258 			call := edge.Site
    259 			cs := &CallSite{
    260 				Parent:   nCaller,
    261 				Name:     call.Common().Value.Name(),
    262 				RecvType: callRecvType(call),
    263 				Resolved: resolved(call),
    264 				Pos:      instrPosition(call),
    265 			}
    266 			nCallee.CallSites = append(nCallee.CallSites, cs)
    267 
    268 			visit(edge.Caller)
    269 		}
    270 	}
    271 
    272 	for s := range sinks {
    273 		visit(s)
    274 	}
    275 	return entries, vulns
    276 }
    277 
    278 // vulnFuncs returns vulnerability information for vulnerable functions in cg.
    279 func vulnFuncs(cg *callgraph.Graph, affVulns affectingVulns, graph *PackageGraph) map[*callgraph.Node][]*osv.Entry {
    280 	m := make(map[*callgraph.Node][]*osv.Entry)
    281 	for f, n := range cg.Nodes {
    282 		p := pkgPath(f)
    283 		vulns := affVulns.ForSymbol(pkgModPath(graph.GetPackage(p)), p, dbFuncName(f))
    284 		if len(vulns) > 0 {
    285 			m[n] = vulns
    286 		}
    287 	}
    288 	return m
    289 }
    290 
    291 func createNode(nodes map[*ssa.Function]*FuncNode, f *ssa.Function, graph *PackageGraph) *FuncNode {
    292 	if fn, ok := nodes[f]; ok {
    293 		return fn
    294 	}
    295 	fn := &FuncNode{
    296 		Name:     f.Name(),
    297 		Package:  graph.GetPackage(pkgPath(f)),
    298 		RecvType: funcRecvType(f),
    299 		Pos:      funcPosition(f),
    300 	}
    301 	nodes[f] = fn
    302 	return fn
    303 }
    304 
    305 func calledVuln(call *FuncNode, osv *osv.Entry, symbol string, pkg *packages.Package) *Vuln {
    306 	return &Vuln{
    307 		Symbol:   symbol,
    308 		Package:  pkg,
    309 		OSV:      osv,
    310 		CallSink: call,
    311 	}
    312 }