| // 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{} |
| addSymbolName(buf, frame, short) |
| 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 |
| addSymbolName(buf, compact[iTop], true) |
| buf.WriteString(" calls ") |
| } |
| if l > 2 { |
| // print next element of the trace, if any |
| addSymbolName(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 ") |
| } |
| addSymbolName(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 addSymbolName(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, ".") |
| } |
| 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) |
| } |