internal/frontend: display vulnerabilities on package page

Under an experiment, look up and display a package's vulnerabilities
on its main page using the client provided by the golang.org/x/vulndb
module.

For golang/go#48223

Change-Id: I310440db16f8ad5fe582fc8ab42999e874f3ca88
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/347949
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index 3bb9e9b..04bf2b5 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -28,6 +28,7 @@
 	"golang.org/x/pkgsite/internal/proxy"
 	"golang.org/x/pkgsite/internal/queue"
 	"golang.org/x/pkgsite/internal/source"
+	vulndbc "golang.org/x/vulndb/client"
 )
 
 var (
@@ -104,6 +105,10 @@
 	}
 
 	rc := cmdconfig.ReportingClient(ctx, cfg)
+	vc, err := vulndbc.NewClient([]string{cfg.VulnDB}, vulndbc.Options{})
+	if err != nil {
+		log.Fatalf(ctx, "vulndbc.NewClient: %v", err)
+	}
 	server, err := frontend.NewServer(frontend.ServerConfig{
 		DataSourceGetter:     dsg,
 		Queue:                fetchQueue,
@@ -115,6 +120,7 @@
 		GoogleTagManagerID:   cfg.GoogleTagManagerID,
 		ServeStats:           cfg.ServeStats,
 		ReportingClient:      rc,
+		VulndbClient:         vc,
 	})
 	if err != nil {
 		log.Fatalf(ctx, "frontend.NewServer: %v", err)
diff --git a/go.mod b/go.mod
index de77081..eec9f70 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,7 @@
 	github.com/golang-migrate/migrate/v4 v4.6.2
 	github.com/golang/protobuf v1.4.2
 	github.com/gomodule/redigo v2.0.0+incompatible // indirect
-	github.com/google/go-cmp v0.5.2
+	github.com/google/go-cmp v0.5.4
 	github.com/google/go-replayers/httpreplay v0.1.0
 	github.com/google/licensecheck v0.3.1
 	github.com/google/safehtml v0.0.2
@@ -40,11 +40,12 @@
 	github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da // indirect
 	go.opencensus.io v0.22.4
 	golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
-	golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449
+	golang.org/x/mod v0.4.1
 	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
 	golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
 	golang.org/x/text v0.3.6
 	golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c
+	golang.org/x/vulndb v0.0.0-20210812203154-5d84be3c9e14
 	google.golang.org/api v0.32.0
 	google.golang.org/genproto v0.0.0-20200923140941-5646d36feee1
 	google.golang.org/grpc v1.32.0
diff --git a/go.sum b/go.sum
index 9c9559c..2b86c14 100644
--- a/go.sum
+++ b/go.sum
@@ -264,8 +264,9 @@
 github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/go-replayers/httpreplay v0.1.0 h1:AX7FUb4BjrrzNvblr/OlgwrmFiep6soj5K2QSDW7BGk=
@@ -698,8 +699,9 @@
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ=
 golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -885,6 +887,8 @@
 golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
 golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c h1:AQsh/7arPVFDBraQa8x7GoVnwnGg1kM7J2ySI0kF5WU=
 golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+golang.org/x/vulndb v0.0.0-20210812203154-5d84be3c9e14 h1:fGz1pt31Ygv69LkbU9kkWMChI2ZPUeZ/IzqEce/NA7s=
+golang.org/x/vulndb v0.0.0-20210812203154-5d84be3c9e14/go.mod h1:xh7j0yEDggyETQM2RIfHFmzOcnAwzHg8j8heomkN1Dc=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1017,8 +1021,9 @@
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
diff --git a/internal/config/config.go b/internal/config/config.go
index 2272d9e..7b1e934 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -181,6 +181,9 @@
 
 	// DisableErrorReporting disables sending errors to the GCP ErrorReporting system.
 	DisableErrorReporting bool
+
+	// VulnDB is the URL of the Go vulnerability DB.
+	VulnDB string
 }
 
 // AppVersionLabel returns the version label for the current instance.  This is
@@ -379,6 +382,7 @@
 		LogLevel:              os.Getenv("GO_DISCOVERY_LOG_LEVEL"),
 		ServeStats:            os.Getenv("GO_DISCOVERY_SERVE_STATS") == "true",
 		DisableErrorReporting: os.Getenv("GO_DISCOVERY_DISABLE_ERROR_REPORTING") == "true",
