mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
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:
parent
24ef03efc4
commit
2647cd0d9e
92 changed files with 6088 additions and 561 deletions
|
@ -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{}
|
||||
|
|
|
@ -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
8
go.mod
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
90
grype/db/listing.go
Normal 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
96
grype/db/listing_entry.go
Normal 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
161
grype/db/listing_test.go
Normal 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
136
grype/db/metadata.go
Normal 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
110
grype/db/metadata_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
18
grype/db/test-fixtures/listing-sorted.json
Normal file
18
grype/db/test-fixtures/listing-sorted.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
18
grype/db/test-fixtures/listing-unsorted.json
Normal file
18
grype/db/test-fixtures/listing-unsorted.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
20
grype/db/test-fixtures/listing.json
Normal file
20
grype/db/test-fixtures/listing.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
5
grype/db/test-fixtures/metadata-gocase/metadata.json
Normal file
5
grype/db/test-fixtures/metadata-gocase/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"built": "2020-06-15T14:02:36Z",
|
||||
"version": 2,
|
||||
"checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8"
|
||||
}
|
28
grype/db/v1/id.go
Normal file
28
grype/db/v1/id.go
Normal 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
40
grype/db/v1/model/id.go
Normal 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
|
||||
}
|
86
grype/db/v1/model/vulnerability.go
Normal file
86
grype/db/v1/model/vulnerability.go
Normal 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
|
||||
}
|
104
grype/db/v1/model/vulnerability_metadata.go
Normal file
104
grype/db/v1/model/vulnerability_metadata.go
Normal 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
30
grype/db/v1/namespace.go
Normal 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)
|
||||
}
|
49
grype/db/v1/namespace_test.go
Normal file
49
grype/db/v1/namespace_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
28
grype/db/v1/reader/open.go
Normal file
28
grype/db/v1/reader/open.go
Normal 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
|
||||
}
|
146
grype/db/v1/reader/reader.go
Normal file
146
grype/db/v1/reader/reader.go
Normal 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
|
||||
}
|
3
grype/db/v1/schema_version.go
Normal file
3
grype/db/v1/schema_version.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package v1
|
||||
|
||||
const SchemaVersion = 1
|
14
grype/db/v1/vulnerability.go
Normal file
14
grype/db/v1/vulnerability.go
Normal 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
|
||||
}
|
20
grype/db/v1/vulnerability_metadata.go
Normal file
20
grype/db/v1/vulnerability_metadata.go
Normal 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
|
||||
}
|
14
grype/db/v1/vulnerability_metadata_store.go
Normal file
14
grype/db/v1/vulnerability_metadata_store.go
Normal 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
|
||||
}
|
18
grype/db/v1/vulnerability_store.go
Normal file
18
grype/db/v1/vulnerability_store.go
Normal 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
|
||||
}
|
10
grype/db/v1/writer/log_adapter.go
Normal file
10
grype/db/v1/writer/log_adapter.go
Normal 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...)
|
||||
}
|
57
grype/db/v1/writer/open.go
Normal file
57
grype/db/v1/writer/open.go
Normal 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
|
||||
}
|
205
grype/db/v1/writer/writer.go
Normal file
205
grype/db/v1/writer/writer.go
Normal 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
|
||||
}
|
542
grype/db/v1/writer/writer_test.go
Normal file
542
grype/db/v1/writer/writer_test.go
Normal 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
28
grype/db/v2/id.go
Normal 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
40
grype/db/v2/model/id.go
Normal 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
|
||||
}
|
86
grype/db/v2/model/vulnerability.go
Normal file
86
grype/db/v2/model/vulnerability.go
Normal 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
|
||||
}
|
104
grype/db/v2/model/vulnerability_metadata.go
Normal file
104
grype/db/v2/model/vulnerability_metadata.go
Normal 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
30
grype/db/v2/namespace.go
Normal 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)
|
||||
}
|
49
grype/db/v2/namespace_test.go
Normal file
49
grype/db/v2/namespace_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
28
grype/db/v2/reader/open.go
Normal file
28
grype/db/v2/reader/open.go
Normal 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
|
||||
}
|
146
grype/db/v2/reader/reader.go
Normal file
146
grype/db/v2/reader/reader.go
Normal 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
|
||||
}
|
3
grype/db/v2/schema_version.go
Normal file
3
grype/db/v2/schema_version.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package v2
|
||||
|
||||
const SchemaVersion = 2
|
14
grype/db/v2/vulnerability.go
Normal file
14
grype/db/v2/vulnerability.go
Normal 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
|
||||
}
|
20
grype/db/v2/vulnerability_metadata.go
Normal file
20
grype/db/v2/vulnerability_metadata.go
Normal 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
|
||||
}
|
14
grype/db/v2/vulnerability_metadata_store.go
Normal file
14
grype/db/v2/vulnerability_metadata_store.go
Normal 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
|
||||
}
|
18
grype/db/v2/vulnerability_store.go
Normal file
18
grype/db/v2/vulnerability_store.go
Normal 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
|
||||
}
|
10
grype/db/v2/writer/log_adapter.go
Normal file
10
grype/db/v2/writer/log_adapter.go
Normal 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...)
|
||||
}
|
57
grype/db/v2/writer/open.go
Normal file
57
grype/db/v2/writer/open.go
Normal 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
|
||||
}
|
205
grype/db/v2/writer/writer.go
Normal file
205
grype/db/v2/writer/writer.go
Normal 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
|
||||
}
|
542
grype/db/v2/writer/writer_test.go
Normal file
542
grype/db/v2/writer/writer_test.go
Normal 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
7
grype/db/v3/advisory.go
Normal 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
16
grype/db/v3/fix.go
Normal 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
28
grype/db/v3/id.go
Normal 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
40
grype/db/v3/model/id.go
Normal 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
|
||||
}
|
115
grype/db/v3/model/vulnerability.go
Normal file
115
grype/db/v3/model/vulnerability.go
Normal 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
|
||||
}
|
87
grype/db/v3/model/vulnerability_metadata.go
Normal file
87
grype/db/v3/model/vulnerability_metadata.go
Normal 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
116
grype/db/v3/namespace.go
Normal 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()
|
||||
}
|
403
grype/db/v3/namespace_test.go
Normal file
403
grype/db/v3/namespace_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
28
grype/db/v3/reader/open.go
Normal file
28
grype/db/v3/reader/open.go
Normal 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
|
||||
}
|
139
grype/db/v3/reader/reader.go
Normal file
139
grype/db/v3/reader/reader.go
Normal 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
|
||||
}
|
3
grype/db/v3/schema_version.go
Normal file
3
grype/db/v3/schema_version.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package v3
|
||||
|
||||
const SchemaVersion = 3
|
19
grype/db/v3/vulnerability.go
Normal file
19
grype/db/v3/vulnerability.go
Normal 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
|
||||
}
|
47
grype/db/v3/vulnerability_metadata.go
Normal file
47
grype/db/v3/vulnerability_metadata.go
Normal 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,
|
||||
}
|
||||
}
|
14
grype/db/v3/vulnerability_metadata_store.go
Normal file
14
grype/db/v3/vulnerability_metadata_store.go
Normal 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
|
||||
}
|
18
grype/db/v3/vulnerability_store.go
Normal file
18
grype/db/v3/vulnerability_store.go
Normal 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
|
||||
}
|
10
grype/db/v3/writer/log_adapter.go
Normal file
10
grype/db/v3/writer/log_adapter.go
Normal 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...)
|
||||
}
|
62
grype/db/v3/writer/open.go
Normal file
62
grype/db/v3/writer/open.go
Normal 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
|
||||
}
|
212
grype/db/v3/writer/writer.go
Normal file
212
grype/db/v3/writer/writer.go
Normal 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
|
||||
}
|
1063
grype/db/v3/writer/writer_test.go
Normal file
1063
grype/db/v3/writer/writer_test.go
Normal file
File diff suppressed because it is too large
Load diff
30
grype/db/vulnerability_metadata_provider.go
Normal file
30
grype/db/vulnerability_metadata_provider.go
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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 {
|
|
@ -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) {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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", "")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{}{}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)...,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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__":
|
||||
|
|
Loading…
Reference in a new issue