src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

template.go (6942B)


      1 // Copyright 2022 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 scan
      6 
      7 import (
      8 	"go/token"
      9 	"io"
     10 	"path"
     11 	"sort"
     12 	"strconv"
     13 	"strings"
     14 	"unicode"
     15 	"unicode/utf8"
     16 
     17 	"golang.org/x/vuln/internal/govulncheck"
     18 	"golang.org/x/vuln/internal/osv"
     19 	"golang.org/x/vuln/internal/traces"
     20 )
     21 
     22 type findingSummary struct {
     23 	*govulncheck.Finding
     24 	Compact string
     25 	OSV     *osv.Entry
     26 }
     27 
     28 type summaryCounters struct {
     29 	VulnerabilitiesCalled   int
     30 	ModulesCalled           int
     31 	VulnerabilitiesImported int
     32 	VulnerabilitiesRequired int
     33 	StdlibCalled            bool
     34 }
     35 
     36 func fixupFindings(osvs []*osv.Entry, findings []*findingSummary) {
     37 	for _, f := range findings {
     38 		f.OSV = getOSV(osvs, f.Finding.OSV)
     39 	}
     40 }
     41 
     42 func groupByVuln(findings []*findingSummary) [][]*findingSummary {
     43 	return groupBy(findings, func(left, right *findingSummary) int {
     44 		return -strings.Compare(left.OSV.ID, right.OSV.ID)
     45 	})
     46 }
     47 
     48 func groupByModule(findings []*findingSummary) [][]*findingSummary {
     49 	return groupBy(findings, func(left, right *findingSummary) int {
     50 		return strings.Compare(left.Trace[0].Module, right.Trace[0].Module)
     51 	})
     52 }
     53 
     54 func groupBy(findings []*findingSummary, compare func(left, right *findingSummary) int) [][]*findingSummary {
     55 	switch len(findings) {
     56 	case 0:
     57 		return nil
     58 	case 1:
     59 		return [][]*findingSummary{findings}
     60 	}
     61 	sort.SliceStable(findings, func(i, j int) bool {
     62 		return compare(findings[i], findings[j]) < 0
     63 	})
     64 	result := [][]*findingSummary{}
     65 	first := 0
     66 	for i, next := range findings {
     67 		if i == first {
     68 			continue
     69 		}
     70 		if compare(findings[first], next) != 0 {
     71 			result = append(result, findings[first:i])
     72 			first = i
     73 		}
     74 	}
     75 	result = append(result, findings[first:])
     76 	return result
     77 }
     78 
     79 func isRequired(findings []*findingSummary) bool {
     80 	for _, f := range findings {
     81 		if f.Trace[0].Module != "" {
     82 			return true
     83 		}
     84 	}
     85 	return false
     86 }
     87 
     88 func isImported(findings []*findingSummary) bool {
     89 	for _, f := range findings {
     90 		if f.Trace[0].Package != "" {
     91 			return true
     92 		}
     93 	}
     94 	return false
     95 }
     96 
     97 func isCalled(findings []*findingSummary) bool {
     98 	for _, f := range findings {
     99 		if f.Trace[0].Function != "" {
    100 			return true
    101 		}
    102 	}
    103 	return false
    104 }
    105 
    106 func getOSV(osvs []*osv.Entry, id string) *osv.Entry {
    107 	for _, entry := range osvs {
    108 		if entry.ID == id {
    109 			return entry
    110 		}
    111 	}
    112 	return &osv.Entry{
    113 		ID:               id,
    114 		DatabaseSpecific: &osv.DatabaseSpecific{},
    115 	}
    116 }
    117 
    118 func newFindingSummary(f *govulncheck.Finding) *findingSummary {
    119 	return &findingSummary{
    120 		Finding: f,
    121 		Compact: compactTrace(f),
    122 	}
    123 }
    124 
    125 // platforms returns a string describing the GOOS, GOARCH,
    126 // or GOOS/GOARCH pairs that the vuln affects for a particular
    127 // module mod. If it affects all of them, it returns the empty
    128 // string.
    129 //
    130 // When mod is an empty string, returns platform information for
    131 // all modules of e.
    132 func platforms(mod string, e *osv.Entry) []string {
    133 	if e == nil {
    134 		return nil
    135 	}
    136 	platforms := map[string]bool{}
    137 	for _, a := range e.Affected {
    138 		if mod != "" && a.Module.Path != mod {
    139 			continue
    140 		}
    141 		for _, p := range a.EcosystemSpecific.Packages {
    142 			for _, os := range p.GOOS {
    143 				// In case there are no specific architectures,
    144 				// just list the os entries.
    145 				if len(p.GOARCH) == 0 {
    146 					platforms[os] = true
    147 					continue
    148 				}
    149 				// Otherwise, list all the os+arch combinations.
    150 				for _, arch := range p.GOARCH {
    151 					platforms[os+"/"+arch] = true
    152 				}
    153 			}
    154 			// Cover the case where there are no specific
    155 			// operating systems listed.
    156 			if len(p.GOOS) == 0 {
    157 				for _, arch := range p.GOARCH {
    158 					platforms[arch] = true
    159 				}
    160 			}
    161 		}
    162 	}
    163 	var keys []string
    164 	for k := range platforms {
    165 		keys = append(keys, k)
    166 	}
    167 	sort.Strings(keys)
    168 	return keys
    169 }
    170 
    171 func posToString(p *govulncheck.Position) string {
    172 	if p == nil || p.Line <= 0 {
    173 		return ""
    174 	}
    175 	return token.Position{
    176 		Filename: AbsRelShorter(p.Filename),
    177 		Offset:   p.Offset,
    178 		Line:     p.Line,
    179 		Column:   p.Column,
    180 	}.String()
    181 }
    182 
    183 func symbol(frame *govulncheck.Frame, short bool) string {
    184 	buf := &strings.Builder{}
    185 	addSymbol(buf, frame, short)
    186 	return buf.String()
    187 }
    188 
    189 func symbolName(frame *govulncheck.Frame) string {
    190 	buf := &strings.Builder{}
    191 	addSymbolName(buf, frame)
    192 	return buf.String()
    193 }
    194 
    195 // compactTrace returns a short description of the call stack.
    196 // It prefers to show you the edge from the top module to other code, along with
    197 // the vulnerable symbol.
    198 // Where the vulnerable symbol directly called by the users code, it will only
    199 // show those two points.
    200 // If the vulnerable symbol is in the users code, it will show the entry point
    201 // and the vulnerable symbol.
    202 func compactTrace(finding *govulncheck.Finding) string {
    203 	compact := traces.Compact(finding)
    204 	if len(compact) == 0 {
    205 		return ""
    206 	}
    207 
    208 	l := len(compact)
    209 	iTop := l - 1
    210 	buf := &strings.Builder{}
    211 	topPos := posToString(compact[iTop].Position)
    212 	if topPos != "" {
    213 		buf.WriteString(topPos)
    214 		buf.WriteString(": ")
    215 	}
    216 
    217 	if l > 1 {
    218 		// print the root of the compact trace
    219 		addSymbol(buf, compact[iTop], true)
    220 		buf.WriteString(" calls ")
    221 	}
    222 	if l > 2 {
    223 		// print next element of the trace, if any
    224 		addSymbol(buf, compact[iTop-1], true)
    225 		buf.WriteString(", which")
    226 		if l > 3 {
    227 			// don't print the third element, just acknowledge it
    228 			buf.WriteString(" eventually")
    229 		}
    230 		buf.WriteString(" calls ")
    231 	}
    232 	addSymbol(buf, compact[0], true) // print the vulnerable symbol
    233 	return buf.String()
    234 }
    235 
    236 // notIdentifier reports whether ch is an invalid identifier character.
    237 func notIdentifier(ch rune) bool {
    238 	return !('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' ||
    239 		'0' <= ch && ch <= '9' ||
    240 		ch == '_' ||
    241 		ch >= utf8.RuneSelf && (unicode.IsLetter(ch) || unicode.IsDigit(ch)))
    242 }
    243 
    244 // importPathToAssumedName is taken from goimports, it works out the natural imported name
    245 // for a package.
    246 // This is used to get a shorter identifier in the compact stack trace
    247 func importPathToAssumedName(importPath string) string {
    248 	base := path.Base(importPath)
    249 	if strings.HasPrefix(base, "v") {
    250 		if _, err := strconv.Atoi(base[1:]); err == nil {
    251 			dir := path.Dir(importPath)
    252 			if dir != "." {
    253 				base = path.Base(dir)
    254 			}
    255 		}
    256 	}
    257 	base = strings.TrimPrefix(base, "go-")
    258 	if i := strings.IndexFunc(base, notIdentifier); i >= 0 {
    259 		base = base[:i]
    260 	}
    261 	return base
    262 }
    263 
    264 func addSymbol(w io.Writer, frame *govulncheck.Frame, short bool) {
    265 	if frame.Function == "" {
    266 		return
    267 	}
    268 	if frame.Package != "" {
    269 		pkg := frame.Package
    270 		if short {
    271 			pkg = importPathToAssumedName(frame.Package)
    272 		}
    273 		io.WriteString(w, pkg)
    274 		io.WriteString(w, ".")
    275 	}
    276 	addSymbolName(w, frame)
    277 }
    278 
    279 func addSymbolName(w io.Writer, frame *govulncheck.Frame) {
    280 	if frame.Receiver != "" {
    281 		if frame.Receiver[0] == '*' {
    282 			io.WriteString(w, frame.Receiver[1:])
    283 		} else {
    284 			io.WriteString(w, frame.Receiver)
    285 		}
    286 		io.WriteString(w, ".")
    287 	}
    288 	funcname := strings.Split(frame.Function, "$")[0]
    289 	io.WriteString(w, funcname)
    290 }