blob: 099542337ed37fc918d86e3eb447287326b74066 [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"
_ "embed"
"fmt"
"path/filepath"
"strconv"
"strings"
"sync"
"golang.org/x/exp/slices"
"golang.org/x/vulndb/cmd/vulnreport/log"
"golang.org/x/vulndb/internal/issues"
"golang.org/x/vulndb/internal/report"
"golang.org/x/vulndb/internal/triage/priority"
)
type triage struct {
*xrefer
*issueParser
*fixer
mu sync.Mutex // protects aliasesToIssues and stats
aliasesToIssues map[string][]int
stats []issuesList
// issues that have already been marked as duplicate
duplicates map[int]bool
}
func (*triage) name() string { return "triage" }
func (*triage) usage() (string, string) {
const desc = "determines priority and finds likely duplicates of the given Github issue (with no args, looks at all open issues)"
return "<no args> | " + ghIssueArgs, desc
}
func (t *triage) close() error {
log.Outf("triaged %d issues:%s%s",
len(t.stats[statTriaged]), listItem, strings.Join(toStrings(t.stats[:len(t.stats)-1]), listItem))
// Print the command to create all high priority reports.
if len(t.stats[statHighPriority]) > 0 {
log.Outf("helpful commands:\n $ vulnreport create %s", t.stats[statHighPriority].issNums())
}
return nil
}
func toStrings(stats []issuesList) (strs []string) {
for i, s := range stats {
strs = append(strs, fmt.Sprintf("%d %s", len(s), statNames[i]))
}
return strs
}
func (t *triage) setup(ctx context.Context, env environment) error {
t.aliasesToIssues = make(map[string][]int)
t.stats = make([]issuesList, len(statNames))
t.issueParser = new(issueParser)
t.fixer = new(fixer)
t.xrefer = new(xrefer)
if err := setupAll(ctx, env, t.issueParser, t.fixer, t.xrefer); err != nil {
return err
}
log.Info("creating alias map for open issues")
t.duplicates = make(map[int]bool)
open, err := t.openIssues(ctx)
if err != nil {
return err
}
for _, iss := range open {
aliases := t.aliases(ctx, iss)
for _, a := range aliases {
t.addAlias(a, iss.Number)
}
if iss.HasLabel(labelDuplicate) {
t.duplicates[iss.Number] = true
}
}
return nil
}
func (t *triage) skip(input any) string {
iss := input.(*issues.Issue)
if iss.HasLabel(labelDirect) {
return "direct external report"
}
if isExcluded(iss) {
return "excluded"
}
if !*force && iss.HasLabel(labelTriaged) {
return "already triaged; use -f to force re-triage"
}
return skip(iss, t.xrefer)
}
func (t *triage) run(ctx context.Context, input any) (err error) {
iss := input.(*issues.Issue)
t.triage(ctx, iss)
return nil
}
func (t *triage) triage(ctx context.Context, iss *issues.Issue) {
labels := []string{labelTriaged}
comments := []string{}
defer func() {
t.editIssue(ctx, iss, labels, comments)
t.addStat(iss, statTriaged, "")
}()
dupes := t.findDuplicates(ctx, iss)
if len(dupes) != 0 {
var strs []string
for d, aliases := range dupes {
ref := t.ic.Reference(d.iss)
if d.fname != "" {
ref = filepath.ToSlash(d.fname)
}
strs = append(strs, fmt.Sprintf("#%d shares alias(es) %s with %s", iss.Number,
strings.Join(aliases, ", "), ref))
comments = append(comments, fmt.Sprintf("Duplicate of #%d", d.iss))
}
slices.Sort(strs)
t.addStat(iss, statDuplicate, strings.Join(strs, listItem))
labels = append(labels, labelDuplicate)
}
mp := t.canonicalModule(modulePath(iss))
pr, notGo := t.modulePriority(mp)
t.addStat(iss, toStat(pr.Priority), pr.Reason)
if notGo != nil {
t.addStat(iss, statNotGo, notGo.Reason)
labels = append(labels, labelPossiblyNotGo)
}
if pr.Priority == priority.High {
labels = append(labels, labelHighPriority)
}
}
func (t *triage) editIssue(ctx context.Context, iss *issues.Issue, labels, comments []string) {
// Preserve any existing labels.
labels = append(labels, iss.Labels...)
// Sort and de-duplicate.
slices.Sort(labels)
labels = slices.Compact(labels)
slices.Sort(comments)
comments = slices.Compact(comments)
if *dry {
if len(labels) != 0 {
log.Infof("issue #%d: would set labels: [%s]", iss.Number, strings.Join(labels, ", "))
}
if len(comments) != 0 {
log.Infof("issue #%d: would add comments: [%s]", iss.Number, strings.Join(comments, ", "))
}
return
}
if err := t.ic.SetLabels(ctx, iss.Number, labels); err != nil {
log.Warnf("issue #%d: could not auto-set label(s) %s\n\t%v", iss.Number, labels, err)
}
// TODO(tatianabradley): Read existing comments to ensure we aren't
// posting the same comment twice.
// (This requires an extra Github API request.)
if err := t.ic.AddComments(ctx, iss.Number, comments); err != nil {
log.Warnf("issue #%d: could not add comment(s) %s\n\t%v", iss.Number, comments, err)
}
}
func toStat(p priority.Priority) int {
switch p {
case priority.Unknown:
return statUnknownPriority
case priority.Low:
return statLowPriority
case priority.High:
return statHighPriority
default:
panic(fmt.Sprintf("unknown priority %d", p))
}
}
func (t *triage) aliases(ctx context.Context, iss *issues.Issue) []string {
aliases := aliases(iss)
if len(aliases) == 0 {
return nil
}
return t.allAliases(ctx, aliases)
}
type vuln struct {
// The issue number for this vulnerability.
iss int
// The filename of the report for this issue.
fname string
}
// findDuplicates returns a map of duplicate issues to the aliases
// that they share with the given issue.
func (t *triage) findDuplicates(ctx context.Context, iss *issues.Issue) map[vuln][]string {
aliases := t.aliases(ctx, iss)
if len(aliases) == 0 {
log.Infof("issue #%d: skipping duplicate search (no aliases found)", iss.Number)
return nil
}
duplicates := make(map[vuln][]string)
for _, a := range aliases {
// Find existing reports with this alias.
if reports := t.rc.ReportsByAlias(a); len(reports) != 0 {
for _, r := range reports {
fname, err := r.YAMLFilename()
if err != nil {
log.Warnf("could not get filename of duplicate report: %s", err)
continue
}
_, _, iss, err := report.ParseFilepath(fname)
if err != nil {
log.Warnf("could not parse duplicate report: %s", err)
continue
}
d := vuln{
iss: iss,
fname: fname,
}
duplicates[d] = append(duplicates[d], a)
}
}
// Find other open issues with this alias.
for _, issNum := range t.lookupAlias(a) {
if iss.Number == issNum {
continue
}
// If the other issue is already marked as a duplicate,
// we don't need to mark this one.
if t.duplicates[issNum] {
continue
}
d := vuln{
iss: issNum,
// no report yet
}
duplicates[d] = append(duplicates[d], a)
t.duplicates[iss.Number] = true
}
}
return duplicates
}
func (t *triage) lookupAlias(a string) []int {
t.mu.Lock()
defer t.mu.Unlock()
return t.aliasesToIssues[a]
}
func (t *triage) addAlias(a string, n int) {
t.mu.Lock()
defer t.mu.Unlock()
t.aliasesToIssues[a] = append(t.aliasesToIssues[a], n)
}
func (t *triage) addStat(iss *issues.Issue, stat int, reason string) {
t.mu.Lock()
defer t.mu.Unlock()
var lg func(string, ...any)
switch stat {
case statTriaged:
// no-op
lg = func(string, ...any) {}
case statLowPriority:
lg = log.Infof
case statHighPriority, statDuplicate, statNotGo:
lg = log.Outf
case statUnknownPriority:
lg = log.Warnf
default:
panic(fmt.Sprintf("BUG: unknown stat: %d", stat))
}
t.stats[stat] = append(t.stats[stat], iss)
lg("issue %s is %s%s%s", t.ic.Reference(iss.Number), statNames[stat], listItem, reason)
}
const (
statHighPriority = iota
statLowPriority
statUnknownPriority
statDuplicate
statNotGo
statTriaged
)
var statNames = []string{
statHighPriority: "high priority",
statLowPriority: "low priority",
statUnknownPriority: "unknown priority",
statDuplicate: "likely duplicate",
statNotGo: "possibly not Go",
statTriaged: "triaged",
}
type issuesList []*issues.Issue
func (i issuesList) issNums() string {
var is []string
for _, iss := range i {
is = append(is, strconv.Itoa(iss.Number))
}
return strings.Join(is, " ")
}