| // 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 ( |
| "fmt" |
| "go/token" |
| "strings" |
| "time" |
| |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/vuln/internal" |
| "golang.org/x/vuln/internal/osv" |
| "golang.org/x/vuln/internal/semver" |
| ) |
| |
| const ( |
| fetchingVulnsMessage = "Fetching vulnerabilities from the database..." |
| checkingSrcVulnsMessage = "Checking the code against the vulnerabilities..." |
| checkingBinVulnsMessage = "Checking the binary against the vulnerabilities..." |
| ) |
| |
| // Result contains information on detected vulnerabilities. |
| // For call graph analysis, it provides information on reachability |
| // of vulnerable symbols through entry points of the program. |
| type Result struct { |
| // EntryFunctions are a subset of Functions representing vulncheck entry points. |
| EntryFunctions []*FuncNode |
| |
| // Vulns contains information on detected vulnerabilities. |
| Vulns []*Vuln |
| } |
| |
| // Vuln provides information on a detected vulnerability. For call |
| // graph mode, Vuln will also contain the information on how the |
| // vulnerability is reachable in the user call graph. |
| type Vuln struct { |
| // OSV contains information on the detected vulnerability in the shared |
| // vulnerability format. |
| // |
| // OSV, Symbol, and Package identify a vulnerability. |
| // |
| // Note that *osv.Entry may describe multiple symbols from multiple |
| // packages. |
| OSV *osv.Entry |
| |
| // Symbol is the name of the detected vulnerable function or method. |
| Symbol string |
| |
| // CallSink is the FuncNode corresponding to Symbol. |
| // |
| // When analyzing binaries, Symbol is not reachable, or cfg.ScanLevel |
| // is symbol, CallSink will be unavailable and set to nil. |
| CallSink *FuncNode |
| |
| // Package of Symbol. |
| // |
| // When the package of symbol is not imported, Package will be |
| // unavailable and set to nil. |
| Package *packages.Package |
| } |
| |
| // A FuncNode describes a function in the call graph. |
| type FuncNode struct { |
| // Name is the name of the function. |
| Name string |
| |
| // RecvType is the receiver object type of this function, if any. |
| RecvType string |
| |
| // Package is the package the function is part of. |
| Package *packages.Package |
| |
| // Position describes the position of the function in the file. |
| Pos *token.Position |
| |
| // CallSites is a set of call sites where this function is called. |
| CallSites []*CallSite |
| } |
| |
| func (fn *FuncNode) String() string { |
| if fn.RecvType == "" { |
| return fmt.Sprintf("%s.%s", fn.Package.PkgPath, fn.Name) |
| } |
| return fmt.Sprintf("%s.%s", fn.RecvType, fn.Name) |
| } |
| |
| // Receiver returns the FuncNode's receiver, with package path removed. |
| // Pointers are preserved if present. |
| func (fn *FuncNode) Receiver() string { |
| return strings.Replace(fn.RecvType, fmt.Sprintf("%s.", fn.Package.PkgPath), "", 1) |
| } |
| |
| // A CallSite describes a function call. |
| type CallSite struct { |
| // Parent is the enclosing function where the call is made. |
| Parent *FuncNode |
| |
| // Name stands for the name of the function (variable) being called. |
| Name string |
| |
| // RecvType is the full path of the receiver object type, if any. |
| RecvType string |
| |
| // Position describes the position of the function in the file. |
| Pos *token.Position |
| |
| // Resolved indicates if the called function can be statically resolved. |
| Resolved bool |
| } |
| |
| // affectingVulns is an internal structure for querying |
| // vulnerabilities that apply to the current program |
| // and platform under consideration. |
| type affectingVulns []*ModVulns |
| |
| // ModVulns groups vulnerabilities per module. |
| type ModVulns struct { |
| Module *packages.Module |
| Vulns []*osv.Entry |
| } |
| |
| func affectingVulnerabilities(vulns []*ModVulns, os, arch string) affectingVulns { |
| now := time.Now() |
| var filtered affectingVulns |
| for _, mod := range vulns { |
| module := mod.Module |
| modVersion := module.Version |
| if module.Replace != nil { |
| modVersion = module.Replace.Version |
| } |
| // TODO(https://golang.org/issues/49264): if modVersion == "", try vcs? |
| var filteredVulns []*osv.Entry |
| for _, v := range mod.Vulns { |
| // Ignore vulnerabilities that have been withdrawn |
| if v.Withdrawn != nil && v.Withdrawn.Before(now) { |
| continue |
| } |
| |
| var filteredAffected []osv.Affected |
| for _, a := range v.Affected { |
| // Vulnerabilities from some databases might contain |
| // information on related but different modules that |
| // were, say, reported in the same CVE. We filter such |
| // information out as it might lead to incorrect results: |
| // Computing a latest fix could consider versions of these |
| // different packages. |
| if a.Module.Path != module.Path { |
| continue |
| } |
| |
| // A module version is affected if |
| // - it is included in one of the affected version ranges |
| // - and module version is not "" |
| if modVersion == "" { |
| // Module version of "" means the module version is not available, |
| // and so we don't want to spam users with potential false alarms. |
| continue |
| } |
| if !semver.Affects(a.Ranges, modVersion) { |
| continue |
| } |
| var filteredImports []osv.Package |
| for _, p := range a.EcosystemSpecific.Packages { |
| if matchesPlatform(os, arch, p) { |
| filteredImports = append(filteredImports, p) |
| } |
| } |
| // If we pruned all existing Packages, then the affected is |
| // empty and we can filter it out. Note that Packages can |
| // be empty for vulnerabilities that have no package or |
| // symbol information available. |
| if len(a.EcosystemSpecific.Packages) != 0 && len(filteredImports) == 0 { |
| continue |
| } |
| a.EcosystemSpecific.Packages = filteredImports |
| filteredAffected = append(filteredAffected, a) |
| } |
| if len(filteredAffected) == 0 { |
| continue |
| } |
| // save the non-empty vulnerability with only |
| // affected symbols. |
| newV := *v |
| newV.Affected = filteredAffected |
| filteredVulns = append(filteredVulns, &newV) |
| } |
| |
| filtered = append(filtered, &ModVulns{ |
| Module: module, |
| Vulns: filteredVulns, |
| }) |
| } |
| return filtered |
| } |
| |
| func matchesPlatform(os, arch string, e osv.Package) bool { |
| return matchesPlatformComponent(os, e.GOOS) && |
| matchesPlatformComponent(arch, e.GOARCH) |
| } |
| |
| // matchesPlatformComponent reports whether a GOOS (or GOARCH) |
| // matches a list of GOOS (or GOARCH) values from an osv.EcosystemSpecificImport. |
| func matchesPlatformComponent(s string, ps []string) bool { |
| // An empty input or an empty GOOS or GOARCH list means "matches everything." |
| if s == "" || len(ps) == 0 { |
| return true |
| } |
| for _, p := range ps { |
| if s == p { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // moduleVulns return vulnerabilities for module. If module is unknown, |
| // it figures the module from package importPath. It returns the module |
| // whose path is the longest prefix of importPath. |
| func (aff affectingVulns) moduleVulns(module, importPath string) *ModVulns { |
| moduleKnown := module != "" && module != internal.UnknownModulePath |
| |
| isStd := IsStdPackage(importPath) |
| var mostSpecificMod *ModVulns // for the case where !moduleKnown |
| for _, mod := range aff { |
| md := mod |
| if isStd && mod.Module.Path == internal.GoStdModulePath { |
| // Standard library packages do not have an associated module, |
| // so we relate them to the artificial stdlib module. |
| return md |
| } |
| |
| if moduleKnown { |
| if mod.Module.Path == module { |
| // If we know exactly which module we need, |
| // return its vulnerabilities. |
| return md |
| } |
| } else if strings.HasPrefix(importPath, md.Module.Path) { |
| // If module is unknown, we try to figure it out from importPath. |
| // We take the module whose path has the longest match to importPath. |
| // TODO: do matching based on path components. |
| if mostSpecificMod == nil || len(mostSpecificMod.Module.Path) < len(md.Module.Path) { |
| mostSpecificMod = md |
| } |
| } |
| } |
| return mostSpecificMod |
| } |
| |
| // ForPackage returns the vulnerabilities for the importPath belonging to |
| // module. |
| // |
| // If module is unknown, ForPackage will resolve it as the most specific |
| // prefix of importPath. |
| func (aff affectingVulns) ForPackage(module, importPath string) []*osv.Entry { |
| mod := aff.moduleVulns(module, importPath) |
| if mod == nil { |
| return nil |
| } |
| |
| if mod.Module.Replace != nil { |
| // standard libraries do not have a module nor replace module |
| importPath = fmt.Sprintf("%s%s", mod.Module.Replace.Path, strings.TrimPrefix(importPath, mod.Module.Path)) |
| } |
| vulns := mod.Vulns |
| packageVulns := []*osv.Entry{} |
| Vuln: |
| for _, v := range vulns { |
| for _, a := range v.Affected { |
| if len(a.EcosystemSpecific.Packages) == 0 { |
| // no packages means all packages are vulnerable |
| packageVulns = append(packageVulns, v) |
| continue Vuln |
| } |
| |
| for _, p := range a.EcosystemSpecific.Packages { |
| if p.Path == importPath { |
| packageVulns = append(packageVulns, v) |
| continue Vuln |
| } |
| } |
| } |
| } |
| return packageVulns |
| } |
| |
| // ForSymbol returns vulnerabilities for symbol in aff.ForPackage(module, importPath). |
| func (aff affectingVulns) ForSymbol(module, importPath, symbol string) []*osv.Entry { |
| vulns := aff.ForPackage(module, importPath) |
| if vulns == nil { |
| return nil |
| } |
| |
| symbolVulns := []*osv.Entry{} |
| vulnLoop: |
| for _, v := range vulns { |
| for _, a := range v.Affected { |
| if len(a.EcosystemSpecific.Packages) == 0 { |
| // no packages means all symbols of all packages are vulnerable |
| symbolVulns = append(symbolVulns, v) |
| continue vulnLoop |
| } |
| |
| for _, p := range a.EcosystemSpecific.Packages { |
| if p.Path != importPath { |
| continue |
| } |
| if len(p.Symbols) > 0 && !contains(p.Symbols, symbol) { |
| continue |
| } |
| symbolVulns = append(symbolVulns, v) |
| continue vulnLoop |
| } |
| } |
| } |
| return symbolVulns |
| } |
| |
| func contains(symbols []string, target string) bool { |
| for _, s := range symbols { |
| if s == target { |
| return true |
| } |
| } |
| return false |
| } |