blob: f800cc8d8577fd34956f9b551ae3d0745cd4ac3f [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 priority contains utilities for prioritizing vulnerability reports.
package priority
import (
"fmt"
"strings"
"golang.org/x/vulndb/internal/report"
)
type Result struct {
Priority Priority
Reason string
}
type NotGoResult struct {
Reason string
}
type Priority int
func (p Priority) String() string {
switch p {
case Unknown:
return "unknown"
case High:
return "high"
case Low:
return "low"
default:
return fmt.Sprintf("%d", p)
}
}
const (
Unknown Priority = iota
Low
High
)
// AnalyzeReport returns the results for a report as a whole:
// - priority is the priority of its highest-priority module
// - not Go if all modules are not Go
func AnalyzeReport(r *report.Report, rc *report.Client, modulesToImports map[string]int) (*Result, *NotGoResult) {
var overall Priority
var reasons []string
var notGoReasons []string
for _, m := range r.Modules {
mp := m.Module
result, notGo := Analyze(mp, rc.ReportsByModule(mp), modulesToImports)
if result.Priority > overall {
overall = result.Priority
}
reasons = append(reasons, result.Reason)
if notGo != nil {
notGoReasons = append(notGoReasons, notGo.Reason)
}
}
result := &Result{
Priority: overall, Reason: strings.Join(reasons, "; "),
}
// If all modules are not Go, the report is not Go.
if len(notGoReasons) == len(r.Modules) {
return result, &NotGoResult{Reason: strings.Join(notGoReasons, "; ")}
}
return result, nil
}
func Analyze(mp string, reportsForModule []*report.Report, modulesToImports map[string]int) (*Result, *NotGoResult) {
sc := stateCounts(reportsForModule)
notGo := isPossiblyNotGo(len(reportsForModule), sc)
importers, ok := modulesToImports[mp]
if !ok {
return &Result{
Priority: Unknown,
Reason: fmt.Sprintf("module %s not found", mp),
}, notGo
}
return priority(mp, importers, sc), notGo
}
// override takes precedence over all other metrics in determining
// a module's priority.
var override map[string]Priority = map[string]Priority{
// argo-cd is primarily a binary and usually has correct version
// information without intervention.
"github.com/argoproj/argo-cd": Low,
"github.com/argoproj/argo-cd/v2": Low,
}
func priority(mp string, importers int, sc map[reportState]int) *Result {
if pr, ok := override[mp]; ok {
return &Result{pr, fmt.Sprintf("%s is in the override list (priority=%s)", mp, pr)}
}
const highPriority = 100
importersStr := func(comp string) string {
return fmt.Sprintf("%s has %d importers (%s %d)", mp, importers, comp, highPriority)
}
if importers >= highPriority {
rev := sc[reviewed]
binary := sc[excludedBinary] + sc[unreviewedUnexcluded]
getReason := func(conj1, conj2 string) string {
return fmt.Sprintf("%s %s reviewed (%d) %s likely-binary reports (%d)",
importersStr(">="), conj1, rev, conj2, binary)
}
if rev > binary {
return &Result{High, getReason("and more", "than")}
} else if rev == binary {
return &Result{High, getReason("and as many", "as")}
}
return &Result{Low, getReason("but fewer", "than")}
}
return &Result{Low, importersStr("<")}
}
func isPossiblyNotGo(numReports int, sc map[reportState]int) *NotGoResult {
if (float32(sc[excludedNotGo])/float32(numReports))*100 > 20 {
return &NotGoResult{
Reason: fmt.Sprintf("more than 20 percent of reports (%d of %d) with this module are NOT_GO_CODE", sc[excludedNotGo], numReports),
}
}
return nil
}
type reportState int
const (
unknownReportState reportState = iota
reviewed
unreviewedStandard
unreviewedUnexcluded
excludedBinary
excludedNotGo
excludedOther
)
func state(r *report.Report) reportState {
if r.IsExcluded() {
switch e := r.Excluded; e {
case report.ExcludedNotGoCode:
return excludedNotGo
case report.ExcludedEffectivelyPrivate,
report.ExcludedNotImportable,
report.ExcludedLegacyFalsePositive:
return excludedBinary
case report.ExcludedNotAVulnerability,
report.ExcludedDependentVulnerabilty,
report.ExcludedWithdrawn:
return excludedOther
default:
return unknownReportState
}
}
switch rs := r.ReviewStatus; rs {
case report.Reviewed:
return reviewed
case report.Unreviewed:
if r.Unexcluded != "" {
return unreviewedUnexcluded
}
return unreviewedStandard
}
return unknownReportState
}
func stateCounts(rs []*report.Report) map[reportState]int {
counts := make(map[reportState]int)
for _, r := range rs {
st := state(r)
if st == unknownReportState {
panic("could not determine report state for " + r.ID)
}
counts[st]++
}
return counts
}