blob: 5177ea159f9a95061659bbe61993301792ad3903 [file] [log] [blame]
// Copyright 2021 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 worker
// This file has the public API of the worker, used by cmd/worker as well
// as the server in this package.
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
"text/template"
"time"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"golang.org/x/time/rate"
"golang.org/x/vulndb/internal/cve4"
"golang.org/x/vulndb/internal/derrors"
"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/observe"
"golang.org/x/vulndb/internal/pkgsite"
"golang.org/x/vulndb/internal/proxy"
"golang.org/x/vulndb/internal/report"
"golang.org/x/vulndb/internal/triage"
"golang.org/x/vulndb/internal/worker/log"
"golang.org/x/vulndb/internal/worker/store"
)
// UpdateCVEsAtCommit performs an update on the store using the given commit.
// Unless force is true, it checks that the update makes sense before doing it.
func UpdateCVEsAtCommit(ctx context.Context, repoPath, commitHashString string, st store.Store, pc *pkgsite.Client, rc *report.Client, force bool) (err error) {
defer derrors.Wrap(&err, "RunCommitUpdate(%q, %q, force=%t)", repoPath, commitHashString, force)
log.Infof(ctx, "updating false positives")
if err := updateFalsePositives(ctx, st); err != nil {
return err
}
repo, err := gitrepo.CloneOrOpen(ctx, repoPath)
if err != nil {
return err
}
var commitHash plumbing.Hash
if commitHashString == "HEAD" {
ref, err := repo.Reference(plumbing.HEAD, true)
if err != nil {
return err
}
commitHash = ref.Hash()
} else {
commitHash = plumbing.NewHash(commitHashString)
}
commit, err := repo.CommitObject(commitHash)
if err != nil {
return err
}
if !force {
if err := checkCVEUpdate(ctx, commit, st); err != nil {
return err
}
}
u := newCVEUpdater(repo, commit, st, rc, func(cve *cve4.CVE) (*triage.Result, error) {
return triage.RefersToGoModule(ctx, cve, pc)
})
return u.update(ctx)
}
// checkCVEUpdate performs sanity checks on a potential update.
// It verifies that there is not an update currently in progress,
// and it makes sure that the update is to a more recent commit.
func checkCVEUpdate(ctx context.Context, commit *object.Commit, st store.Store) error {
ctx, span := observe.Start(ctx, "checkUpdate")
defer span.End()
urs, err := st.ListCommitUpdateRecords(ctx, 1)
if err != nil {
return err
}
if len(urs) == 0 {
// No updates, we're good.
return nil
}
// If the most recent update started recently but didn't finish, don't proceed to avoid
// concurrent updates.
lu := urs[0]
if lu.EndedAt.IsZero() && time.Since(lu.StartedAt) < 2*time.Hour {
return &CheckUpdateError{
msg: fmt.Sprintf("latest update started %s ago and has not finished", time.Since(lu.StartedAt)),
}
}
if commit.Committer.When.Before(lu.CommitTime) {
return &CheckUpdateError{
msg: fmt.Sprintf("commit %s time %s is before latest update commit %s time %s",
commit.Hash, commit.Committer.When.Format(time.RFC3339),
lu.CommitHash, lu.CommitTime.Format(time.RFC3339)),
}
}
return nil
}
// CheckUpdateError is an error returned from UpdateCommit that can be avoided
// calling UpdateCommit with force set to true.
type CheckUpdateError struct {
msg string
}
func (c *CheckUpdateError) Error() string {
return c.msg
}
// GHSAListFunc is the type of a function that lists GitHub security advisories.
type GHSAListFunc func(_ context.Context, since time.Time) ([]*ghsa.SecurityAdvisory, error)
// UpdateGHSAs updates the store with the current state of GitHub's security advisories.
func UpdateGHSAs(ctx context.Context, list GHSAListFunc, st store.Store) (_ UpdateGHSAStats, err error) {
defer derrors.Wrap(&err, "UpdateGHSAs")
// Find the most recent update time of the records we have in the store.
grs, err := getGHSARecords(ctx, st)
var since time.Time
for _, gr := range grs {
if gr.GHSA.UpdatedAt.After(since) {
since = gr.GHSA.UpdatedAt
}
}
// We want to start just after that time.
since = since.Add(time.Nanosecond)
// Do the update.
return updateGHSAs(ctx, list, since, st)
}
func getGHSARecords(ctx context.Context, st store.Store) ([]*store.LegacyGHSARecord, error) {
var rs []*store.LegacyGHSARecord
err := st.RunTransaction(ctx, func(ctx context.Context, tx store.Transaction) error {
var err error
rs, err = tx.GetLegacyGHSARecords()
return err
})
if err != nil {
return nil, err
}
return rs, nil
}
// Limit GitHub issue creation requests to this many per second.
const issueQPS = 1
// The limiter used to throttle pkgsite requests.
// The second argument to rate.NewLimiter is the burst, which
// basically lets you exceed the rate briefly.
var issueRateLimiter = rate.NewLimiter(rate.Every(time.Duration(1000/float64(issueQPS))*time.Millisecond), 1)
// CreateIssues creates issues on the x/vulndb issue tracker for allReports.
func CreateIssues(ctx context.Context, st store.Store, client *issues.Client, pc *proxy.Client, rc *report.Client, limit int) (err error) {
defer derrors.Wrap(&err, "CreateIssues(destination: %s)", client.Destination())
ctx, span := observe.Start(ctx, "CreateIssues")
defer span.End()
if err := createCVEIssues(ctx, st, client, pc, rc, limit); err != nil {
return err
}
return createGHSAIssues(ctx, st, client, pc, rc, limit)
}
// xref returns cross-references for a report: Information about other reports
// for the same CVE, GHSA, or module.
func xref(r *report.Report, rc *report.Client) string {
aliasTitle, moduleTitle, noneMessage := "!! Possible duplicate report !!",
"Cross references:", "No existing reports found with this module or alias."
return rc.XRef(r).ToString(aliasTitle, moduleTitle, noneMessage)
}
func createCVEIssues(ctx context.Context, st store.Store, client *issues.Client, pc *proxy.Client, rc *report.Client, limit int) (err error) {
defer derrors.Wrap(&err, "createCVEIssues(destination: %s)", client.Destination())
needsIssue, err := st.ListCVE4RecordsWithTriageState(ctx, store.TriageStateNeedsIssue)
if err != nil {
return err
}
log.Infof(ctx, "createCVEIssues starting; destination: %s, total needing issue: %d",
client.Destination(), len(needsIssue))
numCreated := 0
for _, cr := range needsIssue {
if limit > 0 && numCreated >= limit {
break
}
ref, err := createIssue(ctx, cr, client, pc, rc)
if err != nil {
return err
}
// Update the CVE4Record in the DB with issue information.
err = st.RunTransaction(ctx, func(ctx context.Context, tx store.Transaction) error {
r, err := tx.GetRecord(cr.ID)
if err != nil {
return err
}
cr := r.(*store.CVE4Record)
cr.TriageState = store.TriageStateIssueCreated
cr.IssueReference = ref
cr.IssueCreatedAt = time.Now()
return tx.SetRecord(cr)
})
if err != nil {
return err
}
numCreated++
}
log.With("limit", limit).Infof(ctx, "createCVEIssues done: %d created", numCreated)
return nil
}
func createGHSAIssues(ctx context.Context, st store.Store, client *issues.Client, pc *proxy.Client, rc *report.Client, limit int) (err error) {
defer derrors.Wrap(&err, "createGHSAIssues(destination: %s)", client.Destination())
sas, err := getGHSARecords(ctx, st)
if err != nil {
return err
}
var needsIssue []*store.LegacyGHSARecord
for _, sa := range sas {
if sa.TriageState == store.TriageStateNeedsIssue {
needsIssue = append(needsIssue, sa)
}
}
log.Infof(ctx, "createGHSAIssues starting; destination: %s, total needing issue: %d",
client.Destination(), len(needsIssue))
numCreated := 0
for _, gr := range needsIssue {
if limit > 0 && numCreated >= limit {
break
}
// TODO(https://github.com/golang/go/issues/54049): Move this
// check to the triage step of the worker.
if isDuplicate(gr.GHSA, pc, rc) {
// Update the LegacyGHSARecord in the DB to reflect that the GHSA
// already has an advisory.
if err = st.RunTransaction(ctx, func(ctx context.Context, tx store.Transaction) error {
r, err := tx.GetRecord(gr.GetID())
if err != nil {
return err
}
g := r.(*store.LegacyGHSARecord)
g.TriageState = store.TriageStateHasVuln
return tx.SetRecord(g)
}); err != nil {
return err
}
// Do not create an issue.
continue
}
ref, err := createIssue(ctx, gr, client, pc, rc)
if err != nil {
return err
}
// Update the LegacyGHSARecord in the DB with issue information.
err = st.RunTransaction(ctx, func(ctx context.Context, tx store.Transaction) error {
r, err := tx.GetRecord(gr.GetID())
if err != nil {
return err
}
g := r.(*store.LegacyGHSARecord)
g.TriageState = store.TriageStateIssueCreated
g.IssueReference = ref
g.IssueCreatedAt = time.Now()
return tx.SetRecord(g)
})
if err != nil {
return err
}
numCreated++
}
log.With("limit", limit).Infof(ctx, "createGHSAIssues done: %d created", numCreated)
return nil
}
func isDuplicate(sa *ghsa.SecurityAdvisory, pc *proxy.Client, rc *report.Client) bool {
r := report.New(sa, pc)
for alias := range rc.XRef(r).Aliases {
if sa.ID == alias {
return true
}
}
return false
}
func NewIssueBody(r *report.Report, desc string, rc *report.Client) (body string, err error) {
// Truncate the description if it is too long.
if len(desc) > 600 {
desc = desc[:600] + "..."
}
rs, err := r.ToString()
if err != nil {
return "", err
}
var b strings.Builder
if err := issueTemplate.Execute(&b, issueTemplateData{
SourceID: r.SourceMeta.ID,
AdvisoryLink: idstr.AdvisoryLink(r.SourceMeta.ID),
Description: desc,
Xrefs: xref(r, rc),
Report: r,
ReportStr: rs,
Pre: "```",
}); err != nil {
return "", err
}
return b.String(), nil
}
func createIssue(ctx context.Context, r store.Record, client *issues.Client, pc *proxy.Client, rc *report.Client) (ref string, err error) {
id := r.GetID()
defer derrors.Wrap(&err, "createIssue(%s)", id)
if r.GetIssueReference() != "" || !r.GetIssueCreatedAt().IsZero() {
log.With(
"ID", id,
"IssueReference", r.GetIssueReference(),
"IssueCreatedAt", r.GetIssueCreatedAt(),
).Errorf(ctx, "%s: triage state is NeedsIssue but issue field(s) non-zero; skipping", id)
return "", nil
}
src := r.GetSource()
if src == nil || reflect.ValueOf(src).IsNil() {
log.With("ID", id).Errorf(ctx, "%s: triage state is NeedsIssue but source record is nil; skipping: %v", id, err)
return "", nil
}
rep := report.New(src, pc,
report.WithModulePath(r.GetUnit()))
body, err := NewIssueBody(rep, r.GetDescription(), rc)
if err != nil {
log.With("ID", id).Errorf(ctx, "%s: triage state is NeedsIssue but could not generate body; skipping: %v", id, err)
return "", nil
}
labels := []string{"NeedsTriage"}
yrLabel := yearLabel(r.GetID())
if yrLabel != "" {
labels = append(labels, yrLabel)
}
// Create the issue.
iss := &issues.Issue{
Title: fmt.Sprintf("x/vulndb: potential Go vuln in %s: %s", r.GetUnit(), r.GetID()),
Body: body,
Labels: labels,
}
if err := issueRateLimiter.Wait(ctx); err != nil {
return "", err
}
num, err := client.CreateIssue(ctx, iss)
if err != nil {
return "", fmt.Errorf("creating issue for %s: %w", id, err)
}
// If we crashed here, we would have filed an issue without recording
// that fact in the DB. That can lead to duplicate issues, but nothing
// worse (we won't miss a CVE).
// TODO(https://go.dev/issue/49733): look for the issue title to avoid duplications.
ref = client.Reference(num)
log.With("ID", id).Infof(ctx, "created issue %s for %s", ref, id)
return ref, nil
}
func yearLabel(cve string) string {
if !strings.HasPrefix(cve, "CVE-") {
return ""
}
parts := strings.Split(cve, "-")
if len(parts) != 3 {
return ""
}
year, err := strconv.Atoi(parts[1])
if err != nil {
return ""
}
if year > 2019 {
return fmt.Sprintf("cve-year-%s", parts[1])
}
return "cve-year-2019-and-earlier"
}
type issueTemplateData struct {
*report.Report
SourceID string
AdvisoryLink string
Description string
Xrefs string
ReportStr string
Pre string // markdown string for a <pre> block
}
var issueTemplate = template.Must(template.New("issue").Parse(`Advisory [{{.SourceID}}]({{.AdvisoryLink}}) references a vulnerability in the following Go modules:
| Module |
| - |{{range .Modules}}
| [{{.Module}}](https://pkg.go.dev/{{.Module}}) |{{end}}
Description:
{{.Description}}
References:{{range .References}}
- {{.Type}}: {{.URL}}{{end}}
{{.Xrefs}}
See [doc/quickstart.md](https://github.com/golang/vulndb/blob/master/doc/quickstart.md) for instructions on how to triage this report.
{{if (and .Pre .ReportStr) -}}
{{.Pre}}
{{.ReportStr}}
{{.Pre}}
{{- end}}`))