| // 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}}`)) |