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 }