blob: 5a47e0f1dceb3179adb2db264b40159fc71b2309 [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/cve4"
"golang.org/x/vulndb/internal/ghsa"
"golang.org/x/vulndb/internal/report"
)
// A CVE4Record contains information about a v4 CVE.
type CVE4Record 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 *cve4.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 CVE4Record,
// from most to least recent.
History []*CVE4RecordSnapshot
}
func (r *CVE4Record) GetID() string { return r.ID }
func (r *CVE4Record) GetUnit() string { return r.Module }
func (r *CVE4Record) GetDescription() string {
if r.CVE == nil || len(r.CVE.Description.Data) == 0 {
return ""
}
return r.CVE.Description.Data[0].Value
}
func (r *CVE4Record) GetSource() report.Source { return r.CVE }
func (r *CVE4Record) GetIssueReference() string { return r.IssueReference }
func (r *CVE4Record) GetIssueCreatedAt() time.Time { return r.IssueCreatedAt }
func (r *CVE4Record) GetTriageState() TriageState { return r.TriageState }
// Validate returns an error if the CVE4Record is not valid.
func (r *CVE4Record) 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 or GHSA.
// 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 or GHSA (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"
// This vulnerability has already been handled under an alias (i.e., a CVE
// or GHSA that refers to the same vulnerability).
TriageStateAlias TriageState = "Alias"
// 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, TriageStateAlias, TriageStateUpdatedSinceIssueCreation, TriageStateFalsePositive, TriageStateHasVuln:
return nil
default:
return fmt.Errorf("bad TriageState %q", s)
}
}
// NewCVE4Record creates a CVE4Record from a CVE, its path and its blob hash.
func NewCVE4Record(cve *cve4.CVE, path, blobHash string, commit *object.Commit) *CVE4Record {
return &CVE4Record{
ID: cve.ID,
CVEState: cve.State,
Path: path,
BlobHash: blobHash,
CommitHash: commit.Hash.String(),
CommitTime: commit.Committer.When.In(time.UTC),
}
}
// CVE4RecordSnapshot holds a previous state of a CVE4Record.
// The fields mean the same as those of CVE4Record.
type CVE4RecordSnapshot struct {
CommitHash string
CVEState string
TriageState TriageState
TriageStateReason string
}
func (r *CVE4Record) Snapshot() *CVE4RecordSnapshot {
return &CVE4RecordSnapshot{
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 LegacyGHSARecord holds information about a GitHub security advisory.
type LegacyGHSARecord 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 *LegacyGHSARecord) GetID() string { return r.GHSA.ID }
func (r *LegacyGHSARecord) GetUnit() string { return r.GHSA.Vulns[0].Package }
func (r *LegacyGHSARecord) GetDescription() string { return r.GHSA.Description }
func (r *LegacyGHSARecord) GetSource() report.Source { return r.GHSA }
func (r *LegacyGHSARecord) GetIssueReference() string { return r.IssueReference }
func (r *LegacyGHSARecord) GetIssueCreatedAt() time.Time { return r.IssueCreatedAt }
func (r *LegacyGHSARecord) GetTriageState() TriageState { return r.TriageState }
func (r *LegacyGHSARecord) Validate() error { 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)
// GetRecord returns the Record with the given id. If not found, it returns (nil, nil).
GetRecord(ctx context.Context, id string) (Record, error)
// ListCVE4RecordsWithTriageState returns all CVE4Records with the given triage state,
// ordered by ID.
ListCVE4RecordsWithTriageState(ctx context.Context, ts TriageState) ([]*CVE4Record, 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
// 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 {
// CreateRecord creates a new record.
// It is an error if one with the same ID already exists.
CreateRecord(Record) error
// SetRecord sets the record in the database.
// It is an error if no such record exists.
SetRecord(Record) error
// GetRecord returns a single record by ID.
// If not found, it returns (nil, nil).
GetRecord(id string) (Record, error)
// GetRecords retrieves records for all CVE IDs between startID and
// endID, inclusive.
GetCVE4Records(startID, endID string) ([]*CVE4Record, error)
// GetLegacyGHSARecords returns all the GHSARecords in the database.
GetLegacyGHSARecords() ([]*LegacyGHSARecord, error)
}