blob: 8c47b7d1d4b2792276b714c05543be32c37d6a5c [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"
"flag"
"testing"
"time"
"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/cveutils"
"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/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.CVERecord) *store.CVERecord {
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 TestNewCVERecord(t *testing.T) {
// Check that NewCVERecord with a TriageState gives a valid CVERecord.
repo, err := gitrepo.ReadTxtarRepo(testRepoPath, time.Now())
if err != nil {
t.Fatal(err)
}
commit := headCommit(t, repo)
pathname := "2021/0xxx/CVE-2021-0001.json"
cve, bh := readCVE(t, repo, commit, pathname)
cr := store.NewCVERecord(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, err := gitrepo.ReadTxtarRepo(testRepoPath, time.Now())
if err != nil {
t.Fatal(err)
}
commit := headCommit(t, repo)
cf, err := pkgsite.CacheFile(t)
if err != nil {
t.Fatal(err)
}
pc, err := pkgsite.TestClient(t, *usePkgsite, cf)
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) (*cveutils.TriageResult, error) {
return cveutils.TriageCVE(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 := readCVE(t, repo, commit, p)
cves = append(cves, cve)
blobHashes = append(blobHashes, bh)
}
// Expected CVERecords after the above CVEs are added to an empty DB.
var rs []*store.CVERecord
for i := 0; i < len(cves); i++ {
r := store.NewCVERecord(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.CVERecord // current state of CVEs collection
curGHSAs []*store.GHSARecord // current state of GHSAs collection
want []*store.CVERecord // expected state of CVEs collection after update
}{
{
name: "empty",
curCVEs: nil,
want: rs,
},
{
name: "no change",
curCVEs: rs,
want: rs,
},
{
name: "pre-issue changes",
curCVEs: []*store.CVERecord{
// NoActionNeeded -> NeedsIssue
modify(rs[0], &store.CVERecord{
BlobHash: "x", // if we don't use a different blob hash, no update will happen
TriageState: store.TriageStateNoActionNeeded,
}),
// NeedsIssue -> NoActionNeeded
modify(rs[1], &store.CVERecord{
BlobHash: "x",
TriageState: store.TriageStateNeedsIssue,
Module: "something",
CVE: cves[1],
}),
// NoActionNeeded, triage state stays the same but other fields change.
modify(rs[2], &store.CVERecord{
TriageState: store.TriageStateNoActionNeeded,
}),
},
want: []*store.CVERecord{
modify(rs[0], &store.CVERecord{
History: []*store.CVERecordSnapshot{{
CommitHash: commitHash,
CVEState: cve4.StatePublic,
TriageState: store.TriageStateNoActionNeeded,
}},
}),
modify(rs[1], &store.CVERecord{
Module: clearString,
CVE: clearCVE,
History: []*store.CVERecordSnapshot{{
CommitHash: commitHash,
CVEState: cve4.StateReserved,
TriageState: store.TriageStateNeedsIssue,
}},
}),
rs[2],
rs[3],
rs[4],
},
},
{
name: "post-issue changes",
curCVEs: []*store.CVERecord{
// IssueCreated -> Updated
modify(rs[0], &store.CVERecord{
BlobHash: "x",
TriageState: store.TriageStateIssueCreated,
}),
modify(rs[1], &store.CVERecord{
BlobHash: "x",
TriageState: store.TriageStateUpdatedSinceIssueCreation,
}),
},
want: []*store.CVERecord{
modify(rs[0], &store.CVERecord{
TriageState: store.TriageStateUpdatedSinceIssueCreation,
TriageStateReason: `CVE changed; affected module = "golang.org/x/mod"`,
History: []*store.CVERecordSnapshot{{
CommitHash: commitHash,
CVEState: cve4.StatePublic,
TriageState: store.TriageStateIssueCreated,
}},
}),
modify(rs[1], &store.CVERecord{
TriageState: store.TriageStateUpdatedSinceIssueCreation,
TriageStateReason: `CVE changed; affected module = ""`,
}),
rs[2],
rs[3],
rs[4],
},
},
{
name: "false positive no Go URLs",
curCVEs: []*store.CVERecord{
// FalsePositive; no change
modify(rs[0], &store.CVERecord{
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.CVERecord{
modify(rs[0], &store.CVERecord{
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],
},
},
{
name: "false positive new Go URLs",
curCVEs: []*store.CVERecord{
// FalsePositive; no change
modify(rs[0], &store.CVERecord{
BlobHash: "x",
TriageState: store.TriageStateFalsePositive,
ReferenceURLs: []string{
"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00477.html",
},
}),
},
want: []*store.CVERecord{
modify(rs[0], &store.CVERecord{
ReferenceURLs: []string{
"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00477.html",
},
History: []*store.CVERecordSnapshot{{
CommitHash: commitHash,
CVEState: "PUBLIC",
TriageState: "FalsePositive",
}},
}),
rs[1], rs[2], rs[3], rs[4],
},
},
{
name: "alias already created",
curCVEs: []*store.CVERecord{rs[0],
rs[1], rs[2], rs[3]},
curGHSAs: []*store.GHSARecord{
{
GHSA: &ghsa.SecurityAdvisory{
ID: "GHSA-xhmf-mmv2-4hhx",
},
TriageState: store.TriageStateIssueCreated,
},
},
want: []*store.CVERecord{
rs[0],
rs[1], rs[2], rs[3], modify(rs[4], &store.CVERecord{
TriageState: store.TriageStateAlias,
}),
},
},
} {
t.Run(test.name, func(t *testing.T) {
mstore := store.NewMemStore()
createCVERecords(t, mstore, test.curCVEs)
createGHSARecords(t, mstore, test.curGHSAs)
if _, err := newCVEUpdater(repo, commit, mstore, rc, needsIssue).update(ctx); err != nil {
t.Fatal(err)
}
got := mstore.CVERecords()
want := map[string]*store.CVERecord{}
for _, cr := range test.want {
want[cr.ID] = cr
}
if diff := cmp.Diff(want, got,
cmpopts.IgnoreFields(store.CVERecord{}, "TriageStateReason"),
cmpopts.IgnoreFields(store.CVERecordSnapshot{}, "TriageStateReason")); diff != "" {
t.Errorf("mismatch (-want, +got):\n%s", diff)
}
})
}
}
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 readCVE(t *testing.T, repo *git.Repository, commit *object.Commit, path string) (*cve4.CVE, string) {
cve, blobHash, err := ReadCVEAtPath(commit, path)
if err != nil {
t.Fatal(err)
}
return cve, blobHash
}
func createCVERecords(t *testing.T, s store.Store, crs []*store.CVERecord) {
err := s.RunTransaction(context.Background(), func(ctx context.Context, tx store.Transaction) error {
for _, cr := range crs {
copy := *cr
if err := tx.CreateCVERecord(&copy); err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
func createGHSARecords(t *testing.T, s store.Store, grs []*store.GHSARecord) {
err := s.RunTransaction(context.Background(), func(ctx context.Context, tx store.Transaction) error {
for _, gr := range grs {
copy := *gr
if err := tx.CreateGHSARecord(&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
}