Port grype-db to grype (#587)

* port grype-db to grype

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* migrate vulnerability provider implementation to db package

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* upgrade path import validations

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix linting issues

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2022-01-12 10:03:22 -05:00 committed by GitHub
parent 24ef03efc4
commit 2647cd0d9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 6088 additions and 561 deletions

View file

@ -28,7 +28,7 @@ import (
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
grypeDb "github.com/anchore/grype-db/pkg/db/v3"
grypeDb "github.com/anchore/grype/grype/db/v3"
)
var persistentOpts = config.CliOnlyOptions{}

View file

@ -3,7 +3,8 @@ package cmd
import (
"testing"
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
"github.com/anchore/grype/grype/db"
grypeDB "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
@ -83,7 +84,7 @@ func TestAboveAllowableSeverity(t *testing.T) {
},
}
metadataProvider := vulnerability.NewMetadataStoreProvider(newMockStore())
metadataProvider := db.NewVulnerabilityMetadataProvider(newMockStore())
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {

8
go.mod
View file

@ -5,11 +5,12 @@ go 1.16
require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/adrg/xdg v0.2.1
github.com/alicebob/sqlittle v1.4.0
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4
github.com/anchore/grype-db v0.0.0-20211207213615-1bcbb779ee96
github.com/anchore/stereoscope v0.0.0-20220110181730-c91cf94a3718
github.com/anchore/syft v0.35.1
github.com/aws/aws-sdk-go v1.31.6 // indirect
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v20.10.11+incompatible
github.com/dustin/go-humanize v1.0.0
@ -19,18 +20,23 @@ require (
github.com/google/go-cmp v0.5.6
github.com/google/uuid v1.2.0
github.com/gookit/color v1.4.2
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.5.9
github.com/hashicorp/go-multierror v1.1.0
github.com/jinzhu/copier v0.3.2
github.com/jinzhu/gorm v1.9.14
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d
github.com/lib/pq v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/olekukonko/tablewriter v0.0.4
github.com/pkg/profile v1.6.0
github.com/scylladb/go-set v1.0.2
github.com/sergi/go-diff v1.1.0
github.com/sirupsen/logrus v1.8.1
github.com/smartystreets/assertions v1.0.0 // indirect
github.com/spf13/afero v1.6.0
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5

442
go.sum

File diff suppressed because it is too large Load diff

View file

@ -9,9 +9,8 @@ import (
"path"
"strconv"
"github.com/anchore/grype-db/pkg/curation"
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
"github.com/anchore/grype-db/pkg/db/v3/reader"
grypeDB "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/db/v3/reader"
"github.com/anchore/grype/grype/event"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/bus"
@ -80,7 +79,7 @@ func (c *Curator) GetStore() (*reader.Reader, error) {
}
func (c *Curator) Status() Status {
metadata, err := curation.NewMetadataFromDir(c.fs, c.dbDir)
metadata, err := NewMetadataFromDir(c.fs, c.dbDir)
if err != nil {
return Status{
Err: fmt.Errorf("failed to parse database metadata (%s): %w", c.dbDir, err),
@ -155,7 +154,7 @@ func (c *Curator) Update() (bool, error) {
// IsUpdateAvailable indicates if there is a new update available as a boolean, and returns the latest listing information
// available for this schema.
func (c *Curator) IsUpdateAvailable() (bool, *curation.ListingEntry, error) {
func (c *Curator) IsUpdateAvailable() (bool, *ListingEntry, error) {
log.Debugf("checking for available database updates")
listing, err := c.ListingFromURL()
@ -170,7 +169,7 @@ func (c *Curator) IsUpdateAvailable() (bool, *curation.ListingEntry, error) {
log.Debugf("found database update candidate: %s", updateEntry)
// compare created data to current db date
current, err := curation.NewMetadataFromDir(c.fs, c.dbDir)
current, err := NewMetadataFromDir(c.fs, c.dbDir)
if err != nil {
return false, nil, fmt.Errorf("current metadata corrupt: %w", err)
}
@ -185,7 +184,7 @@ func (c *Curator) IsUpdateAvailable() (bool, *curation.ListingEntry, error) {
}
// UpdateTo updates the existing DB with the specific other version provided from a listing entry.
func (c *Curator) UpdateTo(listing *curation.ListingEntry, downloadProgress, importProgress *progress.Manual, stage *progress.Stage) error {
func (c *Curator) UpdateTo(listing *ListingEntry, downloadProgress, importProgress *progress.Manual, stage *progress.Stage) error {
stage.Current = "downloading"
// note: the temp directory is persisted upon download/validation/activation failure to allow for investigation
tempDir, err := c.download(listing, downloadProgress)
@ -253,7 +252,7 @@ func (c *Curator) ImportFrom(dbArchivePath string) error {
return c.fs.RemoveAll(tempDir)
}
func (c *Curator) download(listing *curation.ListingEntry, downloadProgress *progress.Manual) (string, error) {
func (c *Curator) download(listing *ListingEntry, downloadProgress *progress.Manual) (string, error) {
tempDir, err := os.MkdirTemp("", "grype-scratch")
if err != nil {
return "", fmt.Errorf("unable to create db temp dir: %w", err)
@ -279,7 +278,7 @@ func (c *Curator) download(listing *curation.ListingEntry, downloadProgress *pro
func (c *Curator) validate(dbDirPath string) error {
// check that the disk checksum still matches the db payload
metadata, err := curation.NewMetadataFromDir(c.fs, dbDirPath)
metadata, err := NewMetadataFromDir(c.fs, dbDirPath)
if err != nil {
return fmt.Errorf("failed to parse database metadata (%s): %w", dbDirPath, err)
}
@ -329,10 +328,10 @@ func (c *Curator) activate(dbDirPath string) error {
}
// ListingFromURL loads a Listing from a URL.
func (c Curator) ListingFromURL() (curation.Listing, error) {
func (c Curator) ListingFromURL() (Listing, error) {
tempFile, err := afero.TempFile(c.fs, "", "grype-db-listing")
if err != nil {
return curation.Listing{}, fmt.Errorf("unable to create listing temp file: %w", err)
return Listing{}, fmt.Errorf("unable to create listing temp file: %w", err)
}
defer func() {
err := c.fs.RemoveAll(tempFile.Name())
@ -344,13 +343,13 @@ func (c Curator) ListingFromURL() (curation.Listing, error) {
// download the listing file
err = c.downloader.GetFile(tempFile.Name(), c.listingURL)
if err != nil {
return curation.Listing{}, fmt.Errorf("unable to download listing: %w", err)
return Listing{}, fmt.Errorf("unable to download listing: %w", err)
}
// parse the listing file
listing, err := curation.NewListingFromFile(c.fs, tempFile.Name())
listing, err := NewListingFromFile(c.fs, tempFile.Name())
if err != nil {
return curation.Listing{}, err
return Listing{}, err
}
return listing, nil
}

View file

@ -15,7 +15,6 @@ import (
"testing"
"time"
"github.com/anchore/grype-db/pkg/curation"
"github.com/anchore/grype/internal"
"github.com/anchore/grype/internal/file"
"github.com/gookit/color"
@ -25,13 +24,6 @@ import (
"github.com/wagoodman/go-progress"
)
func mustUrl(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
return u
}
type testGetter struct {
file map[string]string
dir map[string]string
@ -181,13 +173,13 @@ func generateCertFixture(t *testing.T) string {
func TestCuratorDownload(t *testing.T) {
tests := []struct {
name string
entry *curation.ListingEntry
entry *ListingEntry
expectedURL string
err bool
}{
{
name: "download populates returned tempdir",
entry: &curation.ListingEntry{
entry: &ListingEntry{
Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
URL: mustUrl(url.Parse("http://a-url/payload.tar.gz")),
Checksum: "sha256:deadbeefcafe",

90
grype/db/listing.go Normal file
View file

@ -0,0 +1,90 @@
package db
import (
"encoding/json"
"fmt"
"io/ioutil"
"sort"
"github.com/spf13/afero"
)
const ListingFileName = "listing.json"
// Listing represents the json file which is served up and made available for applications to download and
// consume one or more vulnerability db flat files.
type Listing struct {
Available map[int][]ListingEntry `json:"available"`
}
// NewListing creates a listing from one or more given ListingEntries.
func NewListing(entries ...ListingEntry) Listing {
listing := Listing{
Available: make(map[int][]ListingEntry),
}
for _, entry := range entries {
if _, ok := listing.Available[entry.Version]; !ok {
listing.Available[entry.Version] = make([]ListingEntry, 0)
}
listing.Available[entry.Version] = append(listing.Available[entry.Version], entry)
}
// sort each entry descending by date
for idx := range listing.Available {
listingEntries := listing.Available[idx]
sort.SliceStable(listingEntries, func(i, j int) bool {
return listingEntries[i].Built.After(listingEntries[j].Built)
})
}
return listing
}
// NewListingFromFile loads a Listing from a given filepath.
func NewListingFromFile(fs afero.Fs, path string) (Listing, error) {
f, err := fs.Open(path)
if err != nil {
return Listing{}, fmt.Errorf("unable to open DB listing path: %w", err)
}
defer f.Close()
var l Listing
err = json.NewDecoder(f).Decode(&l)
if err != nil {
return Listing{}, fmt.Errorf("unable to parse DB listing: %w", err)
}
// sort each entry descending by date
for idx := range l.Available {
listingEntries := l.Available[idx]
sort.SliceStable(listingEntries, func(i, j int) bool {
return listingEntries[i].Built.After(listingEntries[j].Built)
})
}
return l, nil
}
// BestUpdate returns the ListingEntry from a Listing that meets the given version constraints.
func (l *Listing) BestUpdate(targetSchema int) *ListingEntry {
if listingEntries, ok := l.Available[targetSchema]; ok {
if len(listingEntries) > 0 {
return &listingEntries[0]
}
}
return nil
}
// Write the current listing to the given filepath.
func (l Listing) Write(toPath string) error {
contents, err := json.MarshalIndent(&l, "", " ")
if err != nil {
return fmt.Errorf("failed to encode listing file: %w", err)
}
err = ioutil.WriteFile(toPath, contents, 0600)
if err != nil {
return fmt.Errorf("failed to write listing file: %w", err)
}
return nil
}

96
grype/db/listing_entry.go Normal file
View file

@ -0,0 +1,96 @@
package db
import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/url"
"path"
"path/filepath"
"time"
"github.com/anchore/grype/internal/file"
"github.com/spf13/afero"
)
// ListingEntry represents basic metadata about a database archive such as what is in the archive (built/version)
// as well as how to obtain and verify the archive (URL/checksum).
type ListingEntry struct {
Built time.Time // RFC 3339
Version int
URL *url.URL
Checksum string
}
// ListingEntryJSON is a helper struct for converting a ListingEntry into JSON (or parsing from JSON)
type ListingEntryJSON struct {
Built string `json:"built"`
Version int `json:"version"`
URL string `json:"url"`
Checksum string `json:"checksum"`
}
// NewListingEntryFromArchive creates a new ListingEntry based on the metadata from a database flat file.
func NewListingEntryFromArchive(fs afero.Fs, metadata Metadata, dbArchivePath string, baseURL *url.URL) (ListingEntry, error) {
checksum, err := file.HashFile(fs, dbArchivePath, sha256.New())
if err != nil {
return ListingEntry{}, fmt.Errorf("unable to find db archive checksum: %w", err)
}
dbArchiveName := filepath.Base(dbArchivePath)
fileURL, _ := url.Parse(baseURL.String())
fileURL.Path = path.Join(fileURL.Path, dbArchiveName)
return ListingEntry{
Built: metadata.Built,
Version: metadata.Version,
URL: fileURL,
Checksum: "sha256:" + checksum,
}, nil
}
// ToListingEntry converts a ListingEntryJSON to a ListingEntry.
func (l ListingEntryJSON) ToListingEntry() (ListingEntry, error) {
build, err := time.Parse(time.RFC3339, l.Built)
if err != nil {
return ListingEntry{}, fmt.Errorf("cannot convert built time (%s): %+v", l.Built, err)
}
u, err := url.Parse(l.URL)
if err != nil {
return ListingEntry{}, fmt.Errorf("cannot parse url (%s): %+v", l.URL, err)
}
return ListingEntry{
Built: build.UTC(),
Version: l.Version,
URL: u,
Checksum: l.Checksum,
}, nil
}
func (l *ListingEntry) UnmarshalJSON(data []byte) error {
var lej ListingEntryJSON
if err := json.Unmarshal(data, &lej); err != nil {
return err
}
le, err := lej.ToListingEntry()
if err != nil {
return err
}
*l = le
return nil
}
func (l *ListingEntry) MarshalJSON() ([]byte, error) {
return json.Marshal(&ListingEntryJSON{
Built: l.Built.Format(time.RFC3339),
Version: l.Version,
Checksum: l.Checksum,
URL: l.URL.String(),
})
}
func (l ListingEntry) String() string {
return fmt.Sprintf("Listing(url=%s)", l.URL)
}

161
grype/db/listing_test.go Normal file
View file

@ -0,0 +1,161 @@
package db
import (
"net/url"
"testing"
"time"
"github.com/anchore/go-version"
"github.com/go-test/deep"
"github.com/spf13/afero"
)
func mustUrl(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
return u
}
func mustConst(u version.Constraints, err error) version.Constraints {
if err != nil {
panic(err)
}
return u
}
func TestNewListingFromPath(t *testing.T) {
tests := []struct {
fixture string
expected Listing
err bool
}{
{
fixture: "test-fixtures/listing.json",
expected: Listing{
Available: map[int][]ListingEntry{
1: {
{
Built: time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC),
URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db-v0.2.0+2020-6-12.tar.gz")),
Version: 1,
Checksum: "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e",
},
},
2: {
{
Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db-v1.1.0+2020-6-13.tar.gz")),
Version: 2,
Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8",
},
},
},
},
},
{
fixture: "test-fixtures/listing-sorted.json",
expected: Listing{
Available: map[int][]ListingEntry{
1: {
{
Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz")),
Version: 1,
Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8",
},
{
Built: time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC),
URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz")),
Version: 1,
Checksum: "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e",
},
},
},
},
},
{
fixture: "test-fixtures/listing-unsorted.json",
expected: Listing{
Available: map[int][]ListingEntry{
1: {
{
Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz")),
Version: 1,
Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8",
},
{
Built: time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC),
URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz")),
Version: 1,
Checksum: "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e",
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
listing, err := NewListingFromFile(afero.NewOsFs(), test.fixture)
if err != nil && !test.err {
t.Fatalf("failed to get metadata: %+v", err)
} else if err == nil && test.err {
t.Fatalf("expected errer but got none")
}
for _, diff := range deep.Equal(listing, test.expected) {
t.Errorf("listing difference: %s", diff)
}
})
}
}
func TestListingBestUpdate(t *testing.T) {
tests := []struct {
fixture string
constraint int
expected *ListingEntry
}{
{
fixture: "test-fixtures/listing.json",
constraint: 2,
expected: &ListingEntry{
Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db-v1.1.0+2020-6-13.tar.gz")),
Version: 2,
Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8",
},
},
{
fixture: "test-fixtures/listing.json",
constraint: 1,
expected: &ListingEntry{
Built: time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC),
URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db-v0.2.0+2020-6-12.tar.gz")),
Version: 1,
Checksum: "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e",
},
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
listing, err := NewListingFromFile(afero.NewOsFs(), test.fixture)
if err != nil {
t.Fatalf("failed to get metadata: %+v", err)
}
actual := listing.BestUpdate(test.constraint)
if actual == nil && test.expected != nil || actual != nil && test.expected == nil {
t.Fatalf("mismatched best candidate expectations")
}
for _, diff := range deep.Equal(actual, test.expected) {
t.Errorf("listing entry difference: %s", diff)
}
})
}
}

136
grype/db/metadata.go Normal file
View file

@ -0,0 +1,136 @@
package db
import (
"encoding/json"
"fmt"
"io/ioutil"
"path"
"time"
"github.com/anchore/grype/internal/file"
"github.com/anchore/grype/internal/log"
"github.com/spf13/afero"
)
const MetadataFileName = "metadata.json"
// Metadata represents the basic identifying information of a database flat file (built/version) and a way to
// verify the contents (checksum).
type Metadata struct {
Built time.Time
Version int
Checksum string
}
// MetadataJSON is a helper struct for parsing and assembling Metadata objects to and from JSON.
type MetadataJSON struct {
Built string `json:"built"` // RFC 3339
Version int `json:"version"`
Checksum string `json:"checksum"`
}
// ToMetadata converts a MetadataJSON object to a Metadata object.
func (m MetadataJSON) ToMetadata() (Metadata, error) {
build, err := time.Parse(time.RFC3339, m.Built)
if err != nil {
return Metadata{}, fmt.Errorf("cannot convert built time (%s): %+v", m.Built, err)
}
metadata := Metadata{
Built: build.UTC(),
Version: m.Version,
Checksum: m.Checksum,
}
return metadata, nil
}
func metadataPath(dir string) string {
return path.Join(dir, MetadataFileName)
}
// NewMetadataFromDir generates a Metadata object from a directory containing a vulnerability.db flat file.
func NewMetadataFromDir(fs afero.Fs, dir string) (*Metadata, error) {
metadataFilePath := metadataPath(dir)
exists, err := file.Exists(fs, metadataFilePath)
if err != nil {
return nil, fmt.Errorf("unable to check if DB metadata path exists (%s): %w", metadataFilePath, err)
}
if !exists {
return nil, nil
}
f, err := fs.Open(metadataFilePath)
if err != nil {
return nil, fmt.Errorf("unable to open DB metadata path (%s): %w", metadataFilePath, err)
}
defer f.Close()
var m Metadata
err = json.NewDecoder(f).Decode(&m)
if err != nil {
return nil, fmt.Errorf("unable to parse DB metadata (%s): %w", metadataFilePath, err)
}
return &m, nil
}
func (m *Metadata) UnmarshalJSON(data []byte) error {
var mj MetadataJSON
if err := json.Unmarshal(data, &mj); err != nil {
return err
}
me, err := mj.ToMetadata()
if err != nil {
return err
}
*m = me
return nil
}
// IsSupersededBy takes a ListingEntry and determines if the entry candidate is newer than what is hinted at
// in the current Metadata object.
func (m *Metadata) IsSupersededBy(entry *ListingEntry) bool {
if m == nil {
log.Debugf("cannot find existing metadata, using update...")
// any valid update beats no database, use it!
return true
}
if entry.Version > m.Version {
log.Debugf("update is a newer version than the current database, using update...")
// the listing is newer than the existing db, use it!
return true
}
if entry.Built.After(m.Built) {
log.Debugf("existing database (%s) is older than candidate update (%s), using update...", m.Built.String(), entry.Built.String())
// the listing is newer than the existing db, use it!
return true
}
log.Debugf("existing database is already up to date")
return false
}
func (m Metadata) String() string {
return fmt.Sprintf("Metadata(built=%s version=%d checksum=%s)", m.Built, m.Version, m.Checksum)
}
// Write out a Metadata object to the given path.
func (m Metadata) Write(toPath string) error {
metadata := MetadataJSON{
Built: m.Built.UTC().Format(time.RFC3339),
Version: m.Version,
Checksum: m.Checksum,
}
contents, err := json.MarshalIndent(&metadata, "", " ")
if err != nil {
return fmt.Errorf("failed to encode metadata file: %w", err)
}
err = ioutil.WriteFile(toPath, contents, 0600)
if err != nil {
return fmt.Errorf("failed to write metadata file: %w", err)
}
return nil
}

110
grype/db/metadata_test.go Normal file
View file

@ -0,0 +1,110 @@
package db
import (
"testing"
"time"
"github.com/go-test/deep"
"github.com/spf13/afero"
)
func TestMetadataParse(t *testing.T) {
tests := []struct {
fixture string
expected *Metadata
err bool
}{
{
fixture: "test-fixtures/metadata-gocase",
expected: &Metadata{
Built: time.Date(2020, 06, 15, 14, 02, 36, 0, time.UTC),
Version: 2,
Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8",
},
},
{
fixture: "test-fixtures/metadata-edt-timezone",
expected: &Metadata{
Built: time.Date(2020, 06, 15, 18, 02, 36, 0, time.UTC),
Version: 2,
Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8",
},
},
{
fixture: "/dev/null/impossible",
err: true,
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
metadata, err := NewMetadataFromDir(afero.NewOsFs(), test.fixture)
if err != nil && !test.err {
t.Fatalf("failed to get metadata: %+v", err)
} else if err == nil && test.err {
t.Fatalf("expected error but got none")
} else if metadata == nil && test.expected != nil {
t.Fatalf("metadata not found: %+v", test.fixture)
}
if metadata != nil && test.expected != nil {
for _, diff := range deep.Equal(*metadata, *test.expected) {
t.Errorf("metadata difference: %s", diff)
}
}
})
}
}
func TestMetadataIsSupercededBy(t *testing.T) {
tests := []struct {
name string
current *Metadata
update *ListingEntry
expectedToSupercede bool
}{
{
name: "prefer updated versions over later dates",
expectedToSupercede: true,
current: &Metadata{
Built: time.Date(2020, 06, 15, 14, 02, 36, 0, time.UTC),
Version: 2,
},
update: &ListingEntry{
Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
Version: 3,
},
},
{
name: "prefer later dates when version is the same",
expectedToSupercede: false,
current: &Metadata{
Built: time.Date(2020, 06, 15, 14, 02, 36, 0, time.UTC),
Version: 1,
},
update: &ListingEntry{
Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
Version: 1,
},
},
{
name: "prefer something over nothing",
expectedToSupercede: true,
current: nil,
update: &ListingEntry{
Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),
Version: 1,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := test.current.IsSupersededBy(test.update)
if test.expectedToSupercede != actual {
t.Errorf("failed supercede assertion: got %+v", actual)
}
})
}
}

View file

@ -0,0 +1,18 @@
{
"available": {
"1": [
{
"built": "2020-06-13T13:13:13-04:00",
"version": 1,
"url": "http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz",
"checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8"
},
{
"built": "2020-06-12T12:12:12-04:00",
"version": 1,
"url": "http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz",
"checksum": "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e"
}
]
}
}

View file

@ -0,0 +1,18 @@
{
"available": {
"1": [
{
"built": "2020-06-12T12:12:12-04:00",
"version": 1,
"url": "http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz",
"checksum": "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e"
},
{
"built": "2020-06-13T13:13:13-04:00",
"version": 1,
"url": "http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz",
"checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8"
}
]
}
}

View file

@ -0,0 +1,20 @@
{
"available": {
"1": [
{
"built": "2020-06-12T12:12:12-04:00",
"version": 1,
"url": "http://localhost:5000/vulnerability-db-v0.2.0+2020-6-12.tar.gz",
"checksum": "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e"
}
],
"2": [
{
"built": "2020-06-13T13:13:13-04:00",
"version": 2,
"url": "http://localhost:5000/vulnerability-db-v1.1.0+2020-6-13.tar.gz",
"checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8"
}
]
}
}

View file

@ -0,0 +1,7 @@
{
"built": "2020-06-15T14:02:36-04:00",
"updated": "2020-06-15T14:02:36-04:00",
"last-check": "2020-06-15T14:02:36-04:00",
"version": 2,
"checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8"
}

View file

@ -0,0 +1,5 @@
{
"built": "2020-06-15T14:02:36Z",
"version": 2,
"checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8"
}

28
grype/db/v1/id.go Normal file
View file

@ -0,0 +1,28 @@
package v1
import (
"time"
)
// ID represents identifying information for a DB and the data it contains.
type ID struct {
// BuildTimestamp is the timestamp used to define the age of the DB, ideally including the age of the data
// contained in the DB, not just when the DB file was created.
BuildTimestamp time.Time
SchemaVersion int
}
type IDReader interface {
GetID() (*ID, error)
}
type IDWriter interface {
SetID(ID) error
}
func NewID(age time.Time) ID {
return ID{
BuildTimestamp: age.UTC(),
SchemaVersion: SchemaVersion,
}
}

40
grype/db/v1/model/id.go Normal file
View file

@ -0,0 +1,40 @@
package model
import (
"fmt"
"time"
v1 "github.com/anchore/grype/grype/db/v1"
)
const (
IDTableName = "id"
)
type IDModel struct {
BuildTimestamp string `gorm:"column:build_timestamp"`
SchemaVersion int `gorm:"column:schema_version"`
}
func NewIDModel(id v1.ID) IDModel {
return IDModel{
BuildTimestamp: id.BuildTimestamp.Format(time.RFC3339Nano),
SchemaVersion: id.SchemaVersion,
}
}
func (IDModel) TableName() string {
return IDTableName
}
func (m *IDModel) Inflate() (v1.ID, error) {
buildTime, err := time.Parse(time.RFC3339Nano, m.BuildTimestamp)
if err != nil {
return v1.ID{}, fmt.Errorf("unable to parse build timestamp (%+v): %w", m.BuildTimestamp, err)
}
return v1.ID{
BuildTimestamp: buildTime,
SchemaVersion: m.SchemaVersion,
}, nil
}

View file

@ -0,0 +1,86 @@
package model
import (
"encoding/json"
"fmt"
v1 "github.com/anchore/grype/grype/db/v1"
)
const (
VulnerabilityTableName = "vulnerability"
GetVulnerabilityIndexName = "get_vulnerability_index"
)
// VulnerabilityModel is a struct used to serialize db.Vulnerability information into a sqlite3 DB.
type VulnerabilityModel struct {
PK uint64 `gorm:"primary_key;auto_increment;"`
ID string `gorm:"column:id"`
RecordSource string `gorm:"column:record_source"`
PackageName string `gorm:"column:package_name; index:get_vulnerability_index"`
Namespace string `gorm:"column:namespace; index:get_vulnerability_index"`
VersionConstraint string `gorm:"column:version_constraint"`
VersionFormat string `gorm:"column:version_format"`
CPEs string `gorm:"column:cpes"`
ProxyVulnerabilities string `gorm:"column:proxy_vulnerabilities"`
FixedInVersion string `gorm:"column:fixed_in_version"`
}
// NewVulnerabilityModel generates a new model from a db.Vulnerability struct.
func NewVulnerabilityModel(vulnerability v1.Vulnerability) VulnerabilityModel {
cpes, err := json.Marshal(vulnerability.CPEs)
if err != nil {
// TODO: just no
panic(err)
}
proxy, err := json.Marshal(vulnerability.ProxyVulnerabilities)
if err != nil {
// TODO: just no
panic(err)
}
return VulnerabilityModel{
ID: vulnerability.ID,
PackageName: vulnerability.PackageName,
RecordSource: vulnerability.RecordSource,
Namespace: vulnerability.Namespace,
VersionConstraint: vulnerability.VersionConstraint,
VersionFormat: vulnerability.VersionFormat,
FixedInVersion: vulnerability.FixedInVersion,
CPEs: string(cpes),
ProxyVulnerabilities: string(proxy),
}
}
// TableName returns the table which all db.Vulnerability model instances are stored into.
func (VulnerabilityModel) TableName() string {
return VulnerabilityTableName
}
// Inflate generates a db.Vulnerability object from the serialized model instance.
func (m *VulnerabilityModel) Inflate() (v1.Vulnerability, error) {
var cpes []string
err := json.Unmarshal([]byte(m.CPEs), &cpes)
if err != nil {
return v1.Vulnerability{}, fmt.Errorf("unable to unmarshal CPEs (%+v): %w", m.CPEs, err)
}
var proxy []string
err = json.Unmarshal([]byte(m.ProxyVulnerabilities), &proxy)
if err != nil {
return v1.Vulnerability{}, fmt.Errorf("unable to unmarshal proxy vulnerabilities (%+v): %w", m.ProxyVulnerabilities, err)
}
return v1.Vulnerability{
ID: m.ID,
RecordSource: m.RecordSource,
PackageName: m.PackageName,
Namespace: m.Namespace,
VersionConstraint: m.VersionConstraint,
VersionFormat: m.VersionFormat,
CPEs: cpes,
ProxyVulnerabilities: proxy,
FixedInVersion: m.FixedInVersion,
}, nil
}

View file

@ -0,0 +1,104 @@
package model
import (
"database/sql"
"encoding/json"
"fmt"
v1 "github.com/anchore/grype/grype/db/v1"
)
const (
VulnerabilityMetadataTableName = "vulnerability_metadata"
)
// VulnerabilityMetadataModel is a struct used to serialize db.VulnerabilityMetadata information into a sqlite3 DB.
type VulnerabilityMetadataModel struct {
ID string `gorm:"primary_key; column:id;"`
RecordSource string `gorm:"primary_key; column:record_source;"`
Severity string `gorm:"column:severity"`
Links string `gorm:"column:links"`
Description string `gorm:"column:description"`
CvssV2 sql.NullString `gorm:"column:cvss_v2"`
CvssV3 sql.NullString `gorm:"column:cvss_v3"`
}
// NewVulnerabilityMetadataModel generates a new model from a db.VulnerabilityMetadata struct.
func NewVulnerabilityMetadataModel(metadata v1.VulnerabilityMetadata) VulnerabilityMetadataModel {
links, err := json.Marshal(metadata.Links)
if err != nil {
// TODO: just no
panic(err)
}
var cvssV2Str sql.NullString
if metadata.CvssV2 != nil {
cvssV2, err := json.Marshal(*metadata.CvssV2)
if err != nil {
// TODO: just no
panic(err)
}
cvssV2Str.String = string(cvssV2)
cvssV2Str.Valid = true
}
var cvssV3Str sql.NullString
if metadata.CvssV3 != nil {
cvssV3, err := json.Marshal(*metadata.CvssV3)
if err != nil {
// TODO: just no
panic(err)
}
cvssV3Str.String = string(cvssV3)
cvssV3Str.Valid = true
}
return VulnerabilityMetadataModel{
ID: metadata.ID,
RecordSource: metadata.RecordSource,
Severity: metadata.Severity,
Links: string(links),
Description: metadata.Description,
CvssV2: cvssV2Str,
CvssV3: cvssV3Str,
}
}
// TableName returns the table which all db.VulnerabilityMetadata model instances are stored into.
func (VulnerabilityMetadataModel) TableName() string {
return VulnerabilityMetadataTableName
}
// Inflate generates a db.VulnerabilityMetadataModel object from the serialized model instance.
func (m *VulnerabilityMetadataModel) Inflate() (v1.VulnerabilityMetadata, error) {
var links []string
var cvssV2, cvssV3 *v1.Cvss
if err := json.Unmarshal([]byte(m.Links), &links); err != nil {
return v1.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal links (%+v): %w", m.Links, err)
}
if m.CvssV2.Valid {
err := json.Unmarshal([]byte(m.CvssV2.String), &cvssV2)
if err != nil {
return v1.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvssV2 data (%+v): %w", m.CvssV2, err)
}
}
if m.CvssV3.Valid {
err := json.Unmarshal([]byte(m.CvssV3.String), &cvssV3)
if err != nil {
return v1.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvssV3 data (%+v): %w", m.CvssV3, err)
}
}
return v1.VulnerabilityMetadata{
ID: m.ID,
RecordSource: m.RecordSource,
Severity: m.Severity,
Links: links,
Description: m.Description,
CvssV2: cvssV2,
CvssV3: cvssV3,
}, nil
}

30
grype/db/v1/namespace.go Normal file
View file

@ -0,0 +1,30 @@
package v1
import (
"fmt"
)
const (
NVDNamespace = "nvd"
)
func RecordSource(feed, group string) string {
switch feed {
case "github", "nvdv2":
return group
default:
return fmt.Sprintf("%s:%s", feed, group)
}
}
func NamespaceForFeedGroup(feed, group string) (string, error) {
switch {
case feed == "vulnerabilities":
return group, nil
case feed == "github":
return group, nil
case feed == "nvdv2" && group == "nvdv2:cves":
return NVDNamespace, nil
}
return "", fmt.Errorf("feed=%q group=%q has no namespace mappings", feed, group)
}

View file

@ -0,0 +1,49 @@
package v1
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNamespaceFromRecordSource(t *testing.T) {
tests := []struct {
Feed, Group string
Namespace string
}{
{
Feed: "vulnerabilities",
Group: "ubuntu:20.04",
Namespace: "ubuntu:20.04",
},
{
Feed: "vulnerabilities",
Group: "alpine:3.9",
Namespace: "alpine:3.9",
},
{
Feed: "vulnerabilities",
Group: "sles:12.5",
Namespace: "sles:12.5",
},
{
Feed: "nvdv2",
Group: "nvdv2:cves",
Namespace: "nvd",
},
{
Feed: "github",
Group: "github:python",
Namespace: "github:python",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("feed=%q group=%q namespace=%q", test.Feed, test.Group, test.Namespace), func(t *testing.T) {
actual, err := NamespaceForFeedGroup(test.Feed, test.Group)
assert.NoError(t, err)
assert.Equal(t, test.Namespace, actual)
})
}
}

View file

@ -0,0 +1,28 @@
package reader
import (
"os"
"github.com/alicebob/sqlittle"
)
// Options defines the information needed to connect and create a sqlite3 database
type config struct {
dbPath string
overwrite bool
}
// Open a new connection to the sqlite3 database file
func Open(cfg *config) (*sqlittle.DB, error) {
if cfg.overwrite {
// the file may or may not exist, so we ignore the error explicitly
_ = os.Remove(cfg.dbPath)
}
db, err := sqlittle.Open(cfg.dbPath)
if err != nil {
return nil, err
}
return db, nil
}

View file

@ -0,0 +1,146 @@
package reader
import (
"fmt"
"github.com/alicebob/sqlittle"
v1 "github.com/anchore/grype/grype/db/v1"
"github.com/anchore/grype/grype/db/v1/model"
)
// Reader holds an instance of the database connection.
type Reader struct {
db *sqlittle.DB
}
// CleanupFn is a callback for closing a DB connection.
type CleanupFn func() error
// New creates a new instance of the store.
func New(dbFilePath string) (*Reader, CleanupFn, error) {
d, err := Open(&config{
dbPath: dbFilePath,
overwrite: false,
})
if err != nil {
return nil, nil, fmt.Errorf("unable to create a new connection to sqlite3 db: %s", err)
}
return &Reader{
db: d,
}, d.Close, nil
}
// GetID fetches the metadata about the databases schema version and build time.
func (b *Reader) GetID() (*v1.ID, error) {
var scanErr error
total := 0
var m model.IDModel
err := b.db.Select(model.IDTableName, func(row sqlittle.Row) {
total++
if scanErr = row.Scan(&m.BuildTimestamp, &m.SchemaVersion); scanErr != nil {
return
}
}, "build_timestamp", "schema_version")
if err != nil {
return nil, fmt.Errorf("unable to query for ID: %w", err)
}
if scanErr != nil {
return nil, scanErr
}
id, err := m.Inflate()
if err != nil {
return nil, err
}
switch {
case total == 0:
return nil, nil
case total > 1:
return nil, fmt.Errorf("discovered more than one DB ID")
}
return &id, nil
}
// GetVulnerability retrieves one or more vulnerabilities given a namespace and package name.
func (b *Reader) GetVulnerability(namespace, name string) ([]v1.Vulnerability, error) {
var scanErr error
var vulnerabilityModels []model.VulnerabilityModel
err := b.db.IndexedSelectEq(model.VulnerabilityTableName, model.GetVulnerabilityIndexName, sqlittle.Key{name, namespace}, func(row sqlittle.Row) {
var m model.VulnerabilityModel
if err := row.Scan(&m.Namespace, &m.PackageName, &m.ID, &m.RecordSource, &m.VersionConstraint, &m.VersionFormat, &m.CPEs, &m.ProxyVulnerabilities, &m.FixedInVersion); err != nil {
scanErr = fmt.Errorf("unable to scan over row: %w", err)
return
}
vulnerabilityModels = append(vulnerabilityModels, m)
}, "namespace", "package_name", "id", "record_source", "version_constraint", "version_format", "cpes", "proxy_vulnerabilities", "fixed_in_version")
if err != nil {
return nil, fmt.Errorf("unable to query: %w", err)
}
if scanErr != nil {
return nil, scanErr
}
vulnerabilities := make([]v1.Vulnerability, 0, len(vulnerabilityModels))
for _, m := range vulnerabilityModels {
vulnerability, err := m.Inflate()
if err != nil {
return nil, err
}
vulnerabilities = append(vulnerabilities, vulnerability)
}
return vulnerabilities, nil
}
// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
func (b *Reader) GetVulnerabilityMetadata(id, recordSource string) (*v1.VulnerabilityMetadata, error) {
total := 0
var m model.VulnerabilityMetadataModel
var scanErr error
err := b.db.PKSelect(model.VulnerabilityMetadataTableName, sqlittle.Key{id, recordSource}, func(row sqlittle.Row) {
total++
if err := row.Scan(&m.ID, &m.RecordSource, &m.Severity, &m.Links, &m.Description, &m.CvssV2.String, &m.CvssV3.String); err != nil {
scanErr = fmt.Errorf("unable to scan over row: %w", err)
return
}
}, "id", "record_source", "severity", "links", "description", "cvss_v2", "cvss_v3")
if err != nil {
return nil, fmt.Errorf("unable to query: %w", err)
}
if scanErr != nil {
return nil, scanErr
}
switch {
case total == 0:
return nil, nil
case total > 1:
return nil, fmt.Errorf("discovered more than one DB metadata record")
}
if m.CvssV2.String != "" {
m.CvssV2.Valid = true
}
if m.CvssV3.String != "" {
m.CvssV3.Valid = true
}
metadata, err := m.Inflate()
if err != nil {
return nil, err
}
return &metadata, nil
}

View file

@ -0,0 +1,3 @@
package v1
const SchemaVersion = 1

View file

@ -0,0 +1,14 @@
package v1
// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.
type Vulnerability struct {
ID string // The identifier of the vulnerability or advisory
RecordSource string // The source of the vulnerability information
PackageName string // The name of the package that is vulnerable
Namespace string // The ecosystem where the package resides
VersionConstraint string // The version range which the given package is vulnerable
VersionFormat string // The format which all version fields should be interpreted as
CPEs []string // The CPEs which are considered vulnerable
ProxyVulnerabilities []string // IDs of other Vulnerabilities that are related to this one (this is how advisories relate to CVEs)
FixedInVersion string // The version which this particular vulnerability was fixed in
}

View file

@ -0,0 +1,20 @@
package v1
// VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching.
type VulnerabilityMetadata struct {
ID string // The identifier of the vulnerability or advisory
RecordSource string // The source of the vulnerability information
Severity string // How severe the vulnerability is (valid values are defined by upstream sources currently)
Links []string // URLs to get more information about the vulnerability or advisory
Description string // Description of the vulnerability
CvssV2 *Cvss // Common Vulnerability Scoring System V2 values
CvssV3 *Cvss // Common Vulnerability Scoring System V3 values
}
// Cvss contains select Common Vulnerability Scoring System fields for a vulnerability.
type Cvss struct {
BaseScore float64 // Ranges from 0 - 10 and defines for qualities intrinsic to a vulnerability
ExploitabilityScore float64 // Indicator of how easy it may be for an attacker to exploit a vulnerability
ImpactScore float64 // Representation of the effects of an exploited vulnerability relative to compromise in confidentiality, integrity, and availability
Vector string // A textual representation of the metric values used to determine the score
}

View file

@ -0,0 +1,14 @@
package v1
type VulnerabilityMetadataStore interface {
VulnerabilityMetadataStoreReader
VulnerabilityMetadataStoreWriter
}
type VulnerabilityMetadataStoreReader interface {
GetVulnerabilityMetadata(id, recordSource string) (*VulnerabilityMetadata, error)
}
type VulnerabilityMetadataStoreWriter interface {
AddVulnerabilityMetadata(metadata ...VulnerabilityMetadata) error
}

View file

@ -0,0 +1,18 @@
package v1
const VulnerabilityStoreFileName = "vulnerability.db"
type VulnerabilityStore interface {
VulnerabilityStoreReader
VulnerabilityStoreWriter
}
type VulnerabilityStoreReader interface {
// GetVulnerability retrieves vulnerabilities associated with a namespace and a package name
GetVulnerability(namespace, name string) ([]Vulnerability, error)
}
type VulnerabilityStoreWriter interface {
// AddVulnerability inserts a new record of a vulnerability into the store
AddVulnerability(vulnerabilities ...Vulnerability) error
}

View file

@ -0,0 +1,10 @@
package writer
import "github.com/anchore/grype/internal/log"
type logAdapter struct {
}
func (l *logAdapter) Print(v ...interface{}) {
log.Error(v...)
}

View file

@ -0,0 +1,57 @@
package writer
import (
"fmt"
"os"
"github.com/jinzhu/gorm"
)
var connectStatements = []string{
// performance improvements (note: will result in lost data on write interruptions).
// on my box it reduces the time to write from 10 minutes to 10 seconds (with ~1GB memory utilization spikes)
`PRAGMA synchronous = OFF`,
`PRAGMA journal_mode = MEMORY`,
}
// config defines the information needed to connect and create a sqlite3 database
type config struct {
dbPath string
overwrite bool
}
// ConnectionString creates a connection string for sqlite3
func (o config) ConnectionString() (string, error) {
if o.dbPath == "" {
return "", fmt.Errorf("no db filepath given")
}
return fmt.Sprintf("file:%s?cache=shared", o.dbPath), nil
}
// open a new connection to a sqlite3 database file
func open(cfg config) (*gorm.DB, error) {
if cfg.overwrite {
// the file may or may not exist, so we ignore the error explicitly
_ = os.Remove(cfg.dbPath)
}
connStr, err := cfg.ConnectionString()
if err != nil {
return nil, err
}
dbObj, err := gorm.Open("sqlite3", connStr)
if err != nil {
return nil, fmt.Errorf("unable to connect to DB: %w", err)
}
dbObj.SetLogger(&logAdapter{})
for _, sqlStmt := range connectStatements {
dbObj.Exec(sqlStmt)
if dbObj.Error != nil {
return nil, fmt.Errorf("unable to execute (%s): %w", sqlStmt, dbObj.Error)
}
}
return dbObj, nil
}

View file

@ -0,0 +1,205 @@
package writer
import (
"fmt"
"sort"
v1 "github.com/anchore/grype/grype/db/v1"
"github.com/anchore/grype/grype/db/v1/model"
"github.com/anchore/grype/internal"
"github.com/go-test/deep"
"github.com/jinzhu/gorm"
// provide the sqlite dialect to gorm via import
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
// Writer holds an instance of the database connection
type Writer struct {
db *gorm.DB
}
// CleanupFn is a callback for closing a DB connection.
type CleanupFn func() error
// New creates a new instance of the store.
func New(dbFilePath string, overwrite bool) (*Writer, CleanupFn, error) {
db, err := open(config{
dbPath: dbFilePath,
overwrite: overwrite,
})
if err != nil {
return nil, nil, err
}
// TODO: automigrate could write to the database,
// we should be validating the database is the correct database based on the version in the ID table before
// automigrating
db.AutoMigrate(&model.IDModel{})
db.AutoMigrate(&model.VulnerabilityModel{})
db.AutoMigrate(&model.VulnerabilityMetadataModel{})
return &Writer{
db: db,
}, db.Close, nil
}
// GetID fetches the metadata about the databases schema version and build time.
func (s *Writer) GetID() (*v1.ID, error) {
var models []model.IDModel
result := s.db.Find(&models)
if result.Error != nil {
return nil, result.Error
}
switch {
case len(models) > 1:
return nil, fmt.Errorf("found multiple DB IDs")
case len(models) == 1:
id, err := models[0].Inflate()
if err != nil {
return nil, err
}
return &id, nil
}
return nil, nil
}
// SetID stores the databases schema version and build time.
func (s *Writer) SetID(id v1.ID) error {
var ids []model.IDModel
// replace the existing ID with the given one
s.db.Find(&ids).Delete(&ids)
m := model.NewIDModel(id)
result := s.db.Create(&m)
if result.RowsAffected != 1 {
return fmt.Errorf("unable to add id (%d rows affected)", result.RowsAffected)
}
return result.Error
}
// GetVulnerability retrieves one or more vulnerabilities given a namespace and package name.
func (s *Writer) GetVulnerability(namespace, packageName string) ([]v1.Vulnerability, error) {
var models []model.VulnerabilityModel
result := s.db.Where("namespace = ? AND package_name = ?", namespace, packageName).Find(&models)
var vulnerabilities = make([]v1.Vulnerability, len(models))
for idx, m := range models {
vulnerability, err := m.Inflate()
if err != nil {
return nil, err
}
vulnerabilities[idx] = vulnerability
}
return vulnerabilities, result.Error
}
// AddVulnerability saves one or more vulnerabilities into the sqlite3 store.
func (s *Writer) AddVulnerability(vulnerabilities ...v1.Vulnerability) error {
for _, vulnerability := range vulnerabilities {
m := model.NewVulnerabilityModel(vulnerability)
result := s.db.Create(&m)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return fmt.Errorf("unable to add vulnerability (%d rows affected)", result.RowsAffected)
}
}
return nil
}
// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
func (s *Writer) GetVulnerabilityMetadata(id, recordSource string) (*v1.VulnerabilityMetadata, error) {
var models []model.VulnerabilityMetadataModel
result := s.db.Where(&model.VulnerabilityMetadataModel{ID: id, RecordSource: recordSource}).Find(&models)
if result.Error != nil {
return nil, result.Error
}
switch {
case len(models) > 1:
return nil, fmt.Errorf("found multiple metadatas for single ID=%q RecordSource=%q", id, recordSource)
case len(models) == 1:
metadata, err := models[0].Inflate()
if err != nil {
return nil, err
}
return &metadata, nil
}
return nil, nil
}
// AddVulnerabilityMetadata stores one or more vulnerability metadata models into the sqlite DB.
func (s *Writer) AddVulnerabilityMetadata(metadata ...v1.VulnerabilityMetadata) error {
for _, m := range metadata {
existing, err := s.GetVulnerabilityMetadata(m.ID, m.RecordSource)
if err != nil {
return fmt.Errorf("failed to verify existing entry: %w", err)
}
if existing != nil {
// merge with the existing entry
cvssV3Diffs := deep.Equal(existing.CvssV3, m.CvssV3)
cvssV2Diffs := deep.Equal(existing.CvssV2, m.CvssV2)
switch {
case existing.Severity != m.Severity:
return fmt.Errorf("existing metadata has mismatched severity (%q!=%q)", existing.Severity, m.Severity)
case existing.Description != m.Description:
return fmt.Errorf("existing metadata has mismatched description (%q!=%q)", existing.Description, m.Description)
case existing.CvssV2 != nil && len(cvssV2Diffs) > 0:
return fmt.Errorf("existing metadata has mismatched cvss-v2: %+v", cvssV2Diffs)
case existing.CvssV3 != nil && len(cvssV3Diffs) > 0:
return fmt.Errorf("existing metadata has mismatched cvss-v3: %+v", cvssV3Diffs)
default:
existing.CvssV2 = m.CvssV2
existing.CvssV3 = m.CvssV3
}
links := internal.NewStringSetFromSlice(existing.Links)
for _, l := range m.Links {
links.Add(l)
}
existing.Links = links.ToSlice()
sort.Strings(existing.Links)
newModel := model.NewVulnerabilityMetadataModel(*existing)
result := s.db.Save(&newModel)
if result.RowsAffected != 1 {
return fmt.Errorf("unable to merge vulnerability metadata (%d rows affected)", result.RowsAffected)
}
if result.Error != nil {
return result.Error
}
} else {
// this is a new entry
newModel := model.NewVulnerabilityMetadataModel(m)
result := s.db.Create(&newModel)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return fmt.Errorf("unable to add vulnerability metadata (%d rows affected)", result.RowsAffected)
}
}
}
return nil
}

View file

@ -0,0 +1,542 @@
package writer
import (
"io/ioutil"
"os"
"testing"
"time"
v1 "github.com/anchore/grype/grype/db/v1"
"github.com/anchore/grype/grype/db/v1/model"
"github.com/anchore/grype/grype/db/v1/reader"
"github.com/go-test/deep"
)
func assertIDReader(t *testing.T, reader v1.IDReader, expected v1.ID) {
t.Helper()
if actual, err := reader.GetID(); err != nil {
t.Fatalf("failed to get ID: %+v", err)
} else {
diffs := deep.Equal(&expected, actual)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("Diff: %+v", d)
}
}
}
}
func TestStore_GetID_SetID(t *testing.T) {
dbTempFile, err := ioutil.TempFile("", "grype-db-test-store")
if err != nil {
t.Fatalf("could not create temp file: %+v", err)
}
defer os.Remove(dbTempFile.Name())
store, cleanupFn, err := New(dbTempFile.Name(), true)
defer cleanupFn()
if err != nil {
t.Fatalf("could not create store: %+v", err)
}
expected := v1.ID{
BuildTimestamp: time.Now().UTC(),
SchemaVersion: 2,
}
if err = store.SetID(expected); err != nil {
t.Fatalf("failed to set ID: %+v", err)
}
assertIDReader(t, store, expected)
// gut check on reader
storeReader, othercleanfn, err := reader.New(dbTempFile.Name())
defer othercleanfn()
if err != nil {
t.Fatalf("could not open db reader: %+v", err)
}
assertIDReader(t, storeReader, expected)
}
func assertVulnerabilityReader(t *testing.T, reader v1.VulnerabilityStoreReader, namespace, name string, expected []v1.Vulnerability) {
if actual, err := reader.GetVulnerability(namespace, name); err != nil {
t.Fatalf("failed to get Vulnerability: %+v", err)
} else {
if len(actual) != len(expected) {
t.Fatalf("unexpected number of vulns: %d", len(actual))
}
for idx := range actual {
diffs := deep.Equal(expected[idx], actual[idx])
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("Diff: %+v", d)
}
}
}
}
}
func TestStore_GetVulnerability_SetVulnerability(t *testing.T) {
dbTempFile, err := ioutil.TempFile("", "grype-db-test-store")
if err != nil {
t.Fatalf("could not create temp file: %+v", err)
}
defer os.Remove(dbTempFile.Name())
store, cleanupFn, err := New(dbTempFile.Name(), true)
defer cleanupFn()
if err != nil {
t.Fatalf("could not create store: %+v", err)
}
extra := []v1.Vulnerability{
{
ID: "my-cve-33333",
RecordSource: "record-source",
PackageName: "package-name-2",
Namespace: "my-namespace",
VersionConstraint: "< 1.0",
VersionFormat: "semver",
CPEs: []string{"a-cool-cpe"},
ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
FixedInVersion: "2.0.1",
},
{
ID: "my-other-cve-33333",
RecordSource: "record-source",
PackageName: "package-name-3",
Namespace: "my-namespace",
VersionConstraint: "< 509.2.2",
VersionFormat: "semver",
CPEs: []string{"a-cool-cpe"},
ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
},
}
expected := []v1.Vulnerability{
{
ID: "my-cve",
RecordSource: "record-source",
PackageName: "package-name",
Namespace: "my-namespace",
VersionConstraint: "< 1.0",
VersionFormat: "semver",
CPEs: []string{"a-cool-cpe"},
ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
FixedInVersion: "1.0.1",
},
{
ID: "my-other-cve",
RecordSource: "record-source",
PackageName: "package-name",
Namespace: "my-namespace",
VersionConstraint: "< 509.2.2",
VersionFormat: "semver",
CPEs: []string{"a-cool-cpe"},
ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
FixedInVersion: "4.0.5",
},
}
total := append(expected, extra...)
if err = store.AddVulnerability(total...); err != nil {
t.Fatalf("failed to set Vulnerability: %+v", err)
}
var allEntries []model.VulnerabilityModel
store.db.Find(&allEntries)
if len(allEntries) != len(total) {
t.Fatalf("unexpected number of entries: %d", len(allEntries))
}
assertVulnerabilityReader(t, store, expected[0].Namespace, expected[0].PackageName, expected)
// gut check on reader
storeReader, othercleanfn, err := reader.New(dbTempFile.Name())
defer othercleanfn()
if err != nil {
t.Fatalf("could not open db reader: %+v", err)
}
assertVulnerabilityReader(t, storeReader, expected[0].Namespace, expected[0].PackageName, expected)
}
func assertVulnerabilityMetadataReader(t *testing.T, reader v1.VulnerabilityMetadataStoreReader, id, recordSource string, expected v1.VulnerabilityMetadata) {
if actual, err := reader.GetVulnerabilityMetadata(id, recordSource); err != nil {
t.Fatalf("failed to get metadata: %+v", err)
} else {
diffs := deep.Equal(&expected, actual)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("Diff: %+v", d)
}
}
}
}
func TestStore_GetVulnerabilityMetadata_SetVulnerabilityMetadata(t *testing.T) {
dbTempFile, err := ioutil.TempFile("", "grype-db-test-store")
if err != nil {
t.Fatalf("could not create temp file: %+v", err)
}
defer os.Remove(dbTempFile.Name())
store, cleanupFn, err := New(dbTempFile.Name(), true)
defer cleanupFn()
if err != nil {
t.Fatalf("could not create store: %+v", err)
}
total := []v1.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v1.Cvss{
BaseScore: 1.1,
ExploitabilityScore: 2.2,
ImpactScore: 3.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NOT",
},
CvssV3: &v1.Cvss{
BaseScore: 1.3,
ExploitabilityScore: 2.1,
ImpactScore: 3.2,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NICE",
},
},
{
ID: "my-other-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "worst description ever",
CvssV2: &v1.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v1.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
}
if err = store.AddVulnerabilityMetadata(total...); err != nil {
t.Fatalf("failed to set metadata: %+v", err)
}
var allEntries []model.VulnerabilityMetadataModel
store.db.Find(&allEntries)
if len(allEntries) != len(total) {
t.Fatalf("unexpected number of entries: %d", len(allEntries))
}
// gut check on reader
storeReader, othercleanfn, err := reader.New(dbTempFile.Name())
defer othercleanfn()
if err != nil {
t.Fatalf("could not open db reader: %+v", err)
}
assertVulnerabilityMetadataReader(t, storeReader, total[0].ID, total[0].RecordSource, total[0])
}
func TestStore_MergeVulnerabilityMetadata(t *testing.T) {
tests := []struct {
name string
add []v1.VulnerabilityMetadata
expected v1.VulnerabilityMetadata
err bool
}{
{
name: "go-case",
add: []v1.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "worst description ever",
CvssV2: &v1.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v1.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
expected: v1.VulnerabilityMetadata{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "worst description ever",
CvssV2: &v1.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v1.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
{
name: "merge-links",
add: []v1.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://google.com"},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://yahoo.com"},
},
},
expected: v1.VulnerabilityMetadata{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re", "https://google.com", "https://yahoo.com"},
},
},
{
name: "bad-severity",
add: []v1.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "meh, push that for next tuesday...",
Links: []string{"https://redhat.com"},
},
},
err: true,
},
{
name: "mismatch-description",
err: true,
add: []v1.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v1.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v1.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "worst description ever",
CvssV2: &v1.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v1.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
},
{
name: "mismatch-cvss2",
err: true,
add: []v1.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v1.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v1.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v1.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:P--VERY",
},
CvssV3: &v1.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
},
{
name: "mismatch-cvss3",
err: true,
add: []v1.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v1.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v1.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v1.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v1.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 0,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dbTempDir, err := ioutil.TempDir("", "grype-db-test-store")
if err != nil {
t.Fatalf("could not create temp file: %+v", err)
}
defer os.RemoveAll(dbTempDir)
store, cleanupFn, err := New(dbTempDir, true)
defer cleanupFn()
if err != nil {
t.Fatalf("could not create store: %+v", err)
}
// add each metadata in order
var theErr error
for _, metadata := range test.add {
err = store.AddVulnerabilityMetadata(metadata)
if err != nil {
theErr = err
break
}
}
if test.err && theErr == nil {
t.Fatalf("expected error but did not get one")
} else if !test.err && theErr != nil {
t.Fatalf("expected no error but got one: %+v", theErr)
} else if test.err && theErr != nil {
// test pass...
return
}
// ensure there is exactly one entry
var allEntries []model.VulnerabilityMetadataModel
store.db.Find(&allEntries)
if len(allEntries) != 1 {
t.Fatalf("unexpected number of entries: %d", len(allEntries))
}
// get the resulting metadata object
if actual, err := store.GetVulnerabilityMetadata(test.expected.ID, test.expected.RecordSource); err != nil {
t.Fatalf("failed to get metadata: %+v", err)
} else {
diffs := deep.Equal(&test.expected, actual)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("Diff: %+v", d)
}
}
}
})
}
}

28
grype/db/v2/id.go Normal file
View file

@ -0,0 +1,28 @@
package v2
import (
"time"
)
// ID represents identifying information for a DB and the data it contains.
type ID struct {
// BuildTimestamp is the timestamp used to define the age of the DB, ideally including the age of the data
// contained in the DB, not just when the DB file was created.
BuildTimestamp time.Time
SchemaVersion int
}
type IDReader interface {
GetID() (*ID, error)
}
type IDWriter interface {
SetID(ID) error
}
func NewID(age time.Time) ID {
return ID{
BuildTimestamp: age.UTC(),
SchemaVersion: SchemaVersion,
}
}

40
grype/db/v2/model/id.go Normal file
View file

@ -0,0 +1,40 @@
package model
import (
"fmt"
"time"
v2 "github.com/anchore/grype/grype/db/v2"
)
const (
IDTableName = "id"
)
type IDModel struct {
BuildTimestamp string `gorm:"column:build_timestamp"`
SchemaVersion int `gorm:"column:schema_version"`
}
func NewIDModel(id v2.ID) IDModel {
return IDModel{
BuildTimestamp: id.BuildTimestamp.Format(time.RFC3339Nano),
SchemaVersion: id.SchemaVersion,
}
}
func (IDModel) TableName() string {
return IDTableName
}
func (m *IDModel) Inflate() (v2.ID, error) {
buildTime, err := time.Parse(time.RFC3339Nano, m.BuildTimestamp)
if err != nil {
return v2.ID{}, fmt.Errorf("unable to parse build timestamp (%+v): %w", m.BuildTimestamp, err)
}
return v2.ID{
BuildTimestamp: buildTime,
SchemaVersion: m.SchemaVersion,
}, nil
}

View file

@ -0,0 +1,86 @@
package model
import (
"encoding/json"
"fmt"
v2 "github.com/anchore/grype/grype/db/v2"
)
const (
VulnerabilityTableName = "vulnerability"
GetVulnerabilityIndexName = "get_vulnerability_index"
)
// VulnerabilityModel is a struct used to serialize db.Vulnerability information into a sqlite3 DB.
type VulnerabilityModel struct {
PK uint64 `gorm:"primary_key;auto_increment;"`
ID string `gorm:"column:id"`
RecordSource string `gorm:"column:record_source"`
PackageName string `gorm:"column:package_name; index:get_vulnerability_index"`
Namespace string `gorm:"column:namespace; index:get_vulnerability_index"`
VersionConstraint string `gorm:"column:version_constraint"`
VersionFormat string `gorm:"column:version_format"`
CPEs string `gorm:"column:cpes"`
ProxyVulnerabilities string `gorm:"column:proxy_vulnerabilities"`
FixedInVersion string `gorm:"column:fixed_in_version"`
}
// NewVulnerabilityModel generates a new model from a db.Vulnerability struct.
func NewVulnerabilityModel(vulnerability v2.Vulnerability) VulnerabilityModel {
cpes, err := json.Marshal(vulnerability.CPEs)
if err != nil {
// TODO: just no
panic(err)
}
proxy, err := json.Marshal(vulnerability.ProxyVulnerabilities)
if err != nil {
// TODO: just no
panic(err)
}
return VulnerabilityModel{
ID: vulnerability.ID,
PackageName: vulnerability.PackageName,
RecordSource: vulnerability.RecordSource,
Namespace: vulnerability.Namespace,
VersionConstraint: vulnerability.VersionConstraint,
VersionFormat: vulnerability.VersionFormat,
FixedInVersion: vulnerability.FixedInVersion,
CPEs: string(cpes),
ProxyVulnerabilities: string(proxy),
}
}
// TableName returns the table which all db.Vulnerability model instances are stored into.
func (VulnerabilityModel) TableName() string {
return VulnerabilityTableName
}
// Inflate generates a db.Vulnerability object from the serialized model instance.
func (m *VulnerabilityModel) Inflate() (v2.Vulnerability, error) {
var cpes []string
err := json.Unmarshal([]byte(m.CPEs), &cpes)
if err != nil {
return v2.Vulnerability{}, fmt.Errorf("unable to unmarshal CPEs (%+v): %w", m.CPEs, err)
}
var proxy []string
err = json.Unmarshal([]byte(m.ProxyVulnerabilities), &proxy)
if err != nil {
return v2.Vulnerability{}, fmt.Errorf("unable to unmarshal proxy vulnerabilities (%+v): %w", m.ProxyVulnerabilities, err)
}
return v2.Vulnerability{
ID: m.ID,
RecordSource: m.RecordSource,
PackageName: m.PackageName,
Namespace: m.Namespace,
VersionConstraint: m.VersionConstraint,
VersionFormat: m.VersionFormat,
CPEs: cpes,
ProxyVulnerabilities: proxy,
FixedInVersion: m.FixedInVersion,
}, nil
}

View file

@ -0,0 +1,104 @@
package model
import (
"database/sql"
"encoding/json"
"fmt"
v2 "github.com/anchore/grype/grype/db/v2"
)
const (
VulnerabilityMetadataTableName = "vulnerability_metadata"
)
// VulnerabilityMetadataModel is a struct used to serialize db.VulnerabilityMetadata information into a sqlite3 DB.
type VulnerabilityMetadataModel struct {
ID string `gorm:"primary_key; column:id;"`
RecordSource string `gorm:"primary_key; column:record_source;"`
Severity string `gorm:"column:severity"`
Links string `gorm:"column:links"`
Description string `gorm:"column:description"`
CvssV2 sql.NullString `gorm:"column:cvss_v2"`
CvssV3 sql.NullString `gorm:"column:cvss_v3"`
}
// NewVulnerabilityMetadataModel generates a new model from a db.VulnerabilityMetadata struct.
func NewVulnerabilityMetadataModel(metadata v2.VulnerabilityMetadata) VulnerabilityMetadataModel {
links, err := json.Marshal(metadata.Links)
if err != nil {
// TODO: just no
panic(err)
}
var cvssV2Str sql.NullString
if metadata.CvssV2 != nil {
cvssV2, err := json.Marshal(*metadata.CvssV2)
if err != nil {
// TODO: just no
panic(err)
}
cvssV2Str.String = string(cvssV2)
cvssV2Str.Valid = true
}
var cvssV3Str sql.NullString
if metadata.CvssV3 != nil {
cvssV3, err := json.Marshal(*metadata.CvssV3)
if err != nil {
// TODO: just no
panic(err)
}
cvssV3Str.String = string(cvssV3)
cvssV3Str.Valid = true
}
return VulnerabilityMetadataModel{
ID: metadata.ID,
RecordSource: metadata.RecordSource,
Severity: metadata.Severity,
Links: string(links),
Description: metadata.Description,
CvssV2: cvssV2Str,
CvssV3: cvssV3Str,
}
}
// TableName returns the table which all db.VulnerabilityMetadata model instances are stored into.
func (VulnerabilityMetadataModel) TableName() string {
return VulnerabilityMetadataTableName
}
// Inflate generates a db.VulnerabilityMetadataModel object from the serialized model instance.
func (m *VulnerabilityMetadataModel) Inflate() (v2.VulnerabilityMetadata, error) {
var links []string
var cvssV2, cvssV3 *v2.Cvss
if err := json.Unmarshal([]byte(m.Links), &links); err != nil {
return v2.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal links (%+v): %w", m.Links, err)
}
if m.CvssV2.Valid {
err := json.Unmarshal([]byte(m.CvssV2.String), &cvssV2)
if err != nil {
return v2.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvssV2 data (%+v): %w", m.CvssV2, err)
}
}
if m.CvssV3.Valid {
err := json.Unmarshal([]byte(m.CvssV3.String), &cvssV3)
if err != nil {
return v2.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvssV3 data (%+v): %w", m.CvssV3, err)
}
}
return v2.VulnerabilityMetadata{
ID: m.ID,
RecordSource: m.RecordSource,
Severity: m.Severity,
Links: links,
Description: m.Description,
CvssV2: cvssV2,
CvssV3: cvssV3,
}, nil
}

30
grype/db/v2/namespace.go Normal file
View file

@ -0,0 +1,30 @@
package v2
import (
"fmt"
)
const (
NVDNamespace = "nvd"
)
func RecordSource(feed, group string) string {
switch feed {
case "github", "nvdv2":
return group
default:
return fmt.Sprintf("%s:%s", feed, group)
}
}
func NamespaceForFeedGroup(feed, group string) (string, error) {
switch {
case feed == "vulnerabilities":
return group, nil
case feed == "github":
return group, nil
case feed == "nvdv2" && group == "nvdv2:cves":
return NVDNamespace, nil
}
return "", fmt.Errorf("feed=%q group=%q has no namespace mappings", feed, group)
}

View file

@ -0,0 +1,49 @@
package v2
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNamespaceFromRecordSource(t *testing.T) {
tests := []struct {
Feed, Group string
Namespace string
}{
{
Feed: "vulnerabilities",
Group: "ubuntu:20.04",
Namespace: "ubuntu:20.04",
},
{
Feed: "vulnerabilities",
Group: "alpine:3.9",
Namespace: "alpine:3.9",
},
{
Feed: "vulnerabilities",
Group: "sles:12.5",
Namespace: "sles:12.5",
},
{
Feed: "nvdv2",
Group: "nvdv2:cves",
Namespace: "nvd",
},
{
Feed: "github",
Group: "github:python",
Namespace: "github:python",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("feed=%q group=%q namespace=%q", test.Feed, test.Group, test.Namespace), func(t *testing.T) {
actual, err := NamespaceForFeedGroup(test.Feed, test.Group)
assert.NoError(t, err)
assert.Equal(t, test.Namespace, actual)
})
}
}

View file

@ -0,0 +1,28 @@
package reader
import (
"os"
"github.com/alicebob/sqlittle"
)
// Options defines the information needed to connect and create a sqlite3 database
type config struct {
dbPath string
overwrite bool
}
// Open a new connection to the sqlite3 database file
func Open(cfg *config) (*sqlittle.DB, error) {
if cfg.overwrite {
// the file may or may not exist, so we ignore the error explicitly
_ = os.Remove(cfg.dbPath)
}
db, err := sqlittle.Open(cfg.dbPath)
if err != nil {
return nil, err
}
return db, nil
}

View file

@ -0,0 +1,146 @@
package reader
import (
"fmt"
"github.com/alicebob/sqlittle"
v2 "github.com/anchore/grype/grype/db/v2"
"github.com/anchore/grype/grype/db/v2/model"
)
// Reader holds an instance of the database connection.
type Reader struct {
db *sqlittle.DB
}
// CleanupFn is a callback for closing a DB connection.
type CleanupFn func() error
// New creates a new instance of the store.
func New(dbFilePath string) (*Reader, CleanupFn, error) {
d, err := Open(&config{
dbPath: dbFilePath,
overwrite: false,
})
if err != nil {
return nil, nil, fmt.Errorf("unable to create a new connection to sqlite3 db: %s", err)
}
return &Reader{
db: d,
}, d.Close, nil
}
// GetID fetches the metadata about the databases schema version and build time.
func (b *Reader) GetID() (*v2.ID, error) {
var scanErr error
total := 0
var m model.IDModel
err := b.db.Select(model.IDTableName, func(row sqlittle.Row) {
total++
if scanErr = row.Scan(&m.BuildTimestamp, &m.SchemaVersion); scanErr != nil {
return
}
}, "build_timestamp", "schema_version")
if err != nil {
return nil, fmt.Errorf("unable to query for ID: %w", err)
}
if scanErr != nil {
return nil, scanErr
}
id, err := m.Inflate()
if err != nil {
return nil, err
}
switch {
case total == 0:
return nil, nil
case total > 1:
return nil, fmt.Errorf("discovered more than one DB ID")
}
return &id, nil
}
// GetVulnerability retrieves one or more vulnerabilities given a namespace and package name.
func (b *Reader) GetVulnerability(namespace, name string) ([]v2.Vulnerability, error) {
var scanErr error
var vulnerabilityModels []model.VulnerabilityModel
err := b.db.IndexedSelectEq(model.VulnerabilityTableName, model.GetVulnerabilityIndexName, sqlittle.Key{name, namespace}, func(row sqlittle.Row) {
var m model.VulnerabilityModel
if err := row.Scan(&m.Namespace, &m.PackageName, &m.ID, &m.RecordSource, &m.VersionConstraint, &m.VersionFormat, &m.CPEs, &m.ProxyVulnerabilities, &m.FixedInVersion); err != nil {
scanErr = fmt.Errorf("unable to scan over row: %w", err)
return
}
vulnerabilityModels = append(vulnerabilityModels, m)
}, "namespace", "package_name", "id", "record_source", "version_constraint", "version_format", "cpes", "proxy_vulnerabilities", "fixed_in_version")
if err != nil {
return nil, fmt.Errorf("unable to query: %w", err)
}
if scanErr != nil {
return nil, scanErr
}
vulnerabilities := make([]v2.Vulnerability, 0, len(vulnerabilityModels))
for _, m := range vulnerabilityModels {
vulnerability, err := m.Inflate()
if err != nil {
return nil, err
}
vulnerabilities = append(vulnerabilities, vulnerability)
}
return vulnerabilities, nil
}
// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
func (b *Reader) GetVulnerabilityMetadata(id, recordSource string) (*v2.VulnerabilityMetadata, error) {
total := 0
var m model.VulnerabilityMetadataModel
var scanErr error
err := b.db.PKSelect(model.VulnerabilityMetadataTableName, sqlittle.Key{id, recordSource}, func(row sqlittle.Row) {
total++
if err := row.Scan(&m.ID, &m.RecordSource, &m.Severity, &m.Links, &m.Description, &m.CvssV2.String, &m.CvssV3.String); err != nil {
scanErr = fmt.Errorf("unable to scan over row: %w", err)
return
}
}, "id", "record_source", "severity", "links", "description", "cvss_v2", "cvss_v3")
if err != nil {
return nil, fmt.Errorf("unable to query: %w", err)
}
if scanErr != nil {
return nil, scanErr
}
switch {
case total == 0:
return nil, nil
case total > 1:
return nil, fmt.Errorf("discovered more than one DB metadata record")
}
if m.CvssV2.String != "" {
m.CvssV2.Valid = true
}
if m.CvssV3.String != "" {
m.CvssV3.Valid = true
}
metadata, err := m.Inflate()
if err != nil {
return nil, err
}
return &metadata, nil
}

View file

@ -0,0 +1,3 @@
package v2
const SchemaVersion = 2

View file

@ -0,0 +1,14 @@
package v2
// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.
type Vulnerability struct {
ID string // The identifier of the vulnerability or advisory
RecordSource string // The source of the vulnerability information
PackageName string // The name of the package that is vulnerable
Namespace string // The ecosystem where the package resides
VersionConstraint string // The version range which the given package is vulnerable
VersionFormat string // The format which all version fields should be interpreted as
CPEs []string // The CPEs which are considered vulnerable
ProxyVulnerabilities []string // IDs of other Vulnerabilities that are related to this one (this is how advisories relate to CVEs)
FixedInVersion string // The version which this particular vulnerability was fixed in
}

View file

@ -0,0 +1,20 @@
package v2
// VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching.
type VulnerabilityMetadata struct {
ID string // The identifier of the vulnerability or advisory
RecordSource string // The source of the vulnerability information
Severity string // How severe the vulnerability is (valid values are defined by upstream sources currently)
Links []string // URLs to get more information about the vulnerability or advisory
Description string // Description of the vulnerability
CvssV2 *Cvss // Common Vulnerability Scoring System V2 values
CvssV3 *Cvss // Common Vulnerability Scoring System V3 values
}
// Cvss contains select Common Vulnerability Scoring System fields for a vulnerability.
type Cvss struct {
BaseScore float64 // Ranges from 0 - 10 and defines for qualities intrinsic to a vulnerability
ExploitabilityScore float64 // Indicator of how easy it may be for an attacker to exploit a vulnerability
ImpactScore float64 // Representation of the effects of an exploited vulnerability relative to compromise in confidentiality, integrity, and availability
Vector string // A textual representation of the metric values used to determine the score
}

View file

@ -0,0 +1,14 @@
package v2
type VulnerabilityMetadataStore interface {
VulnerabilityMetadataStoreReader
VulnerabilityMetadataStoreWriter
}
type VulnerabilityMetadataStoreReader interface {
GetVulnerabilityMetadata(id, recordSource string) (*VulnerabilityMetadata, error)
}
type VulnerabilityMetadataStoreWriter interface {
AddVulnerabilityMetadata(metadata ...VulnerabilityMetadata) error
}

View file

@ -0,0 +1,18 @@
package v2
const VulnerabilityStoreFileName = "vulnerability.db"
type VulnerabilityStore interface {
VulnerabilityStoreReader
VulnerabilityStoreWriter
}
type VulnerabilityStoreReader interface {
// GetVulnerability retrieves vulnerabilities associated with a namespace and a package name
GetVulnerability(namespace, name string) ([]Vulnerability, error)
}
type VulnerabilityStoreWriter interface {
// AddVulnerability inserts a new record of a vulnerability into the store
AddVulnerability(vulnerabilities ...Vulnerability) error
}

View file

@ -0,0 +1,10 @@
package writer
import "github.com/anchore/grype/internal/log"
type logAdapter struct {
}
func (l *logAdapter) Print(v ...interface{}) {
log.Error(v...)
}

View file

@ -0,0 +1,57 @@
package writer
import (
"fmt"
"os"
"github.com/jinzhu/gorm"
)
var connectStatements = []string{
// performance improvements (note: will result in lost data on write interruptions).
// on my box it reduces the time to write from 10 minutes to 10 seconds (with ~1GB memory utilization spikes)
`PRAGMA synchronous = OFF`,
`PRAGMA journal_mode = MEMORY`,
}
// config defines the information needed to connect and create a sqlite3 database
type config struct {
dbPath string
overwrite bool
}
// ConnectionString creates a connection string for sqlite3
func (o config) ConnectionString() (string, error) {
if o.dbPath == "" {
return "", fmt.Errorf("no db filepath given")
}
return fmt.Sprintf("file:%s?cache=shared", o.dbPath), nil
}
// open a new connection to a sqlite3 database file
func open(cfg config) (*gorm.DB, error) {
if cfg.overwrite {
// the file may or may not exist, so we ignore the error explicitly
_ = os.Remove(cfg.dbPath)
}
connStr, err := cfg.ConnectionString()
if err != nil {
return nil, err
}
dbObj, err := gorm.Open("sqlite3", connStr)
if err != nil {
return nil, fmt.Errorf("unable to connect to DB: %w", err)
}
dbObj.SetLogger(&logAdapter{})
for _, sqlStmt := range connectStatements {
dbObj.Exec(sqlStmt)
if dbObj.Error != nil {
return nil, fmt.Errorf("unable to execute (%s): %w", sqlStmt, dbObj.Error)
}
}
return dbObj, nil
}

View file

@ -0,0 +1,205 @@
package writer
import (
"fmt"
"sort"
v2 "github.com/anchore/grype/grype/db/v2"
"github.com/anchore/grype/grype/db/v2/model"
"github.com/anchore/grype/internal"
"github.com/go-test/deep"
"github.com/jinzhu/gorm"
// provide the sqlite dialect to gorm via import
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
// Writer holds an instance of the database connection
type Writer struct {
db *gorm.DB
}
// CleanupFn is a callback for closing a DB connection.
type CleanupFn func() error
// New creates a new instance of the store.
func New(dbFilePath string, overwrite bool) (*Writer, CleanupFn, error) {
db, err := open(config{
dbPath: dbFilePath,
overwrite: overwrite,
})
if err != nil {
return nil, nil, err
}
// TODO: automigrate could write to the database,
// we should be validating the database is the correct database based on the version in the ID table before
// automigrating
db.AutoMigrate(&model.IDModel{})
db.AutoMigrate(&model.VulnerabilityModel{})
db.AutoMigrate(&model.VulnerabilityMetadataModel{})
return &Writer{
db: db,
}, db.Close, nil
}
// GetID fetches the metadata about the databases schema version and build time.
func (s *Writer) GetID() (*v2.ID, error) {
var models []model.IDModel
result := s.db.Find(&models)
if result.Error != nil {
return nil, result.Error
}
switch {
case len(models) > 1:
return nil, fmt.Errorf("found multiple DB IDs")
case len(models) == 1:
id, err := models[0].Inflate()
if err != nil {
return nil, err
}
return &id, nil
}
return nil, nil
}
// SetID stores the databases schema version and build time.
func (s *Writer) SetID(id v2.ID) error {
var ids []model.IDModel
// replace the existing ID with the given one
s.db.Find(&ids).Delete(&ids)
m := model.NewIDModel(id)
result := s.db.Create(&m)
if result.RowsAffected != 1 {
return fmt.Errorf("unable to add id (%d rows affected)", result.RowsAffected)
}
return result.Error
}
// GetVulnerability retrieves one or more vulnerabilities given a namespace and package name.
func (s *Writer) GetVulnerability(namespace, packageName string) ([]v2.Vulnerability, error) {
var models []model.VulnerabilityModel
result := s.db.Where("namespace = ? AND package_name = ?", namespace, packageName).Find(&models)
var vulnerabilities = make([]v2.Vulnerability, len(models))
for idx, m := range models {
vulnerability, err := m.Inflate()
if err != nil {
return nil, err
}
vulnerabilities[idx] = vulnerability
}
return vulnerabilities, result.Error
}
// AddVulnerability saves one or more vulnerabilities into the sqlite3 store.
func (s *Writer) AddVulnerability(vulnerabilities ...v2.Vulnerability) error {
for _, vulnerability := range vulnerabilities {
m := model.NewVulnerabilityModel(vulnerability)
result := s.db.Create(&m)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return fmt.Errorf("unable to add vulnerability (%d rows affected)", result.RowsAffected)
}
}
return nil
}
// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
func (s *Writer) GetVulnerabilityMetadata(id, recordSource string) (*v2.VulnerabilityMetadata, error) {
var models []model.VulnerabilityMetadataModel
result := s.db.Where(&model.VulnerabilityMetadataModel{ID: id, RecordSource: recordSource}).Find(&models)
if result.Error != nil {
return nil, result.Error
}
switch {
case len(models) > 1:
return nil, fmt.Errorf("found multiple metadatas for single ID=%q RecordSource=%q", id, recordSource)
case len(models) == 1:
metadata, err := models[0].Inflate()
if err != nil {
return nil, err
}
return &metadata, nil
}
return nil, nil
}
// AddVulnerabilityMetadata stores one or more vulnerability metadata models into the sqlite DB.
func (s *Writer) AddVulnerabilityMetadata(metadata ...v2.VulnerabilityMetadata) error {
for _, m := range metadata {
existing, err := s.GetVulnerabilityMetadata(m.ID, m.RecordSource)
if err != nil {
return fmt.Errorf("failed to verify existing entry: %w", err)
}
if existing != nil {
// merge with the existing entry
cvssV3Diffs := deep.Equal(existing.CvssV3, m.CvssV3)
cvssV2Diffs := deep.Equal(existing.CvssV2, m.CvssV2)
switch {
case existing.Severity != m.Severity:
return fmt.Errorf("existing metadata has mismatched severity (%q!=%q)", existing.Severity, m.Severity)
case existing.Description != m.Description:
return fmt.Errorf("existing metadata has mismatched description (%q!=%q)", existing.Description, m.Description)
case existing.CvssV2 != nil && len(cvssV2Diffs) > 0:
return fmt.Errorf("existing metadata has mismatched cvss-v2: %+v", cvssV2Diffs)
case existing.CvssV3 != nil && len(cvssV3Diffs) > 0:
return fmt.Errorf("existing metadata has mismatched cvss-v3: %+v", cvssV3Diffs)
default:
existing.CvssV2 = m.CvssV2
existing.CvssV3 = m.CvssV3
}
links := internal.NewStringSetFromSlice(existing.Links)
for _, l := range m.Links {
links.Add(l)
}
existing.Links = links.ToSlice()
sort.Strings(existing.Links)
newModel := model.NewVulnerabilityMetadataModel(*existing)
result := s.db.Save(&newModel)
if result.RowsAffected != 1 {
return fmt.Errorf("unable to merge vulnerability metadata (%d rows affected)", result.RowsAffected)
}
if result.Error != nil {
return result.Error
}
} else {
// this is a new entry
newModel := model.NewVulnerabilityMetadataModel(m)
result := s.db.Create(&newModel)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return fmt.Errorf("unable to add vulnerability metadata (%d rows affected)", result.RowsAffected)
}
}
}
return nil
}

View file

@ -0,0 +1,542 @@
package writer
import (
"io/ioutil"
"os"
"testing"
"time"
v2 "github.com/anchore/grype/grype/db/v2"
"github.com/anchore/grype/grype/db/v2/model"
"github.com/anchore/grype/grype/db/v2/reader"
"github.com/go-test/deep"
)
func assertIDReader(t *testing.T, reader v2.IDReader, expected v2.ID) {
t.Helper()
if actual, err := reader.GetID(); err != nil {
t.Fatalf("failed to get ID: %+v", err)
} else {
diffs := deep.Equal(&expected, actual)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("Diff: %+v", d)
}
}
}
}
func TestStore_GetID_SetID(t *testing.T) {
dbTempFile, err := ioutil.TempFile("", "grype-db-test-store")
if err != nil {
t.Fatalf("could not create temp file: %+v", err)
}
defer os.Remove(dbTempFile.Name())
store, cleanupFn, err := New(dbTempFile.Name(), true)
defer cleanupFn()
if err != nil {
t.Fatalf("could not create store: %+v", err)
}
expected := v2.ID{
BuildTimestamp: time.Now().UTC(),
SchemaVersion: 2,
}
if err = store.SetID(expected); err != nil {
t.Fatalf("failed to set ID: %+v", err)
}
assertIDReader(t, store, expected)
// gut check on reader
storeReader, othercleanfn, err := reader.New(dbTempFile.Name())
defer othercleanfn()
if err != nil {
t.Fatalf("could not open db reader: %+v", err)
}
assertIDReader(t, storeReader, expected)
}
func assertVulnerabilityReader(t *testing.T, reader v2.VulnerabilityStoreReader, namespace, name string, expected []v2.Vulnerability) {
if actual, err := reader.GetVulnerability(namespace, name); err != nil {
t.Fatalf("failed to get Vulnerability: %+v", err)
} else {
if len(actual) != len(expected) {
t.Fatalf("unexpected number of vulns: %d", len(actual))
}
for idx := range actual {
diffs := deep.Equal(expected[idx], actual[idx])
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("Diff: %+v", d)
}
}
}
}
}
func TestStore_GetVulnerability_SetVulnerability(t *testing.T) {
dbTempFile, err := ioutil.TempFile("", "grype-db-test-store")
if err != nil {
t.Fatalf("could not create temp file: %+v", err)
}
defer os.Remove(dbTempFile.Name())
store, cleanupFn, err := New(dbTempFile.Name(), true)
defer cleanupFn()
if err != nil {
t.Fatalf("could not create store: %+v", err)
}
extra := []v2.Vulnerability{
{
ID: "my-cve-33333",
RecordSource: "record-source",
PackageName: "package-name-2",
Namespace: "my-namespace",
VersionConstraint: "< 1.0",
VersionFormat: "semver",
CPEs: []string{"a-cool-cpe"},
ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
FixedInVersion: "2.0.1",
},
{
ID: "my-other-cve-33333",
RecordSource: "record-source",
PackageName: "package-name-3",
Namespace: "my-namespace",
VersionConstraint: "< 509.2.2",
VersionFormat: "semver",
CPEs: []string{"a-cool-cpe"},
ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
},
}
expected := []v2.Vulnerability{
{
ID: "my-cve",
RecordSource: "record-source",
PackageName: "package-name",
Namespace: "my-namespace",
VersionConstraint: "< 1.0",
VersionFormat: "semver",
CPEs: []string{"a-cool-cpe"},
ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
FixedInVersion: "1.0.1",
},
{
ID: "my-other-cve",
RecordSource: "record-source",
PackageName: "package-name",
Namespace: "my-namespace",
VersionConstraint: "< 509.2.2",
VersionFormat: "semver",
CPEs: []string{"a-cool-cpe"},
ProxyVulnerabilities: []string{"another-cve", "an-other-cve"},
FixedInVersion: "4.0.5",
},
}
total := append(expected, extra...)
if err = store.AddVulnerability(total...); err != nil {
t.Fatalf("failed to set Vulnerability: %+v", err)
}
var allEntries []model.VulnerabilityModel
store.db.Find(&allEntries)
if len(allEntries) != len(total) {
t.Fatalf("unexpected number of entries: %d", len(allEntries))
}
assertVulnerabilityReader(t, store, expected[0].Namespace, expected[0].PackageName, expected)
// gut check on reader
storeReader, othercleanfn, err := reader.New(dbTempFile.Name())
defer othercleanfn()
if err != nil {
t.Fatalf("could not open db reader: %+v", err)
}
assertVulnerabilityReader(t, storeReader, expected[0].Namespace, expected[0].PackageName, expected)
}
func assertVulnerabilityMetadataReader(t *testing.T, reader v2.VulnerabilityMetadataStoreReader, id, recordSource string, expected v2.VulnerabilityMetadata) {
if actual, err := reader.GetVulnerabilityMetadata(id, recordSource); err != nil {
t.Fatalf("failed to get metadata: %+v", err)
} else {
diffs := deep.Equal(&expected, actual)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("Diff: %+v", d)
}
}
}
}
func TestStore_GetVulnerabilityMetadata_SetVulnerabilityMetadata(t *testing.T) {
dbTempFile, err := ioutil.TempFile("", "grype-db-test-store")
if err != nil {
t.Fatalf("could not create temp file: %+v", err)
}
defer os.Remove(dbTempFile.Name())
store, cleanupFn, err := New(dbTempFile.Name(), true)
defer cleanupFn()
if err != nil {
t.Fatalf("could not create store: %+v", err)
}
total := []v2.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v2.Cvss{
BaseScore: 1.1,
ExploitabilityScore: 2.2,
ImpactScore: 3.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NOT",
},
CvssV3: &v2.Cvss{
BaseScore: 1.3,
ExploitabilityScore: 2.1,
ImpactScore: 3.2,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NICE",
},
},
{
ID: "my-other-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "worst description ever",
CvssV2: &v2.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v2.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
}
if err = store.AddVulnerabilityMetadata(total...); err != nil {
t.Fatalf("failed to set metadata: %+v", err)
}
var allEntries []model.VulnerabilityMetadataModel
store.db.Find(&allEntries)
if len(allEntries) != len(total) {
t.Fatalf("unexpected number of entries: %d", len(allEntries))
}
// gut check on reader
storeReader, othercleanfn, err := reader.New(dbTempFile.Name())
defer othercleanfn()
if err != nil {
t.Fatalf("could not open db reader: %+v", err)
}
assertVulnerabilityMetadataReader(t, storeReader, total[0].ID, total[0].RecordSource, total[0])
}
func TestStore_MergeVulnerabilityMetadata(t *testing.T) {
tests := []struct {
name string
add []v2.VulnerabilityMetadata
expected v2.VulnerabilityMetadata
err bool
}{
{
name: "go-case",
add: []v2.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "worst description ever",
CvssV2: &v2.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v2.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
expected: v2.VulnerabilityMetadata{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "worst description ever",
CvssV2: &v2.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v2.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
{
name: "merge-links",
add: []v2.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://google.com"},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://yahoo.com"},
},
},
expected: v2.VulnerabilityMetadata{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re", "https://google.com", "https://yahoo.com"},
},
},
{
name: "bad-severity",
add: []v2.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "meh, push that for next tuesday...",
Links: []string{"https://redhat.com"},
},
},
err: true,
},
{
name: "mismatch-description",
err: true,
add: []v2.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v2.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v2.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "worst description ever",
CvssV2: &v2.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v2.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
},
{
name: "mismatch-cvss2",
err: true,
add: []v2.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v2.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v2.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v2.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:P--VERY",
},
CvssV3: &v2.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
},
{
name: "mismatch-cvss3",
err: true,
add: []v2.VulnerabilityMetadata{
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v2.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v2.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 2.5,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
{
ID: "my-cve",
RecordSource: "record-source",
Severity: "pretty bad",
Links: []string{"https://ancho.re"},
Description: "best description ever",
CvssV2: &v2.Cvss{
BaseScore: 4.1,
ExploitabilityScore: 5.2,
ImpactScore: 6.3,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY",
},
CvssV3: &v2.Cvss{
BaseScore: 1.4,
ExploitabilityScore: 0,
ImpactScore: 3.6,
Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD",
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dbTempDir, err := ioutil.TempDir("", "grype-db-test-store")
if err != nil {
t.Fatalf("could not create temp file: %+v", err)
}
defer os.RemoveAll(dbTempDir)
store, cleanupFn, err := New(dbTempDir, true)
defer cleanupFn()
if err != nil {
t.Fatalf("could not create store: %+v", err)
}
// add each metadata in order
var theErr error
for _, metadata := range test.add {
err = store.AddVulnerabilityMetadata(metadata)
if err != nil {
theErr = err
break
}
}
if test.err && theErr == nil {
t.Fatalf("expected error but did not get one")
} else if !test.err && theErr != nil {
t.Fatalf("expected no error but got one: %+v", theErr)
} else if test.err && theErr != nil {
// test pass...
return
}
// ensure there is exactly one entry
var allEntries []model.VulnerabilityMetadataModel
store.db.Find(&allEntries)
if len(allEntries) != 1 {
t.Fatalf("unexpected number of entries: %d", len(allEntries))
}
// get the resulting metadata object
if actual, err := store.GetVulnerabilityMetadata(test.expected.ID, test.expected.RecordSource); err != nil {
t.Fatalf("failed to get metadata: %+v", err)
} else {
diffs := deep.Equal(&test.expected, actual)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("Diff: %+v", d)
}
}
}
})
}
}

