blob: 9c8570ac3687e7bfb59fdf4892011852db83d922 [file] [log] [blame]
// Copyright 2022 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 govulncheck
import (
"fmt"
"go/ast"
"go/token"
"sort"
"strconv"
"strings"
"golang.org/x/tools/go/packages"
"golang.org/x/vuln/vulncheck"
)
// A PackageError contains errors from loading a set of packages.
type PackageError struct {
Errors []packages.Error
}
func (e *PackageError) Error() string {
var b strings.Builder
fmt.Fprintln(&b, "Packages contain errors:")
for _, e := range e.Errors {
fmt.Fprintln(&b, e)
}
return b.String()
}
// loadPackages loads the packages matching patterns using cfg, after setting
// the cfg mode flags that vulncheck needs for analysis.
// If the packages contain errors, a PackageError is returned containing a list of the errors,
// along with the packages themselves.
func loadPackages(cfg LegacyConfig) ([]*vulncheck.Package, error) {
patterns := cfg.Patterns
cfg.SourceLoadConfig.Mode |= packages.NeedName | packages.NeedImports | packages.NeedTypes |
packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps |
packages.NeedModule
pkgs, err := packages.Load(cfg.SourceLoadConfig, patterns...)
vpkgs := vulncheck.Convert(pkgs)
if err != nil {
return nil, err
}
var perrs []packages.Error
packages.Visit(pkgs, nil, func(p *packages.Package) {
perrs = append(perrs, p.Errors...)
})
if len(perrs) > 0 {
err = &PackageError{perrs}
}
return vpkgs, err
}
// callInfo is information about calls to vulnerable functions.
type callInfo struct {
// callStacks contains all call stacks to vulnerable functions.
callStacks map[*vulncheck.Vuln][]vulncheck.CallStack
// vulnGroups contains vulnerabilities grouped by ID and package.
vulnGroups [][]*vulncheck.Vuln
// moduleVersions is a map of module paths to versions.
moduleVersions map[string]string
// topPackages contains the top-level packages in the call info.
topPackages map[string]bool
}
// getCallInfo computes call stacks and related information from a vulncheck.Result.
// It also makes a set of top-level packages from pkgs.
func getCallInfo(r *vulncheck.Result, pkgs []*vulncheck.Package) *callInfo {
pset := map[string]bool{}
for _, p := range pkgs {
pset[p.PkgPath] = true
}
cs := vulncheck.CallStacks(r)
updateInitPositions(cs, pkgs)
return &callInfo{
callStacks: cs,
vulnGroups: groupByIDAndPackage(r.Vulns),
moduleVersions: moduleVersionMap(r.Modules),
topPackages: pset,
}
}
func groupByIDAndPackage(vs []*vulncheck.Vuln) [][]*vulncheck.Vuln {
groups := map[[2]string][]*vulncheck.Vuln{}
for _, v := range vs {
key := [2]string{v.OSV.ID, v.PkgPath}
groups[key] = append(groups[key], v)
}
var res [][]*vulncheck.Vuln
for _, g := range groups {
res = append(res, g)
}
sort.Slice(res, func(i, j int) bool {
return res[i][0].PkgPath < res[j][0].PkgPath
})
return res
}
// moduleVersionMap builds a map from module paths to versions.
func moduleVersionMap(mods []*vulncheck.Module) map[string]string {
moduleVersions := map[string]string{}
for _, m := range mods {
v := m.Version
if m.Replace != nil {
v = m.Replace.Version
}
moduleVersions[m.Path] = v
}
return moduleVersions
}
// updateInitPositions populates non-existing positions of init functions
// and their respective calls in callStacks (see #51575).
func updateInitPositions(callStacks map[*vulncheck.Vuln][]vulncheck.CallStack, pkgs []*vulncheck.Package) {
pMap := pkgMap(pkgs)
for _, css := range callStacks {
for _, cs := range css {
for i, _ := range cs {
updateInitPosition(&cs[i], pMap)
if i != len(cs)-1 {
updateInitCallPosition(&cs[i], cs[i+1], pMap)
}
}
}
}
}
// updateInitCallPosition updates the position of a call to init in a stack frame, if
// one already does not exist:
//
// P1.init -> P2.init: position of call to P2.init is the position of "import P2"
// statement in P1
//
// P.init -> P.init#d: P.init is an implicit init. We say it calls the explicit
// P.init#d at the place of "package P" statement.
func updateInitCallPosition(curr *vulncheck.StackEntry, next vulncheck.StackEntry, pkgs map[string]*vulncheck.Package) {
call := curr.Call
if !isInit(next.Function) || (call.Pos != nil && call.Pos.IsValid()) {
// Skip non-init functions and inits whose call site position is available.
return
}
pkg := pkgs[curr.Function.PkgPath]
var pos token.Position
if curr.Function.Name == "init" && curr.Function.PkgPath == next.Function.PkgPath {
// We have implicit P.init calling P.init#d. Set the call position to
// be at "package P" statement position.
pos = packageStatementPos(pkg)
} else {
// Choose the beginning of the import statement as the position.
pos = importStatementPos(pkg, next.Function.PkgPath)
}
call.Pos = &pos
}
func importStatementPos(pkg *vulncheck.Package, importPath string) token.Position {
var importSpec *ast.ImportSpec
spec:
for _, f := range pkg.Syntax {
for _, impSpec := range f.Imports {
// Import spec paths have quotation marks.
impSpecPath, err := strconv.Unquote(impSpec.Path.Value)
if err != nil {
panic(fmt.Sprintf("import specification: package path has no quotation marks: %v", err))
}
if impSpecPath == importPath {
importSpec = impSpec
break spec
}
}
}
if importSpec == nil {
// for sanity, in case of a wild call graph imprecision
return token.Position{}
}
// Choose the beginning of the import statement as the position.
return pkg.Fset.Position(importSpec.Pos())
}
func packageStatementPos(pkg *vulncheck.Package) token.Position {
if len(pkg.Syntax) == 0 {
return token.Position{}
}
// Choose beginning of the package statement as the position. Pick
// the first file since it is as good as any.
return pkg.Fset.Position(pkg.Syntax[0].Package)
}
// updateInitPosition updates the position of P.init function in a stack frame if one
// is not available. The new position is the position of the "package P" statement.
func updateInitPosition(se *vulncheck.StackEntry, pkgs map[string]*vulncheck.Package) {
fun := se.Function
if !isInit(fun) || (fun.Pos != nil && fun.Pos.IsValid()) {
// Skip non-init functions and inits whose position is available.
return
}
pos := packageStatementPos(pkgs[fun.PkgPath])
fun.Pos = &pos
}
func isInit(f *vulncheck.FuncNode) bool {
// A source init function, or anonymous functions used in inits, will
// be named "init#x" by vulncheck (more precisely, ssa), where x is a
// positive integer. Implicit inits are named simply "init".
return f.Name == "init" || strings.HasPrefix(f.Name, "init#")
}
// pkgMap creates a map from package paths to packages for all pkgs
// and their transitive imports.
func pkgMap(pkgs []*vulncheck.Package) map[string]*vulncheck.Package {
m := make(map[string]*vulncheck.Package)
var visit func(*vulncheck.Package)
visit = func(p *vulncheck.Package) {
if _, ok := m[p.PkgPath]; ok {
return
}
m[p.PkgPath] = p
for _, i := range p.Imports {
visit(i)
}
}
for _, p := range pkgs {
visit(p)
}
return m
}