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=