291 lines
6.8 KiB
Go
291 lines
6.8 KiB
Go
// Copyright 2022 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 scan
|
|
|
|
import (
|
|
"go/token"
|
|
"io"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"golang.org/x/vuln/internal/govulncheck"
|
|
"golang.org/x/vuln/internal/osv"
|
|
"golang.org/x/vuln/internal/traces"
|
|
)
|
|
|
|
type findingSummary struct {
|
|
*govulncheck.Finding
|
|
Compact string
|
|
OSV *osv.Entry
|
|
}
|
|
|
|
type summaryCounters struct {
|
|
VulnerabilitiesCalled int
|
|
ModulesCalled int
|
|
VulnerabilitiesImported int
|
|
VulnerabilitiesRequired int
|
|
StdlibCalled bool
|
|
}
|
|
|
|
func fixupFindings(osvs []*osv.Entry, findings []*findingSummary) {
|
|
for _, f := range findings {
|
|
f.OSV = getOSV(osvs, f.Finding.OSV)
|
|
}
|
|
}
|
|
|
|
func groupByVuln(findings []*findingSummary) [][]*findingSummary {
|
|
return groupBy(findings, func(left, right *findingSummary) int {
|
|
return -strings.Compare(left.OSV.ID, right.OSV.ID)
|
|
})
|
|
}
|
|
|
|
func groupByModule(findings []*findingSummary) [][]*findingSummary {
|
|
return groupBy(findings, func(left, right *findingSummary) int {
|
|
return strings.Compare(left.Trace[0].Module, right.Trace[0].Module)
|
|
})
|
|
}
|
|
|
|
func groupBy(findings []*findingSummary, compare func(left, right *findingSummary) int) [][]*findingSummary {
|
|
switch len(findings) {
|
|
case 0:
|
|
return nil
|
|
case 1:
|
|
return [][]*findingSummary{findings}
|
|
}
|
|
sort.SliceStable(findings, func(i, j int) bool {
|
|
return compare(findings[i], findings[j]) < 0
|
|
})
|
|
result := [][]*findingSummary{}
|
|
first := 0
|
|
for i, next := range findings {
|
|
if i == first {
|
|
continue
|
|
}
|
|
if compare(findings[first], next) != 0 {
|
|
result = append(result, findings[first:i])
|
|
first = i
|
|
}
|
|
}
|
|
result = append(result, findings[first:])
|
|
return result
|
|
}
|
|
|
|
func isRequired(findings []*findingSummary) bool {
|
|
for _, f := range findings {
|
|
if f.Trace[0].Module != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isImported(findings []*findingSummary) bool {
|
|
for _, f := range findings {
|
|
if f.Trace[0].Package != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isCalled(findings []*findingSummary) bool {
|
|
for _, f := range findings {
|
|
if f.Trace[0].Function != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getOSV(osvs []*osv.Entry, id string) *osv.Entry {
|
|
for _, entry := range osvs {
|
|
if entry.ID == id {
|
|
return entry
|
|
}
|
|
}
|
|
return &osv.Entry{
|
|
ID: id,
|
|
DatabaseSpecific: &osv.DatabaseSpecific{},
|
|
}
|
|
}
|
|
|
|
func newFindingSummary(f *govulncheck.Finding) *findingSummary {
|
|
return &findingSummary{
|
|
Finding: f,
|
|
Compact: compactTrace(f),
|
|
}
|
|
}
|
|
|
|
// platforms returns a string describing the GOOS, GOARCH,
|
|
// or GOOS/GOARCH pairs that the vuln affects for a particular
|
|
// module mod. If it affects all of them, it returns the empty
|
|
// string.
|
|
//
|
|
// When mod is an empty string, returns platform information for
|
|
// all modules of e.
|
|
func platforms(mod string, e *osv.Entry) []string {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
platforms := map[string]bool{}
|
|
for _, a := range e.Affected {
|
|
if mod != "" && a.Module.Path != mod {
|
|
continue
|
|
}
|
|
for _, p := range a.EcosystemSpecific.Packages {
|
|
for _, os := range p.GOOS {
|
|
// In case there are no specific architectures,
|
|
// just list the os entries.
|
|
if len(p.GOARCH) == 0 {
|
|
platforms[os] = true
|
|
continue
|
|
}
|
|
// Otherwise, list all the os+arch combinations.
|
|
for _, arch := range p.GOARCH {
|
|
platforms[os+"/"+arch] = true
|
|
}
|
|
}
|
|
// Cover the case where there are no specific
|
|
// operating systems listed.
|
|
if len(p.GOOS) == 0 {
|
|
for _, arch := range p.GOARCH {
|
|
platforms[arch] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var keys []string
|
|
for k := range platforms {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func posToString(p *govulncheck.Position) string {
|
|
if p == nil || p.Line <= 0 {
|
|
return ""
|
|
}
|
|
return token.Position{
|
|
Filename: AbsRelShorter(p.Filename),
|
|
Offset: p.Offset,
|
|
Line: p.Line,
|
|
Column: p.Column,
|
|
}.String()
|
|
}
|
|
|
|
func symbol(frame *govulncheck.Frame, short bool) string {
|
|
buf := &strings.Builder{}
|
|
addSymbol(buf, frame, short)
|
|
return buf.String()
|
|
}
|
|
|
|
func symbolName(frame *govulncheck.Frame) string {
|
|
buf := &strings.Builder{}
|
|
addSymbolName(buf, frame)
|
|
return buf.String()
|
|
}
|
|
|
|
// compactTrace returns a short description of the call stack.
|
|
// It prefers to show you the edge from the top module to other code, along with
|
|
// the vulnerable symbol.
|
|
// Where the vulnerable symbol directly called by the users code, it will only
|
|
// show those two points.
|
|
// If the vulnerable symbol is in the users code, it will show the entry point
|
|
// and the vulnerable symbol.
|
|
func compactTrace(finding *govulncheck.Finding) string {
|
|
compact := traces.Compact(finding)
|
|
if len(compact) == 0 {
|
|
return ""
|
|
}
|
|
|
|
l := len(compact)
|
|
iTop := l - 1
|
|
buf := &strings.Builder{}
|
|
topPos := posToString(compact[iTop].Position)
|
|
if topPos != "" {
|
|
buf.WriteString(topPos)
|
|
buf.WriteString(": ")
|
|
}
|
|
|
|
if l > 1 {
|
|
// print the root of the compact trace
|
|
addSymbol(buf, compact[iTop], true)
|
|
buf.WriteString(" calls ")
|
|
}
|
|
if l > 2 {
|
|
// print next element of the trace, if any
|
|
addSymbol(buf, compact[iTop-1], true)
|
|
buf.WriteString(", which")
|
|
if l > 3 {
|
|
// don't print the third element, just acknowledge it
|
|
buf.WriteString(" eventually")
|
|
}
|
|
buf.WriteString(" calls ")
|
|
}
|
|
addSymbol(buf, compact[0], true) // print the vulnerable symbol
|
|
return buf.String()
|
|
}
|
|
|
|
// notIdentifier reports whether ch is an invalid identifier character.
|
|
func notIdentifier(ch rune) bool {
|
|
return !('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' ||
|
|
'0' <= ch && ch <= '9' ||
|
|
ch == '_' ||
|
|
ch >= utf8.RuneSelf && (unicode.IsLetter(ch) || unicode.IsDigit(ch)))
|
|
}
|
|
|
|
// importPathToAssumedName is taken from goimports, it works out the natural imported name
|
|
// for a package.
|
|
// This is used to get a shorter identifier in the compact stack trace
|
|
func importPathToAssumedName(importPath string) string {
|
|
base := path.Base(importPath)
|
|
if strings.HasPrefix(base, "v") {
|
|
if _, err := strconv.Atoi(base[1:]); err == nil {
|
|
dir := path.Dir(importPath)
|
|
if dir != "." {
|
|
base = path.Base(dir)
|
|
}
|
|
}
|
|
}
|
|
base = strings.TrimPrefix(base, "go-")
|
|
if i := strings.IndexFunc(base, notIdentifier); i >= 0 {
|
|
base = base[:i]
|
|
}
|
|
return base
|
|
}
|
|
|
|
func addSymbol(w io.Writer, frame *govulncheck.Frame, short bool) {
|
|
if frame.Function == "" {
|
|
return
|
|
}
|
|
if frame.Package != "" {
|
|
pkg := frame.Package
|
|
if short {
|
|
pkg = importPathToAssumedName(frame.Package)
|
|
}
|
|
io.WriteString(w, pkg)
|
|
io.WriteString(w, ".")
|
|
}
|
|
addSymbolName(w, frame)
|
|
}
|
|
|
|
func addSymbolName(w io.Writer, frame *govulncheck.Frame) {
|
|
if frame.Receiver != "" {
|
|
if frame.Receiver[0] == '*' {
|
|
io.WriteString(w, frame.Receiver[1:])
|
|
} else {
|
|
io.WriteString(w, frame.Receiver)
|
|
}
|
|
io.WriteString(w, ".")
|
|
}
|
|
funcname := strings.Split(frame.Function, "$")[0]
|
|
io.WriteString(w, funcname)
|
|
}
|