7
grype/db/v3/advisory.go Normal file
View file

@ -0,0 +1,7 @@
package v3
// Advisory represents published statements regarding a vulnerability (and potentially about it's resolution).
type Advisory struct {
ID string
Link string
}

16
grype/db/v3/fix.go Normal file
View file

@ -0,0 +1,16 @@
package v3
type FixState string
const (
UnknownFixState FixState = "unknown"
FixedState FixState = "fixed"
NotFixedState FixState = "not-fixed"
WontFixState FixState = "wont-fix"
)
// Fix represents all information about known fixes for a stated vulnerability.
type Fix struct {
Versions []string // The version(s) which this particular vulnerability was fixed in
State FixState
}

28
grype/db/v3/id.go Normal file
View file

@ -0,0 +1,28 @@
package v3
import (
"time"
)
// ID represents identifying information for a DB and the data it contains.
type ID struct {
// BuildTimestamp is the timestamp used to define the age of the DB, ideally including the age of the data
// contained in the DB, not just when the DB file was created.
BuildTimestamp time.Time
SchemaVersion int
}
type IDReader interface {
GetID() (*ID, error)
}
type IDWriter interface {
SetID(ID) error
}
func NewID(age time.Time) ID {
return ID{
BuildTimestamp: age.UTC(),
SchemaVersion: SchemaVersion,
}
}

