337 lines
9.8 KiB
Go
337 lines
9.8 KiB
Go
|
|
// 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
|
||
|
|
}
|
||
|
|
if !affected(modVersion, a) {
|
||
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
// affected checks if modVersion is affected by a:
|
||
|
|
// - it is included in one of the affected version ranges
|
||
|
|
// - and module version is not "" and "(devel)"
|
||
|
|
func affected(modVersion string, a osv.Affected) bool {
|
||
|
|
const devel = "(devel)"
|
||
|
|
if modVersion == "" || modVersion == devel {
|
||
|
|
// Module version of "" means the module version is not available
|
||
|
|
// and devel means it is in development stage. Either way, we don't
|
||
|
|
// know the exact version so we don't want to spam users with
|
||
|
|
// potential false alarms.
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return semver.Affects(a.Ranges, modVersion)
|
||
|
|
}
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|