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 }