40
grype/db/v3/model/id.go Normal file
View file

@ -0,0 +1,40 @@
package model
import (
"fmt"
"time"
v3 "github.com/anchore/grype/grype/db/v3"
)
const (
IDTableName = "id"
)
type IDModel struct {
BuildTimestamp string `gorm:"column:build_timestamp"`
SchemaVersion int `gorm:"column:schema_version"`
}
func NewIDModel(id v3.ID) IDModel {
return IDModel{
BuildTimestamp: id.BuildTimestamp.Format(time.RFC3339Nano),
SchemaVersion: id.SchemaVersion,
}
}
func (IDModel) TableName() string {
return IDTableName
}
func (m *IDModel) Inflate() (v3.ID, error) {
buildTime, err := time.Parse(time.RFC3339Nano, m.BuildTimestamp)
if err != nil {
return v3.ID{}, fmt.Errorf("unable to parse build timestamp (%+v): %w", m.BuildTimestamp, err)
}
return v3.ID{
BuildTimestamp: buildTime,
SchemaVersion: m.SchemaVersion,
}, nil
}

View file

@ -0,0 +1,115 @@
package model
import (
"encoding/json"
"fmt"
v3 "github.com/anchore/grype/grype/db/v3"
)
const (
VulnerabilityTableName = "vulnerability"
GetVulnerabilityIndexName = "get_vulnerability_index"
)
// VulnerabilityModel is a struct used to serialize db.Vulnerability information into a sqlite3 DB.
type VulnerabilityModel struct {
PK uint64 `gorm:"primary_key;auto_increment;"`
ID string `gorm:"column:id"`
PackageName string `gorm:"column:package_name; index:get_vulnerability_index"`
Namespace string `gorm:"column:namespace; index:get_vulnerability_index"`
VersionConstraint string `gorm:"column:version_constraint"`
VersionFormat string `gorm:"column:version_format"`
CPEs string `gorm:"column:cpes"`
RelatedVulnerabilities string `gorm:"column:related_vulnerabilities"`
FixedInVersions string `gorm:"column:fixed_in_versions"`
FixState string `gorm:"column:fix_state"`
Advisories string `gorm:"column:advisories"`
}
// NewVulnerabilityModel generates a new model from a db.Vulnerability struct.
func NewVulnerabilityModel(vulnerability v3.Vulnerability) VulnerabilityModel {
cpes, err := json.Marshal(vulnerability.CPEs)
if err != nil {
// TODO: just no
panic(err)
}
related, err := json.Marshal(vulnerability.RelatedVulnerabilities)
if err != nil {
// TODO: just no
panic(err)
}
advisories, err := json.Marshal(vulnerability.Advisories)
if err != nil {
// TODO: just no
panic(err)
}
fixedInVersions, err := json.Marshal(vulnerability.Fix.Versions)
if err != nil {
// TODO: just no
panic(err)
}
return VulnerabilityModel{
ID: vulnerability.ID,
PackageName: vulnerability.PackageName,
Namespace: vulnerability.Namespace,
VersionConstraint: vulnerability.VersionConstraint,
VersionFormat: vulnerability.VersionFormat,
FixedInVersions: string(fixedInVersions),
FixState: string(vulnerability.Fix.State),
Advisories: string(advisories),
CPEs: string(cpes),
RelatedVulnerabilities: string(related),
}
}
// TableName returns the table which all db.Vulnerability model instances are stored into.
func (VulnerabilityModel) TableName() string {
return VulnerabilityTableName
}
// Inflate generates a db.Vulnerability object from the serialized model instance.
func (m *VulnerabilityModel) Inflate() (v3.Vulnerability, error) {
var cpes []string
err := json.Unmarshal([]byte(m.CPEs), &cpes)
if err != nil {
return v3.Vulnerability{}, fmt.Errorf("unable to unmarshal CPEs (%+v): %w", m.CPEs, err)
}
var related []v3.VulnerabilityReference
err = json.Unmarshal([]byte(m.RelatedVulnerabilities), &related)
if err != nil {
return v3.Vulnerability{}, fmt.Errorf("unable to unmarshal related vulnerabilities (%+v): %w", m.RelatedVulnerabilities, err)
}
var advisories []v3.Advisory
err = json.Unmarshal([]byte(m.Advisories), &advisories)
if err != nil {
return v3.Vulnerability{}, fmt.Errorf("unable to unmarshal advisories (%+v): %w", m.Advisories, err)
}
var versions []string
err = json.Unmarshal([]byte(m.FixedInVersions), &versions)
if err != nil {
return v3.Vulnerability{}, fmt.Errorf("unable to unmarshal versions (%+v): %w", m.FixedInVersions, err)
}
return v3.Vulnerability{
ID: m.ID,
PackageName: m.PackageName,
Namespace: m.Namespace,
VersionConstraint: m.VersionConstraint,
VersionFormat: m.VersionFormat,
CPEs: cpes,
RelatedVulnerabilities: related,
Fix: v3.Fix{
Versions: versions,
State: v3.FixState(m.FixState),
},
Advisories: advisories,
}, nil
}

