cmd/vulnreport,internal/cveclient: use cve v5 in vulnreport

The vulnreport command now fetches CVE records in JSON 5.0 format
instead of the legacy 4.0 format.

This change also adds a new function, Fetch, which makes an
unauthenticated HTTP request to the CVE5 database to grab a CVE
record.

This function is used in vulnreport instead of the much-slower
cvelistrepo.FetchCVE (which clones the whole cvelistrepo).

Change-Id: Ic255e98d7c1a52301810dc53712fc2ab4a648e70
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/547560
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/vulnreport/main.go b/cmd/vulnreport/main.go
index 36fc7ef..7ae0ec3 100644
--- a/cmd/vulnreport/main.go
+++ b/cmd/vulnreport/main.go
@@ -20,16 +20,13 @@
 	"runtime/pprof"
 	"strconv"
 	"strings"
-	"sync"
 	"time"
 
-	"github.com/go-git/go-git/v5"
 	"github.com/google/go-cmp/cmp"
 	"golang.org/x/exp/constraints"
 	"golang.org/x/exp/maps"
 	"golang.org/x/exp/slices"
-	"golang.org/x/vulndb/internal/cvelistrepo"
-	"golang.org/x/vulndb/internal/cveschema"
+	"golang.org/x/vulndb/internal/cveclient"
 	"golang.org/x/vulndb/internal/cveschema5"
 	"golang.org/x/vulndb/internal/database"
 	"golang.org/x/vulndb/internal/derrors"
@@ -45,18 +42,17 @@
 )
 
 var (
-	localRepoPath = flag.String("local-cve-repo", "", "path to local repo, instead of cloning remote")
-	issueRepo     = flag.String("issue-repo", "github.com/golang/vulndb", "repo to create issues in")
-	githubToken   = flag.String("ghtoken", "", "GitHub access token (default: value of VULN_GITHUB_ACCESS_TOKEN)")
-	skipSymbols   = flag.Bool("skip-symbols", false, "for lint and fix, don't load package for symbols checks")
-	skipAlias     = flag.Bool("skip-alias", false, "for fix, skip adding new GHSAs and CVEs")
-	graphQL       = flag.Bool("graphql", false, "for create, fetch GHSAs from the Github GraphQL API instead of the OSV database")
-	preferCVE     = flag.Bool("cve", false, "for create, prefer CVEs over GHSAs as canonical source")
-	updateIssue   = flag.Bool("up", false, "for commit, create a CL that updates (doesn't fix) the tracking bug")
-	closedOk      = flag.Bool("closed-ok", false, "for create & create-excluded, allow closed issues to be created")
-	cpuprofile    = flag.String("cpuprofile", "", "write cpuprofile to file")
-	quiet         = flag.Bool("q", false, "quiet mode (suppress info logs)")
-	force         = flag.Bool("f", false, "for fix, force Fix to run even if there are no lint errors")
+	issueRepo   = flag.String("issue-repo", "github.com/golang/vulndb", "repo to create issues in")
+	githubToken = flag.String("ghtoken", "", "GitHub access token (default: value of VULN_GITHUB_ACCESS_TOKEN)")
+	skipSymbols = flag.Bool("skip-symbols", false, "for lint and fix, don't load package for symbols checks")
+	skipAlias   = flag.Bool("skip-alias", false, "for fix, skip adding new GHSAs and CVEs")
+	graphQL     = flag.Bool("graphql", false, "for create, fetch GHSAs from the Github GraphQL API instead of the OSV database")
+	preferCVE   = flag.Bool("cve", false, "for create, prefer CVEs over GHSAs as canonical source")
+	updateIssue = flag.Bool("up", false, "for commit, create a CL that updates (doesn't fix) the tracking bug")
+	closedOk    = flag.Bool("closed-ok", false, "for create & create-excluded, allow closed issues to be created")
+	cpuprofile  = flag.String("cpuprofile", "", "write cpuprofile to file")
+	quiet       = flag.Bool("q", false, "quiet mode (suppress info logs)")
+	force       = flag.Bool("f", false, "for fix, force Fix to run even if there are no lint errors")
 )
 
 var (
@@ -277,28 +273,6 @@
 	allowClosed     bool
 }
 
