internal/report,cmd/vulnreport: add GitHub Security Advisories

Reports now contain a list of GitHub Security Advisory IDs
that correspond to its CVEs.

The vulnreport fix command will populate this field.

Change-Id: I9488a603f2aab6f91ff84ec5a7e55fb5cde0085d
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/388676
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/cmd/vulnreport/main.go b/cmd/vulnreport/main.go
index 8111e31..e9afb3f 100644
--- a/cmd/vulnreport/main.go
+++ b/cmd/vulnreport/main.go
@@ -18,11 +18,13 @@
 	"sort"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/go-git/go-git/v5"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/vulndb/internal/cvelistrepo"
 	"golang.org/x/vulndb/internal/derrors"
+	"golang.org/x/vulndb/internal/ghsa"
 	"golang.org/x/vulndb/internal/gitrepo"
 	"golang.org/x/vulndb/internal/issues"
 	"golang.org/x/vulndb/internal/report"
@@ -37,6 +39,7 @@
 )
 
 func main() {
+	ctx := context.Background()
 	flag.Usage = func() {
 		fmt.Fprintf(flag.CommandLine.Output(), "usage: vulnreport [cmd] [filename.yaml]\n")
 		fmt.Fprintf(flag.CommandLine.Output(), "  create [githubIssueNumber]: creates a new vulnerability YAML report\n")
@@ -72,7 +75,7 @@
 		if *localRepoPath != "" {
 			repoPath = *localRepoPath
 		}
-		if err := create(context.Background(), githubID, *githubToken, *issueRepo, repoPath); err != nil {
+		if err := create(ctx, githubID, *githubToken, *issueRepo, repoPath); err != nil {
 			log.Fatal(err)
 		}
 	case "lint":
@@ -84,11 +87,24 @@
 			log.Fatal(err)
 		}
 	case "fix":
-		if err := multi(fix, names); err != nil {
+		var GHSAsByCVE map[string][]string
+		if *githubToken == "" {
+			fmt.Println("flag -ghtoken not provided, so not fixing GHSAs")
+		} else {
+			fmt.Println("querying GitHub for GHSAs...")
+			var err error
+			GHSAsByCVE, err = loadGHSAsByCVE(ctx, *githubToken)
+			if err != nil {
+				log.Fatal(err)
+			}
+			fmt.Println("fixing...")
+		}
+		f := func(name string) error { return fix(name, GHSAsByCVE) }
+		if err := multi(f, names); err != nil {
 			log.Fatal(err)
 		}
 	case "set-dates":
-		repo, err := gitrepo.Open(context.Background(), ".")
+		repo, err := gitrepo.Open(ctx, ".")
 		if err != nil {
 			log.Fatal(err)
 		}
@@ -194,7 +210,7 @@
 	return nil
 }
 
-func fix(filename string) (err error) {
+func fix(filename string, GHSAsByCVE map[string][]string) (err error) {
 	defer derrors.Wrap(&err, "fix(%q)", filename)
 	r, err := report.Read(filename)
 	if err != nil {
@@ -208,6 +224,9 @@
 			return err
 		}
 	}
+	if GHSAsByCVE != nil {
+		fixGHSAs(r, GHSAsByCVE)
+	}
 
 	// Write unconditionally in order to format.
 	return r.Write(filename)
@@ -343,3 +362,36 @@
 	e.SetIndent("", "\t")
 	return e.Encode(cve)
 }
+
+// loadGHSAsByCVE returns a map from CVE ID to GHSA IDs.
+// It does this by using the GitHub API to list all Go security
+// advisories with CVEs.
+func loadGHSAsByCVE(ctx context.Context, accessToken string) (_ map[string][]string, err error) {
+	defer derrors.Wrap(&err, "loadGHSAsByCVE")
+
+	const withCVE = true
+	sas, err := ghsa.List(ctx, accessToken, time.Time{}, withCVE)
+	if err != nil {
+		return nil, err
+	}
+	m := map[string][]string{}
+	for _, sa := range sas {
+		for _, id := range sa.Identifiers {
+			if id.Type == "CVE" {
+				m[id.Value] = append(m[id.Value], sa.PrettyID())
+			}
+		}
+	}
+	return m, nil
+}
+
+// fixGHSAs replaces r.GHSAs with a sorted list of GitHub Security
+// Advisory IDs that correspond to the CVEs.
+func fixGHSAs(r *report.Report, GHSAsByCVE map[string][]string) {
+	var gids []string
+	for _, cid := range r.CVEs {
+		gids = append(gids, GHSAsByCVE[cid]...)
+	}
+	sort.Strings(gids)
+	r.GHSAs = gids
+}
diff --git a/internal/ghsa/ghsa.go b/internal/ghsa/ghsa.go
index 973ba84..02ccd33 100644
--- a/internal/ghsa/ghsa.go
+++ b/internal/ghsa/ghsa.go
@@ -37,7 +37,7 @@
 }
 
 // An Identifier identifies an advisory according to some scheme or
-// organization, given by the Type field. Examples are GitHub and CVE.
+// organization, given by the Type field. Example types are GHSA and CVE.
 type Identifier struct {
 	Type  string
 	Value string
diff --git a/internal/report/report.go b/internal/report/report.go
index a0130ee..2f48343 100644
--- a/internal/report/report.go
+++ b/internal/report/report.go
@@ -68,11 +68,14 @@
 	LastModified *time.Time `yaml:"last_modified,omitempty"`
 	Withdrawn    *time.Time `yaml:",omitempty"`
 
+	// CVE are CVE IDs for existing CVEs.
 	// If we are assigning a CVE ID ourselves, use CVEMetdata.ID instead.
-	// CVE are CVE IDs for existing CVEs, if there is more than one.
-	// Use either CVE or CVEs, but not both.
-	CVEs   []string `yaml:",omitempty"`
-	Credit string   `yaml:",omitempty"`
+	CVEs []string `yaml:",omitempty"`
+	// GHSAs are the IDs of GitHub Security Advisories that match
+	// the above CVEs.
+	GHSAs []string `yaml:",omitempty"`
+
+	Credit string `yaml:",omitempty"`
 	// Symbols originally identified as vulnerable.
 	Symbols []string `yaml:",omitempty"`
 	// Additional vulnerable symbols, computed from Symbols via static analysis