View file

@ -0,0 +1,87 @@
package model
import (
"encoding/json"
"fmt"
v3 "github.com/anchore/grype/grype/db/v3"
)
const (
VulnerabilityMetadataTableName = "vulnerability_metadata"
)
// VulnerabilityMetadataModel is a struct used to serialize db.VulnerabilityMetadata information into a sqlite3 DB.
type VulnerabilityMetadataModel struct {
ID string `gorm:"primary_key; column:id;"`
Namespace string `gorm:"primary_key; column:namespace;"`
DataSource string `gorm:"column:data_source"`
RecordSource string `gorm:"column:record_source"`
Severity string `gorm:"column:severity"`
URLs string `gorm:"column:urls"`
Description string `gorm:"column:description"`
Cvss string `gorm:"column:cvss"`
}
// NewVulnerabilityMetadataModel generates a new model from a db.VulnerabilityMetadata struct.
func NewVulnerabilityMetadataModel(metadata v3.VulnerabilityMetadata) VulnerabilityMetadataModel {
links, err := json.Marshal(metadata.URLs)
if err != nil {
// TODO: just no
panic(err)
}
if metadata.Cvss == nil {
metadata.Cvss = make([]v3.Cvss, 0)
}
var cvssStr string
cvss, err := json.Marshal(metadata.Cvss)
if err != nil {
// TODO: just no
panic(err)
}
cvssStr = string(cvss)
return VulnerabilityMetadataModel{
ID: metadata.ID,
Namespace: metadata.Namespace,
DataSource: metadata.DataSource,
RecordSource: metadata.RecordSource,
Severity: metadata.Severity,
URLs: string(links),
Description: metadata.Description,
Cvss: cvssStr,
}
}
// TableName returns the table which all db.VulnerabilityMetadata model instances are stored into.
func (VulnerabilityMetadataModel) TableName() string {
return VulnerabilityMetadataTableName
}
// Inflate generates a db.VulnerabilityMetadataModel object from the serialized model instance.
func (m *VulnerabilityMetadataModel) Inflate() (v3.VulnerabilityMetadata, error) {
var links []string
var cvss []v3.Cvss
if err := json.Unmarshal([]byte(m.URLs), &links); err != nil {
return v3.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal URLs (%+v): %w", m.URLs, err)
}
err := json.Unmarshal([]byte(m.Cvss), &cvss)
if err != nil {
return v3.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvss data (%+v): %w", m.Cvss, err)
}
return v3.VulnerabilityMetadata{
ID: m.ID,
Namespace: m.Namespace,
DataSource: m.DataSource,
RecordSource: m.RecordSource,
Severity: m.Severity,
URLs: links,
Description: m.Description,
Cvss: cvss,
}, nil
}