-var (
-	once    sync.Once
-	cveRepo *git.Repository
-)
-
-func loadCVERepo(ctx context.Context) *git.Repository {
-	// Loading the CVE git repo takes a while, so do it on demand only.
-	once.Do(func() {
-		infolog.Println("cloning CVE repo (this takes a while)")
-		repoPath := cvelistrepo.URLv4
-		if *localRepoPath != "" {
-			repoPath = *localRepoPath
-		}
-		var err error
-		cveRepo, err = gitrepo.CloneOrOpen(ctx, repoPath)
-		if err != nil {
-			log.Fatal(err)
-		}
-	})
-	return cveRepo
-}
-
 func setupCreate(ctx context.Context, args []string) ([]int, *createCfg, error) {
 	if *githubToken == "" {
 		return nil, nil, fmt.Errorf("githubToken must be provided")
@@ -518,31 +492,45 @@
 // For now, it prefers the first GHSA in the list, followed by the first CVE in the list
 // (if no GHSA is present). If no GHSAs or CVEs are present, it returns a new empty Report.
 func reportFromAlias(ctx context.Context, id, modulePath, alias string, cfg *createCfg) (*report.Report, error) {
-	var r *report.Report
 	switch {
 	case ghsa.IsGHSA(alias) && *graphQL:
 		ghsa, err := cfg.ghsaClient.FetchGHSA(ctx, alias)
 		if err != nil {
 			return nil, err
 		}
-		r = report.GHSAToReport(ghsa, modulePath, cfg.proxyClient)
+		r := report.GHSAToReport(ghsa, modulePath, cfg.proxyClient)
+		r.ID = id
+		return r, nil
 	case ghsa.IsGHSA(alias):
 		ghsa, err := genericosv.Fetch(alias)
 		if err != nil {
 			return nil, err
 		}
-		r = ghsa.ToReport(id, cfg.proxyClient)
+		return ghsa.ToReport(id, cfg.proxyClient), nil
 	case cveschema5.IsCVE(alias):
-		cve := &cveschema.CVE{}
-		if err := cvelistrepo.FetchCVE(ctx, loadCVERepo(ctx), alias, cve); err != nil {
-			return nil, err
+		cve, err := cveclient.Fetch(alias)
+		if err != nil {
+			// If a CVE is not found, it is most likely a CVE we reserved but haven't
+			// published yet.
+			infolog.Printf("no published record found for %s, creating basic report", alias)
+			return basicReport(id, modulePath), nil
 		}
-		r = report.CVEToReport(cve, id, modulePath, cfg.proxyClient)
-	default:
-		r = &report.Report{}
+		return report.CVE5ToReport(cve, id, modulePath, cfg.proxyClient), nil
 	}
-	r.ID = id
-	return r, nil
+
+	infolog.Printf("alias %s is not a CVE or GHSA, creating basic report", alias)
+	return basicReport(id, modulePath), nil
+}
+
+func basicReport(id, modulePath string) *report.Report {
+	return &report.Report{
+		ID: id,
+		Modules: []*report.Module{
+			{
+				Module: modulePath,
+			},
+		},
+	}
 }
 
 type parsedIssue struct {
diff --git a/internal/cveclient/fetch.go b/internal/cveclient/fetch.go
new file mode 100644
index 0000000..3fc1f83
--- /dev/null
+++ b/internal/cveclient/fetch.go
@@ -0,0 +1,17 @@
+// Copyright 2023 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 cveclient
+
+import "golang.org/x/vulndb/internal/cveschema5"
+
+// Fetch returns the CVE record associated with the ID.
+// It is intended one-off (non-batch) requests, and
+// is much faster than cvelistrepo.FetchCVE.
+func Fetch(id string) (*cveschema5.CVERecord, error) {
+	c := New(Config{
+		Endpoint: ProdEndpoint,
+	})
+	return c.RetrieveRecord(id)
+}