gopls: sync golang.org/x/vuln@3af8368ee4fe

golang.org/x/vuln/exp/govulncheck API was significantly
changed. The previous Main is gone, and we need to use
Source. The returned data types changed significantly
too.

Change-Id: Ic702ffe9a94a8ddd1867a0f2766bb49e2133d3a3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/448975
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
This commit is contained in:
Hana (Hyang-Ah) Kim 2022-11-09 06:57:43 -05:00 committed by Hyang-Ah Hana Kim
parent d66e9b4ac8
commit bc08991678
17 changed files with 289 additions and 477 deletions

View File

@ -11,8 +11,8 @@ require (
golang.org/x/sync v0.1.0
golang.org/x/sys v0.2.0
golang.org/x/text v0.4.0
golang.org/x/tools v0.2.0
golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9
golang.org/x/tools v0.2.1-0.20221108172846-9474ca31d0df
golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe
gopkg.in/yaml.v3 v3.0.1
honnef.co/go/tools v0.3.3
mvdan.cc/gofumpt v0.4.0

View File

@ -61,7 +61,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -80,8 +79,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 h1:KaYZQUtEEaV8aVADIHAuYBTjo77aUcCvC7KTGKM3J1I=
golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68=
golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe h1:qptQiQwEpETwDiz85LKtChqif9xhVkAm8Nhxs0xnTww=
golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe/go.mod h1:8nFLBv8KFyZ2VuczUYssYKh+fcBR3BuXDG/HIWcxlwM=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -1,145 +0,0 @@
// Copyright 2021 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.
//go:build go1.18
// +build go1.18
// Package govulncheck supports the govulncheck command.
package govulncheck
import (
"encoding/json"
"go/build"
"os"
"path/filepath"
"sync"
"time"
"golang.org/x/vuln/client"
"golang.org/x/vuln/osv"
)
// The cache uses a single JSON index file for each vulnerability database
// which contains the map from packages to the time the last
// vulnerability for that package was added/modified and the time that
// the index was retrieved from the vulnerability database. The JSON
// format is as follows:
//
// $GOPATH/pkg/mod/cache/download/vulndb/{db hostname}/indexes/index.json
// {
// Retrieved time.Time
// Index client.DBIndex
// }
//
// Each package also has a JSON file which contains the array of vulnerability
// entries for the package. The JSON format is as follows:
//
// $GOPATH/pkg/mod/cache/download/vulndb/{db hostname}/{import path}/vulns.json
// []*osv.Entry
// FSCache is a thread-safe file-system cache implementing osv.Cache
//
// TODO: use something like cmd/go/internal/lockedfile for thread safety?
type FSCache struct {
mu sync.Mutex
rootDir string
}
// Assert that *FSCache implements client.Cache.
var _ client.Cache = (*FSCache)(nil)
// use cfg.GOMODCACHE available in cmd/go/internal?
var defaultCacheRoot = filepath.Join(build.Default.GOPATH, "/pkg/mod/cache/download/vulndb")
func DefaultCache() *FSCache {
return &FSCache{rootDir: defaultCacheRoot}
}
type cachedIndex struct {
Retrieved time.Time
Index client.DBIndex
}
func (c *FSCache) ReadIndex(dbName string) (client.DBIndex, time.Time, error) {
c.mu.Lock()
defer c.mu.Unlock()
b, err := os.ReadFile(filepath.Join(c.rootDir, dbName, "index.json"))
if err != nil {
if os.IsNotExist(err) {
return nil, time.Time{}, nil
}
return nil, time.Time{}, err
}
var index cachedIndex
if err := json.Unmarshal(b, &index); err != nil {
return nil, time.Time{}, err
}
return index.Index, index.Retrieved, nil
}
func (c *FSCache) WriteIndex(dbName string, index client.DBIndex, retrieved time.Time) error {
c.mu.Lock()
defer c.mu.Unlock()
path := filepath.Join(c.rootDir, dbName)
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
j, err := json.Marshal(cachedIndex{
Index: index,
Retrieved: retrieved,
})
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(path, "index.json"), j, 0666); err != nil {
return err
}
return nil
}
func (c *FSCache) ReadEntries(dbName string, p string) ([]*osv.Entry, error) {
c.mu.Lock()
defer c.mu.Unlock()
ep, err := client.EscapeModulePath(p)
if err != nil {
return nil, err
}
b, err := os.ReadFile(filepath.Join(c.rootDir, dbName, ep, "vulns.json"))
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var entries []*osv.Entry
if err := json.Unmarshal(b, &entries); err != nil {
return nil, err
}
return entries, nil
}
func (c *FSCache) WriteEntries(dbName string, p string, entries []*osv.Entry) error {
c.mu.Lock()
defer c.mu.Unlock()
ep, err := client.EscapeModulePath(p)
if err != nil {
return err
}
path := filepath.Join(c.rootDir, dbName, ep)
if err := os.MkdirAll(path, 0777); err != nil {
return err
}
j, err := json.Marshal(entries)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(path, "vulns.json"), j, 0666); err != nil {
return err
}
return nil
}