116
grype/db/v3/namespace.go Normal file
View file

@ -0,0 +1,116 @@
package v3
import (
"fmt"
"strings"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/internal"
"github.com/anchore/syft/syft/distro"
syftPkg "github.com/anchore/syft/syft/pkg"
)
const (
NVDNamespace = "nvd"
MSRCNamespacePrefix = "msrc"
VulnDBNamespace = "vulndb"
)
func RecordSource(feed, group string) string {
return fmt.Sprintf("%s:%s", feed, group)
}
func NamespaceForFeedGroup(feed, group string) (string, error) {
switch {
case feed == "vulnerabilities":
return group, nil
case feed == "github":
return group, nil
case feed == "nvdv2" && group == "nvdv2:cves":
return NVDNamespace, nil
case feed == "vulndb" && group == "vulndb:vulnerabilities":
return VulnDBNamespace, nil
case feed == "microsoft" && strings.HasPrefix(group, MSRCNamespacePrefix+":"):
return group, nil
}
return "", fmt.Errorf("feed=%q group=%q has no namespace mappings", feed, group)
}
// NamespaceFromDistro returns the correct Feed Service namespace for the given
// distro. A namespace is a distinct identifier from the Feed Service, and it
// can be a combination of distro name and version(s), for example "amzn:8".
// This is critical to query the database and correlate the distro version with
// feed contents. Namespaces have to exist in the Feed Service, otherwise,
// this causes no results to be returned when the database is queried.
func NamespaceForDistro(d distro.Distro) string {
var versionSegments []int
if d.Version != nil {
versionSegments = d.Version.Segments()
}
if len(versionSegments) > 0 {
switch d.Type {
// derived from https://github.com/anchore/anchore-engine/blob/5bbbe6b9744f2fb806198ae5d6f0cfe3b367fd9d/anchore_engine/services/policy_engine/__init__.py#L149-L159
case distro.CentOS, distro.RedHat, distro.Fedora, distro.RockyLinux, distro.AlmaLinux:
// TODO: there is no mapping of fedora version to RHEL latest version (only the name)
return fmt.Sprintf("rhel:%d", versionSegments[0])
case distro.AmazonLinux:
return fmt.Sprintf("amzn:%d", versionSegments[0])
case distro.OracleLinux:
return fmt.Sprintf("ol:%d", versionSegments[0])
case distro.Alpine:
// XXX this assumes that a major and minor versions will always exist in Segments
return fmt.Sprintf("alpine:%d.%d", versionSegments[0], versionSegments[1])
case distro.SLES:
return fmt.Sprintf("sles:%d.%d", versionSegments[0], versionSegments[1])
case distro.Windows:
return fmt.Sprintf("%s:%d", MSRCNamespacePrefix, versionSegments[0])
}
}
return fmt.Sprintf("%s:%s", strings.ToLower(d.Type.String()), d.FullVersion())
}
func NamespacesIndexedByCPE() []string {
return []string{NVDNamespace, VulnDBNamespace}
}
func NamespacePackageNamersForLanguage(l syftPkg.Language) map[string]NamerByPackage {
namespaces := make(map[string]NamerByPackage)
switch l {
case syftPkg.Ruby:
namespaces["github:gem"] = defaultPackageNamer
case syftPkg.Java:
namespaces["github:java"] = githubJavaPackageNamer
case syftPkg.JavaScript:
namespaces["github:npm"] = defaultPackageNamer
case syftPkg.Python:
namespaces["github:python"] = defaultPackageNamer
default:
namespaces[fmt.Sprintf("github:%s", l)] = defaultPackageNamer
}
return namespaces
}
type NamerByPackage func(p pkg.Package) []string
func defaultPackageNamer(p pkg.Package) []string {
return []string{p.Name}
}
func githubJavaPackageNamer(p pkg.Package) []string {
names := internal.NewStringSet()
// all github advisories are stored by "<group-name>:<artifact-name>"
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok {
if metadata.PomGroupID != "" {
if metadata.PomArtifactID != "" {
names.Add(fmt.Sprintf("%s:%s", metadata.PomGroupID, metadata.PomArtifactID))
}
if metadata.ManifestName != "" {
names.Add(fmt.Sprintf("%s:%s", metadata.PomGroupID, metadata.ManifestName))
}
}
}
return names.ToSlice()
}

