| // 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. | 
 |  | 
 | // Command issue provides a tool for creating an issue on the x/vulndb issue | 
 | // tracker. | 
 | // | 
 | // This is used to creating missing issues that were not created by the vulndb | 
 | // worker for various reasons. | 
 | package main | 
 |  | 
 | import ( | 
 | 	"context" | 
 | 	"flag" | 
 | 	"fmt" | 
 | 	"log" | 
 | 	"os" | 
 | 	"sort" | 
 | 	"strings" | 
 |  | 
 | 	"golang.org/x/vulndb/internal" | 
 | 	"golang.org/x/vulndb/internal/ghsa" | 
 | 	"golang.org/x/vulndb/internal/gitrepo" | 
 | 	"golang.org/x/vulndb/internal/idstr" | 
 | 	"golang.org/x/vulndb/internal/issues" | 
 | 	"golang.org/x/vulndb/internal/proxy" | 
 | 	"golang.org/x/vulndb/internal/report" | 
 | 	"golang.org/x/vulndb/internal/worker" | 
 | ) | 
 |  | 
 | var ( | 
 | 	githubToken = flag.String("ghtoken", os.Getenv("VULN_GITHUB_ACCESS_TOKEN"), "GitHub access token") | 
 | 	issueRepo   = flag.String("issue-repo", "github.com/golang/vulndb", "repo to create issues in") | 
 | ) | 
 |  | 
 | func main() { | 
 | 	ctx := context.Background() | 
 | 	flag.Usage = func() { | 
 | 		fmt.Fprintf(flag.CommandLine.Output(), "usage: issue [cmd] [filename | cves]\n") | 
 | 		fmt.Fprintf(flag.CommandLine.Output(), "	triage [filename]: create issues to triage on the tracker for the aliases listed in the file\n") | 
 | 		fmt.Fprintf(flag.CommandLine.Output(), "	excluded [filename]: create excluded issues on the tracker for the aliases listed in the file\n") | 
 | 		fmt.Fprintf(flag.CommandLine.Output(), "	placeholder [cve(s)]: create a placeholder issue on the tracker for the given CVE(s)\n") | 
 | 		fmt.Fprintf(flag.CommandLine.Output(), "\n") | 
 | 		fmt.Fprintf(flag.CommandLine.Output(), "Flags:\n") | 
 | 		flag.PrintDefaults() | 
 | 	} | 
 | 	flag.Parse() | 
 | 	if flag.NArg() != 2 { | 
 | 		flag.Usage() | 
 | 		os.Exit(1) | 
 | 	} | 
 | 	cmd := flag.Args()[0] | 
 | 	filename := flag.Args()[1] | 
 | 	owner, repoName, err := gitrepo.ParseGitHubRepo(*issueRepo) | 
 | 	if err != nil { | 
 | 		log.Fatal(err) | 
 | 	} | 
 | 	c := issues.NewClient(ctx, &issues.Config{Owner: owner, Repo: repoName, Token: *githubToken}) | 
 | 	ghsaClient := ghsa.NewClient(ctx, *githubToken) | 
 | 	pc := proxy.NewDefaultClient() | 
 | 	switch cmd { | 
 | 	case "triage": | 
 | 		err = createIssueToTriage(ctx, c, ghsaClient, pc, filename) | 
 | 	case "excluded": | 
 | 		err = createExcluded(ctx, c, ghsaClient, pc, filename) | 
 | 	case "placeholder": | 
 | 		err = createPlaceholder(ctx, c, flag.Args()[1:]) | 
 | 	default: | 
 | 		err = fmt.Errorf("unsupported command: %q", cmd) | 
 | 	} | 
 | 	if err != nil { | 
 | 		log.Fatal(err) | 
 | 	} | 
 | } | 
 |  | 
 | func createIssueToTriage(ctx context.Context, c *issues.Client, ghsaClient *ghsa.Client, pc *proxy.Client, filename string) (err error) { | 
 | 	aliases, err := parseAliases(filename) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	for _, alias := range aliases { | 
 | 		if err := constructIssue(ctx, c, ghsaClient, pc, alias, []string{"NeedsTriage"}); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | func createExcluded(ctx context.Context, c *issues.Client, ghsaClient *ghsa.Client, pc *proxy.Client, filename string) (err error) { | 
 | 	records, err := parseExcluded(filename) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	for _, r := range records { | 
 | 		if err := constructIssue(ctx, c, ghsaClient, pc, r.identifier, []string{r.label}); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | func createPlaceholder(ctx context.Context, c *issues.Client, args []string) error { | 
 | 	for _, arg := range args { | 
 | 		if !idstr.IsCVE(arg) { | 
 | 			return fmt.Errorf("%q is not a CVE", arg) | 
 | 		} | 
 | 		aliases := []string{arg} | 
 | 		packages := []string{"<placeholder>"} | 
 | 		bodies := []string{fmt.Sprintf("This is a placeholder issue for %q.", arg)} | 
 | 		if err := publishIssue(ctx, c, packages, aliases, bodies, []string{"first party"}); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | func constructIssue(ctx context.Context, c *issues.Client, ghsaClient *ghsa.Client, pc *proxy.Client, alias string, labels []string) (err error) { | 
 | 	var ghsas []*ghsa.SecurityAdvisory | 
 | 	if strings.HasPrefix(alias, "GHSA") { | 
 | 		sa, err := ghsaClient.FetchGHSA(ctx, alias) | 
 | 		if err != nil { | 
 | 			return err | 
 | 		} | 
 | 		ghsas = append(ghsas, sa) | 
 | 	} else if strings.HasPrefix(alias, "CVE") { | 
 | 		ghsas, err = ghsaClient.ListForCVE(ctx, alias) | 
 | 		if err != nil { | 
 | 			return err | 
 | 		} | 
 | 		if len(ghsas) == 0 { | 
 | 			fmt.Printf("%q does not have a GHSA\n", alias) | 
 | 			return nil | 
 | 		} | 
 | 		if len(ghsas) > 1 { | 
 | 			fmt.Printf("%q has multiple GHSAs\n", alias) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// Only include the first package path in the issue. | 
 | 	pkgPath := "unknown" | 
 | 	if len(ghsas[0].Vulns) != 0 { | 
 | 		pkgPath = ghsas[0].Vulns[0].Package | 
 | 	} | 
 | 	// Put all the identifiers in the title. | 
 | 	var ( | 
 | 		ids    []string | 
 | 		bodies []string | 
 | 	) | 
 | 	rc, err := report.NewDefaultClient(ctx) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	for _, sa := range ghsas { | 
 | 		for _, id := range sa.Identifiers { | 
 | 			ids = append(ids, id.Value) | 
 | 		} | 
 | 		r := report.New(sa, pc) | 
 | 		body, err := worker.NewIssueBody(r, sa.Description, rc) | 
 | 		if err != nil { | 
 | 			return err | 
 | 		} | 
 | 		bodies = append(bodies, body) | 
 | 	} | 
 | 	return publishIssue(ctx, c, []string{pkgPath}, ids, bodies, labels) | 
 | } | 
 |  | 
 | func publishIssue(ctx context.Context, c *issues.Client, packages, aliases, bodies, labels []string) error { | 
 | 	sort.Strings(aliases) | 
 | 	iss := &issues.Issue{ | 
 | 		Title: fmt.Sprintf("x/vulndb: potential Go vuln in %s: %s", strings.Join(packages, ", "), | 
 | 			strings.Join(aliases, ", ")), | 
 | 		Body:   strings.Join(bodies, "\n\n----------\n\n"), | 
 | 		Labels: labels, | 
 | 	} | 
 | 	issNum, err := c.CreateIssue(ctx, iss) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	fmt.Printf("published issue https://%s/issues/%d (%s)\n", *issueRepo, issNum, strings.Join(aliases, ", ")) | 
 | 	return nil | 
 | } | 
 |  | 
 | type record struct { | 
 | 	identifier string | 
 | 	label      string | 
 | } | 
 |  | 
 | func parseAliases(filename string) (aliases []string, err error) { | 
 | 	lines, err := internal.ReadFileLines(filename) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 | 	aliases = append(aliases, lines...) | 
 | 	return aliases, nil | 
 | } | 
 |  | 
 | func parseExcluded(filename string) (records []*record, err error) { | 
 | 	lines, err := internal.ReadFileLines(filename) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 | 	for i, line := range lines { | 
 | 		parts := strings.Split(line, ",") | 
 | 		if len(parts) != 2 { | 
 | 			return nil, fmt.Errorf("wrong number of fields on line %d: %q", i, line) | 
 | 		} | 
 | 		er, ok := report.ToExcludedType(parts[0]) | 
 | 		if !ok { | 
 | 			return nil, fmt.Errorf("%s is not a valid excluded reason", parts[0]) | 
 | 		} | 
 | 		r := &record{ | 
 | 			label:      er.ToLabel(), | 
 | 			identifier: parts[1], | 
 | 		} | 
 | 		records = append(records, r) | 
 | 	} | 
 | 	return records, nil | 
 | } |