View File

@ -1,165 +0,0 @@
// Copyright 2021 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.
//go:build go1.18
// +build go1.18
package govulncheck
import (
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"golang.org/x/sync/errgroup"
"golang.org/x/vuln/client"
"golang.org/x/vuln/osv"
)
func TestCache(t *testing.T) {
tmpDir := t.TempDir()
cache := &FSCache{rootDir: tmpDir}
dbName := "vulndb.golang.org"
_, _, err := cache.ReadIndex(dbName)
if err != nil {
t.Fatalf("ReadIndex failed for non-existent database: %v", err)
}
if err = os.Mkdir(filepath.Join(tmpDir, dbName), 0777); err != nil {
t.Fatalf("os.Mkdir failed: %v", err)
}
_, _, err = cache.ReadIndex(dbName)
if err != nil {
t.Fatalf("ReadIndex failed for database without cached index: %v", err)
}
now := time.Now()
expectedIdx := client.DBIndex{
"a.vuln.example.com": time.Time{}.Add(time.Hour),
"b.vuln.example.com": time.Time{}.Add(time.Hour * 2),
"c.vuln.example.com": time.Time{}.Add(time.Hour * 3),
}
if err = cache.WriteIndex(dbName, expectedIdx, now); err != nil {
t.Fatalf("WriteIndex failed to write index: %v", err)
}
idx, retrieved, err := cache.ReadIndex(dbName)
if err != nil {
t.Fatalf("ReadIndex failed for database with cached index: %v", err)
}
if !reflect.DeepEqual(idx, expectedIdx) {
t.Errorf("ReadIndex returned unexpected index, got:\n%s\nwant:\n%s", idx, expectedIdx)
}
if !retrieved.Equal(now) {
t.Errorf("ReadIndex returned unexpected retrieved: got %s, want %s", retrieved, now)
}
if _, err = cache.ReadEntries(dbName, "vuln.example.com"); err != nil {
t.Fatalf("ReadEntires failed for non-existent package: %v", err)
}
expectedEntries := []*osv.Entry{
{ID: "001"},
{ID: "002"},
{ID: "003"},
}
if err := cache.WriteEntries(dbName, "vuln.example.com", expectedEntries); err != nil {
t.Fatalf("WriteEntries failed: %v", err)
}
entries, err := cache.ReadEntries(dbName, "vuln.example.com")
if err != nil {
t.Fatalf("ReadEntries failed for cached package: %v", err)
}
if !reflect.DeepEqual(entries, expectedEntries) {
t.Errorf("ReadEntries returned unexpected entries, got:\n%v\nwant:\n%v", entries, expectedEntries)
}
}
func TestConcurrency(t *testing.T) {
tmpDir := t.TempDir()
cache := &FSCache{rootDir: tmpDir}
dbName := "vulndb.golang.org"
g := new(errgroup.Group)
for i := 0; i < 1000; i++ {
i := i
g.Go(func() error {
id := i % 5
p := fmt.Sprintf("example.com/package%d", id)
entries, err := cache.ReadEntries(dbName, p)
if err != nil {
return err
}
err = cache.WriteEntries(dbName, p, append(entries, &osv.Entry{ID: fmt.Sprint(id)}))
if err != nil {
return err
}
return nil
})
}
if err := g.Wait(); err != nil {
t.Errorf("error in parallel cache entries read/write: %v", err)
}
// sanity checking
for i := 0; i < 5; i++ {
id := fmt.Sprint(i)
p := fmt.Sprintf("example.com/package%s", id)
es, err := cache.ReadEntries(dbName, p)
if err != nil {
t.Fatalf("failed to read entries: %v", err)
}
for _, e := range es {
if e.ID != id {
t.Errorf("want %s ID for vuln entry; got %s", id, e.ID)
}
}
}
// do similar for cache index
start := time.Now()
for i := 0; i < 1000; i++ {
i := i
g.Go(func() error {
id := i % 5
p := fmt.Sprintf("package%v", id)
idx, _, err := cache.ReadIndex(dbName)
if err != nil {
return err
}
if idx == nil {
idx = client.DBIndex{}
}
// sanity checking
if rt, ok := idx[p]; ok && rt.Before(start) {
return fmt.Errorf("unexpected past time in index: %v before start %v", rt, start)
}
now := time.Now()
idx[p] = now
if err := cache.WriteIndex(dbName, idx, now); err != nil {
return err
}
return nil
})
}
if err := g.Wait(); err != nil {
t.Errorf("error in parallel cache index read/write: %v", err)
}
}