View file

@ -0,0 +1,403 @@
package v3
import (
"fmt"
"testing"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/syft/syft/distro"
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
)
func Test_NamespaceFromRecordSource(t *testing.T) {
tests := []struct {
Feed, Group string
Namespace string
}{
{
Feed: "vulnerabilities",
Group: "ubuntu:20.04",
Namespace: "ubuntu:20.04",
},
{
Feed: "vulnerabilities",
Group: "alpine:3.9",
Namespace: "alpine:3.9",
},
{
Feed: "nvdv2",
Group: "nvdv2:cves",
Namespace: "nvd",
},
{
Feed: "github",
Group: "github:python",
Namespace: "github:python",
},
{
Feed: "vulndb",
Group: "vulndb:vulnerabilities",
Namespace: "vulndb",
},
{
Feed: "microsoft",
Group: "msrc:11769",
Namespace: "msrc:11769",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("feed=%q group=%q namespace=%q", test.Feed, test.Group, test.Namespace), func(t *testing.T) {
actual, err := NamespaceForFeedGroup(test.Feed, test.Group)
assert.NoError(t, err)
assert.Equal(t, test.Namespace, actual)
})
}
}
func Test_NamespaceForDistro(t *testing.T) {
tests := []struct {
dist distro.Type
version string
expected string
}{
// regression: https://github.com/anchore/grype/issues/221
{
dist: distro.RedHat,
version: "8.3",
expected: "rhel:8",
},
{
dist: distro.CentOS,
version: "8.3",
expected: "rhel:8",
},
{
dist: distro.AmazonLinux,
version: "8.3",
expected: "amzn:8",
},
{
dist: distro.OracleLinux,
version: "8.3",
expected: "ol:8",
},
{
dist: distro.Fedora,
version: "31.1",
// TODO: this is incorrect and will be solved in a future issue (to map the fedora version to the rhel latest version)
expected: "rhel:31",
},
// end of regression #221
{
dist: distro.RedHat,
version: "8",
expected: "rhel:8",
},
{
dist: distro.AmazonLinux,
version: "2",
expected: "amzn:2",
},
{
dist: distro.OracleLinux,
version: "6",
expected: "ol:6",
},
{
dist: distro.Alpine,
version: "1.3.1",
expected: "alpine:1.3",
},
{
dist: distro.Debian,
version: "8",
expected: "debian:8",
},
{
dist: distro.Fedora,
version: "31",
expected: "rhel:31",
},
{
dist: distro.Busybox,
version: "3.1.1",
expected: "busybox:3.1.1",
},
{
dist: distro.CentOS,
version: "7",
expected: "rhel:7",
},
{
dist: distro.Ubuntu,
version: "18.04",
expected: "ubuntu:18.04",
},
{
// TODO: this is not correct. This should be mapped to a feed source.
dist: distro.ArchLinux,
version: "", // ArchLinux doesn't expose a version
expected: "archlinux:",
},
{
// TODO: this is not correct. This should be mapped to a feed source.
dist: distro.OpenSuseLeap,
version: "15.2",
expected: "opensuseleap:15.2",
},
{
// TODO: this is not correct. This should be mapped to a feed source.
dist: distro.Photon,
version: "4.0",
expected: "photon:4.0",
},
{
dist: distro.SLES,
version: "12.5",
expected: "sles:12.5",
},
{
dist: distro.Windows,
version: "471816",
expected: "msrc:471816",
},
{
dist: distro.RockyLinux,
version: "8.5",
expected: "rhel:8",
},
{
dist: distro.AlmaLinux,
version: "8.5",
expected: "rhel:8",
},
}
observedDistros := strset.New()
allDistros := strset.New()
for _, d := range distro.All {
allDistros.Add(d.String())
}
// TOOD: what do we do with mariner
allDistros.Remove(distro.Mariner.String())
for _, test := range tests {
name := fmt.Sprintf("%s:%s", test.dist, test.version)
t.Run(name, func(t *testing.T) {
d, err := distro.NewDistro(test.dist, test.version, "")
assert.NoError(t, err)
observedDistros.Add(d.Type.String())
assert.Equal(t, NamespaceForDistro(d), test.expected)
})
}
assert.ElementsMatch(t, allDistros.List(), observedDistros.List(), "at least one distro doesn't have a corresponding test")
}
func Test_NamespacesIndexedByCPE(t *testing.T) {
assert.ElementsMatch(t, NamespacesIndexedByCPE(), []string{"nvd", "vulndb"})
}
func Test_NamespacesForLanguage(t *testing.T) {
tests := []struct {
language syftPkg.Language
namerInput *pkg.Package
expectedNamespaces []string
expectedNames []string
}{
// default languages
{
language: syftPkg.Rust,
namerInput: &pkg.Package{
Name: "a-name",
},
expectedNamespaces: []string{
"github:rust",
},
expectedNames: []string{
"a-name",
},
},
{
language: syftPkg.Go,
namerInput: &pkg.Package{
Name: "a-name",
},
expectedNamespaces: []string{
"github:go",
},
expectedNames: []string{
"a-name",
},
},
// supported languages
{
language: syftPkg.Ruby,
namerInput: &pkg.Package{
Name: "a-name",
},
expectedNamespaces: []string{
"github:gem",
},
expectedNames: []string{
"a-name",
},
},
{
language: syftPkg.JavaScript,
namerInput: &pkg.Package{
Name: "a-name",
},
expectedNamespaces: []string{
"github:npm",
},
expectedNames: []string{
"a-name",
},
},
{
language: syftPkg.Python,
namerInput: &pkg.Package{
Name: "a-name",
},
expectedNamespaces: []string{
"github:python",
},
expectedNames: []string{
"a-name",
},
},
{
language: syftPkg.Java,
namerInput: &pkg.Package{
Name: "a-name",
Metadata: pkg.JavaMetadata{
VirtualPath: "v-path",
PomArtifactID: "art-id",
PomGroupID: "g-id",
ManifestName: "man-name",
},
},
expectedNamespaces: []string{
"github:java",
},
expectedNames: []string{
"g-id:art-id",
"g-id:man-name",
},
},
}
observedLanguages := strset.New()
allLanguages := strset.New()
for _, l := range syftPkg.AllLanguages {
allLanguages.Add(string(l))
}
// remove PHP for coverage as feed has not been updated
allLanguages.Remove(string(syftPkg.PHP))
for _, test := range tests {
t.Run(string(test.language), func(t *testing.T) {
observedLanguages.Add(string(test.language))
var actualNamespaces, actualNames []string
namers := NamespacePackageNamersForLanguage(test.language)
for namespace, namerFn := range namers {
actualNamespaces = append(actualNamespaces, namespace)
actualNames = append(actualNames, namerFn(*test.namerInput)...)
}
assert.ElementsMatch(t, actualNamespaces, test.expectedNamespaces)
assert.ElementsMatch(t, actualNames, test.expectedNames)
})
}
assert.ElementsMatch(t, allLanguages.List(), observedLanguages.List(), "at least one language doesn't have a corresponding test")
}
func Test_githubJavaPackageNamer(t *testing.T) {
tests := []struct {
name string
namerInput pkg.Package
expected []string
}{
{
name: "both artifact and manifest",
namerInput: pkg.Package{
Name: "a-name",
Metadata: pkg.JavaMetadata{
VirtualPath: "v-path",
PomArtifactID: "art-id",
PomGroupID: "g-id",
ManifestName: "man-name",
},
},
expected: []string{
"g-id:art-id",
"g-id:man-name",
},
},
{
name: "no group id",
namerInput: pkg.Package{
Name: "a-name",
Metadata: pkg.JavaMetadata{
VirtualPath: "v-path",
PomArtifactID: "art-id",
ManifestName: "man-name",
},
},
expected: []string{},
},
{
name: "only manifest",
namerInput: pkg.Package{
Name: "a-name",
Metadata: pkg.JavaMetadata{
VirtualPath: "v-path",
PomGroupID: "g-id",
ManifestName: "man-name",
},
},
expected: []string{
"g-id:man-name",
},
},
{
name: "only artifact",
namerInput: pkg.Package{
Name: "a-name",
Metadata: pkg.JavaMetadata{
VirtualPath: "v-path",
PomArtifactID: "art-id",
PomGroupID: "g-id",
},
},
expected: []string{
"g-id:art-id",
},
},
{
name: "no artifact or manifest",
namerInput: pkg.Package{
Name: "a-name",
Metadata: pkg.JavaMetadata{
VirtualPath: "v-path",
PomGroupID: "g-id",
},
},
expected: []string{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, githubJavaPackageNamer(test.namerInput), test.expected)
})
}
}

