blob: 40f07d7b4835591b54a55ec1f5d667a0226fca60 [file] [log] [blame]
// 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
}