cmd/vulnreport: populate report with exported symbols

The fix subcommand will re-populate the symbols fields of the report
with all of the vulnerable exported symbols.

Change-Id: I5b0e097b367e74c52ea123022e268b91e54aec17
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/379776
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/vulnreport/exported_functions.go b/cmd/vulnreport/exported_functions.go
new file mode 100644
index 0000000..f98c81f
--- /dev/null
+++ b/cmd/vulnreport/exported_functions.go
@@ -0,0 +1,99 @@
+// 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.
+
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+
+	"golang.org/x/exp/vulncheck"
+	"golang.org/x/tools/go/packages"
+	vdbclient "golang.org/x/vuln/client"
+	"golang.org/x/vuln/osv"
+	"golang.org/x/vulndb/internal/database"
+	"golang.org/x/vulndb/internal/derrors"
+	"golang.org/x/vulndb/internal/report"
+)
+
+// A reportClient is a vulndb.Client that returns the Entry for a single report.
+type reportClient struct {
+	vdbclient.Client
+	entry           *osv.Entry
+	entriesByModule map[string][]*osv.Entry
+}
+
+// newReportClient creates a reportClient from a given report.
+func newReportClient(r *report.Report) *reportClient {
+	entries := map[string][]*osv.Entry{}
+	entry, modules := database.GenerateOSVEntry("?", "?", *r)
+	for _, m := range modules {
+		entries[m] = append(entries[m], &entry)
+	}
+	return &reportClient{entry: &entry, entriesByModule: entries}
+}
+
+// GetByModule implements vdbclient.Client.GetByModule.
+func (e *reportClient) GetByModule(ctx context.Context, m string) ([]*osv.Entry, error) {
+	return e.entriesByModule[m], nil
+}
+
+// exportedFunctions returns a set of vulnerable functions exported by a set of packages
+// from the same module.
+func exportedFunctions(pkgs []*packages.Package, rc *reportClient) (_ map[string]bool, err error) {
+	defer derrors.Wrap(&err, "exportedFunctions(%q)", pkgs[0].PkgPath)
+
+	if pkgs[0].Module == nil {
+		return nil, errors.New("pkgs[0] is missing Module")
+	}
+	if !affected(rc.entry, pkgs[0].Module.Version) {
+		fmt.Fprintf(os.Stderr, "version %s of module %s is not affected by this vuln\n",
+			pkgs[0].Module.Version, pkgs[0].Module.Path)
+		return map[string]bool{}, nil
+	}
+	vpkgs := vulncheck.Convert(pkgs)
+	res, err := vulncheck.Source(context.Background(), vpkgs, &vulncheck.Config{Client: rc})
+	if err != nil {
+		return nil, err
+	}
+	// Return the name of all entry points.
+	// Note that "main" and "init" are both possible entries.
+	// Both have clear meanings: "main" means that invoking
+	// the program is a problem, and "init" means that very likely
+	// some global state is altered, and so every exported function
+	// is vulnerable. For now, we leave it to consumers to use this
+	// information as they wish.
+	names := map[string]bool{}
+	for _, ei := range res.Calls.Entries {
+		e := res.Calls.Functions[ei]
+		if e.PkgPath == pkgs[0].PkgPath {
+			names[symbolName(e)] = true
+		}
+	}
+	return names, nil
+}
+
+func symbolName(fn *vulncheck.FuncNode) string {
+	if fn.RecvType == "" {
+		return fn.Name
+	}
+	// Remove package path from type.
+	i := strings.LastIndexByte(fn.RecvType, '.')
+	if i < 0 {
+		return fn.RecvType + "." + fn.Name
+	}
+	return fn.RecvType[i+1:] + "." + fn.Name
+}
+
+func affected(e *osv.Entry, version string) bool {
+	for _, a := range e.Affected {
+		if a.Ranges.AffectsSemver(version) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/cmd/vulnreport/exported_functions_test.go b/cmd/vulnreport/exported_functions_test.go
new file mode 100644
index 0000000..863dfe7
--- /dev/null
+++ b/cmd/vulnreport/exported_functions_test.go
@@ -0,0 +1,60 @@
+// 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.
+
+package main
+
+import (
+	"path"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/tools/go/packages/packagestest"
+	"golang.org/x/vulndb/internal/report"
+)
+
+func TestExportedFunctions(t *testing.T) {
+	e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
+		{
+			Name: "example.com/m",
+			Files: map[string]interface{}{
+				"p/a.go": `
+					package p
+					func vuln() {}
+					func ok() {}
+				`,
+				"p/b.go": `
+					package p
+					func Exp() { vuln() }
+					func Trans() { Exp() }
+					func Fine() { ok() }
+				`,
+			},
+		},
+	})
+	defer e.Cleanup()
+
+	rc := newReportClient(&report.Report{
+		Module:  "example.com/m",
+		Package: "example.com/m/p",
+		Symbols: []string{"vuln"},
+	})
+	pkgs, err := loadPackage(e.Config, path.Join(e.Temp(), "m/p"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Clear Module.Dir so vulncheck doesn't think that the module is local and ignore it.
+	// Set Module.Version so vulncheck doesn't filter it out.
+	for _, p := range pkgs {
+		p.Module.Dir = ""
+		p.Module.Version = "v1.0.0"
+	}
+	got, err := exportedFunctions(pkgs, rc)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := map[string]bool{"Exp": true, "Trans": true}
+	if !cmp.Equal(got, want) {
+		t.Errorf("\ngot\n\t%v\nwant\n\t%v", got, want)
+	}
+}
diff --git a/cmd/vulnreport/main.go b/cmd/vulnreport/main.go
index f2fd9db..6fbcd46 100644
--- a/cmd/vulnreport/main.go
+++ b/cmd/vulnreport/main.go
@@ -9,14 +9,18 @@
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"flag"
 	"fmt"
+	"go/build"
 	"log"
+	"os"
+	"reflect"
+	"sort"
 	"strconv"
 	"strings"
 
-	"os"
-
+	"golang.org/x/tools/go/packages"
 	"golang.org/x/vulndb/internal/cvelistrepo"
 	"golang.org/x/vulndb/internal/derrors"
 	"golang.org/x/vulndb/internal/gitrepo"
@@ -172,14 +176,113 @@
 	if err != nil {
 		return err
 	}
-
+	fixed := false
 	if lints := r.Lint(); len(lints) > 0 {
 		r.Fix()
+		fixed = true
+	}
+	added, err := addExportedReportSymbols(r)
+	if err != nil {
+		return err
+	}
+	if fixed || added {
 		return r.Write(filename)
 	}
 	return nil
 }
 
+func addExportedReportSymbols(r *report.Report) (bool, error) {
+	if r.Module == "" || len(r.Symbols) == 0 {
+		return false, nil
+	}
+	if len(r.OS) > 0 || len(r.Arch) > 0 {
+		return false, errors.New("specific GOOS/GOARCH not yet implemented")
+	}
+	rc := newReportClient(r)
+	added, err := addExportedSymbols(r.Module, r.Package, &r.Symbols, rc)
+	if err != nil {
+		return false, err
+	}
+	for i, ap := range r.AdditionalPackages {
+		// Need to take pointer from r because r.AdditionalPackages is a slice of values.
+		a, err := addExportedSymbols(ap.Module, ap.Package, &r.AdditionalPackages[i].Symbols, rc)
+		if err != nil {
+			return false, err
+		}
+		if a {
+			added = true
+		}
+	}
+	return added, nil
+}
+
+func addExportedSymbols(module, pkgPath string, symbols *[]string, c *reportClient) (added bool, err error) {
+	defer derrors.Wrap(&err, "addExportedSymbols(%q, %q)", module, pkgPath)
+
+	if pkgPath == "" {
+		pkgPath = module
+	}
+	pkgs, err := loadPackage(&packages.Config{}, pkgPath)
+	if err != nil {
+		return false, err
+	}
+	if len(pkgs) == 0 {
+		return false, errors.New("no packages found")
+	}
+	// First package should match package path and module.
+	if pkgs[0].PkgPath != pkgPath {
+		return false, fmt.Errorf("first package had import path %s, wanted %s", pkgs[0].PkgPath, pkgPath)
+	}
+	if pm := pkgs[0].Module; pm == nil || pm.Path != module {
+		return false, fmt.Errorf("got module %v, expected %s", pm, module)
+	}
+	newsyms, err := exportedFunctions(pkgs, c)
+	if err != nil {
+		return false, err
+	}
+	oldsyms := map[string]bool{}
+	for _, s := range *symbols {
+		oldsyms[s] = true
+	}
+	if reflect.DeepEqual(newsyms, oldsyms) {
+		return false, nil
+	}
+	for _, s := range *symbols {
+		newsyms[s] = true
+	}
+	var newslice []string
+	for s := range newsyms {
+		newslice = append(newslice, s)
+	}
+	sort.Strings(newslice)
+	*symbols = newslice
+	return true, nil
+}
+
+// loadPackage loads the package at the given import path, with enough
+// information for constructing a call graph.
+func loadPackage(cfg *packages.Config, importPath string) ([]*packages.Package, error) {
+	cfg.Mode |= packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles |
+		packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes |
+		packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps |
+		packages.NeedModule
+	cfg.BuildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(build.Default.BuildTags, ","))}
+	pkgs, err := packages.Load(cfg, importPath)
+	if err != nil {
+		return nil, err
+	}
+	var msgs []string
+	packages.Visit(pkgs, nil, func(pkg *packages.Package) {
+		for _, err := range pkg.Errors {
+			msgs = append(msgs, err.Msg)
+		}
+	})
+	if len(msgs) > 0 {
+		return nil, fmt.Errorf("packages.Load:\n%s", strings.Join(msgs, "\n"))
+	}
+	return pkgs, nil
+}
+
 func newCVE(filename string) (err error) {
 	defer derrors.Wrap(&err, "newCVE(%q)", filename)
 	cve, err := report.ToCVE(filename)
diff --git a/go.mod b/go.mod
index 4558ff8..0d7e7b8 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@
 	github.com/google/safehtml v0.0.2
 	github.com/jba/templatecheck v0.6.0
 	golang.org/x/exp v0.0.0-20220124173137-7a6bfc487013
+	golang.org/x/exp/vulncheck v0.0.0-20220114162006-9d54fb35363c
 	golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57
 	golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
@@ -57,7 +58,7 @@
 	go.opencensus.io v0.23.0 // indirect
 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
 	golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
-	golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827 // indirect
+	golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351 // indirect
diff --git a/go.sum b/go.sum
index 3d903b0..da1f3ad 100644
--- a/go.sum
+++ b/go.sum
@@ -165,6 +165,7 @@
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
 github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -225,6 +226,8 @@
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
 github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -470,6 +473,7 @@
 go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
 go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
 go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
+go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
 go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=
 go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
 go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
@@ -511,8 +515,11 @@
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20211123021643-48cbe7f80d7c/go.mod h1:b9TAUYHmRtqA6klRHApnXMnj+OyLce4yF5cZCUbk2ps=
 golang.org/x/exp v0.0.0-20220124173137-7a6bfc487013 h1:pOVOgyxif1oxpIkCWpWqNehz3kSt+/hRyqV+qehuLbo=
 golang.org/x/exp v0.0.0-20220124173137-7a6bfc487013/go.mod h1:M50CtfS+xv2iy/epuEazynj250ScQ0/DOjcsin9UE8k=
+golang.org/x/exp/vulncheck v0.0.0-20220114162006-9d54fb35363c h1:9ESGI8ZFZ81F1nPfavAX6U97Zte77knZknUnGTrSS6w=
+golang.org/x/exp/vulncheck v0.0.0-20220114162006-9d54fb35363c/go.mod h1:HF28XewMFGXG3D7EgmemgILbLRiYH0qjmXOQM5HuF+g=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -682,8 +689,9 @@
 golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827 h1:A0Qkn7Z/n8zC1xd9LTw17AiKlBRK64tw3ejWQiEqca0=
 golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -762,6 +770,7 @@
 golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
 golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
+golang.org/x/vuln v0.0.0-20211207171702-7209860d2c63/go.mod h1:zIQqHjf9sHpn0TOli6Vdy9mrmuePs9lmnGBRAzECxz0=
 golang.org/x/vuln v0.0.0-20211220180837-4e75679f7993 h1:+QWJBIQ63o9opsIqWfZsj+iJheV4Yq2c56nWwh3aP4g=
 golang.org/x/vuln v0.0.0-20211220180837-4e75679f7993/go.mod h1:S6B12KDXRRbuVwwScAnv6c9S3pmk/FRmBMcsb2sVcqI=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=