blob: cd34ec557b6cc1c77c6b2a838b7b0e209c0cf6f8 [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.
//go:build go1.17
// +build go1.17
package worker
import (
"context"
"errors"
"flag"
"testing"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/vulndb/internal/cve4"
"golang.org/x/vulndb/internal/cvelistrepo"
"golang.org/x/vulndb/internal/ghsa"
"golang.org/x/vulndb/internal/gitrepo"
"golang.org/x/vulndb/internal/pkgsite"
"golang.org/x/vulndb/internal/report"
"golang.org/x/vulndb/internal/triage"
"golang.org/x/vulndb/internal/worker/store"
)
var usePkgsite = flag.Bool("pkgsite", false, "use pkg.go.dev for tests")
const clearString = "**CLEAR**"
var clearCVE = &cve4.CVE{}
func modify(r, m *store.CVE4Record) *store.CVE4Record {
modString := func(p *string, s string) {
if s == clearString {
*p = ""
} else if s != "" {
*p = s
}
}
c := *r
modString(&c.BlobHash, m.BlobHash)
modString(&c.CommitHash, m.CommitHash)
modString(&c.CVEState, m.CVEState)
if m.TriageState != "" {
if m.TriageState == clearString {
c.TriageState = ""
} else {
c.TriageState = m.TriageState
}
}
modString(&c.TriageStateReason, m.TriageStateReason)
modString(&c.Module, m.Module)
if m.CVE == clearCVE {
c.CVE = nil
} else if m.CVE != nil {
c.CVE = m.CVE
}
modString(&c.IssueReference, m.IssueReference)
if !m.IssueCreatedAt.IsZero() {
panic("unsupported modification")
}
if m.ReferenceURLs != nil {
c.ReferenceURLs = m.ReferenceURLs
}
if m.History != nil {
c.History = m.History
}
return &c
}
func TestNewCVE4Record(t *testing.T) {
// Check that NewCVE4Record with a TriageState gives a valid CVE4Record.
_, commit, err := gitrepo.TxtarRepoAndHead(testRepoPath)
if err != nil {
t.Fatal(err)
}
pathname := "2021/0xxx/CVE-2021-0001.json"
cve, bh := readCVE4(t, commit, pathname)
cr := store.NewCVE4Record(cve, pathname, bh, commit)
cr.TriageState = store.TriageStateNeedsIssue
if err := cr.Validate(); err != nil {
t.Fatal(err)
}
}
func TestDoUpdate(t *testing.T) {
ctx := context.Background()
repo, commit, err := gitrepo.TxtarRepoAndHead(testRepoPath)
if err != nil {
t.Fatal(err)
}
pc, err := pkgsite.TestClient(t, *usePkgsite)
if err != nil {
t.Fatal(err)
}
rc, err := report.NewTestClient(map[string]*report.Report{
"data/reports/GO-1999-0001.yaml": {CVEs: []string{"CVE-2020-9283"}},
})
if err != nil {
t.Fatal(err)
}
needsIssue := func(cve *cve4.CVE) (*triage.Result, error) {
return triage.RefersToGoModule(ctx, cve, pc)
}
commitHash := commit.Hash.String()
paths := []string{
"2021/0xxx/CVE-2021-0001.json",
"2021/0xxx/CVE-2021-0010.json",
"2021/1xxx/CVE-2021-1384.json",
"2020/9xxx/CVE-2020-9283.json",
"2022/39xxx/CVE-2022-39213.json",
}
var (
cves []*cve4.CVE
blobHashes []string
)
for _, p := range paths {
cve, bh := readCVE4(t, commit, p)
cves = append(cves, cve)
blobHashes = append(blobHashes, bh)
}
// Expected CVE4Records after the above CVEs are added to an empty DB.
var rs []*store.CVE4Record
for i := 0; i < len(cves); i++ {
r := store.NewCVE4Record(cves[i], paths[i], blobHashes[i], commit)
rs = append(rs, r)
}
rs[0].TriageState = store.TriageStateNeedsIssue // a public CVE, has a golang.org path
rs[0].Module = "golang.org/x/mod"
rs[0].CVE = cves[0]
rs[1].TriageState = store.TriageStateNoActionNeeded // state is reserved
rs[2].TriageState = store.TriageStateNoActionNeeded // state is rejected
rs[3].TriageState = store.TriageStateHasVuln
rs[4].TriageState = store.TriageStateNeedsIssue
rs[4].Module = "github.com/pandatix/go-cvss"
rs[4].CVE = cves[4]
for _, test := range []struct {
name string
curCVEs []*store.CVE4Record // current state of CVEs collection
curGHSAs []*store.LegacyGHSARecord // current state of GHSAs collection
want []*store.CVE4Record // expected state of CVEs collection after update
wantUpdate *store.CommitUpdateRecord // expected update record
}{
{
name: "empty",
curCVEs: nil,
want: rs,
wantUpdate: &store.CommitUpdateRecord{
NumTotal: 5,
NumProcessed: 5,
NumAdded: 5,
},
},
{
name: "no change",
curCVEs: rs,
want: rs,
wantUpdate: &store.CommitUpdateRecord{
NumTotal: 5,
NumProcessed: 5,
NumAdded: 0,
},
},
{
name: "pre-issue changes",
curCVEs: []*store.CVE4Record{
// NoActionNeeded -> NeedsIssue
modify(rs[0], &store.CVE4Record{
BlobHash: "x", // if we don't use a different blob hash, no update will happen
TriageState: store.TriageStateNoActionNeeded,
}),
// NeedsIssue -> NoActionNeeded
modify(rs[1], &store.CVE4Record{
BlobHash: "x",
TriageState: store.TriageStateNeedsIssue,
Module: "something",
CVE: cves[1],
}),
// NoActionNeeded, triage state stays the same but other fields change.
modify(rs[2], &store.CVE4Record{
TriageState: store.TriageStateNoActionNeeded,
}),
},
want: []*store.CVE4Record{
modify(rs[0], &store.CVE4Record{
History: []*store.CVE4RecordSnapshot{{
CommitHash: commitHash,
CVEState: cve4.StatePublic,
TriageState: store.TriageStateNoActionNeeded,
}},
}),
modify(rs[1], &store.CVE4Record{
Module: clearString,
CVE: clearCVE,
History: []*store.CVE4RecordSnapshot{{
CommitHash: commitHash,
CVEState: cve4.StateReserved,
TriageState: store.TriageStateNeedsIssue,
}},
}),
rs[2],
rs[3],
rs[4],
},
wantUpdate: &store.CommitUpdateRecord{
NumTotal: 5,
NumProcessed: 5,
NumAdded: 2,
NumModified: 2,
},
},
{
name: "post-issue changes",
curCVEs: []*store.CVE4Record{
// IssueCreated -> Updated
modify(rs[0], &store.CVE4Record{
BlobHash: "x",
TriageState: store.TriageStateIssueCreated,
}),
modify(rs[1], &store.CVE4Record{
BlobHash: "x",
TriageState: store.TriageStateUpdatedSinceIssueCreation,
}),
},
want: []*store.CVE4Record{
modify(rs[0], &store.CVE4Record{
TriageState: store.TriageStateUpdatedSinceIssueCreation,
TriageStateReason: `CVE changed; affected module = "golang.org/x/mod"`,
History: []*store.CVE4RecordSnapshot{{
CommitHash: commitHash,
CVEState: cve4.StatePublic,
TriageState: store.TriageStateIssueCreated,
}},
}),
modify(rs[1], &store.CVE4Record{
TriageState: store.TriageStateUpdatedSinceIssueCreation,
TriageStateReason: `CVE changed; affected module = ""`,
}),
rs[2],
rs[3],
rs[4],
},
wantUpdate: &store.CommitUpdateRecord{
NumTotal: 5,
NumProcessed: 5,
NumAdded: 3,
NumModified: 2,
},
},
{
name: "false positive no Go URLs",
curCVEs: []*store.CVE4Record{
// FalsePositive; no change
modify(rs[0], &store.CVE4Record{
BlobHash: "x",
TriageState: store.TriageStateFalsePositive,
ReferenceURLs: []string{
"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00477.html",
"https://golang.org/x/mod",
},
}),
},
want: []*store.CVE4Record{
modify(rs[0], &store.CVE4Record{
TriageState: store.TriageStateFalsePositive,
ReferenceURLs: []string{
"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00477.html",
"https://golang.org/x/mod",
},
}),
rs[1], rs[2], rs[3], rs[4],
},
wantUpdate: &store.CommitUpdateRecord{
NumTotal: 5,
NumProcessed: 5,
NumAdded: 4,
NumModified: 1,
},
},
{
name: "false positive new Go URLs",
curCVEs: []*store.CVE4Record{
// FalsePositive; no change
modify(rs[0], &store.CVE4Record{
BlobHash: "x",
TriageState: store.TriageStateFalsePositive,
ReferenceURLs: []string{
"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00477.html",
},
}),
},
want: []*store.CVE4Record{
modify(rs[0], &store.CVE4Record{
ReferenceURLs: []string{
"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00477.html",
},
History: []*store.CVE4RecordSnapshot{{
CommitHash: commitHash,
CVEState: "PUBLIC",
TriageState: "FalsePositive",
}},
}),
rs[1], rs[2], rs[3], rs[4],
},
wantUpdate: &store.CommitUpdateRecord{
NumTotal: 5,
NumProcessed: 5,
NumAdded: 4,
NumModified: 1,
},
},
{
name: "alias already created",
curCVEs: []*store.CVE4Record{rs[0],
rs[1], rs[2], rs[3]},
curGHSAs: []*store.LegacyGHSARecord{
{
GHSA: &ghsa.SecurityAdvisory{
ID: "GHSA-xhmf-mmv2-4hhx",
},
TriageState: store.TriageStateIssueCreated,
},
},
want: []*store.CVE4Record{
rs[0],
rs[1], rs[2], rs[3], modify(rs[4], &store.CVE4Record{
TriageState: store.TriageStateAlias,
}),
},
wantUpdate: &store.CommitUpdateRecord{
NumTotal: 5,
NumProcessed: 5,
NumAdded: 1,
},
},
} {
t.Run(test.name, func(t *testing.T) {
mstore := store.NewMemStore()
createCVE4Records(t, mstore, test.curCVEs)
createLegacyGHSARecords(t, mstore, test.curGHSAs)
if err := newCVEUpdater(repo, commit, mstore, rc, needsIssue).update(ctx); err != nil {
t.Fatal(err)
}
got := mstore.CVE4Records()
want := map[string]*store.CVE4Record{}
for _, cr := range test.want {
want[cr.ID] = cr
}
if diff := cmp.Diff(want, got,
cmpopts.IgnoreFields(store.CVE4Record{}, "TriageStateReason"),
cmpopts.IgnoreFields(store.CVE4RecordSnapshot{}, "TriageStateReason")); diff != "" {
t.Errorf("mismatch (-want, +got):\n%s", diff)
}
gotUpdates, err := mstore.ListCommitUpdateRecords(ctx, -1)
if err != nil {
t.Fatal(err)
}
wantUpdates := []*store.CommitUpdateRecord{test.wantUpdate}
if diff := cmp.Diff(wantUpdates, gotUpdates,
cmpopts.IgnoreFields(store.CommitUpdateRecord{}, "ID",
"StartedAt", "EndedAt", "CommitHash", "CommitTime",
"UpdatedAt")); diff != "" {
t.Errorf("updates mismatch (-want, +got):\n%s", diff)
}
if len(gotUpdates) > 0 {
got := gotUpdates[0]
if got.StartedAt.IsZero() {
t.Error("CommitUpdateRecord.StartedAt is zero, want non-zero")
}
if got.EndedAt.IsZero() {
t.Error("CommitUpdateRecord.EndedAt is zero, want non-zero")
}
if got.Error != "" {
t.Errorf("CommitUpdateRecord.Error = %s, want no error", got.Error)
}
}
})
}
}
func TestDoUpdateError(t *testing.T) {
ctx := context.Background()
repo, commit, err := gitrepo.TxtarRepoAndHead(testRepoPath)
if err != nil {
t.Fatal(err)
}
rc, err := report.NewTestClient(nil)
if err != nil {
t.Fatal(err)
}
needsIssue := func(cve *cve4.CVE) (*triage.Result, error) { return nil, nil }
for _, test := range []struct {
name string
errOnRunTransaction, errOnSetCommitUpdate bool
wantErrs []error
// whether to expect a meaningful update record
wantValidUpdateRecord bool
}{
{
name: "transaction error",
errOnRunTransaction: true,
wantErrs: []error{transactionErr},
wantValidUpdateRecord: true,
},
{
name: "commit error",
errOnSetCommitUpdate: true,
wantErrs: []error{commitUpdateErr},
},
{
name: "transaction and commit error",
errOnRunTransaction: true,
errOnSetCommitUpdate: true,
wantErrs: []error{transactionErr, commitUpdateErr},
},
} {
t.Run(test.name, func(t *testing.T) {
mstore := newErrStore(test.errOnRunTransaction, test.errOnSetCommitUpdate)
err := newCVEUpdater(repo, commit, mstore, rc, needsIssue).update(ctx)
for _, wantErr := range test.wantErrs {
if !errors.Is(err, wantErr) {
t.Fatalf("newCVEUpdater: want err = %v, got %v", wantErr, err)
}
}
gotUpdates, err := mstore.ListCommitUpdateRecords(ctx, -1)
if err != nil {
t.Fatalf("ListCommitUpdateRecords = %s", err)
}
if len(gotUpdates) == 0 {
t.Fatalf("no update record created")
}
got := gotUpdates[0]
if got.StartedAt.IsZero() {
t.Error("CommitUpdateRecord.StartedAt is zero, want non-zero")
}
if test.wantValidUpdateRecord {
if got.EndedAt.IsZero() {
t.Error("CommitUpdateRecord.EndedAt is zero, want non-zero")
}
if got.Error == "" {
t.Error("CommitUpdateRecord.Error is empty, want an error")
}
}
})
}
}
type transactionErrStore struct {
*store.MemStore
errOnRunTransaction, errOnSetCommitUpdate bool
}
func newErrStore(errOnRunTransaction, errOnSetCommitUpdate bool) *transactionErrStore {
return &transactionErrStore{
MemStore: store.NewMemStore(),
errOnRunTransaction: errOnRunTransaction,
errOnSetCommitUpdate: errOnSetCommitUpdate,
}
}
var transactionErr = errors.New("transaction error occurred")
func (s *transactionErrStore) RunTransaction(ctx context.Context, f func(context.Context, store.Transaction) error) error {
if s.errOnRunTransaction {
return transactionErr
}
return s.MemStore.RunTransaction(ctx, f)
}
var commitUpdateErr = errors.New("commit update occurred")
func (s *transactionErrStore) SetCommitUpdateRecord(ctx context.Context, ur *store.CommitUpdateRecord) error {
if s.errOnSetCommitUpdate {
return commitUpdateErr
}
return s.MemStore.SetCommitUpdateRecord(ctx, ur)
}
func TestGroupFilesByDirectory(t *testing.T) {
for _, test := range []struct {
in []cvelistrepo.File
want [][]cvelistrepo.File
}{
{in: nil, want: nil},
{
in: []cvelistrepo.File{{DirPath: "a"}},
want: [][]cvelistrepo.File{{{DirPath: "a"}}},
},
{
in: []cvelistrepo.File{
{DirPath: "a", Filename: "f1"},
{DirPath: "a", Filename: "f2"},
},
want: [][]cvelistrepo.File{{
{DirPath: "a", Filename: "f1"},
{DirPath: "a", Filename: "f2"},
}},
},
{
in: []cvelistrepo.File{
{DirPath: "a", Filename: "f1"},
{DirPath: "a", Filename: "f2"},
{DirPath: "b", Filename: "f1"},
{DirPath: "c", Filename: "f1"},
{DirPath: "c", Filename: "f2"},
},
want: [][]cvelistrepo.File{
{
{DirPath: "a", Filename: "f1"},
{DirPath: "a", Filename: "f2"},
},
{
{DirPath: "b", Filename: "f1"},
},
{
{DirPath: "c", Filename: "f1"},
{DirPath: "c", Filename: "f2"},
},
},
},
} {
got, err := groupFilesByDirectory(test.in)
if err != nil {
t.Fatalf("%v: %v", test.in, err)
}
if diff := cmp.Diff(got, test.want, cmp.AllowUnexported(cvelistrepo.File{})); diff != "" {
t.Errorf("%v: (-want, +got)\n%s", test.in, diff)
}
}
_, err := groupFilesByDirectory([]cvelistrepo.File{{DirPath: "a"}, {DirPath: "b"}, {DirPath: "a"}})
if err == nil {
t.Error("got nil, want error")
}
}
func readCVE4(t *testing.T, commit *object.Commit, path string) (*cve4.CVE, string) {
cve, blobHash, err := ReadCVEAtPath(commit, path)
if err != nil {
t.Fatal(err)
}
return cve, blobHash
}
func createCVE4Records(t *testing.T, s store.Store, crs []*store.CVE4Record) {
err := s.RunTransaction(context.Background(), func(ctx context.Context, tx store.Transaction) error {
for _, cr := range crs {
copy := *cr
if err := tx.CreateRecord(&copy); err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
func createLegacyGHSARecords(t *testing.T, s store.Store, grs []*store.LegacyGHSARecord) {
err := s.RunTransaction(context.Background(), func(ctx context.Context, tx store.Transaction) error {
for _, gr := range grs {
copy := *gr
if err := tx.CreateRecord(&copy); err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
// headCommit returns the commit at the repo HEAD.
func headCommit(t *testing.T, repo *git.Repository) *object.Commit {
h, err := gitrepo.HeadHash(repo)
if err != nil {
t.Fatal(err)
}
commit, err := repo.CommitObject(h)
if err != nil {
t.Fatal(err)
}
return commit
}