+		VulnDB:                GetEnv("GO_DISCOVERY_VULN_DB", "https://storage.googleapis.com/go-vulndb"),
 	}
 	log.SetLevel(cfg.LogLevel)
 
diff --git a/internal/experiment.go b/internal/experiment.go
index f36f6ac..6efe44c 100644
--- a/internal/experiment.go
+++ b/internal/experiment.go
@@ -14,6 +14,7 @@
 	ExperimentSkipInsertSymbols      = "skip-insert-symbols"
 	ExperimentStyleGuide             = "styleguide"
 	ExperimentSymbolSearch           = "symbol-search"
+	ExperimentVulns                  = "vulns"
 )
 
 // Experiments represents all of the active experiments in the codebase and
@@ -27,6 +28,7 @@
 	ExperimentSkipInsertSymbols:      "Don't insert data into symbols tables.",
 	ExperimentStyleGuide:             "Enable the styleguide.",
 	ExperimentSymbolSearch:           "Enable searching for symbols.",
+	ExperimentVulns:                  "Enable vulnerability reporting.",
 }
 
 // Experiment holds data associated with an experimental feature for frontend
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index 7e32c4f..570f9f7 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -32,6 +32,7 @@
 	"golang.org/x/pkgsite/internal/queue"
 	"golang.org/x/pkgsite/internal/static"
 	"golang.org/x/pkgsite/internal/version"
+	vulndbc "golang.org/x/vulndb/client"
 )
 
 // Server can be installed to serve the go discovery frontend.
@@ -50,6 +51,7 @@
 	serveStats           bool
 	reportingClient      *errorreporting.Client
 	fileMux              *http.ServeMux
+	vulndbClient         *vulndbc.Client
 
 	mu        sync.Mutex // Protects all fields below
 	templates map[string]*template.Template
@@ -69,6 +71,7 @@
 	GoogleTagManagerID   string
 	ServeStats           bool
 	ReportingClient      *errorreporting.Client
+	VulndbClient         *vulndbc.Client
 }
 
 // NewServer creates a new Server for the given database and template directory.
@@ -95,6 +98,7 @@
 		serveStats:           scfg.ServeStats,
 		reportingClient:      scfg.ReportingClient,
 		fileMux:              http.NewServeMux(),
+		vulndbClient:         scfg.VulndbClient,
 	}
 	errorPageBytes, err := s.renderErrorPage(context.Background(), http.StatusInternalServerError, "error", nil)
 	if err != nil {
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index 5d66d40..1c4e610 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -17,10 +17,12 @@
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/cookie"
 	"golang.org/x/pkgsite/internal/derrors"
+	"golang.org/x/pkgsite/internal/experiment"
 	"golang.org/x/pkgsite/internal/log"
 	"golang.org/x/pkgsite/internal/middleware"
 	"golang.org/x/pkgsite/internal/stdlib"
 	"golang.org/x/pkgsite/internal/version"
+	"golang.org/x/vulndb/osv"
 )
 
 // UnitPage contains data needed to render the unit template.
@@ -85,6 +87,9 @@
 
 	// Details contains data specific to the type of page being rendered.
 	Details interface{}
+
+	// Vulns holds vulnerability information.
+	Vulns []Vuln
 }
 
 // serveUnitPage serves a unit page for a path.
@@ -204,6 +209,17 @@
 	if ok {
 		page.MetaDescription = metaDescription(main.DocSynopsis)
 	}
+
+	// Get vulnerability information.
+	var vulns []Vuln
+	if s.vulndbClient != nil && experiment.IsActive(ctx, internal.ExperimentVulns) {
+		getEntries := func(m string) ([]*osv.Entry, error) { return s.vulndbClient.Get([]string{m}) }
+		vulns, err = Vulns(um.ModulePath, um.Version, um.Path, getEntries)
+		if err != nil {
+			vulns = []Vuln{{Details: fmt.Sprintf("could not get vulnerability data: %v", err)}}
+		}
+		page.Vulns = vulns
+	}
 	s.servePage(ctx, w, tabSettings.TemplateName, page)
 	return nil
 }