View file

@ -0,0 +1,28 @@
package reader
import (
"os"
"github.com/alicebob/sqlittle"
)
// Options defines the information needed to connect and create a sqlite3 database
type config struct {
dbPath string
overwrite bool
}
// Open a new connection to the sqlite3 database file
func Open(cfg *config) (*sqlittle.DB, error) {
if cfg.overwrite {
// the file may or may not exist, so we ignore the error explicitly
_ = os.Remove(cfg.dbPath)
}
db, err := sqlittle.Open(cfg.dbPath)
if err != nil {
return nil, err
}
return db, nil
}

View file

@ -0,0 +1,139 @@
package reader
import (
"fmt"
v3 "github.com/anchore/grype/grype/db/v3"
"github.com/alicebob/sqlittle"
"github.com/anchore/grype/grype/db/v3/model"
)
// Reader holds an instance of the database connection.
type Reader struct {
db *sqlittle.DB
}
// CleanupFn is a callback for closing a DB connection.
type CleanupFn func() error
// New creates a new instance of the store.
func New(dbFilePath string) (*Reader, CleanupFn, error) {
d, err := Open(&config{
dbPath: dbFilePath,
overwrite: false,
})
if err != nil {
return nil, nil, fmt.Errorf("unable to create a new connection to sqlite3 db: %s", err)
}
return &Reader{
db: d,
}, d.Close, nil
}
// GetID fetches the metadata about the databases schema version and build time.
func (b *Reader) GetID() (*v3.ID, error) {
var scanErr error
total := 0
var m model.IDModel
err := b.db.Select(model.IDTableName, func(row sqlittle.Row) {
total++
if scanErr = row.Scan(&m.BuildTimestamp, &m.SchemaVersion); scanErr != nil {
return
}
}, "build_timestamp", "schema_version")
if err != nil {
return nil, fmt.Errorf("unable to query for ID: %w", err)
}
if scanErr != nil {
return nil, scanErr
}
id, err := m.Inflate()
if err != nil {
return nil, err
}
switch {
case total == 0:
return nil, nil
case total > 1:
return nil, fmt.Errorf("discovered more than one DB ID")
}
return &id, nil
}
// GetVulnerability retrieves one or more vulnerabilities given a namespace and package name.
func (b *Reader) GetVulnerability(namespace, name string) ([]v3.Vulnerability, error) {
var scanErr error
var vulnerabilityModels []model.VulnerabilityModel
err := b.db.IndexedSelectEq(model.VulnerabilityTableName, model.GetVulnerabilityIndexName, sqlittle.Key{name, namespace}, func(row sqlittle.Row) {
var m model.VulnerabilityModel
if err := row.Scan(&m.Namespace, &m.PackageName, &m.ID, &m.VersionConstraint, &m.VersionFormat, &m.CPEs, &m.RelatedVulnerabilities, &m.FixedInVersions, &m.FixState, &m.Advisories); err != nil {
scanErr = fmt.Errorf("unable to scan over row: %w", err)
return
}
vulnerabilityModels = append(vulnerabilityModels, m)
}, "namespace", "package_name", "id", "version_constraint", "version_format", "cpes", "related_vulnerabilities", "fixed_in_versions", "fix_state", "advisories")
if err != nil {
return nil, fmt.Errorf("unable to query: %w", err)
}
if scanErr != nil {
return nil, scanErr
}
vulnerabilities := make([]v3.Vulnerability, 0, len(vulnerabilityModels))
for _, m := range vulnerabilityModels {
vulnerability, err := m.Inflate()
if err != nil {
return nil, err
}
vulnerabilities = append(vulnerabilities, vulnerability)
}
return vulnerabilities, nil
}
// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
func (b *Reader) GetVulnerabilityMetadata(id, namespace string) (*v3.VulnerabilityMetadata, error) {
total := 0
var m model.VulnerabilityMetadataModel
var scanErr error
err := b.db.PKSelect(model.VulnerabilityMetadataTableName, sqlittle.Key{id, namespace}, func(row sqlittle.Row) {
total++
if err := row.Scan(&m.ID, &m.Namespace, &m.DataSource, &m.RecordSource, &m.Severity, &m.URLs, &m.Description, &m.Cvss); err != nil {
scanErr = fmt.Errorf("unable to scan over row: %w", err)
return
}
}, "id", "namespace", "data_source", "record_source", "severity", "urls", "description", "cvss")
if err != nil {
return nil, fmt.Errorf("unable to query: %w", err)
}
if scanErr != nil {
return nil, scanErr
}
switch {
case total == 0:
return nil, nil
case total > 1:
return nil, fmt.Errorf("discovered more than one DB metadata record")
}
metadata, err := m.Inflate()
if err != nil {
return nil, err
}
return &metadata, nil
}

View file

@ -0,0 +1,3 @@
package v3
const SchemaVersion = 3

View file

@ -0,0 +1,19 @@
package v3
// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.
type Vulnerability struct {
ID string // The identifier of the vulnerability or advisory
PackageName string // The name of the package that is vulnerable
Namespace string // The ecosystem where the package resides
VersionConstraint string // The version range which the given package is vulnerable
VersionFormat string // The format which all version fields should be interpreted as
CPEs []string // The CPEs which are considered vulnerable
RelatedVulnerabilities []VulnerabilityReference // Other Vulnerabilities that are related to this one (e.g. GHSA relate to CVEs, or how distro CVE relates to NVD record)
Fix Fix // All information about fixed versions
Advisories []Advisory // Any vendor advisories about fixes or other notifications about this vulnerability
}
type VulnerabilityReference struct {
ID string
Namespace string
}

View file

@ -0,0 +1,47 @@
package v3
// VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching.
type VulnerabilityMetadata struct {
ID string // The identifier of the vulnerability or advisory
Namespace string // Where this entry is valid within
DataSource string // A URL where the data was sourced from
RecordSource string // The source of the vulnerability information (relative to the immediate upstream in the enterprise feedgroup)
Severity string // How severe the vulnerability is (valid values are defined by upstream sources currently)
URLs []string // URLs to get more information about the vulnerability or advisory
Description string // Description of the vulnerability
Cvss []Cvss // Common Vulnerability Scoring System values
}
// Cvss contains select Common Vulnerability Scoring System fields for a vulnerability.
type Cvss struct {
// VendorMetadata captures non-standard CVSS fields that vendors can sometimes
// include when providing CVSS information. This vendor-specific metadata type
// allows to capture that data for persisting into the database
VendorMetadata interface{}
Metrics CvssMetrics
Vector string // A textual representation of the metric values used to determine the score
Version string // The version of the CVSS spec, for example 2.0, 3.0, or 3.1
}
// CvssMetrics are the quantitative values that make up a CVSS score.
type CvssMetrics struct {
// BaseScore ranges from 0 - 10 and defines qualities intrinsic to the severity of a vulnerability.
BaseScore float64
// ExploitabilityScore is a pointer to avoid having a 0 value by default.
// It is an indicator of how easy it may be for an attacker to exploit
// a vulnerability
ExploitabilityScore *float64
// ImpactScore represents the effects of an exploited vulnerability
// relative to compromise in confidentiality, integrity, and availability.
// It is an optional parameter, so that is why it is a pointer instead of
// a regular field
ImpactScore *float64
}
func NewCvssMetrics(baseScore, exploitabilityScore, impactScore float64) CvssMetrics {
return CvssMetrics{
BaseScore: baseScore,
ExploitabilityScore: &exploitabilityScore,
ImpactScore: &impactScore,
}
}

View file

@ -0,0 +1,14 @@
package v3
type VulnerabilityMetadataStore interface {
VulnerabilityMetadataStoreReader
VulnerabilityMetadataStoreWriter
}
type VulnerabilityMetadataStoreReader interface {
GetVulnerabilityMetadata(id, namespace string) (*VulnerabilityMetadata, error)
}
type VulnerabilityMetadataStoreWriter interface {
AddVulnerabilityMetadata(metadata ...VulnerabilityMetadata) error
}

View file

@ -0,0 +1,18 @@
package v3
const VulnerabilityStoreFileName = "vulnerability.db"
type VulnerabilityStore interface {
VulnerabilityStoreReader
VulnerabilityStoreWriter
}
type VulnerabilityStoreReader interface {
// GetVulnerability retrieves vulnerabilities associated with a namespace and a package name
GetVulnerability(namespace, name string) ([]Vulnerability, error)
}
type VulnerabilityStoreWriter interface {
// AddVulnerability inserts a new record of a vulnerability into the store
AddVulnerability(vulnerabilities ...Vulnerability) error
}

View file

@ -0,0 +1,10 @@
package writer
import "github.com/anchore/grype/internal/log"
type logAdapter struct {
}
func (l *logAdapter) Print(v ...interface{}) {
log.Error(v...)
}

View file

@ -0,0 +1,62 @@
package writer
import (
"fmt"
"os"
"github.com/jinzhu/gorm"
)
var connectStatements = []string{
// performance improvements (note: will result in lost data on write interruptions).
// on my box it reduces the time to write from 10 minutes to 10 seconds (with ~1GB memory utilization spikes)
`PRAGMA synchronous = OFF`,
`PRAGMA journal_mode = MEMORY`,
}
// config defines the information needed to connect and create a sqlite3 database
type config struct {
dbPath string
overwrite bool
}
// ConnectionString creates a connection string for sqlite3
func (o config) ConnectionString() (string, error) {
if o.dbPath == "" {
return "", fmt.Errorf("no db filepath given")
}
return fmt.Sprintf("file:%s?cache=shared", o.dbPath), nil
}
// open a new connection to a sqlite3 database file
func open(cfg config) (*gorm.DB, error) {
if cfg.overwrite {
// the file may or may not exist, so we ignore the error explicitly
if _, err := os.Stat(cfg.dbPath); !os.IsNotExist(err) {
rmErr := os.Remove(cfg.dbPath)
if rmErr != nil {
return nil, rmErr
}
}
}
connStr, err := cfg.ConnectionString()
if err != nil {
return nil, err
}
dbObj, err := gorm.Open("sqlite3", connStr)
if err != nil {
return nil, fmt.Errorf("unable to connect to DB: %w", err)
}
dbObj.SetLogger(&logAdapter{})
for _, sqlStmt := range connectStatements {
dbObj.Exec(sqlStmt)
if dbObj.Error != nil {
return nil, fmt.Errorf("unable to execute (%s): %w", sqlStmt, dbObj.Error)
}
}
return dbObj, nil
}

View file

@ -0,0 +1,212 @@
package writer
import (
"fmt"
"sort"
v3 "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/db/v3/model"
"github.com/anchore/grype/internal"
"github.com/go-test/deep"
"github.com/jinzhu/gorm"
// provide the sqlite dialect to gorm via import
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
// Writer holds an instance of the database connection
type Writer struct {
db *gorm.DB
}
// CleanupFn is a callback for closing a DB connection.
type CleanupFn func() error
// New creates a new instance of the store.
func New(dbFilePath string, overwrite bool) (*Writer, CleanupFn, error) {
db, err := open(config{
dbPath: dbFilePath,
overwrite: overwrite,
})
if err != nil {
return nil, nil, err
}
// TODO: automigrate could write to the database,
// we should be validating the database is the correct database based on the version in the ID table before
// automigrating
db.AutoMigrate(&model.IDModel{})
db.AutoMigrate(&model.VulnerabilityModel{})
db.AutoMigrate(&model.VulnerabilityMetadataModel{})
return &Writer{
db: db,
}, db.Close, nil
}
// GetID fetches the metadata about the databases schema version and build time.
func (s *Writer) GetID() (*v3.ID, error) {
var models []model.IDModel
result := s.db.Find(&models)
if result.Error != nil {
return nil, result.Error
}
switch {
case len(models) > 1:
return nil, fmt.Errorf("found multiple DB IDs")
case len(models) == 1:
id, err := models[0].Inflate()
if err != nil {
return nil, err
}
return &id, nil
}
return nil, nil
}
// SetID stores the databases schema version and build time.
func (s *Writer) SetID(id v3.ID) error {
var ids []model.IDModel
// replace the existing ID with the given one
s.db.Find(&ids).Delete(&ids)
m := model.NewIDModel(id)
result := s.db.Create(&m)
if result.RowsAffected != 1 {
return fmt.Errorf("unable to add id (%d rows affected)", result.RowsAffected)
}
return result.Error
}
// GetVulnerability retrieves one or more vulnerabilities given a namespace and package name.
func (s *Writer) GetVulnerability(namespace, packageName string) ([]v3.Vulnerability, error) {
var models []model.VulnerabilityModel
result := s.db.Where("namespace = ? AND package_name = ?", namespace, packageName).Find(&models)
var vulnerabilities = make([]v3.Vulnerability, len(models))
for idx, m := range models {
vulnerability, err := m.Inflate()
if err != nil {
return nil, err
}
vulnerabilities[idx] = vulnerability
}
return vulnerabilities, result.Error
}
// AddVulnerability saves one or more vulnerabilities into the sqlite3 store.
func (s *Writer) AddVulnerability(vulnerabilities ...v3.Vulnerability) error {
for _, vulnerability := range vulnerabilities {
m := model.NewVulnerabilityModel(vulnerability)
result := s.db.Create(&m)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return fmt.Errorf("unable to add vulnerability (%d rows affected)", result.RowsAffected)
}
}
return nil
}
// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.
func (s *Writer) GetVulnerabilityMetadata(id, namespace string) (*v3.VulnerabilityMetadata, error) {
var models []model.VulnerabilityMetadataModel
result := s.db.Where(&model.VulnerabilityMetadataModel{ID: id, Namespace: namespace}).Find(&models)
if result.Error != nil {
return nil, result.Error
}
switch {
case len(models) > 1:
return nil, fmt.Errorf("found multiple metadatas for single ID=%q Namespace=%q", id, namespace)
case len(models) == 1:
metadata, err := models[0].Inflate()
if err != nil {
return nil, err
}
return &metadata, nil
}
return nil, nil
}
// nolint:gocognit
// AddVulnerabilityMetadata stores one or more vulnerability metadata models into the sqlite DB.
func (s *Writer) AddVulnerabilityMetadata(metadata ...v3.VulnerabilityMetadata) error {
for _, m := range metadata {
existing, err := s.GetVulnerabilityMetadata(m.ID, m.Namespace)
if err != nil {
return fmt.Errorf("failed to verify existing entry: %w", err)
}
if existing != nil {
// merge with the existing entry
switch {
case existing.Severity != m.Severity:
return fmt.Errorf("existing metadata has mismatched severity (%q!=%q)", existing.Severity, m.Severity)
case existing.Description != m.Description:
return fmt.Errorf("existing metadata has mismatched description (%q!=%q)", existing.Description, m.Description)
}
incoming:
// go through all incoming CVSS and see if they are already stored.
// If they exist already in the database then skip adding them,
// preventing a duplicate
for _, incomingCvss := range m.Cvss {
for _, existingCvss := range existing.Cvss {
if len(deep.Equal(incomingCvss, existingCvss)) == 0 {
// duplicate found, so incoming CVSS shouldn't get added
continue incoming
}
}
// a duplicate CVSS entry wasn't found, so append the incoming CVSS
existing.Cvss = append(existing.Cvss, incomingCvss)
}
links := internal.NewStringSetFromSlice(existing.URLs)
for _, l := range m.URLs {
links.Add(l)
}
existing.URLs = links.ToSlice()
sort.Strings(existing.URLs)
newModel := model.NewVulnerabilityMetadataModel(*existing)
result := s.db.Save(&newModel)
if result.RowsAffected != 1 {
return fmt.Errorf("unable to merge vulnerability metadata (%d rows affected)", result.RowsAffected)
}
if result.Error != nil {
return result.Error
}
} else {
// this is a new entry
newModel := model.NewVulnerabilityMetadataModel(m)
result := s.db.Create(&newModel)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return fmt.Errorf("unable to add vulnerability metadata (%d rows affected)", result.RowsAffected)
}
}
}
return nil
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
package db
import (
"fmt"
"github.com/anchore/grype/grype/vulnerability"
grypeDB "github.com/anchore/grype/grype/db/v3"
)
var _ vulnerability.MetadataProvider = (*VulnerabilityMetadataProvider)(nil)
type VulnerabilityMetadataProvider struct {
reader grypeDB.VulnerabilityMetadataStoreReader
}
func NewVulnerabilityMetadataProvider(reader grypeDB.VulnerabilityMetadataStoreReader) *VulnerabilityMetadataProvider {
return &VulnerabilityMetadataProvider{
reader: reader,
}
}
func (pr *VulnerabilityMetadataProvider) GetMetadata(id, namespace string) (*vulnerability.Metadata, error) {
metadata, err := pr.reader.GetVulnerabilityMetadata(id, namespace)
if err != nil {
return nil, fmt.Errorf("metadata provider failed to fetch id='%s' recordsource='%s': %w", id, namespace, err)
}
return vulnerability.NewMetadata(metadata)
}

View file

