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 }