diff --git a/internal/frontend/vulns.go b/internal/frontend/vulns.go
new file mode 100644
index 0000000..e6aeabe
--- /dev/null
+++ b/internal/frontend/vulns.go
@@ -0,0 +1,53 @@
+// 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 frontend
+
+import (
+	"golang.org/x/mod/semver"
+	"golang.org/x/pkgsite/internal/derrors"
+	"golang.org/x/vulndb/osv"
+)
+
+// A Vuln contains information to display about a vulnerability.
+type Vuln struct {
+	// A description of the vulnerability, or the problem in obtaining it.
+	Details string
+	// The version is which the vulnerability has been fixed.
+	FixedVersion string
+}
+
+// Vulns obtains vulnerability information for the given package.
+// the getVulnEntries function should have the same signature and
+// behavior as golang.org/x/vulndb/client.Client.Get.
+// It is passed to facilitate testing.
+func Vulns(modulePath, version, packagePath string, getVulnEntries func(string) ([]*osv.Entry, error)) (_ []Vuln, err error) {
+	defer derrors.Wrap(&err, "Vulns(%q, %q)", modulePath, version)
+
+	// Get all the vulns for this module.
+	entries, err := getVulnEntries(modulePath)
+	if err != nil {
+		return nil, err
+	}
+	// Each entry describes a single vuln. Select the ones that apply to this
+	// package at this version.
+	var vulns []Vuln
+	for _, e := range entries {
+		if e.Package.Name == packagePath && e.Affects.AffectsSemver(version) {
+			// Choose the latest fixed version, if any.
+			var fixed string
+			for _, r := range e.Affects.Ranges {
+				if r.Fixed != "" && (fixed == "" || semver.Compare(r.Fixed, fixed) > 0) {
+					fixed = r.Fixed
+				}
+			}
+			vulns = append(vulns, Vuln{
+				Details: e.Details,
+				// TODO(golang/go#48223): handle stdlib versions
+				FixedVersion: "v" + fixed,
+			})
+		}
+	}
+	return vulns, nil
+}
diff --git a/internal/frontend/vulns_test.go b/internal/frontend/vulns_test.go
new file mode 100644
index 0000000..f82e2a5
--- /dev/null
+++ b/internal/frontend/vulns_test.go
@@ -0,0 +1,55 @@
+// 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 frontend
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/vulndb/osv"
+)
+
+func TestVulns(t *testing.T) {
+	e := osv.Entry{
+		Package: osv.Package{Name: "bad.com"},
+		Details: "bad",
+		Affects: osv.Affects{
+			Ranges: []osv.AffectsRange{{
+				Type:  osv.TypeSemver,
+				Fixed: "1.2.3",
+			}},
+		},
+	}
+	get := func(modulePath string) ([]*osv.Entry, error) {
+		switch modulePath {
+		case "good.com":
+			return nil, nil
+		case "bad.com":
+			return []*osv.Entry{&e}, nil
+		default:
+			return nil, fmt.Errorf("unknown module %q", modulePath)
+		}
+	}
+
+	got, err := Vulns("good.com", "v1.0.0", "good.com", get)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if got != nil {
+		t.Errorf("got %v, want nil", got)
+	}
+	got, err = Vulns("bad.com", "v1.0.0", "bad.com", get)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := []Vuln{{
+		Details:      "bad",
+		FixedVersion: "v1.2.3",
+	}}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("mismatch (-want, +got):\n%s", diff)
+	}
+}
diff --git a/static/frontend/unit/_header.tmpl b/static/frontend/unit/_header.tmpl
index 0deade6..146eb28 100644
--- a/static/frontend/unit/_header.tmpl
+++ b/static/frontend/unit/_header.tmpl
@@ -220,6 +220,17 @@
       />&nbsp; Redirected from <span data-test-id="redirected-banner-text">{{.}}</span>.
     </div>
   {{end}}
+  {{if .Experiments.IsActive "vulns"}}
+    {{with .Vulns}}
+      <div class="go-Message go-Message--alert">
+	This package has vulnerabilities.<br/>
+	{{range .}}
+	  <p>{{.Details}}</p>
+	  {{with .FixedVersion}}Fixed in {{.}}.{{end}}
+	{{end}}
+      </div>
+    {{end}}
+  {{end}}
   {{if .Unit.Deprecated}}
     <div class="go-Message go-Message--warning">
       <img