src

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

vulncheck.go (10004B)


      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 	"fmt"
      9 	"go/token"
     10 	"strings"
     11 	"time"
     12 
     13 	"golang.org/x/tools/go/packages"
     14 	"golang.org/x/vuln/internal"
     15 	"golang.org/x/vuln/internal/osv"
     16 	"golang.org/x/vuln/internal/semver"
     17 )
     18 
     19 const (
     20 	fetchingVulnsMessage    = "Fetching vulnerabilities from the database..."
     21 	checkingSrcVulnsMessage = "Checking the code against the vulnerabilities..."
     22 	checkingBinVulnsMessage = "Checking the binary against the vulnerabilities..."
     23 )
     24 
     25 // Result contains information on detected vulnerabilities.
     26 // For call graph analysis, it provides information on reachability
     27 // of vulnerable symbols through entry points of the program.
     28 type Result struct {
     29 	// EntryFunctions are a subset of Functions representing vulncheck entry points.
     30 	EntryFunctions []*FuncNode
     31 
     32 	// Vulns contains information on detected vulnerabilities.
     33 	Vulns []*Vuln
     34 }
     35 
     36 // Vuln provides information on a detected vulnerability. For call
     37 // graph mode, Vuln will also contain the information on how the
     38 // vulnerability is reachable in the user call graph.
     39 type Vuln struct {
     40 	// OSV contains information on the detected vulnerability in the shared
     41 	// vulnerability format.
     42 	//
     43 	// OSV, Symbol, and Package identify a vulnerability.
     44 	//
     45 	// Note that *osv.Entry may describe multiple symbols from multiple
     46 	// packages.
     47 	OSV *osv.Entry
     48 
     49 	// Symbol is the name of the detected vulnerable function or method.
     50 	Symbol string
     51 
     52 	// CallSink is the FuncNode corresponding to Symbol.
     53 	//
     54 	// When analyzing binaries, Symbol is not reachable, or cfg.ScanLevel
     55 	// is symbol, CallSink will be unavailable and set to nil.
     56 	CallSink *FuncNode
     57 
     58 	// Package of Symbol.
     59 	//
     60 	// When the package of symbol is not imported, Package will be
     61 	// unavailable and set to nil.
     62 	Package *packages.Package
     63 }
     64 
     65 // A FuncNode describes a function in the call graph.
     66 type FuncNode struct {
     67 	// Name is the name of the function.
     68 	Name string
     69 
     70 	// RecvType is the receiver object type of this function, if any.
     71 	RecvType string
     72 
     73 	// Package is the package the function is part of.
     74 	Package *packages.Package
     75 
     76 	// Position describes the position of the function in the file.
     77 	Pos *token.Position
     78 
     79 	// CallSites is a set of call sites where this function is called.
     80 	CallSites []*CallSite
     81 }
     82 
     83 func (fn *FuncNode) String() string {
     84 	if fn.RecvType == "" {
     85 		return fmt.Sprintf("%s.%s", fn.Package.PkgPath, fn.Name)
     86 	}
     87 	return fmt.Sprintf("%s.%s", fn.RecvType, fn.Name)
     88 }
     89 
     90 // Receiver returns the FuncNode's receiver, with package path removed.
     91 // Pointers are preserved if present.
     92 func (fn *FuncNode) Receiver() string {
     93 	return strings.Replace(fn.RecvType, fmt.Sprintf("%s.", fn.Package.PkgPath), "", 1)
     94 }
     95 
     96 // A CallSite describes a function call.
     97 type CallSite struct {
     98 	// Parent is the enclosing function where the call is made.
     99 	Parent *FuncNode
    100 
    101 	// Name stands for the name of the function (variable) being called.
    102 	Name string
    103 
    104 	// RecvType is the full path of the receiver object type, if any.
    105 	RecvType string
    106 
    107 	// Position describes the position of the function in the file.
    108 	Pos *token.Position
    109 
    110 	// Resolved indicates if the called function can be statically resolved.
    111 	Resolved bool
    112 }
    113 
    114 // affectingVulns is an internal structure for querying
    115 // vulnerabilities that apply to the current program
    116 // and platform under consideration.
    117 type affectingVulns []*ModVulns
    118 
    119 // ModVulns groups vulnerabilities per module.
    120 type ModVulns struct {
    121 	Module *packages.Module
    122 	Vulns  []*osv.Entry
    123 }
    124 
    125 func affectingVulnerabilities(vulns []*ModVulns, os, arch string) affectingVulns {
    126 	now := time.Now()
    127 	var filtered affectingVulns
    128 	for _, mod := range vulns {
    129 		module := mod.Module
    130 		modVersion := module.Version
    131 		if module.Replace != nil {
    132 			modVersion = module.Replace.Version
    133 		}
    134 		// TODO(https://golang.org/issues/49264): if modVersion == "", try vcs?
    135 		var filteredVulns []*osv.Entry
    136 		for _, v := range mod.Vulns {
    137 			// Ignore vulnerabilities that have been withdrawn
    138 			if v.Withdrawn != nil && v.Withdrawn.Before(now) {
    139 				continue
    140 			}
    141 
    142 			var filteredAffected []osv.Affected
    143 			for _, a := range v.Affected {
    144 				// Vulnerabilities from some databases might contain
    145 				// information on related but different modules that
    146 				// were, say, reported in the same CVE. We filter such
    147 				// information out as it might lead to incorrect results:
    148 				// Computing a latest fix could consider versions of these
    149 				// different packages.
    150 				if a.Module.Path != module.Path {
    151 					continue
    152 				}
    153 				if !affected(modVersion, a) {
    154 					continue
    155 				}
    156 
    157 				var filteredImports []osv.Package
    158 				for _, p := range a.EcosystemSpecific.Packages {
    159 					if matchesPlatform(os, arch, p) {
    160 						filteredImports = append(filteredImports, p)
    161 					}
    162 				}
    163 				// If we pruned all existing Packages, then the affected is
    164 				// empty and we can filter it out. Note that Packages can
    165 				// be empty for vulnerabilities that have no package or
    166 				// symbol information available.
    167 				if len(a.EcosystemSpecific.Packages) != 0 && len(filteredImports) == 0 {
    168 					continue
    169 				}
    170 				a.EcosystemSpecific.Packages = filteredImports
    171 				filteredAffected = append(filteredAffected, a)
    172 			}
    173 			if len(filteredAffected) == 0 {
    174 				continue
    175 			}
    176 			// save the non-empty vulnerability with only
    177 			// affected symbols.
    178 			newV := *v
    179 			newV.Affected = filteredAffected
    180 			filteredVulns = append(filteredVulns, &newV)
    181 		}
    182 
    183 		filtered = append(filtered, &ModVulns{
    184 			Module: module,
    185 			Vulns:  filteredVulns,
    186 		})
    187 	}
    188 	return filtered
    189 }
    190 
    191 // affected checks if modVersion is affected by a:
    192 //   - it is included in one of the affected version ranges
    193 //   - and module version is not "" and "(devel)"
    194 func affected(modVersion string, a osv.Affected) bool {
    195 	const devel = "(devel)"
    196 	if modVersion == "" || modVersion == devel {
    197 		// Module version of "" means the module version is not available
    198 		// and devel means it is in development stage. Either way, we don't
    199 		// know the exact version so we don't want to spam users with
    200 		// potential false alarms.
    201 		return false
    202 	}
    203 	return semver.Affects(a.Ranges, modVersion)
    204 }
    205 
    206 func matchesPlatform(os, arch string, e osv.Package) bool {
    207 	return matchesPlatformComponent(os, e.GOOS) &&
    208 		matchesPlatformComponent(arch, e.GOARCH)
    209 }
    210 
    211 // matchesPlatformComponent reports whether a GOOS (or GOARCH)
    212 // matches a list of GOOS (or GOARCH) values from an osv.EcosystemSpecificImport.
    213 func matchesPlatformComponent(s string, ps []string) bool {
    214 	// An empty input or an empty GOOS or GOARCH list means "matches everything."
    215 	if s == "" || len(ps) == 0 {
    216 		return true
    217 	}
    218 	for _, p := range ps {
    219 		if s == p {
    220 			return true
    221 		}
    222 	}
    223 	return false
    224 }
    225 
    226 // moduleVulns return vulnerabilities for module. If module is unknown,
    227 // it figures the module from package importPath. It returns the module
    228 // whose path is the longest prefix of importPath.
    229 func (aff affectingVulns) moduleVulns(module, importPath string) *ModVulns {
    230 	moduleKnown := module != "" && module != internal.UnknownModulePath
    231 
    232 	isStd := IsStdPackage(importPath)
    233 	var mostSpecificMod *ModVulns // for the case where !moduleKnown
    234 	for _, mod := range aff {
    235 		md := mod
    236 		if isStd && mod.Module.Path == internal.GoStdModulePath {
    237 			// Standard library packages do not have an associated module,
    238 			// so we relate them to the artificial stdlib module.
    239 			return md
    240 		}
    241 
    242 		if moduleKnown {
    243 			if mod.Module.Path == module {
    244 				// If we know exactly which module we need,
    245 				// return its vulnerabilities.
    246 				return md
    247 			}
    248 		} else if strings.HasPrefix(importPath, md.Module.Path) {
    249 			// If module is unknown, we try to figure it out from importPath.
    250 			// We take the module whose path has the longest match to importPath.
    251 			// TODO: do matching based on path components.
    252 			if mostSpecificMod == nil || len(mostSpecificMod.Module.Path) < len(md.Module.Path) {
    253 				mostSpecificMod = md
    254 			}
    255 		}
    256 	}
    257 	return mostSpecificMod
    258 }
    259 
    260 // ForPackage returns the vulnerabilities for the importPath belonging to
    261 // module.
    262 //
    263 // If module is unknown, ForPackage will resolve it as the most specific
    264 // prefix of importPath.
    265 func (aff affectingVulns) ForPackage(module, importPath string) []*osv.Entry {
    266 	mod := aff.moduleVulns(module, importPath)
    267 	if mod == nil {
    268 		return nil
    269 	}
    270 
    271 	if mod.Module.Replace != nil {
    272 		// standard libraries do not have a module nor replace module
    273 		importPath = fmt.Sprintf("%s%s", mod.Module.Replace.Path, strings.TrimPrefix(importPath, mod.Module.Path))
    274 	}
    275 	vulns := mod.Vulns
    276 	packageVulns := []*osv.Entry{}
    277 Vuln:
    278 	for _, v := range vulns {
    279 		for _, a := range v.Affected {
    280 			if len(a.EcosystemSpecific.Packages) == 0 {
    281 				// no packages means all packages are vulnerable
    282 				packageVulns = append(packageVulns, v)
    283 				continue Vuln
    284 			}
    285 
    286 			for _, p := range a.EcosystemSpecific.Packages {
    287 				if p.Path == importPath {
    288 					packageVulns = append(packageVulns, v)
    289 					continue Vuln
    290 				}
    291 			}
    292 		}
    293 	}
    294 	return packageVulns
    295 }
    296 
    297 // ForSymbol returns vulnerabilities for symbol in aff.ForPackage(module, importPath).
    298 func (aff affectingVulns) ForSymbol(module, importPath, symbol string) []*osv.Entry {
    299 	vulns := aff.ForPackage(module, importPath)
    300 	if vulns == nil {
    301 		return nil
    302 	}
    303 
    304 	symbolVulns := []*osv.Entry{}
    305 vulnLoop:
    306 	for _, v := range vulns {
    307 		for _, a := range v.Affected {
    308 			if len(a.EcosystemSpecific.Packages) == 0 {
    309 				// no packages means all symbols of all packages are vulnerable
    310 				symbolVulns = append(symbolVulns, v)
    311 				continue vulnLoop
    312 			}
    313 
    314 			for _, p := range a.EcosystemSpecific.Packages {
    315 				if p.Path != importPath {
    316 					continue
    317 				}
    318 				if len(p.Symbols) > 0 && !contains(p.Symbols, symbol) {
    319 					continue
    320 				}
    321 				symbolVulns = append(symbolVulns, v)
    322 				continue vulnLoop
    323 			}
    324 		}
    325 	}
    326 	return symbolVulns
    327 }
    328 
    329 func contains(symbols []string, target string) bool {
    330 	for _, s := range symbols {
    331 		if s == target {
    332 			return true
    333 		}
    334 	}
    335 	return false
    336 }