blob: 63c0c04dc4e36a5d39cd5c724ad00bf6ac566531 [file] [log] [blame]
// 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 (
"fmt"
"go/token"
"sort"
"strings"
"golang.org/x/vuln/internal"
"golang.org/x/vuln/internal/govulncheck"
"golang.org/x/vuln/internal/osv"
)
type summaries struct {
Affected []vulnSummary `json:"affected,omitempty"`
Unaffected []vulnSummary `json:"unaffected,omitempty"`
AffectedModules int `json:"affected_modules,omitempty"`
StdlibAffected bool `json:"stdlib_affected,omitempty"`
}
type vulnSummary struct {
OSV string
Details string
URL string
Modules []*moduleSummary
Affected bool
}
type moduleSummary struct {
IsStd bool
Module string
FoundVersion string
FixedVersion string
Platforms []string
Traces []traceSummary
}
type traceSummary struct {
Symbol string
Compact string
Trace []frameSummary
}
type frameSummary struct {
Symbol string
Name string
Position string
}
func createSummaries(osvs []*osv.Entry, findings []*govulncheck.Finding) summaries {
s := summaries{}
// group findings by osv
grouped := map[string][]*govulncheck.Finding{}
var osvids []string
for _, f := range findings {
list, found := grouped[f.OSV]
if !found {
osvids = append(osvids, f.OSV)
}
grouped[f.OSV] = append(list, f)
}
// unaffected are (imported) OSVs none of
// which vulnerabilities are called.
for _, osvid := range osvids {
list := grouped[osvid]
entry := createVulnSummary(osvs, osvid, list)
if entry.Affected {
s.Affected = append(s.Affected, entry)
} else {
s.Unaffected = append(s.Unaffected, entry)
}
}
mods := make(map[string]struct{})
for _, a := range s.Affected {
for _, m := range a.Modules {
if m.IsStd {
s.StdlibAffected = true
} else {
mods[m.Module] = struct{}{}
}
}
}
s.AffectedModules = len(mods)
return s
}
func createVulnSummary(osvs []*osv.Entry, osvid string, findings []*govulncheck.Finding) vulnSummary {
seen := map[string]struct{}{}
vInfo := vulnSummary{
Affected: IsCalled(findings),
OSV: osvid,
}
osv := findOSV(osvs, osvid)
if osv != nil {
vInfo.Details = osv.Details
if osv.DatabaseSpecific != nil {
vInfo.URL = osv.DatabaseSpecific.URL
}
}
for _, f := range findings {
lastFrame := f.Trace[0]
// find the right module summary, or create it if this is the first stack for that module
var ms *moduleSummary
for _, check := range vInfo.Modules {
if check.Module == lastFrame.Module {
ms = check
break
}
}
if ms == nil {
ms = &moduleSummary{
IsStd: lastFrame.Module == internal.GoStdModulePath,
Module: lastFrame.Module,
FoundVersion: moduleVersionString(lastFrame.Module, lastFrame.Package, lastFrame.Version),
FixedVersion: moduleVersionString(lastFrame.Module, lastFrame.Package, f.FixedVersion),
Platforms: platforms(lastFrame.Module, osv),
}
vInfo.Modules = append(vInfo.Modules, ms)
}
css := newTraceSummary(f)
if css.Compact == "" {
continue
}
// Suppress duplicate compact call stack summaries.
// Note that different call stacks can yield same summaries.
if _, wasSeen := seen[css.Compact]; !wasSeen {
seen[css.Compact] = struct{}{}
ms.Traces = append(ms.Traces, css)
}
}
return vInfo
}
func findOSV(osvs []*osv.Entry, id string) *osv.Entry {
for _, entry := range osvs {
if entry.ID == id {
return entry
}
}
return nil
}
func newTraceSummary(f *govulncheck.Finding) traceSummary {
css := traceSummary{
Compact: summarizeTrace(f),
}
if len(f.Trace) == 1 && f.Trace[0].Function == "" {
return css
}
for i := len(f.Trace) - 1; i >= 0; i-- {
frame := f.Trace[i]
symbol := frame.Function
if frame.Receiver != "" {
symbol = fmt.Sprint(frame.Receiver, ".", symbol)
}
buf := &strings.Builder{}
addSymbolName(buf, frame)
css.Trace = append(css.Trace, frameSummary{
Symbol: symbol,
Name: buf.String(),
Position: posToString(frame.Position),
})
}
css.Symbol = css.Trace[len(css.Trace)-1].Symbol
return css
}
// 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()
}
// wrap wraps s to fit in maxWidth by breaking it into lines at whitespace. If a
// single word is longer than maxWidth, it is retained as its own line.
func wrap(s string, maxWidth int) string {
var b strings.Builder
w := 0
for _, f := range strings.Fields(s) {
if w > 0 && w+len(f)+1 > maxWidth {
b.WriteByte('\n')
w = 0
}
if w != 0 {
b.WriteByte(' ')
w++
}
b.WriteString(f)
w += len(f)
}
return b.String()
}