/
source.go
373 lines (329 loc) · 10.8 KB
/
source.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vulncheck
import (
"context"
"fmt"
"go/token"
"sync"
"github.com/boss-net/go-vuln/internal/client"
"github.com/boss-net/go-vuln/internal/govulncheck"
"github.com/boss-net/go-vuln/internal/osv"
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/ssa"
)
// Source detects vulnerabilities in packages. The result will contain:
//
// 1) An ImportGraph related to an import of a package with some known
// vulnerabilities.
//
// 2) A RequireGraph related to a require of a module with a package that has
// some known vulnerabilities.
//
// 3) A CallGraph leading to the use of a known vulnerable function or method.
func Source(ctx context.Context, pkgs []*packages.Package, cfg *govulncheck.Config, client *client.Client, graph *PackageGraph) (_ *Result, err error) {
// buildSSA builds a whole program that assumes all packages use the same FileSet.
// Check all packages in pkgs are using the same FileSet.
// TODO(https://go.dev/issue/59729): take FileSet out of Package and
// let Source take a single FileSet. That will make the enforcement
// clearer from the API level.
var fset *token.FileSet
for _, p := range pkgs {
if fset == nil {
fset = p.Fset
} else {
if fset != p.Fset {
return nil, fmt.Errorf("[]*Package must have created with the same FileSet")
}
}
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// If we are building the callgraph, build ssa and the callgraph in parallel
// with fetching vulnerabilities. If the vulns set is empty, return without
// waiting for SSA construction or callgraph to finish.
var (
wg sync.WaitGroup // guards entries, cg, and buildErr
entries []*ssa.Function
cg *callgraph.Graph
buildErr error
)
if cfg.ScanLevel.WantSymbols() {
wg.Add(1)
go func() {
defer wg.Done()
prog, ssaPkgs := buildSSA(pkgs, fset)
entries = entryPoints(ssaPkgs)
cg, buildErr = callGraph(ctx, prog, entries)
}()
}
mods := extractModules(pkgs)
mv, err := FetchVulnerabilities(ctx, client, mods)
if err != nil {
return nil, err
}
modVulns := moduleVulnerabilities(mv)
modVulns = modVulns.filter("", "")
result := &Result{}
vulnPkgModSlice(pkgs, modVulns, result)
// Return result immediately if not in symbol mode or
// if there are no vulnerable packages.
if !cfg.ScanLevel.WantSymbols() || len(result.EntryPackages) == 0 {
return result, nil
}
wg.Wait() // wait for build to finish
if buildErr != nil {
return nil, err
}
vulnCallGraphSlice(entries, modVulns, cg, result, graph)
return result, nil
}
// vulnPkgModSlice computes the slice of pkgs imports and requires graph
// leading to imports/requires of vulnerable packages/modules in modVulns
// and stores the computed slices to result.
func vulnPkgModSlice(pkgs []*packages.Package, modVulns moduleVulnerabilities, result *Result) {
// analyzedPkgs contains information on packages analyzed thus far.
// If a package is mapped to false, this means it has been visited
// but it does not lead to a vulnerable imports. Otherwise, a
// visited package is mapped to true.
analyzedPkgs := make(map[*packages.Package]bool)
for _, pkg := range pkgs {
// Top level packages that lead to vulnerable imports are
// stored as result.EntryPackages graph entry points.
if vulnerable := vulnImportSlice(pkg, modVulns, result, analyzedPkgs); vulnerable {
result.EntryPackages = append(result.EntryPackages, pkg)
}
}
}
// vulnImportSlice checks if pkg has some vulnerabilities or transitively imports
// a package with known vulnerabilities. If that is the case, populates result.Imports
// graph with this reachability information and returns the result.Imports package
// node for pkg. Otherwise, returns nil.
func vulnImportSlice(pkg *packages.Package, modVulns moduleVulnerabilities, result *Result, analyzed map[*packages.Package]bool) bool {
if vulnerable, ok := analyzed[pkg]; ok {
return vulnerable
}
analyzed[pkg] = false
// Recursively compute which direct dependencies lead to an import of
// a vulnerable package and remember the nodes of such dependencies.
transitiveVulnerable := false
for _, imp := range pkg.Imports {
if impVulnerable := vulnImportSlice(imp, modVulns, result, analyzed); impVulnerable {
transitiveVulnerable = true
}
}
// Check if pkg has known vulnerabilities.
vulns := modVulns.vulnsForPackage(pkg.PkgPath)
// If pkg is not vulnerable nor it transitively leads
// to vulnerabilities, jump out.
if !transitiveVulnerable && len(vulns) == 0 {
return false
}
// Create Vuln entry for each symbol of known OSV entries for pkg.
for _, osv := range vulns {
for _, affected := range osv.Affected {
for _, p := range affected.EcosystemSpecific.Packages {
if p.Path != pkg.PkgPath {
continue
}
symbols := p.Symbols
if len(symbols) == 0 {
symbols = allSymbols(pkg.Types)
}
for _, symbol := range symbols {
vuln := &Vuln{
OSV: osv,
Symbol: symbol,
ImportSink: pkg,
}
result.Vulns = append(result.Vulns, vuln)
}
}
}
}
analyzed[pkg] = true
return true
}
// vulnCallGraphSlice checks if known vulnerabilities are transitively reachable from sources
// via call graph cg. If so, populates result.Calls graph with this reachability information.
func vulnCallGraphSlice(sources []*ssa.Function, modVulns moduleVulnerabilities, cg *callgraph.Graph, result *Result, graph *PackageGraph) {
sinksWithVulns := vulnFuncs(cg, modVulns)
// Compute call graph backwards reachable
// from vulnerable functions and methods.
var sinks []*callgraph.Node
for n := range sinksWithVulns {
sinks = append(sinks, n)
}
bcg := callGraphSlice(sinks, false)
// Interesect backwards call graph with forward
// reachable graph to remove redundant edges.
var filteredSources []*callgraph.Node
for _, e := range sources {
if n, ok := bcg.Nodes[e]; ok {
filteredSources = append(filteredSources, n)
}
}
fcg := callGraphSlice(filteredSources, true)
// Get the sinks that are in fact reachable from entry points.
filteredSinks := make(map[*callgraph.Node][]*osv.Entry)
for n, vs := range sinksWithVulns {
if fn, ok := fcg.Nodes[n.Func]; ok {
filteredSinks[fn] = vs
}
}
// Transform the resulting call graph slice into
// vulncheck representation and store it to result.
vulnCallGraph(filteredSources, filteredSinks, result, graph)
}
// callGraphSlice computes a slice of callgraph beginning at starts
// in the direction (forward/backward) controlled by forward flag.
func callGraphSlice(starts []*callgraph.Node, forward bool) *callgraph.Graph {
g := &callgraph.Graph{Nodes: make(map[*ssa.Function]*callgraph.Node)}
visited := make(map[*callgraph.Node]bool)
var visit func(*callgraph.Node)
visit = func(n *callgraph.Node) {
if visited[n] {
return
}
visited[n] = true
var edges []*callgraph.Edge
if forward {
edges = n.Out
} else {
edges = n.In
}
for _, edge := range edges {
nCallee := g.CreateNode(edge.Callee.Func)
nCaller := g.CreateNode(edge.Caller.Func)
callgraph.AddEdge(nCaller, edge.Site, nCallee)
if forward {
visit(edge.Callee)
} else {
visit(edge.Caller)
}
}
}
for _, s := range starts {
visit(s)
}
return g
}
// vulnCallGraph creates vulnerability call graph from sources -> sinks reachability info.
func vulnCallGraph(sources []*callgraph.Node, sinks map[*callgraph.Node][]*osv.Entry, result *Result, graph *PackageGraph) {
nodes := make(map[*ssa.Function]*FuncNode)
// First create entries and sinks and store relevant information.
for _, s := range sources {
fn := createNode(nodes, s.Func, graph)
result.EntryFunctions = append(result.EntryFunctions, fn)
}
for s, vulns := range sinks {
f := s.Func
funNode := createNode(nodes, s.Func, graph)
// Populate CallSink field for each detected vuln symbol.
for _, osv := range vulns {
if vulnMatchesPackage(osv, funNode.Package.PkgPath) {
addCallSinkForVuln(funNode, osv, dbFuncName(f), funNode.Package.PkgPath, result)
}
}
}
visited := make(map[*callgraph.Node]bool)
var visit func(*callgraph.Node)
visit = func(n *callgraph.Node) {
if visited[n] {
return
}
visited[n] = true
for _, edge := range n.In {
nCallee := createNode(nodes, edge.Callee.Func, graph)
nCaller := createNode(nodes, edge.Caller.Func, graph)
call := edge.Site
cs := &CallSite{
Parent: nCaller,
Name: call.Common().Value.Name(),
RecvType: callRecvType(call),
Resolved: resolved(call),
Pos: instrPosition(call),
}
nCallee.CallSites = append(nCallee.CallSites, cs)
visit(edge.Caller)
}
}
for s := range sinks {
visit(s)
}
}
// vulnFuncs returns vulnerability information for vulnerable functions in cg.
func vulnFuncs(cg *callgraph.Graph, modVulns moduleVulnerabilities) map[*callgraph.Node][]*osv.Entry {
m := make(map[*callgraph.Node][]*osv.Entry)
for f, n := range cg.Nodes {
vulns := modVulns.vulnsForSymbol(pkgPath(f), dbFuncName(f))
if len(vulns) > 0 {
m[n] = vulns
}
}
return m
}
// pkgPath returns the path of the f's enclosing package, if any.
// Otherwise, returns "".
func pkgPath(f *ssa.Function) string {
if f.Package() != nil && f.Package().Pkg != nil {
return f.Package().Pkg.Path()
}
return ""
}
func createNode(nodes map[*ssa.Function]*FuncNode, f *ssa.Function, graph *PackageGraph) *FuncNode {
if fn, ok := nodes[f]; ok {
return fn
}
fn := &FuncNode{
Name: f.Name(),
Package: graph.GetPackage(pkgPath(f)),
RecvType: funcRecvType(f),
Pos: funcPosition(f),
}
nodes[f] = fn
return fn
}
// addCallSinkForVuln adds callID as call sink to vuln of result.Vulns
// identified with <osv, symbol, pkg>.
func addCallSinkForVuln(call *FuncNode, osv *osv.Entry, symbol, pkg string, result *Result) {
for _, vuln := range result.Vulns {
if vuln.OSV == osv && vuln.Symbol == symbol && vuln.ImportSink.PkgPath == pkg {
vuln.CallSink = call
return
}
}
}
// extractModules collects modules in `pkgs` up to uniqueness of
// module path and version.
func extractModules(pkgs []*packages.Package) []*packages.Module {
modMap := map[string]*packages.Module{}
seen := map[*packages.Package]bool{}
var extract func(*packages.Package, map[string]*packages.Module)
extract = func(pkg *packages.Package, modMap map[string]*packages.Module) {
if pkg == nil || seen[pkg] {
return
}
if pkg.Module != nil {
if pkg.Module.Replace != nil {
modMap[pkg.Module.Replace.Path] = pkg.Module
} else {
modMap[pkg.Module.Path] = pkg.Module
}
}
seen[pkg] = true
for _, imp := range pkg.Imports {
extract(imp, modMap)
}
}
for _, pkg := range pkgs {
extract(pkg, modMap)
}
modules := []*packages.Module{}
for _, mod := range modMap {
modules = append(modules, mod)
}
return modules
}