blob: 95de132d8db69cfe259b51b39e3691435ec48d7f [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/cmd/vulnreport/priority"
"golang.org/x/vulndb/internal/issues"
)
type triage struct {
*xrefer
*issueParser
*fixer
mu sync.Mutex // protects aliasesToIssues and stats
aliasesToIssues map[string][]int
modulesToImports map[string]int
stats []issuesList
}
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))
m, err := env.ModuleMap()
if err != nil {
return err
}
t.modulesToImports = m
t.fixer = new(fixer)
t.issueParser = new(issueParser)
t.xrefer = new(xrefer)
return setupAll(ctx, env, t.fixer, t.issueParser, t.xrefer)
}
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}
defer func() {
// Preserve any existing labels.
labels = append(labels, iss.Labels...)
slices.Sort(labels)
labels = slices.Compact(labels)
if *dry {
log.Infof("issue #%d: would set labels: [%s]", iss.Number, strings.Join(labels, ", "))
} else {
if err := t.ic.SetLabels(ctx, iss.Number, labels); err != nil {
log.Warnf("issue #%d: could not auto-set label(s) %s", iss.Number, labels)
}
}
t.addStat(iss, statTriaged, "")
}()
xrefs := t.findDuplicates(ctx, iss)
if len(xrefs) != 0 {
var strs []string
for ref, aliases := range xrefs {
strs = append(strs, fmt.Sprintf("#%d shares alias(es) %s with %s", iss.Number,
strings.Join(aliases, ", "),
filepath.ToSlash(ref)))
}
t.addStat(iss, statDuplicate, strings.Join(strs, listItem))
labels = append(labels, labelPossibleDuplicate)
}
mp := t.canonicalModule(modulePath(iss))
tr := priority.Analyze(mp, t.rc.ReportsByModule(mp), t.modulesToImports)
t.addStat(iss, toStat(tr.Priority), tr.Reason)
if tr.NotGo {
t.addStat(iss, statNotGo, tr.NotGoReason)
labels = append(labels, labelPossiblyNotGo)
}
if tr.Priority == priority.High {
labels = append(labels, labelHighPriority)
}
}
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) findDuplicates(ctx context.Context, iss *issues.Issue) map[string][]string {
aliases := aliases(iss)
if len(aliases) == 0 {
log.Infof("issue #%d: skipping duplicate search (no aliases found)", iss.Number)
return nil
}
aliases = t.allAliases(ctx, aliases)
xrefs := make(map[string][]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 {
fname = r.ID
}
xrefs[fname] = append(xrefs[fname], a)
}
}
// Find other issues with this alias.
// Note: this currently only operates on other issues that have
// been seen by the triage command, not all issues on the tracker.
for _, issNum := range t.lookupAlias(a) {
ref := t.ic.Reference(issNum)
xrefs[ref] = append(xrefs[ref], a)
}
t.addAlias(a, iss.Number)
}
return xrefs
}
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, " ")
}