blob: 8e1c0d098fb4a0a298d0980816a35f9e567808e9 [file] [log] [blame]
// 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 audit finds vulnerabilities affecting Go packages.
package audit
import (
"fmt"
"go/token"
"io"
"strings"
"golang.org/x/tools/go/packages"
"golang.org/x/vulndb/osv"
)
// Preamble with types and common functionality used by vulnerability detection mechanisms in detect_*.go files.
// Finding represents a finding for the use of a vulnerable symbol or an imported vulnerable package.
// Provides info on symbol location, trace leading up to the symbol use, and associated vulnerabilities.
type Finding struct {
Symbol string
Position *token.Position `json:",omitempty"`
Type SymbolType
Vulns []osv.Entry
Trace []TraceElem
// Approximate measure for indicating how useful the finding might be to the audit client.
// The smaller the weight, the more useful is the finding.
weight int
}
// SymbolType represents a type of a symbol use: function, global, or an import statement.
type SymbolType int
// enum values for SymbolType.
const (
FunctionType SymbolType = iota
ImportType
GlobalType
)
// TraceElem represents an entry in the finding trace. Represents a function call or an import statement.
type TraceElem struct {
Description string
Position *token.Position `json:",omitempty"`
}
// Write method for findings showing the trace and the associated vulnerabilities.
func (f Finding) Write(w io.Writer) {
var pos string
if f.Position != nil {
pos = fmt.Sprintf(" (%s)", f.Position)
}
fmt.Fprintf(w, "Trace:\n%s%s\n", f.Symbol, pos)
writeTrace(w, f.Trace)
io.WriteString(w, "\n")
writeVulns(w, f.Vulns)
io.WriteString(w, "\n")
}
// writeTrace in reverse order, e.g., entry point is written last.
func writeTrace(w io.Writer, trace []TraceElem) {
for i := len(trace) - 1; i >= 0; i-- {
trace[i].Write(w)
io.WriteString(w, "\n")
}
}
func writeVulns(w io.Writer, vulns []osv.Entry) {
fmt.Fprintf(w, "Vulnerabilities:\n")
for _, v := range vulns {
fmt.Fprintf(w, "%s (%s)\n", v.Package.Name, v.EcosystemSpecific.URL)
}
}
func (e TraceElem) Write(w io.Writer) {
var pos string
if e.Position != nil {
pos = fmt.Sprintf(" (%s)", e.Position)
}
fmt.Fprintf(w, "%s%s", e.Description, pos)
}
// MarshalText implements the encoding.TextMarshaler interface.
func (s SymbolType) MarshalText() ([]byte, error) {
var name string
switch s {
default:
name = "unrecognized"
case FunctionType:
name = "function"
case ImportType:
name = "import"
case GlobalType:
name = "global"
}
return []byte(name), nil
}
type modVulns struct {
mod *packages.Module
vulns []*osv.Entry
}
type ModuleVulnerabilities []modVulns
func matchesPlatform(os, arch string, e osv.GoSpecific) bool {
matchesOS := len(e.GOOS) == 0
matchesArch := len(e.GOARCH) == 0
for _, o := range e.GOOS {
if os == o {
matchesOS = true
break
}
}
for _, a := range e.GOARCH {
if arch == a {
matchesArch = true
break
}
}
return matchesOS && matchesArch
}
func (mv ModuleVulnerabilities) Filter(os, arch string) ModuleVulnerabilities {
var filteredMod ModuleVulnerabilities
for _, mod := range mv {
module := mod.mod
modVersion := module.Version
if module.Replace != nil {
modVersion = module.Replace.Version
}
var filteredVulns []*osv.Entry
for _, v := range mod.vulns {
// A module version is affected if
// - it is incuded in one of the affected version ranges
// - module version is ""
// The latter means the module version is not available, which
// should happen only for top-level packages for which we want
// to be more conservative.
// TODO: issue warning for "" cases above?
affectsVersion := modVersion == "" || v.Affects.AffectsSemver(modVersion)
if affectsVersion && matchesPlatform(os, arch, v.EcosystemSpecific) {
filteredVulns = append(filteredVulns, v)
}
}
filteredMod = append(filteredMod, modVulns{
mod: module,
vulns: filteredVulns,
})
}
return filteredMod
}
func (mv ModuleVulnerabilities) Num() int {
var num int
for _, m := range mv {
num += len(m.vulns)
}
return num
}
// VulnsForPackage returns the vulnerabilities for the module which is the most
// specific prefixof importPath, or nil if there is no matching module with
// vulnerabilities.
func (mv ModuleVulnerabilities) VulnsForPackage(importPath string) []*osv.Entry {
var mostSpecificMod *modVulns
for _, mod := range mv {
md := mod
if strings.HasPrefix(importPath, md.mod.Path) {
if mostSpecificMod == nil || len(mostSpecificMod.mod.Path) < len(md.mod.Path) {
mostSpecificMod = &md
}
}
}
if mostSpecificMod == nil {
return nil
}
if mostSpecificMod.mod.Replace != nil {
importPath = fmt.Sprintf("%s%s", mostSpecificMod.mod.Replace.Path, strings.TrimPrefix(importPath, mostSpecificMod.mod.Path))
}
vulns := mostSpecificMod.vulns
packageVulns := []*osv.Entry{}
for _, v := range vulns {
if v.Package.Name == importPath {
packageVulns = append(packageVulns, v)
}
}
return packageVulns
}
func (mv ModuleVulnerabilities) VulnsForSymbol(importPath, symbol string) []*osv.Entry {
vulns := mv.VulnsForPackage(importPath)
if vulns == nil {
return nil
}
symbolVulns := []*osv.Entry{}
for _, v := range vulns {
if len(v.EcosystemSpecific.Symbols) == 0 {
symbolVulns = append(symbolVulns, v)
continue
}
for _, s := range v.EcosystemSpecific.Symbols {
if s == symbol {
symbolVulns = append(symbolVulns, v)
break
}
}
}
return symbolVulns
}