internal/worker/store: support GitHub security advisories
Add GHSARecord, for recording information about GitHub service
advisories.
Add methods to create, modify and retrieve GHSARecords.
Change-Id: I2f8e52d9fea6ec3888ff48b7ae2c17cc5b77a8c6
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/384095
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/internal/worker/store/fire_store.go b/internal/worker/store/fire_store.go
index ad49a55..9921b6a 100644
--- a/internal/worker/store/fire_store.go
+++ b/internal/worker/store/fire_store.go
@@ -31,6 +31,7 @@
// - CVEs for CVERecords
// - CommitUpdates for CommitUpdateRecords
// - DirHashes for directory hashes
+// - GHSAs for GHSARecords.
type FireStore struct {
namespace string
client *firestore.Client
@@ -42,6 +43,7 @@
updateCollection = "Updates"
cveCollection = "CVEs"
dirHashCollection = "DirHashes"
+ ghsaCollection = "GHSAs"
)
// NewFireStore creates a new FireStore, backed by a client to Firestore. Since
@@ -205,6 +207,11 @@
return fs.nsDoc.Collection(cveCollection).Doc(id)
}
+// ghsaRecordRef returns a DocumentRef to the GHSARecord with id.
+func (fs *FireStore) ghsaRecordRef(id string) *firestore.DocumentRef {
+ return fs.nsDoc.Collection(ghsaCollection).Doc(id)
+}
+
// fsTransaction implements Transaction
type fsTransaction struct {
s *FireStore
@@ -259,6 +266,46 @@
return crs, nil
}
+// CreateGHSARecord implements Transaction.CreateGHSARecord.
+func (tx *fsTransaction) CreateGHSARecord(r *GHSARecord) (err error) {
+ defer derrors.Wrap(&err, "FireStore.CreateGHSARecord(%s)", r.GHSA.ID)
+
+ return tx.t.Create(tx.s.ghsaRecordRef(r.GHSA.ID), r)
+}
+
+// SetGHSARecord implements Transaction.SetGHSARecord.
+func (tx *fsTransaction) SetGHSARecord(r *GHSARecord) (err error) {
+ defer derrors.Wrap(&err, "SetGHSARecord(%s)", r.GHSA.ID)
+
+ return tx.t.Set(tx.s.ghsaRecordRef(r.GHSA.ID), r)
+}
+
+// GetGHSARecords implements Transaction.GetGHSARecords.
+func (tx *fsTransaction) GetGHSARecords() (_ []*GHSARecord, err error) {
+ defer derrors.Wrap(&err, "GetGHSARecords()")
+
+ q := tx.s.nsDoc.Collection(ghsaCollection).
+ OrderBy(firestore.DocumentID, firestore.Asc)
+ iter := tx.t.Documents(q)
+ docsnaps, err := iter.GetAll()
+ if err != nil {
+ return nil, err
+ }
+ return docsnapsToGHSARecords(docsnaps)
+}
+
+func docsnapsToGHSARecords(docsnaps []*firestore.DocumentSnapshot) ([]*GHSARecord, error) {
+ var grs []*GHSARecord
+ for _, ds := range docsnaps {
+ var gr GHSARecord
+ if err := ds.DataTo(&gr); err != nil {
+ return nil, err
+ }
+ grs = append(grs, &gr)
+ }
+ return grs, nil
+}
+
// Clear removes all documents in the namespace.
func (s *FireStore) Clear(ctx context.Context) (err error) {
defer derrors.Wrap(&err, "Clear")
diff --git a/internal/worker/store/mem_store.go b/internal/worker/store/mem_store.go
index 89c819a..a52f1b0 100644
--- a/internal/worker/store/mem_store.go
+++ b/internal/worker/store/mem_store.go
@@ -20,6 +20,7 @@
cveRecords map[string]*CVERecord
updateRecords map[string]*CommitUpdateRecord
dirHashes map[string]string
+ ghsaRecords map[string]*GHSARecord
}
// NewMemStore creates a new, empty MemStore.
@@ -34,6 +35,7 @@
ms.cveRecords = map[string]*CVERecord{}
ms.updateRecords = map[string]*CommitUpdateRecord{}
ms.dirHashes = map[string]string{}
+ ms.ghsaRecords = map[string]*GHSARecord{}
return nil
}
@@ -158,3 +160,30 @@
})
return crs, nil
}
+
+// CreateGHSARecord implements Transaction.CreateGHSARecord.
+func (tx *memTransaction) CreateGHSARecord(r *GHSARecord) error {
+ if _, ok := tx.ms.ghsaRecords[r.GHSA.ID]; ok {
+ return fmt.Errorf("GHSARecord %s already exists", r.GHSA.ID)
+ }
+ tx.ms.ghsaRecords[r.GHSA.ID] = r
+ return nil
+}
+
+// SetGHSARecord implements Transaction.SetGHSARecord.n
+func (tx *memTransaction) SetGHSARecord(r *GHSARecord) error {
+ if _, ok := tx.ms.ghsaRecords[r.GHSA.ID]; !ok {
+ return fmt.Errorf("GHSARecord %s does not exist", r.GHSA.ID)
+ }
+ tx.ms.ghsaRecords[r.GHSA.ID] = r
+ return nil
+}
+
+// GetGHSARecords returns all the GHSARecords in the database.
+func (tx *memTransaction) GetGHSARecords() ([]*GHSARecord, error) {
+ var recs []*GHSARecord
+ for _, r := range tx.ms.ghsaRecords {
+ recs = append(recs, r)
+ }
+ return recs, nil
+}
diff --git a/internal/worker/store/store.go b/internal/worker/store/store.go
index 6d1645b..7d05d4a 100644
--- a/internal/worker/store/store.go
+++ b/internal/worker/store/store.go
@@ -13,6 +13,7 @@
"github.com/go-git/go-git/v5/plumbing/object"
"golang.org/x/vulndb/internal/cveschema"
+ "golang.org/x/vulndb/internal/ghsa"
)
// A CVERecord contains information about a CVE.
@@ -170,6 +171,16 @@
UpdatedAt time.Time `firestore:",serverTimestamp"`
}
+// A GHSARecord holds information about a GitHub security advisory.
+type GHSARecord struct {
+ // GHSA is the advisory.
+ GHSA *ghsa.SecurityAdvisory
+ // TriageState is the state of our triage processing on the CVE.
+ TriageState TriageState
+ // TriageStateReason is an explanation of TriageState.
+ TriageStateReason string
+}
+
// A Store is a storage system for the CVE database.
type Store interface {
// CreateCommitUpdateRecord creates a new CommitUpdateRecord. It should be called at the start
@@ -216,4 +227,15 @@
// GetCVERecords retrieves CVERecords for all CVE IDs between startID and
// endID, inclusive.
GetCVERecords(startID, endID string) ([]*CVERecord, error)
+
+ // CreateGHSARecord creates a new GHSARecord. It is an error if one with the same ID
+ // already exists.
+ CreateGHSARecord(*GHSARecord) error
+
+ // SetGHSARecord sets the GHSA record in the database. It is
+ // an error if no such record exists.
+ SetGHSARecord(*GHSARecord) error
+
+ // GetGHSARecords returns all the GHSARecords in the database.
+ GetGHSARecords() ([]*GHSARecord, error)
}
diff --git a/internal/worker/store/store_test.go b/internal/worker/store/store_test.go
index faf9ff9..35dd90b 100644
--- a/internal/worker/store/store_test.go
+++ b/internal/worker/store/store_test.go
@@ -9,12 +9,14 @@
import (
"context"
+ "sort"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/vulndb/internal/cveschema"
+ "golang.org/x/vulndb/internal/ghsa"
)
func must(t *testing.T, err error) {
@@ -34,6 +36,9 @@
t.Run("DirHashes", func(t *testing.T) {
testDirHashes(t, s)
})
+ t.Run("GHSAs", func(t *testing.T) {
+ testGHSAs(t, s)
+ })
}
func testUpdates(t *testing.T, s Store) {
@@ -194,6 +199,57 @@
}
}
+func testGHSAs(t *testing.T, s Store) {
+ ctx := context.Background()
+ // Create two records.
+ gs := []*GHSARecord{
+ {
+ GHSA: &ghsa.SecurityAdvisory{ID: "g1", Summary: "one"},
+ TriageState: TriageStateNeedsIssue,
+ },
+ {
+ GHSA: &ghsa.SecurityAdvisory{ID: "g2", Summary: "two"},
+ TriageState: TriageStateNeedsIssue,
+ },
+ }
+ err := s.RunTransaction(ctx, func(ctx context.Context, tx Transaction) error {
+ for _, g := range gs {
+ if err := tx.CreateGHSARecord(g); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Modify one of them.
+ gs[1].TriageState = TriageStateIssueCreated
+ err = s.RunTransaction(ctx, func(ctx context.Context, tx Transaction) error {
+ return tx.SetGHSARecord(gs[1])
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Retrieve and compare.
+ var got []*GHSARecord
+ err = s.RunTransaction(ctx, func(ctx context.Context, tx Transaction) error {
+ var err error
+ got, err = tx.GetGHSARecords()
+ return err
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(got) != len(gs) {
+ t.Fatalf("got %d records, want %d", len(got), len(gs))
+ }
+ sort.Slice(got, func(i, j int) bool { return got[i].GHSA.ID < got[j].GHSA.ID })
+ if diff := cmp.Diff(gs, got); diff != "" {
+ t.Errorf("mismatch (-want, +got):\n%s", diff)
+ }
+}
+
func createCVERecords(t *testing.T, ctx context.Context, s Store, crs []*CVERecord) {
err := s.RunTransaction(ctx, func(ctx context.Context, tx Transaction) error {
for _, cr := range crs {