@ -1,41 +1,44 @@
package vulnerability
package db
import (
"fmt"
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
"github.com/anchore/grype/grype/cpe"
grypeDB "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/distro"
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/facebookincubator/nvdtools/wfn"
)
type StoreAdapter struct {
store grypeDB.VulnerabilityStoreReader
var _ vulnerability.Provider = (*VulnerabilityProvider)(nil)
type VulnerabilityProvider struct {
reader grypeDB.VulnerabilityStoreReader
}
func NewProviderFromStore(store grypeDB.VulnerabilityStoreReader) *StoreAdapter {
return &StoreAdapter{
store: store,
func NewVulnerabilityProvider(reader grypeDB.VulnerabilityStoreReader) *VulnerabilityProvider {
return &VulnerabilityProvider{
reader: reader,
}
}
func (pr *StoreAdapter) GetByDistro(d *distro.Distro, p pkg.Package) ([]Vulnerability, error) {
func (pr *VulnerabilityProvider) GetByDistro(d *distro.Distro, p pkg.Package) ([]vulnerability.Vulnerability, error) {
if d == nil {
return nil, nil
}
namespace := grypeDB.NamespaceForDistro(*d)
allPkgVulns, err := pr.store.GetVulnerability(namespace, p.Name)
allPkgVulns, err := pr.reader.GetVulnerability(namespace, p.Name)
if err != nil {
return nil, fmt.Errorf("provider failed to fetch namespace='%s' pkg='%s': %w", namespace, p.Name, err)
}
var vulnerabilities []Vulnerability
var vulnerabilities []vulnerability.Vulnerability
for _, vuln := range allPkgVulns {
vulnObj, err := NewVulnerability(vuln)
vulnObj, err := vulnerability.NewVulnerability(vuln)
if err != nil {
return nil, fmt.Errorf("provider failed to parse distro='%s': %w", d, err)
}
@ -46,8 +49,8 @@ func (pr *StoreAdapter) GetByDistro(d *distro.Distro, p pkg.Package) ([]Vulnerab
return vulnerabilities, nil
}
func (pr *StoreAdapter) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]Vulnerability, error) {
vulns := make([]Vulnerability, 0)
func (pr *VulnerabilityProvider) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]vulnerability.Vulnerability, error) {
vulns := make([]vulnerability.Vulnerability, 0)
namersByNamespace := grypeDB.NamespacePackageNamersForLanguage(l)
if namersByNamespace == nil {
@ -56,13 +59,13 @@ func (pr *StoreAdapter) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]Vuln
for namespace, namer := range namersByNamespace {
for _, name := range namer(p) {
allPkgVulns, err := pr.store.GetVulnerability(namespace, name)
allPkgVulns, err := pr.reader.GetVulnerability(namespace, name)
if err != nil {
return nil, fmt.Errorf("provider failed to fetch namespace='%s' pkg='%s': %w", namespace, name, err)
}
for _, vuln := range allPkgVulns {
vulnObj, err := NewVulnerability(vuln)
vulnObj, err := vulnerability.NewVulnerability(vuln)
if err != nil {
return nil, fmt.Errorf("provider failed to parse language='%s': %w", l, err)
}
@ -75,8 +78,8 @@ func (pr *StoreAdapter) GetByLanguage(l syftPkg.Language, p pkg.Package) ([]Vuln
return vulns, nil
}
func (pr *StoreAdapter) GetByCPE(requestCPE syftPkg.CPE) ([]Vulnerability, error) {
vulns := make([]Vulnerability, 0)
func (pr *VulnerabilityProvider) GetByCPE(requestCPE syftPkg.CPE) ([]vulnerability.Vulnerability, error) {
vulns := make([]vulnerability.Vulnerability, 0)
namespaces := grypeDB.NamespacesIndexedByCPE()
if namespaces == nil {
@ -88,7 +91,7 @@ func (pr *StoreAdapter) GetByCPE(requestCPE syftPkg.CPE) ([]Vulnerability, error
}
for _, namespace := range namespaces {
allPkgVulns, err := pr.store.GetVulnerability(namespace, requestCPE.Product)
allPkgVulns, err := pr.reader.GetVulnerability(namespace, requestCPE.Product)
if err != nil {
return nil, fmt.Errorf("provider failed to fetch namespace='%s' product='%s': %w", namespace, requestCPE.Product, err)
}
@ -103,7 +106,7 @@ func (pr *StoreAdapter) GetByCPE(requestCPE syftPkg.CPE) ([]Vulnerability, error
candidateMatchCpes := cpe.MatchWithoutVersion(requestCPE, vulnCPEs)
if len(candidateMatchCpes) > 0 {
vulnObj, err := NewVulnerability(vuln)
vulnObj, err := vulnerability.NewVulnerability(vuln)
if err != nil {
return nil, fmt.Errorf("provider failed to parse cpe='%s': %w", requestCPE.BindToFmtString(), err)
}

View file

@ -1,6 +1,6 @@
package vulnerability
package db
import grypeDB "github.com/anchore/grype-db/pkg/db/v3"
import grypeDB "github.com/anchore/grype/grype/db/v3"
type mockStore struct {
data map[string]map[string][]grypeDB.Vulnerability

View file

@ -1,8 +1,10 @@
package vulnerability
package db
import (
"testing"
"github.com/anchore/grype/grype/vulnerability"
"github.com/stretchr/testify/assert"
"github.com/go-test/deep"
@ -14,7 +16,7 @@ import (
)
func TestGetByDistro(t *testing.T) {
provider := NewProviderFromStore(newMockStore())
provider := NewVulnerabilityProvider(newMockStore())
d, err := distro.NewDistro(distro.Debian, "8", "")
if err != nil {
@ -30,20 +32,20 @@ func TestGetByDistro(t *testing.T) {
t.Fatalf("failed to get by distro: %+v", err)
}
expected := []Vulnerability{
expected := []vulnerability.Vulnerability{
{
Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
ID: "CVE-2014-fake-1",
Namespace: "debian:8",
CPEs: []syftPkg.CPE{},
Advisories: []Advisory{},
Advisories: []vulnerability.Advisory{},
},
{
Constraint: version.MustGetConstraint("< 2013.0.2-1", version.DebFormat),
ID: "CVE-2013-fake-2",
Namespace: "debian:8",
CPEs: []syftPkg.CPE{},
Advisories: []Advisory{},
Advisories: []vulnerability.Advisory{},
},
}
@ -57,7 +59,7 @@ func TestGetByDistro(t *testing.T) {
}
func TestGetByDistro_nilDistro(t *testing.T) {
provider := NewProviderFromStore(newMockStore())
provider := NewVulnerabilityProvider(newMockStore())
p := pkg.Package{
Name: "neutron",
@ -81,13 +83,13 @@ func TestGetByCPE(t *testing.T) {
tests := []struct {
name string
cpe syftPkg.CPE
expected []Vulnerability
expected []vulnerability.Vulnerability
err bool
}{
{
name: "match from name and target SW",
cpe: must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*")),
expected: []Vulnerability{
expected: []vulnerability.Vulnerability{
{
Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
ID: "CVE-2014-fake-4",
@ -95,7 +97,7 @@ func TestGetByCPE(t *testing.T) {
must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*")),
},
Namespace: "nvd",
Advisories: []Advisory{},
Advisories: []vulnerability.Advisory{},
},
},
},
@ -103,7 +105,7 @@ func TestGetByCPE(t *testing.T) {
{
name: "match from vendor & name",
cpe: must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:*:*:*")),
expected: []Vulnerability{
expected: []vulnerability.Vulnerability{
{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
@ -111,7 +113,7 @@ func TestGetByCPE(t *testing.T) {
must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*")),
},
Namespace: "nvd",
Advisories: []Advisory{},
Advisories: []vulnerability.Advisory{},
},
{
Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
@ -120,7 +122,7 @@ func TestGetByCPE(t *testing.T) {
must(syftPkg.NewCPE("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*")),
},
Namespace: "nvd",
Advisories: []Advisory{},
Advisories: []vulnerability.Advisory{},
},
},
},
@ -135,7 +137,7 @@ func TestGetByCPE(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
provider := NewProviderFromStore(newMockStore())
provider := NewVulnerabilityProvider(newMockStore())
actual, err := provider.GetByCPE(test.cpe)
if err != nil && !test.err {

View file

@ -56,7 +56,7 @@ func LoadVulnerabilityDB(cfg db.Config, update bool) (vulnerability.Provider, vu
status := dbCurator.Status()
return vulnerability.NewProviderFromStore(store), vulnerability.NewMetadataStoreProvider(store), &status, status.Err
return db.NewVulnerabilityProvider(store), db.NewVulnerabilityMetadataProvider(store), &status, status.Err
}
func SetLogger(logger logger.Logger) {

View file

@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/assert"
grypeDb "github.com/anchore/grype-db/pkg/db/v3"
grypeDb "github.com/anchore/grype/grype/db/v3"
)
var (

View file

@ -3,7 +3,9 @@ package apk
import (
"testing"
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
"github.com/anchore/grype/grype/db"
grypeDB "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher/common"
"github.com/anchore/grype/grype/pkg"
@ -50,7 +52,7 @@ func TestSecDBOnlyMatch(t *testing.T) {
},
}
provider := vulnerability.NewProviderFromStore(&store)
provider := db.NewVulnerabilityProvider(&store)
m := Matcher{}
d, err := distro.NewDistro(distro.Alpine, "3.12.0", "")
@ -135,7 +137,7 @@ func TestBothSecdbAndNvdMatches(t *testing.T) {
},
}
provider := vulnerability.NewProviderFromStore(&store)
provider := db.NewVulnerabilityProvider(&store)
m := Matcher{}
d, err := distro.NewDistro(distro.Alpine, "3.12.0", "")
@ -221,7 +223,7 @@ func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {
},
}
provider := vulnerability.NewProviderFromStore(&store)
provider := db.NewVulnerabilityProvider(&store)
m := Matcher{}
d, err := distro.NewDistro(distro.Alpine, "3.12.0", "")
@ -294,7 +296,7 @@ func TestNvdOnlyMatches(t *testing.T) {
},
}
provider := vulnerability.NewProviderFromStore(&store)
provider := db.NewVulnerabilityProvider(&store)
m := Matcher{}
d, err := distro.NewDistro(distro.Alpine, "3.12.0", "")
@ -371,7 +373,7 @@ func TestNvdMatchesWithSecDBFix(t *testing.T) {
},
}
provider := vulnerability.NewProviderFromStore(&store)
provider := db.NewVulnerabilityProvider(&store)
m := Matcher{}
d, err := distro.NewDistro(distro.Alpine, "3.12.0", "")
@ -424,7 +426,7 @@ func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) {
},
}
provider := vulnerability.NewProviderFromStore(&store)
provider := db.NewVulnerabilityProvider(&store)
m := Matcher{}
d, err := distro.NewDistro(distro.Alpine, "3.12.0", "")
@ -467,7 +469,7 @@ func TestDistroMatchBySourceIndirection(t *testing.T) {
},
}
provider := vulnerability.NewProviderFromStore(&store)
provider := db.NewVulnerabilityProvider(&store)
m := Matcher{}
d, err := distro.NewDistro(distro.Alpine, "3.12.0", "")
@ -537,7 +539,7 @@ func TestNVDMatchBySourceIndirection(t *testing.T) {
},
}
provider := vulnerability.NewProviderFromStore(&store)
provider := db.NewVulnerabilityProvider(&store)
m := Matcher{}
d, err := distro.NewDistro(distro.Alpine, "3.12.0", "")

View file

@ -3,7 +3,9 @@ package common
import (
"testing"
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
"github.com/anchore/grype/grype/db"
grypeDB "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/version"
@ -376,7 +378,7 @@ func TestFindMatchesByPackageCPE(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := FindMatchesByPackageCPE(vulnerability.NewProviderFromStore(newMockStore()), test.p, matcher)
actual, err := FindMatchesByPackageCPE(db.NewVulnerabilityProvider(newMockStore()), test.p, matcher)
assert.NoError(t, err)
assertMatchesUsingIDsForVulnerabilities(t, test.expected, actual)
for idx, e := range test.expected {

View file

@ -4,9 +4,10 @@ import (
"fmt"
"testing"
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
"github.com/anchore/grype/grype/db"
grypeDB "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/distro"
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
@ -59,7 +60,7 @@ func TestMatches(t *testing.T) {
},
}
provider := vulnerability.NewProviderFromStore(&store)
provider := db.NewVulnerabilityProvider(&store)
tests := []struct {
name string

View file

@ -5,7 +5,7 @@ import (
"github.com/google/go-cmp/cmp"
grypeDb "github.com/anchore/grype-db/pkg/db/v3"
grypeDb "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/match"
)

View file

@ -3,7 +3,7 @@ package models
import (
"testing"
grypeDb "github.com/anchore/grype-db/pkg/db/v3"
grypeDb "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"

View file

@ -6,7 +6,7 @@ import (
"sort"
"strings"
grypeDb "github.com/anchore/grype-db/pkg/db/v3"
grypeDb "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"

View file

@ -1,7 +1,7 @@
package vulnerability
import (
grypeDb "github.com/anchore/grype-db/pkg/db/v3"
grypeDb "github.com/anchore/grype/grype/db/v3"
)
type Fix struct {

View file

@ -1,7 +1,7 @@
package vulnerability
import (
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
grypeDB "github.com/anchore/grype/grype/db/v3"
)
type Metadata struct {

View file

@ -1,26 +0,0 @@
package vulnerability
import (
"fmt"
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
)
type MetadataStoreAdapter struct {
store grypeDB.VulnerabilityMetadataStoreReader
}
func NewMetadataStoreProvider(store grypeDB.VulnerabilityMetadataStoreReader) *MetadataStoreAdapter {
return &MetadataStoreAdapter{
store: store,
}
}
func (pr *MetadataStoreAdapter) GetMetadata(id, namespace string) (*Metadata, error) {
metadata, err := pr.store.GetVulnerabilityMetadata(id, namespace)
if err != nil {
return nil, fmt.Errorf("metadata provider failed to fetch id='%s' recordsource='%s': %w", id, namespace, err)
}
return NewMetadata(metadata)
}

View file

@ -1,5 +1,5 @@
package vulnerability
import grypeDB "github.com/anchore/grype-db/pkg/db/v3"
import grypeDB "github.com/anchore/grype/grype/db/v3"
const SchemaVersion = grypeDB.SchemaVersion

View file

@ -3,8 +3,7 @@ package vulnerability
import (
"fmt"
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
grypeDB "github.com/anchore/grype/grype/db/v3"
"github.com/anchore/grype/grype/version"
"github.com/anchore/syft/syft/pkg"
)

View file

@ -6,10 +6,13 @@ import (
"github.com/spf13/afero"
)
func Exists(fs afero.Fs, path string) bool {
func Exists(fs afero.Fs, path string) (bool, error) {
info, err := fs.Stat(path)
if os.IsNotExist(err) {
return false
return false, nil
} else if err != nil {
return false, err
}
return !info.IsDir()
return !info.IsDir(), nil
}

View file

@ -6,6 +6,14 @@ func NewStringSet() StringSet {
return make(StringSet)
}
func NewStringSetFromSlice(start []string) StringSet {
ret := make(StringSet)
for _, s := range start {
ret.Add(s)
}
return ret
}
func (s StringSet) Add(i string) {
s[i] = struct{}{}
}

View file

@ -1,7 +1,7 @@
package integration
import (
grypeDB "github.com/anchore/grype-db/pkg/db/v3"
grypeDB "github.com/anchore/grype/grype/db/v3"
)
// integrity check

View file

@ -3,6 +3,8 @@ package integration
import (
"testing"
"github.com/anchore/grype/grype/db"
"github.com/anchore/grype/grype"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
@ -364,7 +366,7 @@ func TestMatchByImage(t *testing.T) {
}
actualResults := grype.FindVulnerabilitiesForPackage(
vulnerability.NewProviderFromStore(theStore),
db.NewVulnerabilityProvider(theStore),
theDistro,
pkg.FromCatalog(theCatalog)...,
)

View file

@ -4,11 +4,12 @@ import (
"fmt"
"testing"
"github.com/anchore/grype/grype/db"
"github.com/anchore/grype/grype/matcher/common"
"github.com/anchore/grype/grype"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/source"
"github.com/go-test/deep"
"github.com/scylladb/go-set/strset"
@ -85,7 +86,7 @@ func TestMatchBySBOMDocument(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
provider := vulnerability.NewProviderFromStore(newMockDbStore())
provider := db.NewVulnerabilityProvider(newMockDbStore())
matches, _, _, err := grype.FindVulnerabilities(provider, fmt.Sprintf("sbom:%s", test.fixture), source.SquashedScope, nil)
assert.NoError(t, err)
details := make([]match.Details, 0)

View file

@ -4,18 +4,20 @@ import os
import sys
import collections
pattern = re.compile(r'github.com/anchore/grype-db/pkg/db/v(?P<version>\d+)')
dir_pattern = r'grype/db/v(?P<version>\d+)'
db_dir_regex = re.compile(dir_pattern)
import_regex = re.compile(rf'github.com/anchore/grype/{dir_pattern}')
def report_schema_versions_found(schema_to_locations):
def report_schema_versions_found(title, schema_to_locations):
for schema, locations in sorted(schema_to_locations.items()):
print("Schema: %s" % schema)
print(f"{title} schema: {schema}")
for location in locations:
print(" %s" % location)
print(f" {location}")
print()
def validate(schema_to_locations):
def assert_single_schema_version(schema_to_locations):
schema_versions_found = list(schema_to_locations.keys())
try:
for x in schema_versions_found:
@ -29,7 +31,7 @@ def validate(schema_to_locations):
sys.exit("No schemas found!")
def main():
def find_db_schema_usages(filter_out_regexes=None, keep_regexes=None):
schema_to_locations = collections.defaultdict(list)
for root, dirs, files in os.walk("."):
@ -37,13 +39,59 @@ def main():
if not file.endswith(".go"):
continue
location = os.path.join(root, file)
if filter_out_regexes:
do_filter = False
for regex in filter_out_regexes:
if regex.findall(location):
do_filter = True
break
if do_filter:
continue
if keep_regexes:
do_keep = False
for regex in keep_regexes:
if regex.findall(location):
do_keep = True
break
if not do_keep:
continue
# keep track of all of the imports (from this point on, this is only possible consumers of db/v# code
with open(location) as f:
for match in pattern.findall(f.read(), re.MULTILINE):
for match in import_regex.findall(f.read(), re.MULTILINE):
schema_to_locations[match].append(location)
report_schema_versions_found(schema_to_locations)
validate(schema_to_locations)
print("Schema Version Found: %s" % list(schema_to_locations.keys())[0])
return schema_to_locations
def assert_schema_version_prefix(schema, locations):
for location in locations:
if f"/grype/db/v{schema}" not in location:
sys.exit(f"found cross-schema reference: {location}")
def validate_schema_consumers():
schema_to_locations = find_db_schema_usages(filter_out_regexes=[db_dir_regex])
report_schema_versions_found("Consumers of", schema_to_locations)
assert_single_schema_version(schema_to_locations)
print("Consuming schema versions found: %s" % list(schema_to_locations.keys())[0])
def validate_schema_definitions():
schema_to_locations = find_db_schema_usages(keep_regexes=[db_dir_regex])
report_schema_versions_found("Definitions of", schema_to_locations)
# make certain that each definition keeps out of other schema definitions
for schema, locations in schema_to_locations.items():
assert_schema_version_prefix(schema, locations)
print("Verified that schema definitions don't cross-import")
def main():
validate_schema_definitions()
print()
validate_schema_consumers()
if __name__ == "__main__":