blob: a312f3efbac2bb0dbea43f33e779386bd25da4a1 [file] [log] [blame]
// Copyright 2024 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 main
import (
"context"
"fmt"
"strconv"
"strings"
"sync"
"golang.org/x/vulndb/cmd/vulnreport/log"
"golang.org/x/vulndb/internal/ghsa"
"golang.org/x/vulndb/internal/gitrepo"
"golang.org/x/vulndb/internal/issues"
"golang.org/x/vulndb/internal/proxy"
"golang.org/x/vulndb/internal/report"
)
type duplicates struct {
gc *ghsa.Client
ic *issues.Client
pc *proxy.Client
rc *report.Client
trackerURL string
isses map[string]*issues.Issue
// protects aliasesToIssues
mu sync.Mutex
aliasesToIssues map[string][]int
}
func (*duplicates) name() string { return "duplicates" }
func (*duplicates) usage() (string, string) {
const desc = "finds likely duplicates of the given Github issue (with no args, looks at all open issues)"
return "<no args> | " + ghIssueArgs, desc
}
func (*duplicates) close() error {
return nil
}
func (c *duplicates) setup(ctx context.Context) error {
if *githubToken == "" {
return fmt.Errorf("githubToken must be provided")
}
owner, repoName, err := gitrepo.ParseGitHubRepo(*issueRepo)
if err != nil {
return err
}
c.trackerURL = fmt.Sprintf("https://github.com/%s/%s/issues", owner, repoName)
c.ic = issues.NewClient(ctx, &issues.Config{Owner: owner, Repo: repoName, Token: *githubToken})
c.gc = ghsa.NewClient(ctx, *githubToken)
c.pc = proxy.NewDefaultClient()
c.isses = make(map[string]*issues.Issue)
localRepo, err := gitrepo.Open(ctx, ".")
if err != nil {
return err
}
rc, err := report.NewClient(localRepo)
if err != nil {
return err
}
c.rc = rc
c.aliasesToIssues = make(map[string][]int)
return nil
}
func (d *duplicates) parseArgs(ctx context.Context, args []string) (issNums []string, err error) {
if len(args) > 0 {
return argsToIDs(args)
}
// If no arguments are provided, operate on all open issues.
is, err := d.ic.Issues(ctx, issues.IssuesOptions{State: "open"})
if err != nil {
return nil, err
}
log.Infof("found %d open issues", len(is))
for _, iss := range is {
n := strconv.Itoa(iss.Number)
d.isses[n] = iss
issNums = append(issNums, n)
}
return issNums, nil
}
func (d *duplicates) run(ctx context.Context, issNum string) (err error) {
iss, ok := d.isses[issNum]
if !ok {
n, err := strconv.Atoi(issNum)
if err != nil {
return err
}
iss, err = d.ic.Issue(ctx, n)
if err != nil {
return err
}
}
parsed, err := parseGithubIssue(iss, d.pc, false)
if err != nil {
return err
}
if len(parsed.aliases) == 0 {
log.Infof("no aliases found for issue #%d (%q), skipping", iss.Number, iss.Title)
return
}
aliases := allAliases(ctx, parsed.aliases, d.gc)
var allXrefs []string
for _, a := range aliases {
var xrefs []string
// Find existing reports with this alias.
if reports := d.rc.ReportsByAlias(a); len(reports) != 0 {
for _, r := range reports {
fname, err := r.YAMLFilename()
if err != nil {
fname = r.ID
}
xrefs = append(xrefs, fname)
}
}
// Find other open issues with this alias.
if issNums, ok := d.aliasesToIssues[a]; ok {
for _, in := range issNums {
xrefs = append(xrefs, d.githubURL(in))
}
}
d.addAlias(a, iss.Number)
if len(xrefs) != 0 {
allXrefs = append(allXrefs, fmt.Sprintf("#%d shares alias %s with %s", iss.Number, a, strings.Join(xrefs, ", ")))
}
}
if len(allXrefs) != 0 {
log.Outf("%s is a likely duplicate:\n - %s", d.githubURL(iss.Number), strings.Join(allXrefs, "\n - "))
} else {
log.Infof("found no existing reports or open issues with aliases in issue #%d", iss.Number)
}
return nil
}
func (d *duplicates) githubURL(n int) string {
return fmt.Sprintf("%s/%d", d.trackerURL, n)
}
func (d *duplicates) addAlias(a string, n int) {
d.mu.Lock()
defer d.mu.Unlock()
d.aliasesToIssues[a] = append(d.aliasesToIssues[a], n)
}