blob: 624b9291c6077fdd709795865c011c2f8a0112d6 [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 store supports permanent data storage for the vuln worker.
package store
import (
"context"
"errors"
"fmt"
"time"
"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.
type CVERecord struct {
// ID is the CVE ID, which is the same as the filename base. E.g. "CVE-2020-0034".
ID string
// Path is the path to the CVE file in the repo.
Path string
// Blobhash is the hash of the CVE's blob in repo, for quick change detection.
BlobHash string
// CommitHash is the commit of the cvelist repo from which this information came.
CommitHash string
// CommitTime is the time of the above commit.
// If zero, it has not been populated.
CommitTime time.Time
// CVEState is the value of the metadata.STATE field.
CVEState string
// TriageState is the state of our triage processing on the CVE.
TriageState TriageState
// TriageStateReason is an explanation of TriageState.
TriageStateReason string
// Module is the Go module path that might be affected.
Module string
// Package is the Go package path that might be affected.
Package string
// CVE is a copy of the CVE, for the NeedsIssue triage state.
CVE *cveschema.CVE
// ReferenceURLs is a list of the URLs in the CVE references,
// for the FalsePositive triage state.
ReferenceURLs []string
// IssueReference is a reference to the GitHub issue that was filed.
// E.g. golang/vulndb#12345.
// Set only after a GitHub issue has been successfully created.
IssueReference string
// IssueCreatedAt is the time when the issue was created.
// Set only after a GitHub issue has been successfully created.
IssueCreatedAt time.Time
// History holds previous states of a CVERecord,
// from most to least recent.
History []*CVERecordSnapshot
}
func (r *CVERecord) GetID() string { return r.ID }
func (r *CVERecord) GetPrettyID() string { return r.ID }
func (r *CVERecord) GetUnit() string { return r.Module }
func (r *CVERecord) GetIssueReference() string { return r.IssueReference }
func (r *CVERecord) GetIssueCreatedAt() time.Time { return r.IssueCreatedAt }
// Validate returns an error if the CVERecord is not valid.
func (r *CVERecord) Validate() error {
if r.ID == "" {
return errors.New("need ID")
}
if r.Path == "" {
return errors.New("need Path")
}
if r.BlobHash == "" {
return errors.New("need BlobHash")
}
if r.CommitHash == "" {
return errors.New("need CommitHash")
}
if r.CommitTime.IsZero() {
return errors.New("need CommitTime")
}
return r.TriageState.Validate()
}
// TriageState is the state of our work on the CVE.
// It is implemented as a string rather than an int so that stored values are
// immune to renumbering.
type TriageState string
const (
// No action is needed on the CVE (perhaps because it is rejected, reserved or invalid).
TriageStateNoActionNeeded TriageState = "NoActionNeeded"
// The CVE needs to have an issue created.
TriageStateNeedsIssue TriageState = "NeedsIssue"
// An issue has been created in the issue tracker.
// The IssueReference and IssueCreatedAt fields have more information.
TriageStateIssueCreated TriageState = "IssueCreated"
// The CVE state was changed after the CVE was created.
TriageStateUpdatedSinceIssueCreation TriageState = "UpdatedSinceIssueCreation"
// Although the triager might think this CVE is relevant to Go, it is not.
TriageStateFalsePositive TriageState = "FalsePositive"
// There is already an entry in the Go vuln DB that covers this CVE.
TriageStateHasVuln TriageState = "HasVuln"
)
// Validate returns an error if the TriageState is not one of the above values.
func (s TriageState) Validate() error {
switch s {
case TriageStateNoActionNeeded, TriageStateNeedsIssue, TriageStateIssueCreated, TriageStateUpdatedSinceIssueCreation, TriageStateFalsePositive, TriageStateHasVuln:
return nil
default:
return fmt.Errorf("bad TriageState %q", s)
}
}
// NewCVERecord creates a CVERecord from a CVE, its path and its blob hash.
func NewCVERecord(cve *cveschema.CVE, path, blobHash string, commit *object.Commit) *CVERecord {
return &CVERecord{
ID: cve.ID,
CVEState: cve.State,
Path: path,
BlobHash: blobHash,
CommitHash: commit.Hash.String(),
CommitTime: commit.Committer.When.In(time.UTC),
}
}
// CVERecordSnapshot holds a previous state of a CVERecord.
// The fields mean the same as those of CVERecord.
type CVERecordSnapshot struct {
CommitHash string
CVEState string
TriageState TriageState
TriageStateReason string
}
func (r *CVERecord) Snapshot() *CVERecordSnapshot {
return &CVERecordSnapshot{
CommitHash: r.CommitHash,
CVEState: r.CVEState,
TriageState: r.TriageState,
TriageStateReason: r.TriageStateReason,
}
}
// A CommitUpdateRecord describes a single update operation, which reconciles
// a commit in the CVE list repo with the DB state.
type CommitUpdateRecord struct {
// The ID of this record in the DB. Needed to modify the record.
ID string
// When the update started and completed. If EndedAt is zero,
// the update is in progress (or it crashed).
StartedAt, EndedAt time.Time
// The repo commit hash that this update is working on.
CommitHash string
// The time the commit occurred.
CommitTime time.Time
// The total number of CVEs being processed in this update.
NumTotal int
// The number currently processed. When this equals NumTotal, the
// update is done.
NumProcessed int
// The number of CVEs added to the DB.
NumAdded int
// The number of CVEs modified.
NumModified int
// The error that stopped the update.
Error string
// The last time this record was updated.
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
// IssueReference is a reference to the GitHub issue that was filed.
// E.g. golang/vulndb#12345.
// Set only after a GitHub issue has been successfully created.
IssueReference string
// IssueCreatedAt is the time when the issue was created.
// Set only after a GitHub issue has been successfully created.
IssueCreatedAt time.Time
}
func (r *GHSARecord) GetID() string { return r.GHSA.ID }
func (r *GHSARecord) GetUnit() string { return r.GHSA.Vulns[0].Package }
func (r *GHSARecord) GetIssueReference() string { return r.IssueReference }
func (r *GHSARecord) GetIssueCreatedAt() time.Time { return r.IssueCreatedAt }
func (r *GHSARecord) GetPrettyID() string { return r.GHSA.PrettyID() }
// A ModuleScanRecord holds information about a vulnerability scan of a module.
type ModuleScanRecord struct {
Path string
Version string
DBTime time.Time // last-modified time of the vuln DB
Error string // if non-empty, error while scanning
VulnIDs []string
FinishedAt time.Time // when the scan completed (successfully or not)
}
// Validate returns an error if the ModuleScanRecord is not valid.
func (r *ModuleScanRecord) Validate() error {
if r.Path == "" {
return errors.New("need Path")
}
if r.Version == "" {
return errors.New("need Version")
}
if r.DBTime.IsZero() {
return errors.New("need DBTime")
}
if r.FinishedAt.IsZero() {
return errors.New("need FinishedAt")
}
return nil
}
// 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
// of an update. On successful return, the CommitUpdateRecord's ID field will be
// set to a new, unique ID.
CreateCommitUpdateRecord(context.Context, *CommitUpdateRecord) error
// SetCommitUpdateRecord modifies the CommitUpdateRecord. Use the same record passed to
// CreateCommitUpdateRecord, because it will have the correct ID.
SetCommitUpdateRecord(context.Context, *CommitUpdateRecord) error
// ListCommitUpdateRecords returns some of the CommitUpdateRecords in the store, from most to
// least recent.
ListCommitUpdateRecords(ctx context.Context, limit int) ([]*CommitUpdateRecord, error)
// GetCVERecord returns the CVERecord with the given id. If not found, it returns (nil, nil).
GetCVERecord(ctx context.Context, id string) (*CVERecord, error)
// ListCVERecordsWithTriageState returns all CVERecords with the given triage state,
// ordered by ID.
ListCVERecordsWithTriageState(ctx context.Context, ts TriageState) ([]*CVERecord, error)
// GetDirectoryHash returns the hash for the tree object corresponding to dir.
// If dir isn't found, it succeeds with the empty string.
GetDirectoryHash(ctx context.Context, dir string) (string, error)
// SetDirectoryHash sets the hash for the given directory.
SetDirectoryHash(ctx context.Context, dir, hash string) error
// CreateModuleScanRecord adds a ModuleScanRecord to the DB.
CreateModuleScanRecord(context.Context, *ModuleScanRecord) error
// GetModuleScanRecord returns the most recent ModuleScanRecord matching the
// given module path, version and DB time. If not found, it returns (nil,
// nil).
GetModuleScanRecord(ctx context.Context, path, version string, dbTime time.Time) (*ModuleScanRecord, error)
// ListModuleScanRecords returns some of the ModuleScanRecords in the store
// from most to least recent. If limit is zero, all records are returned.
ListModuleScanRecords(ctx context.Context, limit int) ([]*ModuleScanRecord, error)
// RunTransaction runs the function in a transaction.
RunTransaction(context.Context, func(context.Context, Transaction) error) error
}
// Transaction supports store operations that run inside a transaction.
type Transaction interface {
// CreateCVERecord creates a new CVERecord. It is an error if one with the same ID
// already exists.
CreateCVERecord(*CVERecord) error
// SetCVERecord sets the CVE record in the database. It is
// an error if no such record exists.
SetCVERecord(r *CVERecord) error
// 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
// GetGHSARecord returns a single GHSARecord by ID.
// If not found, it returns (nil, nil).
GetGHSARecord(id string) (*GHSARecord, error)
// GetGHSARecords returns all the GHSARecords in the database.
GetGHSARecords() ([]*GHSARecord, error)
}