internal/govulncheck: rename internal vulncheck package to govulncheck

This changes is part of a series of CLs that replaces the uses of
"vulncheck" with "govulncheck."

Change-Id: I4849087e1bd28e0dac6b2a6b80e5c3298c07c659
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/476058
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/internal/govulncheck/govulncheck.go b/internal/govulncheck/govulncheck.go
index 0c5297f..dac90f0 100644
--- a/internal/govulncheck/govulncheck.go
+++ b/internal/govulncheck/govulncheck.go
@@ -5,12 +5,355 @@
 package govulncheck
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
+	"net/http"
+	"sort"
+	"strings"
+	"time"
 
+	bq "cloud.google.com/go/bigquery"
+	"cloud.google.com/go/civil"
+	"golang.org/x/exp/maps"
+	"golang.org/x/pkgsite-metrics/internal/bigquery"
+	"golang.org/x/pkgsite-metrics/internal/derrors"
+	"golang.org/x/pkgsite-metrics/internal/log"
+	"golang.org/x/pkgsite-metrics/internal/scan"
 	"golang.org/x/vuln/exp/govulncheck"
+	"google.golang.org/api/iterator"
 )
 
+// EnqueueQueryParams for vulncheck/enqueue
+type EnqueueQueryParams struct {
+	Suffix string // appended to task queue IDs to generate unique tasks
+	Mode   string // type of analysis to run
+	Min    int    // minimum import-by count for a module to be included
+	File   string // path to file containing modules; if missing, use DB
+}
+
+// Request contains information passed
+// to a scan endpoint.
+type Request struct {
+	scan.ModuleURLPath
+	QueryParams
+}
+
+// QueryParams has query parameters for a vulncheck scan request.
+type QueryParams struct {
+	ImportedBy int    // imported-by count
+	Mode       string // vulncheck mode (VTA, etc)
+	Insecure   bool   // if true, run outside sandbox
+	Serve      bool   // serve results back to client instead of writing them to BigQuery
+}
+
+// These methods implement queue.Task.
+func (r *Request) Name() string { return r.Module + "@" + r.Version }
+
+func (r *Request) Path() string { return r.ModuleURLPath.Path() }
+
+func (r *Request) Params() string {
+	return scan.FormatParams(r.QueryParams)
+}
+
+// ParseRequest parses an http request r for an endpoint
+// prefix and produces a corresponding ScanRequest.
+//
+// The module and version should have one of the following three forms:
+//   - <module>/@v/<version>
+//   - <module>@<version>
+//   - <module>/@latest
+//
+// (These are the same forms that the module proxy accepts.)
+func ParseRequest(r *http.Request, prefix string) (*Request, error) {
+	mp, err := scan.ParseModuleURLPath(strings.TrimPrefix(r.URL.Path, prefix))
+	if err != nil {
+		return nil, err
+	}
+
+	rp := QueryParams{ImportedBy: -1}
+	if err := scan.ParseParams(r, &rp); err != nil {
+		return nil, err
+	}
+	if rp.ImportedBy < 0 {
+		return nil, errors.New(`missing or negative "importedby" query param`)
+	}
+	return &Request{
+		ModuleURLPath: mp,
+		QueryParams:   rp,
+	}, nil
+}
+
+func ConvertGovulncheckOutput(v *govulncheck.Vuln) (vulns []*Vuln) {
+	for _, module := range v.Modules {
+		for pkgNum, pkg := range module.Packages {
+			addedSymbols := make(map[string]bool)
+			baseVuln := &Vuln{
+				ID:          v.OSV.ID,
+				ModulePath:  module.Path,
+				PackagePath: pkg.Path,
+				CallSink:    bigquery.NullInt(0),
+				ImportSink:  bigquery.NullInt(pkgNum + 1),
+				RequireSink: bigquery.NullInt(pkgNum + 1),
+			}
+
+			// For each called symbol, reconstruct sinks and create the corresponding bigquery vuln
+			for symbolNum, cs := range pkg.CallStacks {
+				addedSymbols[cs.Symbol] = true
+				toAdd := *baseVuln
+				toAdd.Symbol = cs.Symbol
+				toAdd.CallSink = bigquery.NullInt(symbolNum + 1)
+				vulns = append(vulns, &toAdd)
+			}
+
+			// Find the rest of the vulnerable imported symbols that haven't been called
+			// and create corresponding bigquery vulns
+			for _, affected := range v.OSV.Affected {
+				if affected.Package.Name == module.Path {
+					for _, imp := range affected.EcosystemSpecific.Imports {
+						if imp.Path == pkg.Path {
+							for _, symbol := range imp.Symbols {
+								if !addedSymbols[symbol] {
+									toAdd := *baseVuln
+									toAdd.Symbol = symbol
+									vulns = append(vulns, &toAdd)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return vulns
+}
+
+const TableName = "vulncheck"
+
+// Note: before modifying Result or Vuln, make sure the change
+// is a valid schema modification.
+// The only supported changes are:
+//   - adding a nullable or repeated column
+//   - dropping a column
+//   - changing a column from required to nullable.
+// See https://cloud.google.com/bigquery/docs/managing-table-schemas for details.
+
+// Result is a row in the BigQuery vulncheck table. It corresponds to a
+// result from the output for vulncheck.Source.
+type Result struct {
+	CreatedAt     time.Time `bigquery:"created_at"`
+	ModulePath    string    `bigquery:"module_path"`
+	Version       string    `bigquery:"version"`
+	Suffix        string    `bigquery:"suffix"`
+	SortVersion   string    `bigquery:"sort_version"`
+	ImportedBy    int       `bigquery:"imported_by"`
+	Error         string    `bigquery:"error"`
+	ErrorCategory string    `bigquery:"error_category"`
+	CommitTime    time.Time `bigquery:"commit_time"`
+	ScanSeconds   float64   `bigquery:"scan_seconds"`
+	ScanMemory    int64     `bigquery:"scan_memory"`
+	PkgsMemory    int64     `bigquery:"pkgs_memory"`
+	ScanMode      string    `bigquery:"scan_mode"`
+	// Workers is the concurrency limit under which a module is
+	// analyzed. Useful for interpreting memory measurements when
+	// there are multiple modules analyzed in the same process.
+	// 0 if no limit is specified, -1 for potential errors.
+	Workers     int     `bigquery:"workers"`
+	WorkVersion         // InferSchema flattens embedded fields
+	Vulns       []*Vuln `bigquery:"vulns"`
+}
+
+// WorkVersion contains information that can be used to avoid duplicate work.
+// Given two WorkVersion values v1 and v2 for the same module path and version,
+// if v1.Equal(v2) then it is not necessary to scan the module.
+type WorkVersion struct {
+	// The version of the currently running code. This tracks changes in the
+	// logic of module scanning and processing.
+	WorkerVersion string `bigquery:"worker_version"`
+	// The version of the bigquery schema.
+	SchemaVersion string ` bigquery:"schema_version"`
+	// The version of the golang.org/x/vuln module used by the current module.
+	VulnVersion string `bigquery:"x_vuln_version"`
+	// When the vuln DB was last modified.
+	VulnDBLastModified time.Time `bigquery:"vulndb_last_modified"`
+}
+
+func (v1 *WorkVersion) Equal(v2 *WorkVersion) bool {
+	if v1 == nil || v2 == nil {
+		return v1 == v2
+	}
+	return v1.WorkerVersion == v2.WorkerVersion &&
+		v1.SchemaVersion == v2.SchemaVersion &&
+		v1.VulnVersion == v2.VulnVersion &&
+		v1.VulnDBLastModified.Equal(v2.VulnDBLastModified)
+}
+
+func (vr *Result) SetUploadTime(t time.Time) { vr.CreatedAt = t }
+
+func (vr *Result) AddError(err error) {
+	if err == nil {
+		return
+	}
+	vr.Error = err.Error()
+	vr.ErrorCategory = derrors.CategorizeError(err)
+}
+
+// Vuln is a record in Result and corresponds to an item in
+// vulncheck.Result.Vulns.
+type Vuln struct {
+	ID          string       `bigquery:"id"`
+	Symbol      string       `bigquery:"symbol"`
+	PackagePath string       `bigquery:"package_path"`
+	ModulePath  string       `bigquery:"module_path"`
+	CallSink    bq.NullInt64 `bigquery:"call_sink"`
+	ImportSink  bq.NullInt64 `bigquery:"import_sink"`
+	RequireSink bq.NullInt64 `bigquery:"require_sink"`
+}
+
+// SchemaVersion changes whenever the vulncheck schema changes.
+var SchemaVersion string
+
+func init() {
+	s, err := bigquery.InferSchema(Result{})
+	if err != nil {
+		panic(err)
+	}
+	SchemaVersion = bigquery.SchemaVersion(s)
+	bigquery.AddTable(TableName, s)
+}
+
+// ReadWorkVersions reads the most recent WorkVersions in the vulncheck table.
+func ReadWorkVersions(ctx context.Context, c *bigquery.Client) (_ map[[2]string]*WorkVersion, err error) {
+	defer derrors.Wrap(&err, "ReadWorkVersions")
+	m := map[[2]string]*WorkVersion{}
+	query := bigquery.PartitionQuery{
+		Table:       c.FullTableName(TableName),
+		Columns:     "module_path, version, worker_version, schema_version, x_vuln_version, vulndb_last_modified",
+		PartitionOn: "module_path, sort_version",
+		OrderBy:     "created_at DESC",
+	}.String()
+	iter, err := c.Query(ctx, query)
+	if err != nil {
+		return nil, err
+	}
+	err = bigquery.ForEachRow(iter, func(r *Result) bool {
+		m[[2]string{r.ModulePath, r.Version}] = &r.WorkVersion
+		return true
+	})
+	if err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// The module path along with the four sort columns should uniquely specify a
+// row, because we do not generate a new row for a (module, version) if the
+// other three versions are identical. (There is actually a fourth component of
+// the work version, the schema version. But since it is represented by a struct
+// in the worker code and the worker version captures every change to that code,
+// it cannot change independently of worker_version.)
+const orderByClauses = `
+			vulndb_last_modified DESC, -- latest version of database
+			x_vuln_version DESC,       -- latest version of x/vuln
+			worker_version DESC,       -- latest version of x/pkgsite-metrics
+			sort_version DESC,         -- latest version of module
+			created_at DESC            -- latest insertion time
+`
+
+func FetchResults(ctx context.Context, c *bigquery.Client) (rows []*Result, err error) {
+	return fetchResults(ctx, c, TableName)
+}
+
+func fetchResults(ctx context.Context, c *bigquery.Client, tableName string) (rows []*Result, err error) {
+	name := c.FullTableName(tableName)
+	query := bigquery.PartitionQuery{
+		Table:       name,
+		PartitionOn: "module_path, scan_mode",
+		OrderBy:     orderByClauses,
+	}.String()
+	log.Infof(ctx, "running latest query on %s", name)
+	iter, err := c.Query(ctx, query)
+	if err != nil {
+		return nil, err
+	}
+	rows, err = bigquery.All[Result](iter)
+	if err != nil {
+		return nil, err
+	}
+	log.Infof(ctx, "got %d rows", len(rows))
+
+	// Check for duplicate rows.
+	modvers := map[string]int{}
+	for _, r := range rows {
+		modvers[r.ModulePath+"@"+r.Version+" "+r.ScanMode]++
+	}
+	keys := maps.Keys(modvers)
+	sort.Strings(keys)
+	for _, k := range keys {
+		if n := modvers[k]; n > 1 {
+			return nil, fmt.Errorf("%s has %d rows", k, n)
+		}
+	}
+	return rows, nil
+}
+
+type ReportResult struct {
+	*Result
+	ReportDate civil.Date `bigquery:"report_date"` // for reporting (e.g. dashboard)
+	InsertedAt time.Time  `bigquery:"inserted_at"` // to disambiguate if >1 insertion for same date
+}
+
+func init() {
+	s, err := bigquery.InferSchema(ReportResult{})
+	if err != nil {
+		panic(err)
+	}
+	bigquery.AddTable(TableName+"-report", s)
+}
+
+func InsertResults(ctx context.Context, c *bigquery.Client, results []*Result, date civil.Date, allowDuplicates bool) (err error) {
+	return insertResults(ctx, c, TableName+"-report", results, date, allowDuplicates)
+}
+
+func insertResults(ctx context.Context, c *bigquery.Client, reportTableName string, results []*Result, date civil.Date, allowDuplicates bool) (err error) {
+	derrors.Wrap(&err, "InsertResults(%s)", date)
+	// Create the report table if it doesn't exist.
+	if err := c.CreateTable(ctx, reportTableName); err != nil {
+		return err
+	}
+
+	if !allowDuplicates {
+		query := fmt.Sprintf("SELECT COUNT(*) FROM `%s` WHERE report_date = '%s'",
+			c.FullTableName(reportTableName), date)
+		iter, err := c.Query(ctx, query)
+		if err != nil {
+			return err
+		}
+		var count struct {
+			n int
+		}
+		err = iter.Next(&count)
+		if err != nil && err != iterator.Done {
+			return err
+		}
+		if count.n > 0 {
+			return fmt.Errorf("already have %d rows for %s", count.n, date)
+		}
+	}
+
+	now := time.Now()
+	var rows []ReportResult
+	for _, r := range results {
+		rows = append(rows, ReportResult{Result: r, ReportDate: date, InsertedAt: now})
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) // to avoid retrying forever on permanent errors
+	defer cancel()
+	const chunkSize = 1024 // Chunk rows to a void exceeding the maximum allowable request size.
+	return bigquery.UploadMany(ctx, c, reportTableName, rows, chunkSize)
+}
+
 // ScanStats represent monitoring information about a given
 // run of govulncheck or vulncheck
 type ScanStats struct {
diff --git a/internal/vulncheck/vulncheck_test.go b/internal/govulncheck/govulncheck_test.go
similarity index 99%
rename from internal/vulncheck/vulncheck_test.go
rename to internal/govulncheck/govulncheck_test.go
index 145d23a..e4da8f7 100644
--- a/internal/vulncheck/vulncheck_test.go
+++ b/internal/govulncheck/govulncheck_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package vulncheck
+package govulncheck
 
 import (
 	"context"
diff --git a/internal/vulncheck/vulncheck.go b/internal/vulncheck/vulncheck.go
deleted file mode 100644
index 25205d1..0000000
--- a/internal/vulncheck/vulncheck.go
+++ /dev/null
@@ -1,367 +0,0 @@
-// 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 vulncheck
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"net/http"
-	"sort"
-	"strings"
-	"time"
-
-	bq "cloud.google.com/go/bigquery"
-	"cloud.google.com/go/civil"
-	"golang.org/x/exp/maps"
-	"golang.org/x/pkgsite-metrics/internal/bigquery"
-	"golang.org/x/pkgsite-metrics/internal/derrors"
-	"golang.org/x/pkgsite-metrics/internal/log"
-	"golang.org/x/pkgsite-metrics/internal/scan"
-	"golang.org/x/vuln/exp/govulncheck"
-	"golang.org/x/vuln/vulncheck"
-	"google.golang.org/api/iterator"
-)
-
-// EnqueueQueryParams for vulncheck/enqueue
-type EnqueueQueryParams struct {
-	Suffix string // appended to task queue IDs to generate unique tasks
-	Mode   string // type of analysis to run
-	Min    int    // minimum import-by count for a module to be included
-	File   string // path to file containing modules; if missing, use DB
-}
-
-// Request contains information passed
-// to a scan endpoint.
-type Request struct {
-	scan.ModuleURLPath
-	QueryParams
-}
-
-// QueryParams has query parameters for a vulncheck scan request.
-type QueryParams struct {
-	ImportedBy int    // imported-by count
-	Mode       string // vulncheck mode (VTA, etc)
-	Insecure   bool   // if true, run outside sandbox
-	Serve      bool   // serve results back to client instead of writing them to BigQuery
-}
-
-// These methods implement queue.Task.
-func (r *Request) Name() string { return r.Module + "@" + r.Version }
-
-func (r *Request) Path() string { return r.ModuleURLPath.Path() }
-
-func (r *Request) Params() string {
-	return scan.FormatParams(r.QueryParams)
-}
-
-// ParseRequest parses an http request r for an endpoint
-// prefix and produces a corresponding ScanRequest.
-//
-// The module and version should have one of the following three forms:
-//   - <module>/@v/<version>
-//   - <module>@<version>
-//   - <module>/@latest
-//
-// (These are the same forms that the module proxy accepts.)
-func ParseRequest(r *http.Request, prefix string) (*Request, error) {
-	mp, err := scan.ParseModuleURLPath(strings.TrimPrefix(r.URL.Path, prefix))
-	if err != nil {
-		return nil, err
-	}
-
-	rp := QueryParams{ImportedBy: -1}
-	if err := scan.ParseParams(r, &rp); err != nil {
-		return nil, err
-	}
-	if rp.ImportedBy < 0 {
-		return nil, errors.New(`missing or negative "importedby" query param`)
-	}
-	return &Request{
-		ModuleURLPath: mp,
-		QueryParams:   rp,
-	}, nil
-}
-
-func ConvertVulncheckOutput(v *vulncheck.Vuln) *Vuln {
-	return &Vuln{
-		ID:          v.OSV.ID,
-		ModulePath:  v.ModPath,
-		PackagePath: v.PkgPath,
-		Symbol:      v.Symbol,
-		CallSink:    bigquery.NullInt(v.CallSink),
-		ImportSink:  bigquery.NullInt(v.ImportSink),
-		RequireSink: bigquery.NullInt(v.RequireSink),
-	}
-}
-
-func ConvertGovulncheckOutput(v *govulncheck.Vuln) (vulns []*Vuln) {
-	for _, module := range v.Modules {
-		for pkgNum, pkg := range module.Packages {
-			addedSymbols := make(map[string]bool)
-			baseVuln := &Vuln{
-				ID:          v.OSV.ID,
-				ModulePath:  module.Path,
-				PackagePath: pkg.Path,
-				CallSink:    bigquery.NullInt(0),
-				ImportSink:  bigquery.NullInt(pkgNum + 1),
-				RequireSink: bigquery.NullInt(pkgNum + 1),
-			}
-
-			// For each called symbol, reconstruct sinks and create the corresponding bigquery vuln
-			for symbolNum, cs := range pkg.CallStacks {
-				addedSymbols[cs.Symbol] = true
-				toAdd := *baseVuln
-				toAdd.Symbol = cs.Symbol
-				toAdd.CallSink = bigquery.NullInt(symbolNum + 1)
-				vulns = append(vulns, &toAdd)
-			}
-
-			// Find the rest of the vulnerable imported symbols that haven't been called
-			// and create corresponding bigquery vulns
-			for _, affected := range v.OSV.Affected {
-				if affected.Package.Name == module.Path {
-					for _, imp := range affected.EcosystemSpecific.Imports {
-						if imp.Path == pkg.Path {
-							for _, symbol := range imp.Symbols {
-								if !addedSymbols[symbol] {
-									toAdd := *baseVuln
-									toAdd.Symbol = symbol
-									vulns = append(vulns, &toAdd)
-								}
-							}
-						}
-					}
-				}
-			}
-		}
-	}
-	return vulns
-}
-
-const TableName = "vulncheck"
-
-// Note: before modifying Result or Vuln, make sure the change
-// is a valid schema modification.
-// The only supported changes are:
-//   - adding a nullable or repeated column
-//   - dropping a column
-//   - changing a column from required to nullable.
-// See https://cloud.google.com/bigquery/docs/managing-table-schemas for details.
-
-// Result is a row in the BigQuery vulncheck table. It corresponds to a
-// result from the output for vulncheck.Source.
-type Result struct {
-	CreatedAt     time.Time `bigquery:"created_at"`
-	ModulePath    string    `bigquery:"module_path"`
-	Version       string    `bigquery:"version"`
-	Suffix        string    `bigquery:"suffix"`
-	SortVersion   string    `bigquery:"sort_version"`
-	ImportedBy    int       `bigquery:"imported_by"`
-	Error         string    `bigquery:"error"`
-	ErrorCategory string    `bigquery:"error_category"`
-	CommitTime    time.Time `bigquery:"commit_time"`
-	ScanSeconds   float64   `bigquery:"scan_seconds"`
-	ScanMemory    int64     `bigquery:"scan_memory"`
-	PkgsMemory    int64     `bigquery:"pkgs_memory"`
-	ScanMode      string    `bigquery:"scan_mode"`
-	// Workers is the concurrency limit under which a module is
-	// analyzed. Useful for interpreting memory measurements when
-	// there are multiple modules analyzed in the same process.
-	// 0 if no limit is specified, -1 for potential errors.
-	Workers     int     `bigquery:"workers"`
-	WorkVersion         // InferSchema flattens embedded fields
-	Vulns       []*Vuln `bigquery:"vulns"`
-}
-
-// WorkVersion contains information that can be used to avoid duplicate work.
-// Given two WorkVersion values v1 and v2 for the same module path and version,
-// if v1.Equal(v2) then it is not necessary to scan the module.
-type WorkVersion struct {
-	// The version of the currently running code. This tracks changes in the
-	// logic of module scanning and processing.
-	WorkerVersion string `bigquery:"worker_version"`
-	// The version of the bigquery schema.
-	SchemaVersion string ` bigquery:"schema_version"`
-	// The version of the golang.org/x/vuln module used by the current module.
-	VulnVersion string `bigquery:"x_vuln_version"`
-	// When the vuln DB was last modified.
-	VulnDBLastModified time.Time `bigquery:"vulndb_last_modified"`
-}
-
-func (v1 *WorkVersion) Equal(v2 *WorkVersion) bool {
-	if v1 == nil || v2 == nil {
-		return v1 == v2
-	}
-	return v1.WorkerVersion == v2.WorkerVersion &&
-		v1.SchemaVersion == v2.SchemaVersion &&
-		v1.VulnVersion == v2.VulnVersion &&
-		v1.VulnDBLastModified.Equal(v2.VulnDBLastModified)
-}
-
-func (vr *Result) SetUploadTime(t time.Time) { vr.CreatedAt = t }
-
-func (vr *Result) AddError(err error) {
-	if err == nil {
-		return
-	}
-	vr.Error = err.Error()
-	vr.ErrorCategory = derrors.CategorizeError(err)
-}
-
-// Vuln is a record in Result and corresponds to an item in
-// vulncheck.Result.Vulns.
-type Vuln struct {
-	ID          string       `bigquery:"id"`
-	Symbol      string       `bigquery:"symbol"`
-	PackagePath string       `bigquery:"package_path"`
-	ModulePath  string       `bigquery:"module_path"`
-	CallSink    bq.NullInt64 `bigquery:"call_sink"`
-	ImportSink  bq.NullInt64 `bigquery:"import_sink"`
-	RequireSink bq.NullInt64 `bigquery:"require_sink"`
-}
-
-// SchemaVersion changes whenever the vulncheck schema changes.
-var SchemaVersion string
-
-func init() {
-	s, err := bigquery.InferSchema(Result{})
-	if err != nil {
-		panic(err)
-	}
-	SchemaVersion = bigquery.SchemaVersion(s)
-	bigquery.AddTable(TableName, s)
-}
-
-// ReadWorkVersions reads the most recent WorkVersions in the vulncheck table.
-func ReadWorkVersions(ctx context.Context, c *bigquery.Client) (_ map[[2]string]*WorkVersion, err error) {
-	defer derrors.Wrap(&err, "ReadWorkVersions")
-	m := map[[2]string]*WorkVersion{}
-	query := bigquery.PartitionQuery{
-		Table:       c.FullTableName(TableName),
-		Columns:     "module_path, version, worker_version, schema_version, x_vuln_version, vulndb_last_modified",
-		PartitionOn: "module_path, sort_version",
-		OrderBy:     "created_at DESC",
-	}.String()
-	iter, err := c.Query(ctx, query)
-	if err != nil {
-		return nil, err
-	}
-	err = bigquery.ForEachRow(iter, func(r *Result) bool {
-		m[[2]string{r.ModulePath, r.Version}] = &r.WorkVersion
-		return true
-	})
-	if err != nil {
-		return nil, err
-	}
-	return m, nil
-}
-
-// The module path along with the four sort columns should uniquely specify a
-// row, because we do not generate a new row for a (module, version) if the
-// other three versions are identical. (There is actually a fourth component of
-// the work version, the schema version. But since it is represented by a struct
-// in the worker code and the worker version captures every change to that code,
-// it cannot change independently of worker_version.)
-const orderByClauses = `
-			vulndb_last_modified DESC, -- latest version of database
-			x_vuln_version DESC,       -- latest version of x/vuln
-			worker_version DESC,       -- latest version of x/pkgsite-metrics
-			sort_version DESC,         -- latest version of module
-			created_at DESC            -- latest insertion time
-`
-
-func FetchResults(ctx context.Context, c *bigquery.Client) (rows []*Result, err error) {
-	return fetchResults(ctx, c, TableName)
-}
-
-func fetchResults(ctx context.Context, c *bigquery.Client, tableName string) (rows []*Result, err error) {
-	name := c.FullTableName(tableName)
-	query := bigquery.PartitionQuery{
-		Table:       name,
-		PartitionOn: "module_path, scan_mode",
-		OrderBy:     orderByClauses,
-	}.String()
-	log.Infof(ctx, "running latest query on %s", name)
-	iter, err := c.Query(ctx, query)
-	if err != nil {
-		return nil, err
-	}
-	rows, err = bigquery.All[Result](iter)
-	if err != nil {
-		return nil, err
-	}
-	log.Infof(ctx, "got %d rows", len(rows))
-
-	// Check for duplicate rows.
-	modvers := map[string]int{}
-	for _, r := range rows {
-		modvers[r.ModulePath+"@"+r.Version+" "+r.ScanMode]++
-	}
-	keys := maps.Keys(modvers)
-	sort.Strings(keys)
-	for _, k := range keys {
-		if n := modvers[k]; n > 1 {
-			return nil, fmt.Errorf("%s has %d rows", k, n)
-		}
-	}
-	return rows, nil
-}
-
-type ReportResult struct {
-	*Result
-	ReportDate civil.Date `bigquery:"report_date"` // for reporting (e.g. dashboard)
-	InsertedAt time.Time  `bigquery:"inserted_at"` // to disambiguate if >1 insertion for same date
-}
-
-func init() {
-	s, err := bigquery.InferSchema(ReportResult{})
-	if err != nil {
-		panic(err)
-	}
-	bigquery.AddTable(TableName+"-report", s)
-}
-
-func InsertResults(ctx context.Context, c *bigquery.Client, results []*Result, date civil.Date, allowDuplicates bool) (err error) {
-	return insertResults(ctx, c, TableName+"-report", results, date, allowDuplicates)
-}
-
-func insertResults(ctx context.Context, c *bigquery.Client, reportTableName string, results []*Result, date civil.Date, allowDuplicates bool) (err error) {
-	derrors.Wrap(&err, "InsertResults(%s)", date)
-	// Create the report table if it doesn't exist.
-	if err := c.CreateTable(ctx, reportTableName); err != nil {
-		return err
-	}
-
-	if !allowDuplicates {
-		query := fmt.Sprintf("SELECT COUNT(*) FROM `%s` WHERE report_date = '%s'",
-			c.FullTableName(reportTableName), date)
-		iter, err := c.Query(ctx, query)
-		if err != nil {
-			return err
-		}
-		var count struct {
-			n int
-		}
-		err = iter.Next(&count)
-		if err != nil && err != iterator.Done {
-			return err
-		}
-		if count.n > 0 {
-			return fmt.Errorf("already have %d rows for %s", count.n, date)
-		}
-	}
-
-	now := time.Now()
-	var rows []ReportResult
-	for _, r := range results {
-		rows = append(rows, ReportResult{Result: r, ReportDate: date, InsertedAt: now})
-	}
-
-	ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) // to avoid retrying forever on permanent errors
-	defer cancel()
-	const chunkSize = 1024 // Chunk rows to a void exceeding the maximum allowable request size.
-	return bigquery.UploadMany(ctx, c, reportTableName, rows, chunkSize)
-}
diff --git a/internal/worker/enqueue.go b/internal/worker/enqueue.go
index 9471fc4..aaa6f76 100644
--- a/internal/worker/enqueue.go
+++ b/internal/worker/enqueue.go
@@ -10,11 +10,11 @@
 
 	"golang.org/x/pkgsite-metrics/internal/config"
 	"golang.org/x/pkgsite-metrics/internal/derrors"
+	"golang.org/x/pkgsite-metrics/internal/govulncheck"
 	"golang.org/x/pkgsite-metrics/internal/log"
 	"golang.org/x/pkgsite-metrics/internal/pkgsitedb"
 	"golang.org/x/pkgsite-metrics/internal/queue"
 	"golang.org/x/pkgsite-metrics/internal/scan"
-	ivulncheck "golang.org/x/pkgsite-metrics/internal/vulncheck"
 )
 
 const defaultMinImportedByCount = 10
@@ -73,15 +73,15 @@
 	return nil
 }
 
-func moduleSpecsToScanRequests(modspecs []scan.ModuleSpec, mode string) []*ivulncheck.Request {
-	var sreqs []*ivulncheck.Request
+func moduleSpecsToScanRequests(modspecs []scan.ModuleSpec, mode string) []*govulncheck.Request {
+	var sreqs []*govulncheck.Request
 	for _, ms := range modspecs {
-		sreqs = append(sreqs, &ivulncheck.Request{
+		sreqs = append(sreqs, &govulncheck.Request{
 			ModuleURLPath: scan.ModuleURLPath{
 				Module:  ms.Path,
 				Version: ms.Version,
 			},
-			QueryParams: ivulncheck.QueryParams{
+			QueryParams: govulncheck.QueryParams{
 				ImportedBy: ms.ImportedBy,
 				Mode:       mode,
 			},
diff --git a/internal/worker/govulncheck.go b/internal/worker/govulncheck.go
index 7561359..9fd8890 100644
--- a/internal/worker/govulncheck.go
+++ b/internal/worker/govulncheck.go
@@ -11,23 +11,23 @@
 	"runtime/debug"
 
 	"golang.org/x/pkgsite-metrics/internal/derrors"
+	"golang.org/x/pkgsite-metrics/internal/govulncheck"
 	"golang.org/x/pkgsite-metrics/internal/log"
-	ivulncheck "golang.org/x/pkgsite-metrics/internal/vulncheck"
 )
 
 type VulncheckServer struct {
 	*Server
-	storedWorkVersions map[[2]string]*ivulncheck.WorkVersion
-	workVersion        *ivulncheck.WorkVersion
+	storedWorkVersions map[[2]string]*govulncheck.WorkVersion
+	workVersion        *govulncheck.WorkVersion
 }
 
 func newVulncheckServer(ctx context.Context, s *Server) (*VulncheckServer, error) {
 	var (
-		swv map[[2]string]*ivulncheck.WorkVersion
+		swv map[[2]string]*govulncheck.WorkVersion
 		err error
 	)
 	if s.bqClient != nil {
-		swv, err = ivulncheck.ReadWorkVersions(ctx, s.bqClient)
+		swv, err = govulncheck.ReadWorkVersions(ctx, s.bqClient)
 		if err != nil {
 			return nil, err
 		}
@@ -39,7 +39,7 @@
 	}, nil
 }
 
-func (h *VulncheckServer) getWorkVersion(ctx context.Context) (_ *ivulncheck.WorkVersion, err error) {
+func (h *VulncheckServer) getWorkVersion(ctx context.Context) (_ *govulncheck.WorkVersion, err error) {
 	defer derrors.Wrap(&err, "VulncheckServer.getWorkVersion")
 	h.mu.Lock()
 	defer h.mu.Unlock()
@@ -53,10 +53,10 @@
 		if err != nil {
 			return nil, err
 		}
-		h.workVersion = &ivulncheck.WorkVersion{
+		h.workVersion = &govulncheck.WorkVersion{
 			VulnDBLastModified: lmt,
 			WorkerVersion:      h.cfg.VersionID,
-			SchemaVersion:      ivulncheck.SchemaVersion,
+			SchemaVersion:      govulncheck.SchemaVersion,
 			VulnVersion:        vulnVersion,
 		}
 		log.Infof(ctx, "vulncheck work version: %+v", h.workVersion)
diff --git a/internal/worker/govulncheck_enqueue.go b/internal/worker/govulncheck_enqueue.go
index 1eca9e9..0a6dad5 100644
--- a/internal/worker/govulncheck_enqueue.go
+++ b/internal/worker/govulncheck_enqueue.go
@@ -16,10 +16,10 @@
 	"golang.org/x/exp/maps"
 	"golang.org/x/pkgsite-metrics/internal/config"
 	"golang.org/x/pkgsite-metrics/internal/derrors"
+	"golang.org/x/pkgsite-metrics/internal/govulncheck"
 	"golang.org/x/pkgsite-metrics/internal/log"
 	"golang.org/x/pkgsite-metrics/internal/queue"
 	"golang.org/x/pkgsite-metrics/internal/scan"
-	ivulncheck "golang.org/x/pkgsite-metrics/internal/vulncheck"
 	"google.golang.org/api/iterator"
 )
 
@@ -35,7 +35,7 @@
 
 func (h *VulncheckServer) enqueue(r *http.Request, allModes bool) error {
 	ctx := r.Context()
-	params := &ivulncheck.EnqueueQueryParams{Min: defaultMinImportedByCount}
+	params := &govulncheck.EnqueueQueryParams{Min: defaultMinImportedByCount}
 	if err := scan.ParseParams(r, params); err != nil {
 		return fmt.Errorf("%w: %v", derrors.InvalidArgument, err)
 	}
@@ -67,14 +67,14 @@
 	return []string{mode}, nil
 }
 
-func createVulncheckQueueTasks(ctx context.Context, cfg *config.Config, params *ivulncheck.EnqueueQueryParams, modes []string) (_ []queue.Task, err error) {
+func createVulncheckQueueTasks(ctx context.Context, cfg *config.Config, params *govulncheck.EnqueueQueryParams, modes []string) (_ []queue.Task, err error) {
 	defer derrors.Wrap(&err, "createVulncheckQueueTasks(%v)", modes)
 	var (
 		tasks    []queue.Task
 		modspecs []scan.ModuleSpec
 	)
 	for _, mode := range modes {
-		var reqs []*ivulncheck.Request
+		var reqs []*govulncheck.Request
 		if mode == ModeBinary {
 			reqs, err = readBinaries(ctx, cfg.BinaryBucket)
 			if err != nil {
@@ -113,7 +113,7 @@
 // gcsBinaryDir is the directory in the GCS bucket that contains binaries that should be scanned.
 const gcsBinaryDir = "binaries"
 
-func readBinaries(ctx context.Context, bucketName string) (reqs []*ivulncheck.Request, err error) {
+func readBinaries(ctx context.Context, bucketName string) (reqs []*govulncheck.Request, err error) {
 	defer derrors.Wrap(&err, "readBinaries(%q)", bucketName)
 	if bucketName == "" {
 		log.Infof(ctx, "binary bucket not configured; not enqueuing binaries")
@@ -136,9 +136,9 @@
 		if err != nil {
 			return nil, err
 		}
-		reqs = append(reqs, &ivulncheck.Request{
+		reqs = append(reqs, &govulncheck.Request{
 			ModuleURLPath: mp,
-			QueryParams:   ivulncheck.QueryParams{Mode: ModeBinary},
+			QueryParams:   govulncheck.QueryParams{Mode: ModeBinary},
 		})
 	}
 	return reqs, nil
diff --git a/internal/worker/govulncheck_enqueue_test.go b/internal/worker/govulncheck_enqueue_test.go
index 6e4535b..65305b0 100644
--- a/internal/worker/govulncheck_enqueue_test.go
+++ b/internal/worker/govulncheck_enqueue_test.go
@@ -12,9 +12,9 @@
 
 	"github.com/google/go-cmp/cmp"
 	"golang.org/x/pkgsite-metrics/internal/config"
+	"golang.org/x/pkgsite-metrics/internal/govulncheck"
 	"golang.org/x/pkgsite-metrics/internal/queue"
 	"golang.org/x/pkgsite-metrics/internal/scan"
-	ivulncheck "golang.org/x/pkgsite-metrics/internal/vulncheck"
 )
 
 var binaryBucket = flag.String("binary-bucket", "", "bucket for scannable binaries")
@@ -27,13 +27,13 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	want := &ivulncheck.Request{
+	want := &govulncheck.Request{
 		ModuleURLPath: scan.ModuleURLPath{
 			Module:  "golang.org/x/pkgsite",
 			Version: "v0.0.0-20221004150836-873fb37c2479",
 			Suffix:  "cmd/worker",
 		},
-		QueryParams: ivulncheck.QueryParams{Mode: ModeBinary},
+		QueryParams: govulncheck.QueryParams{Mode: ModeBinary},
 	}
 	found := false
 	for _, sr := range sreqs {
@@ -51,14 +51,14 @@
 }
 
 func TestCreateQueueTasks(t *testing.T) {
-	vreq := func(path, version, mode string, importedBy int) *ivulncheck.Request {
-		return &ivulncheck.Request{
+	vreq := func(path, version, mode string, importedBy int) *govulncheck.Request {
+		return &govulncheck.Request{
 			ModuleURLPath: scan.ModuleURLPath{Module: path, Version: version},
-			QueryParams:   ivulncheck.QueryParams{Mode: mode, ImportedBy: importedBy},
+			QueryParams:   govulncheck.QueryParams{Mode: mode, ImportedBy: importedBy},
 		}
 	}
 
-	params := &ivulncheck.EnqueueQueryParams{Min: 8, File: "testdata/modules.txt"}
+	params := &govulncheck.EnqueueQueryParams{Min: 8, File: "testdata/modules.txt"}
 	gotTasks, err := createVulncheckQueueTasks(context.Background(), &config.Config{}, params, []string{ModeGovulncheck})
 	if err != nil {
 		t.Fatal(err)
@@ -68,7 +68,7 @@
 		vreq("github.com/pkg/errors", "v0.9.1", ModeGovulncheck, 10),
 		vreq("golang.org/x/net", "v0.4.0", ModeGovulncheck, 20),
 	}
-	if diff := cmp.Diff(wantTasks, gotTasks, cmp.AllowUnexported(ivulncheck.Request{})); diff != "" {
+	if diff := cmp.Diff(wantTasks, gotTasks, cmp.AllowUnexported(govulncheck.Request{})); diff != "" {
 		t.Errorf("mismatch (-want, +got):\n%s", diff)
 	}
 
@@ -86,7 +86,7 @@
 		vreq("golang.org/x/net", "v0.4.0", ModeGovulncheck, 20),
 	}
 
-	if diff := cmp.Diff(wantTasks, gotTasks, cmp.AllowUnexported(ivulncheck.Request{})); diff != "" {
+	if diff := cmp.Diff(wantTasks, gotTasks, cmp.AllowUnexported(govulncheck.Request{})); diff != "" {
 		t.Errorf("mismatch (-want, +got):\n%s", diff)
 	}
 }
diff --git a/internal/worker/govulncheck_results.go b/internal/worker/govulncheck_results.go
index c743279..af2f7d3 100644
--- a/internal/worker/govulncheck_results.go
+++ b/internal/worker/govulncheck_results.go
@@ -12,9 +12,9 @@
 	"cloud.google.com/go/civil"
 	"golang.org/x/exp/event"
 	"golang.org/x/pkgsite-metrics/internal/derrors"
+	"golang.org/x/pkgsite-metrics/internal/govulncheck"
 	"golang.org/x/pkgsite-metrics/internal/log"
 	"golang.org/x/pkgsite-metrics/internal/scan"
-	ivulncheck "golang.org/x/pkgsite-metrics/internal/vulncheck"
 )
 
 var insertResultsCounter = event.NewCounter("insert-results", &event.MetricOptions{Namespace: metricNamespace})
@@ -44,12 +44,12 @@
 	}
 	ctx := r.Context()
 	log.Infof(ctx, "reading results")
-	results, err := ivulncheck.FetchResults(ctx, h.bqClient)
+	results, err := govulncheck.FetchResults(ctx, h.bqClient)
 	if err != nil {
 		return err
 	}
 	log.Infof(ctx, "inserting %d results for %s", len(results), date)
-	if err := ivulncheck.InsertResults(ctx, h.bqClient, results, date, allowDuplicates); err != nil {
+	if err := govulncheck.InsertResults(ctx, h.bqClient, results, date, allowDuplicates); err != nil {
 		return err
 	}
 	fmt.Fprintf(w, "%d results for %s inserted successfully.\n", len(results), date)
diff --git a/internal/worker/govulncheck_scan.go b/internal/worker/govulncheck_scan.go
index 92d1f29..6055dc3 100644
--- a/internal/worker/govulncheck_scan.go
+++ b/internal/worker/govulncheck_scan.go
@@ -27,7 +27,6 @@
 	"golang.org/x/pkgsite-metrics/internal/proxy"
 	"golang.org/x/pkgsite-metrics/internal/sandbox"
 	"golang.org/x/pkgsite-metrics/internal/version"
-	ivulncheck "golang.org/x/pkgsite-metrics/internal/vulncheck"
 	vulnclient "golang.org/x/vuln/client"
 	govulncheckapi "golang.org/x/vuln/exp/govulncheck"
 )
@@ -76,7 +75,7 @@
 	}()
 
 	ctx := r.Context()
-	sreq, err := ivulncheck.ParseRequest(r, "/vulncheck/scan")
+	sreq, err := govulncheck.ParseRequest(r, "/vulncheck/scan")
 	if err != nil {
 		return fmt.Errorf("%w: %v", derrors.InvalidArgument, err)
 	}
@@ -117,7 +116,7 @@
 		return nil
 	}
 	var err error
-	h.storedWorkVersions, err = ivulncheck.ReadWorkVersions(ctx, h.bqClient)
+	h.storedWorkVersions, err = govulncheck.ReadWorkVersions(ctx, h.bqClient)
 	return err
 }
 
@@ -126,7 +125,7 @@
 	proxyClient *proxy.Client
 	dbClient    vulnclient.Client
 	bqClient    *bigquery.Client
-	workVersion *ivulncheck.WorkVersion
+	workVersion *govulncheck.WorkVersion
 	goMemLimit  uint64
 	gcsBucket   *storage.BucketHandle
 	insecure    bool
@@ -172,11 +171,11 @@
 	return s.err
 }
 
-func (s *scanner) ScanModule(ctx context.Context, w http.ResponseWriter, sreq *ivulncheck.Request) error {
+func (s *scanner) ScanModule(ctx context.Context, w http.ResponseWriter, sreq *govulncheck.Request) error {
 	if sreq.Module == "std" {
 		return nil // ignore the standard library
 	}
-	row := &ivulncheck.Result{
+	row := &govulncheck.Result{
 		ModulePath:  sreq.Module,
 		Suffix:      sreq.Suffix,
 		WorkVersion: *s.workVersion,
@@ -227,7 +226,7 @@
 	}
 	log.Infof(ctx, "scanner.runScanModule returned %d vulns for %s: row.Vulns=%d err=%v", len(vulns), sreq.Path(), len(row.Vulns), err)
 
-	if err := writeResult(ctx, sreq.Serve, w, s.bqClient, ivulncheck.TableName, row); err != nil {
+	if err := writeResult(ctx, sreq.Serve, w, s.bqClient, govulncheck.TableName, row); err != nil {
 		return err
 	}
 
@@ -245,7 +244,7 @@
 	impRow.ScanMemory = 0
 	impRow.Vulns = vulnsForMode(vulns, modeImports)
 	log.Infof(ctx, "scanner.runScanModule also storing imports vulns for %s: row.Vulns=%d", sreq.Path(), len(impRow.Vulns))
-	return writeResult(ctx, sreq.Serve, w, s.bqClient, ivulncheck.TableName, &impRow)
+	return writeResult(ctx, sreq.Serve, w, s.bqClient, govulncheck.TableName, &impRow)
 }
 
 // vulnsForMode returns vulns that make sense to report for
@@ -256,12 +255,12 @@
 // modified to have CallSink=0. For ModeBinary, these are
 // exactly the input vulns since binary analysis does not
 // distinguish between called and imported vulnerabilities.
-func vulnsForMode(vulns []*ivulncheck.Vuln, mode string) []*ivulncheck.Vuln {
+func vulnsForMode(vulns []*govulncheck.Vuln, mode string) []*govulncheck.Vuln {
 	if mode == ModeBinary {
 		return vulns
 	}
 
-	var vs []*ivulncheck.Vuln
+	var vs []*govulncheck.Vuln
 	for _, v := range vulns {
 		if mode == ModeGovulncheck {
 			// Return only the called vulns for ModeGovulncheck.
@@ -298,7 +297,7 @@
 
 // runScanModule fetches the module version from the proxy, and analyzes it for
 // vulnerabilities.
-func (s *scanner) runScanModule(ctx context.Context, modulePath, version, binaryDir, mode string, stats *scanStats) (bvulns []*ivulncheck.Vuln, err error) {
+func (s *scanner) runScanModule(ctx context.Context, modulePath, version, binaryDir, mode string, stats *scanStats) (bvulns []*govulncheck.Vuln, err error) {
 	err = doScan(ctx, modulePath, version, s.insecure, func() error {
 		var vulns []*govulncheckapi.Vuln
 		if s.insecure {
@@ -310,7 +309,7 @@
 			return err
 		}
 		for _, v := range vulns {
-			bvulns = append(bvulns, ivulncheck.ConvertGovulncheckOutput(v)...)
+			bvulns = append(bvulns, govulncheck.ConvertGovulncheckOutput(v)...)
 		}
 		return nil
 	})
diff --git a/internal/worker/govulncheck_scan_test.go b/internal/worker/govulncheck_scan_test.go
index 660fa15..1c2e6ec 100644
--- a/internal/worker/govulncheck_scan_test.go
+++ b/internal/worker/govulncheck_scan_test.go
@@ -17,8 +17,8 @@
 	"golang.org/x/pkgsite-metrics/internal/bigquery"
 	"golang.org/x/pkgsite-metrics/internal/config"
 	"golang.org/x/pkgsite-metrics/internal/derrors"
+	"golang.org/x/pkgsite-metrics/internal/govulncheck"
 	"golang.org/x/pkgsite-metrics/internal/proxy"
-	ivulncheck "golang.org/x/pkgsite-metrics/internal/vulncheck"
 	vulnc "golang.org/x/vuln/client"
 )
 
@@ -35,13 +35,13 @@
 }
 
 func TestVulnsForMode(t *testing.T) {
-	vulns := []*ivulncheck.Vuln{
-		&ivulncheck.Vuln{Symbol: "A", CallSink: bigquery.NullInt(0)},
-		&ivulncheck.Vuln{Symbol: "B"},
-		&ivulncheck.Vuln{Symbol: "C", CallSink: bigquery.NullInt(9)},
+	vulns := []*govulncheck.Vuln{
+		&govulncheck.Vuln{Symbol: "A", CallSink: bigquery.NullInt(0)},
+		&govulncheck.Vuln{Symbol: "B"},
+		&govulncheck.Vuln{Symbol: "C", CallSink: bigquery.NullInt(9)},
 	}
 
-	vulnsStr := func(vulns []*ivulncheck.Vuln) string {
+	vulnsStr := func(vulns []*govulncheck.Vuln) string {
 		var vs []string
 		for _, v := range vulns {
 			vs = append(vs, fmt.Sprintf("%s:%d", v.Symbol, v.CallSink.Int64))