View File

@ -1,32 +0,0 @@
#!/bin/bash -eu
# Copyright 2020 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.
set -o pipefail
# Copy golang.org/x/vuln/cmd/govulncheck/internal/govulncheck into this directory.
# Assume the x/vuln repo is a sibling of the tools repo.
rm -f *.go
cp ../../../../vuln/cmd/govulncheck/internal/govulncheck/*.go .
sed -i '' 's/\"golang.org\/x\/vuln\/internal\/semver\"/\"golang.org\/x\/tools\/gopls\/internal\/govulncheck\/semver\"/g' *.go
sed -i '' -e '4 i\
' -e '4 i\
//go:build go1.18' -e '4 i\
// +build go1.18' *.go
# Copy golang.org/x/vuln/internal/semver that
# golang.org/x/vuln/cmd/govulncheck/internal/govulncheck
# depends on.
mkdir -p semver
cd semver
rm -f *.go
cp ../../../../../vuln/internal/semver/*.go .
sed -i '' -e '4 i\
' -e '4 i\
//go:build go1.18' -e '4 i\
// +build go1.18' *.go

View File

@ -1,46 +0,0 @@
// Copyright 2020 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 govulncheck
import "golang.org/x/vuln/osv"
// TODO(hyangah): Find a better package for these types
// unless golang.org/x/vuln/exp/govulncheck starts to export these.
// Summary is the govulncheck result.
type Summary struct {
// Vulnerabilities affecting the analysis target binary or source code.
Affecting []Vuln
// Vulnerabilities that may be imported but the vulnerable symbols are
// not called. For binary analysis, this will be always empty.
NonAffecting []Vuln
}
// Vuln represents a vulnerability relevant to a (module, package).
type Vuln struct {
OSV *osv.Entry
PkgPath string // Package path.
ModPath string // Module path.
FoundIn string // <package path>@<version> if we know when it was introduced. Empty otherwise.
FixedIn string // <package path>@<version> if fix is available. Empty otherwise.
// Trace contains a call stack for each affecting symbol.
// For vulnerabilities found from binary analysis, and vulnerabilities
// that are reported as Unaffecting ones, this will be always empty.
Trace []Trace
}
// Trace represents a sample trace for a vulnerable symbol.
type Trace struct {
Symbol string // Name of the detected vulnerable function or method.
Desc string // One-line description of the callstack.
Stack []StackEntry // Call stack.
Seen int // Number of similar call stacks.
}
// StackEntry represents a call stack entry.
type StackEntry struct {
FuncName string // Function name is the function name, adjusted to remove pointer annotation.
CallSite string // Position of the call/reference site. It is one of the formats token.Pos.String() returns or empty if unknown.
}

View File

@ -0,0 +1,44 @@
// 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.
//go:build go1.18
// +build go1.18
// Package govulncheck provides an experimental govulncheck API.
package govulncheck
import "golang.org/x/vuln/exp/govulncheck"
var (
// Source reports vulnerabilities that affect the analyzed packages.
Source = govulncheck.Source
// DefaultCache constructs cache for a vulnerability database client.
DefaultCache = govulncheck.DefaultCache
)
type (
// Config is the configuration for Main.
Config = govulncheck.Config
// Result is the result of executing Source.
Result = govulncheck.Result
// Vuln represents a single OSV entry.
Vuln = govulncheck.Vuln
// Module represents a specific vulnerability relevant to a
// single module or package.
Module = govulncheck.Module
// Package is a Go package with known vulnerable symbols.
Package = govulncheck.Package
// CallStacks contains a representative call stack for each
// vulnerable symbol that is called.
CallStack = govulncheck.CallStack
// StackFrame represents a call stack entry.
StackFrame = govulncheck.StackFrame
)

View File

@ -0,0 +1,133 @@
// 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.
//go:build !go1.18
// +build !go1.18
package govulncheck
import (
"go/token"
"golang.org/x/vuln/osv"
)
// Result is the result of executing Source or Binary.
type Result struct {
// Vulns contains all vulnerabilities that are called or imported by
// the analyzed module.
Vulns []*Vuln
}
// Vuln represents a single OSV entry.
type Vuln struct {
// OSV contains all data from the OSV entry for this vulnerability.
OSV *osv.Entry
// Modules contains all of the modules in the OSV entry where a
// vulnerable package is imported by the target source code or binary.
//
// For example, a module M with two packages M/p1 and M/p2, where only p1
// is vulnerable, will appear in this list if and only if p1 is imported by
// the target source code or binary.
Modules []*Module
}
func (v *Vuln) IsCalled() bool {
return false
}
// Module represents a specific vulnerability relevant to a single module.
type Module struct {
// Path is the module path of the module containing the vulnerability.
//
// Importable packages in the standard library will have the path "stdlib".
Path string
// FoundVersion is the module version where the vulnerability was found.
FoundVersion string
// FixedVersion is the module version where the vulnerability was
// fixed. If there are multiple fixed versions in the OSV report, this will
// be the latest fixed version.
//
// This is empty if a fix is not available.
FixedVersion string
// Packages contains all the vulnerable packages in OSV entry that are
// imported by the target source code or binary.
//
// For example, given a module M with two packages M/p1 and M/p2, where
// both p1 and p2 are vulnerable, p1 and p2 will each only appear in this
// list they are individually imported by the target source code or binary.
Packages []*Package
}
// Package is a Go package with known vulnerable symbols.
type Package struct {
// Path is the import path of the package containing the vulnerability.
Path string
// CallStacks contains a representative call stack for each
// vulnerable symbol that is called.
//
// For vulnerabilities found from binary analysis, only CallStack.Symbol
// will be provided.
//
// For non-affecting vulnerabilities reported from the source mode
// analysis, this will be empty.
CallStacks []CallStack
}
// CallStacks contains a representative call stack for a vulnerable
// symbol.
type CallStack struct {
// Symbol is the name of the detected vulnerable function
// or method.
//
// This follows the naming convention in the OSV report.
Symbol string
// Summary is a one-line description of the callstack, used by the
// default govulncheck mode.
//
// Example: module3.main calls github.com/shiyanhui/dht.DHT.Run
Summary string
// Frames contains an entry for each stack in the call stack.
//
// Frames are sorted starting from the entry point to the
// imported vulnerable symbol. The last frame in Frames should match
// Symbol.
Frames []*StackFrame
}
// StackFrame represents a call stack entry.
type StackFrame struct {
// PackagePath is the import path.
PkgPath string
// FuncName is the function name.
FuncName string
// RecvType is the fully qualified receiver type,
// if the called symbol is a method.
//
// The client can create the final symbol name by
// prepending RecvType to FuncName.
RecvType string
// Position describes an arbitrary source position
// including the file, line, and column location.
// A Position is valid if the line number is > 0.
Position token.Position
}
func (sf *StackFrame) Name() string {
return ""
}
func (sf *StackFrame) Pos() string {
return ""
}

View File

@ -239,7 +239,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
name: name,
folder: folder,
moduleUpgrades: map[span.URI]map[string]string{},
vulns: map[span.URI][]govulncheck.Vuln{},
vulns: map[span.URI][]*govulncheck.Vuln{},
filesByURI: map[span.URI]*fileBase{},
filesByBase: map[string][]*fileBase{},
rootURI: root,

View File

@ -61,7 +61,7 @@ type View struct {
// Each modfile has a map of module name to upgrade version.
moduleUpgrades map[span.URI]map[string]string
vulns map[span.URI][]govulncheck.Vuln
vulns map[span.URI][]*govulncheck.Vuln
// keep track of files by uri and by basename, a single file may be mapped
// to multiple uris, and the same basename may map to multiple files
@ -1035,16 +1035,16 @@ func (v *View) ClearModuleUpgrades(modfile span.URI) {
delete(v.moduleUpgrades, modfile)
}
func (v *View) Vulnerabilities(modfile span.URI) []govulncheck.Vuln {
func (v *View) Vulnerabilities(modfile span.URI) []*govulncheck.Vuln {
v.mu.Lock()
defer v.mu.Unlock()
vulns := make([]govulncheck.Vuln, len(v.vulns[modfile]))
vulns := make([]*govulncheck.Vuln, len(v.vulns[modfile]))
copy(vulns, v.vulns[modfile])
return vulns
}
func (v *View) SetVulnerabilities(modfile span.URI, vulns []govulncheck.Vuln) {
func (v *View) SetVulnerabilities(modfile span.URI, vulns []*govulncheck.Vuln) {
v.mu.Lock()
defer v.mu.Unlock()

View File

@ -82,9 +82,11 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error {
}
if v.AsSummary {
// vulnchecklib.Main calls os.Exit and never returns.
vulnchecklib.Main(loadCfg, args...)
return nil
if err := vulnchecklib.Main(loadCfg, args...); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.Exit(0)
}
// TODO(hyangah): delete.
res, err := vulnchecklib.Govulncheck(ctx, &loadCfg, pattern)

View File

@ -880,34 +880,31 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc
return fmt.Errorf("failed to run govulncheck: %v", err)
}
var summary govulncheck.Summary
if err := json.Unmarshal(stdout, &summary); err != nil {
var result govulncheck.Result
if err := json.Unmarshal(stdout, &result); err != nil {
// TODO: for easy debugging, log the failed stdout somewhere?
return fmt.Errorf("failed to parse govulncheck output: %v", err)
}
vulns := append(summary.Affecting, summary.NonAffecting...)
vulns := result.Vulns
deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), vulns)
c.s.diagnoseSnapshot(deps.snapshot, nil, false)
if len(summary.Affecting) == 0 {
affecting := make([]string, 0, len(vulns))
for _, v := range vulns {
if v.IsCalled() {
affecting = append(affecting, v.OSV.ID)
}
}
if len(affecting) == 0 {
return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Type: protocol.Info,
Message: "No vulnerabilities found",
})
}
set := make(map[string]bool)
for _, v := range summary.Affecting {
set[v.OSV.ID] = true
}
list := make([]string, 0, len(set))
for k := range set {
list = append(list, k)
}
sort.Strings(list)
sort.Strings(affecting)
return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Type: protocol.Warning,
Message: fmt.Sprintf("Found %v", strings.Join(list, ", ")),
Message: fmt.Sprintf("Found %v", strings.Join(affecting, ", ")),
})
})
return err

View File

@ -197,13 +197,19 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
vs := snapshot.View().Vulnerabilities(fh.URI())
// TODO(suzmue): should we just store the vulnerabilities like this?
affecting := make(map[string][]govulncheck.Vuln)
nonaffecting := make(map[string][]govulncheck.Vuln)
type modVuln struct {
mod *govulncheck.Module
vuln *govulncheck.Vuln
}
affecting := make(map[string][]modVuln)
nonaffecting := make(map[string][]modVuln)
for _, v := range vs {
if len(v.Trace) > 0 {
affecting[v.ModPath] = append(affecting[v.ModPath], v)
} else {
nonaffecting[v.ModPath] = append(nonaffecting[v.ModPath], v)
for _, m := range v.Modules {
if v.IsCalled() {
affecting[m.Path] = append(affecting[m.Path], modVuln{mod: m, vuln: v})
} else {
nonaffecting[m.Path] = append(nonaffecting[m.Path], modVuln{mod: m, vuln: v})
}
}
}
@ -222,25 +228,30 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
// Fixes will include only the upgrades for warning level diagnostics.
var fixes []source.SuggestedFix
var warning, info []string
for _, v := range nonaffectingVulns {
for _, mv := range nonaffectingVulns {
mod, vuln := mv.mod, mv.vuln
// Only show the diagnostic if the vulnerability was calculated
// for the module at the current version.
if semver.IsValid(v.FoundIn) && semver.Compare(req.Mod.Version, v.FoundIn) != 0 {
// TODO(hyangah): this prevents from surfacing vulnerable modules
// since module version selection is affected by dependency module
// requirements and replace/exclude/go.work use. Drop this check
// and annotate only the module path.
if semver.IsValid(mod.FoundVersion) && semver.Compare(req.Mod.Version, mod.FoundVersion) != 0 {
continue
}
info = append(info, v.OSV.ID)
info = append(info, vuln.OSV.ID)
}
for _, v := range affectingVulns {
for _, mv := range affectingVulns {
mod, vuln := mv.mod, mv.vuln
// Only show the diagnostic if the vulnerability was calculated
// for the module at the current version.
if semver.IsValid(v.FoundIn) && semver.Compare(req.Mod.Version, v.FoundIn) != 0 {
if semver.IsValid(mod.FoundVersion) && semver.Compare(req.Mod.Version, mod.FoundVersion) != 0 {
continue
}
warning = append(warning, v.OSV.ID)
warning = append(warning, vuln.OSV.ID)
// Upgrade to the exact version we offer the user, not the most recent.
// TODO(hakim): Produce fixes only for affecting vulnerabilities (if len(v.Trace) > 0)
if fixedVersion := v.FixedIn; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 {
if fixedVersion := mod.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 {
cmd, err := getUpgradeCodeAction(fh, req, fixedVersion)
if err != nil {
return nil, err
@ -297,7 +308,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot,
return vulnDiagnostics, nil
}
func formatMessage(v govulncheck.Vuln) string {
func formatMessage(v *govulncheck.Vuln) string {
details := []byte(v.OSV.Details)
// Remove any new lines that are not preceded or followed by a new line.
for i, r := range details {

View File

@ -71,7 +71,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle,
}
// Get the vulnerability info.
affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI()), req)
affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI()), req.Mod.Path)
// Get the `go mod why` results for the given file.
why, err := snapshot.ModWhy(ctx, fh)
@ -94,7 +94,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle,
isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path)
header := formatHeader(req.Mod.Path, options)
explanation = formatExplanation(explanation, req, options, isPrivate)
vulns := formatVulnerabilities(affecting, nonaffecting, options)
vulns := formatVulnerabilities(req.Mod.Path, affecting, nonaffecting, options)
return &protocol.Hover{
Contents: protocol.MarkupContent{
@ -117,33 +117,25 @@ func formatHeader(modpath string, options *source.Options) string {
return b.String()
}
func compareVuln(i, j govulncheck.Vuln) bool {
if i.OSV.ID == j.OSV.ID {
return i.PkgPath < j.PkgPath
}
return i.OSV.ID < j.OSV.ID
}
func lookupVulns(vulns []govulncheck.Vuln, req *modfile.Require) (affecting, nonaffecting []govulncheck.Vuln) {
modpath, modversion := req.Mod.Path, req.Mod.Version
var info, warning []govulncheck.Vuln
func lookupVulns(vulns []*govulncheck.Vuln, modpath string) (affecting, nonaffecting []*govulncheck.Vuln) {
for _, vuln := range vulns {
if vuln.ModPath != modpath || vuln.FoundIn != modversion {
continue
}
if len(vuln.Trace) == 0 {
info = append(info, vuln)
} else {
warning = append(warning, vuln)
for _, mod := range vuln.Modules {
if mod.Path != modpath {
continue
}
if vuln.IsCalled() {
affecting = append(affecting, vuln)
} else {
nonaffecting = append(nonaffecting, vuln)
}
}
}
sort.Slice(info, func(i, j int) bool { return compareVuln(info[i], info[j]) })
sort.Slice(warning, func(i, j int) bool { return compareVuln(warning[i], warning[j]) })
return warning, info
sort.Slice(nonaffecting, func(i, j int) bool { return nonaffecting[i].OSV.ID < nonaffecting[j].OSV.ID })
sort.Slice(affecting, func(i, j int) bool { return affecting[i].OSV.ID < affecting[j].OSV.ID })
return affecting, nonaffecting
}
func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options *source.Options) string {
func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulncheck.Vuln, options *source.Options) string {
if len(affecting) == 0 && len(nonaffecting) == 0 {
return ""
}
@ -163,10 +155,7 @@ func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options *
}
}
for _, v := range affecting {
fix := "No fix is available."
if v.FixedIn != "" {
fix = "Fixed in " + v.FixedIn + "."
}
fix := fixedVersionInfo(v, modPath)
if useMarkdown {
fmt.Fprintf(&b, "- [**%v**](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix)
@ -178,10 +167,7 @@ func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options *
fmt.Fprintf(&b, "\n**FYI:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n")
}
for _, v := range nonaffecting {
fix := "No fix is available."
if v.FixedIn != "" {
fix = "Fixed in " + v.FixedIn + "."
}
fix := fixedVersionInfo(v, modPath)
if useMarkdown {
fmt.Fprintf(&b, "- [%v](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix)
} else {
@ -192,6 +178,20 @@ func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options *
return b.String()
}
func fixedVersionInfo(v *govulncheck.Vuln, modPath string) string {
fix := "No fix is available."
for _, m := range v.Modules {
if m.Path != modPath {
continue
}
if m.FixedVersion != "" {
fix = "Fixed in " + m.FixedVersion + "."
}
break
}
return fix
}
func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string {
text = strings.TrimSuffix(text, "\n")
splt := strings.Split(text, "\n")

View File

@ -278,11 +278,11 @@ type View interface {
// Vulnerabilites returns known vulnerabilities for the given modfile.
// TODO(suzmue): replace command.Vuln with a different type, maybe
// https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary?
Vulnerabilities(modfile span.URI) []govulncheck.Vuln
Vulnerabilities(modfile span.URI) []*govulncheck.Vuln
// SetVulnerabilities resets the list of vulnerabilites that exists for the given modules
// required by modfile.
SetVulnerabilities(modfile span.URI, vulnerabilities []govulncheck.Vuln)
SetVulnerabilities(modfile span.URI, vulnerabilities []*govulncheck.Vuln)
// FileKind returns the type of a file
FileKind(FileHandle) FileKind

View File

@ -9,6 +9,7 @@ package vulncheck
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
@ -216,19 +217,32 @@ func trimPosPrefix(summary string) string {
const GoVersionForVulnTest = "_GOPLS_TEST_VULNCHECK_GOVERSION"
func init() {
Main = func(cfg packages.Config, patterns ...string) {
// never return
err := gvcapi.Main(gvcapi.Config{
AnalysisType: "source",
OutputType: "summary",
Patterns: patterns,
SourceLoadConfig: &cfg,
GoVersion: os.Getenv(GoVersionForVulnTest),
Main = func(cfg packages.Config, patterns ...string) error {
// Set the mode that Source needs.
cfg.Mode = packages.NeedName | packages.NeedImports | packages.NeedTypes |
packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps |
packages.NeedModule
pkgs, err := packages.Load(&cfg, patterns...)
if err != nil {
return err
}
cli, err := client.NewClient(findGOVULNDB(&cfg), client.Options{
HTTPCache: gvc.DefaultCache(),
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
return err
}
os.Exit(0)
res, err := gvcapi.Source(context.Background(), &gvcapi.Config{
Client: cli,
GoVersion: os.Getenv(GoVersionForVulnTest),
}, vulncheck.Convert(pkgs))
if err != nil {
return err
}
if err := json.NewEncoder(os.Stdout).Encode(res); err != nil {
return err
}
return nil
}
}

View File

@ -19,4 +19,4 @@ import (
// With go1.18+, this is swapped with the real implementation.
var Govulncheck func(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) = nil
var Main func(cfg packages.Config, patterns ...string) = nil
var Main func(cfg packages.Config, patterns ...string) error = nil