mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
deduplicate range events
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
6933716613
commit
27b4268022
6 changed files with 360 additions and 80 deletions
12
grype/db/v6/blob.go
Normal file
12
grype/db/v6/blob.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package v6
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/OneOfOne/xxhash"
|
||||
)
|
||||
|
||||
func BlobDigest(content string) string {
|
||||
h := xxhash.New64()
|
||||
h.Write([]byte(content)) // TODO: handle error?
|
||||
return fmt.Sprintf("xx64:%x", h.Sum(nil))
|
||||
}
|
|
@ -6,6 +6,10 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func sp(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func populateFixture1(store v6.Store) error {
|
||||
// Define providers
|
||||
ghsaProvider := &v6.Provider{
|
||||
|
@ -36,15 +40,15 @@ func populateFixture1(store v6.Store) error {
|
|||
{
|
||||
ProviderID: ghsaProvider.ID,
|
||||
Name: "GHSA-xxxx-xxxx-xxxx1",
|
||||
Modified: time.Now().UTC().Format(time.RFC3339),
|
||||
Published: time.Now().UTC().Format(time.RFC3339),
|
||||
SummaryDigest: "sha256:exampledigest1",
|
||||
DetailDigest: "sha256:exampledetaildigest1",
|
||||
Modified: sp(time.Now().UTC().Format(time.RFC3339)),
|
||||
Published: sp(time.Now().UTC().Format(time.RFC3339)),
|
||||
SummaryDigest: sp("sha256:exampledigest1"),
|
||||
DetailDigest: sp("sha256:exampledetaildigest1"),
|
||||
Aliases: &[]v6.Alias{
|
||||
{Alias: "CVE-2024-12341"},
|
||||
},
|
||||
Severities: &[]v6.Severity{
|
||||
{Type: "CVSS_V3", Score: "5.0", Source: "nvd@nist.gov", Priority: "secondary"},
|
||||
{Type: "CVSS_V3", Score: "5.0", Source: sp("nvd@nist.gov"), Priority: sp("secondary")},
|
||||
},
|
||||
References: &[]v6.Reference{
|
||||
{Type: "ADVISORY", URL: "https://example.com/GHSA-xxxx-xxxx-xxxx1"},
|
||||
|
@ -72,15 +76,15 @@ func populateFixture1(store v6.Store) error {
|
|||
{
|
||||
ProviderID: ghsaProvider.ID,
|
||||
Name: "GHSA-xxxx-xxxx-xxxx2",
|
||||
Modified: time.Now().UTC().Format(time.RFC3339),
|
||||
Published: time.Now().UTC().Format(time.RFC3339),
|
||||
SummaryDigest: "sha256:exampledigest2",
|
||||
DetailDigest: "sha256:exampledetaildigest2",
|
||||
Modified: sp(time.Now().UTC().Format(time.RFC3339)),
|
||||
Published: sp(time.Now().UTC().Format(time.RFC3339)),
|
||||
SummaryDigest: sp("sha256:exampledigest2"),
|
||||
DetailDigest: sp("sha256:exampledetaildigest2"),
|
||||
Aliases: &[]v6.Alias{
|
||||
{Alias: "CVE-2024-12342"},
|
||||
},
|
||||
Severities: &[]v6.Severity{
|
||||
{Type: "CVSS_V3", Score: "7.5", Source: "nvd@nist.gov", Priority: "primary"},
|
||||
{Type: "CVSS_V3", Score: "7.5", Source: sp("nvd@nist.gov"), Priority: sp("primary")},
|
||||
},
|
||||
References: &[]v6.Reference{
|
||||
{Type: "ADVISORY", URL: "https://example.com/GHSA-xxxx-xxxx-xxxx2"},
|
||||
|
@ -113,15 +117,15 @@ func populateFixture1(store v6.Store) error {
|
|||
{
|
||||
ProviderID: ubuntuProvider.ID,
|
||||
Name: "CVE-2024-12343",
|
||||
Modified: time.Now().UTC().Format(time.RFC3339),
|
||||
Published: time.Now().UTC().Format(time.RFC3339),
|
||||
SummaryDigest: "sha256:exampledigest3",
|
||||
DetailDigest: "sha256:exampledetaildigest3",
|
||||
Modified: sp(time.Now().UTC().Format(time.RFC3339)),
|
||||
Published: sp(time.Now().UTC().Format(time.RFC3339)),
|
||||
SummaryDigest: sp("sha256:exampledigest3"),
|
||||
DetailDigest: sp("sha256:exampledetaildigest3"),
|
||||
Aliases: &[]v6.Alias{
|
||||
{Alias: "GHSA-xxxx-xxxx-xxxx3"},
|
||||
},
|
||||
Severities: &[]v6.Severity{
|
||||
{Type: "CVSS_V3", Score: "6.0", Source: "nvd@nist.gov", Priority: "primary"},
|
||||
{Type: "CVSS_V3", Score: "6.0", Source: sp("nvd@nist.gov"), Priority: sp("primary")},
|
||||
},
|
||||
References: &[]v6.Reference{
|
||||
{Type: "ADVISORY", URL: "https://example.com/CVE-2024-12343"},
|
||||
|
@ -157,15 +161,15 @@ func populateFixture1(store v6.Store) error {
|
|||
vulnerabilities = append(vulnerabilities, &v6.Vulnerability{
|
||||
ProviderID: ghsaProvider.ID,
|
||||
Name: fmt.Sprintf("GHSA-xxxx-xxxx-xxxx%d", i+3),
|
||||
Modified: time.Now().UTC().Format(time.RFC3339),
|
||||
Published: time.Now().UTC().Format(time.RFC3339),
|
||||
SummaryDigest: fmt.Sprintf("sha256:exampledigest%d", i+3),
|
||||
DetailDigest: fmt.Sprintf("sha256:exampledetaildigest%d", i+3),
|
||||
Modified: sp(time.Now().UTC().Format(time.RFC3339)),
|
||||
Published: sp(time.Now().UTC().Format(time.RFC3339)),
|
||||
SummaryDigest: sp(fmt.Sprintf("sha256:exampledigest%d", i+3)),
|
||||
DetailDigest: sp(fmt.Sprintf("sha256:exampledetaildigest%d", i+3)),
|
||||
Aliases: &[]v6.Alias{
|
||||
{Alias: fmt.Sprintf("CVE-2024-1234%d", i+3)},
|
||||
},
|
||||
Severities: &[]v6.Severity{
|
||||
{Type: "CVSS_V3", Score: "4.0", Source: "nvd@nist.gov", Priority: "primary"},
|
||||
{Type: "CVSS_V3", Score: "4.0", Source: sp("nvd@nist.gov"), Priority: sp("primary")},
|
||||
},
|
||||
References: &[]v6.Reference{
|
||||
{Type: "ADVISORY", URL: fmt.Sprintf("https://example.com/GHSA-xxxx-xxxx-xxxx%d", i+3)},
|
||||
|
|
|
@ -27,7 +27,7 @@ func All() []any {
|
|||
&Package{},
|
||||
&Provider{},
|
||||
&RangeEvent{},
|
||||
&RangeEventMetadata{},
|
||||
//&RangeEventMetadata{},
|
||||
&Range{},
|
||||
&Reference{},
|
||||
&Related{},
|
||||
|
@ -53,19 +53,19 @@ type Vulnerability struct {
|
|||
Name string `gorm:"column:name;not null;index;index:idx_vulnerability_provider"`
|
||||
|
||||
// Modified is the time the entry was last modified, as an RFC3339-formatted timestamp in UTC (ending in “Z”) (mirrors the OSV field)
|
||||
Modified string `gorm:"column:modified"`
|
||||
Modified *string `gorm:"column:modified"`
|
||||
|
||||
// Published is the time the entry should be considered to have been published, as an RFC3339-formatted time stamp in UTC (ending in “Z”) (mirrors the OSV field)
|
||||
Published string `gorm:"column:published"`
|
||||
Published *string `gorm:"column:published"`
|
||||
|
||||
// Withdrawn is the time the entry should be considered to have been withdrawn, as an RFC3339-formatted timestamp in UTC (ending in “Z”) (mirrors the OSV field)
|
||||
Withdrawn string `gorm:"column:withdrawn"`
|
||||
Withdrawn *string `gorm:"column:withdrawn"`
|
||||
|
||||
// SummaryDigest is a self describing hash (e.g. sha256:123... not 123...) of the summary field from the OSV summary field. This digest is searched against the Blob table/DB.
|
||||
SummaryDigest string `gorm:"column:summary_digest"`
|
||||
SummaryDigest *string `gorm:"column:summary_digest"`
|
||||
|
||||
// DetailDigest is a self describing hash (e.g. sha256:123... not 123...) of the detail field from the OSV summary field. This digest is searched against the Blob table/DB.
|
||||
DetailDigest string `gorm:"column:detail_digest"`
|
||||
DetailDigest *string `gorm:"column:detail_digest"`
|
||||
|
||||
// References are URLs to external resources that provide more information about the vulnerability (mirrors the OSV field)
|
||||
References *[]Reference `gorm:"foreignKey:VulnerabilityID"`
|
||||
|
@ -117,7 +117,7 @@ type Alias struct {
|
|||
ID int64 `gorm:"column:id;primaryKey"`
|
||||
//VulnerabilityID int64 `gorm:"column:vulnerability_id;not null"`
|
||||
|
||||
Alias string `gorm:"column:alias;not null,index:idx_alias"`
|
||||
Alias string `gorm:"column:alias;not null,index:idx_alias,unique"`
|
||||
}
|
||||
|
||||
// Related represents a single related vulnerability name
|
||||
|
@ -126,7 +126,7 @@ type Related struct {
|
|||
//VulnerabilityID int64 `gorm:"column:vulnerability_id;not null"`
|
||||
|
||||
// Name of the related vulnerability (e.g. CVE-2024-34102 or GHSA-85rg-8m6h-825p)
|
||||
Name string `gorm:"column:name;not null,index:idx_related"`
|
||||
Name string `gorm:"column:name;not null,index:idx_related,unique"`
|
||||
|
||||
//// Reason is a free-form text field that describes the relationship between the two vulnerabilities ("CVE-2022-12345 might be related to CVE-2022-54321 because both affect the same software library but are distinct issues")
|
||||
//Reason string `gorm:"column:reason"`
|
||||
|
@ -143,10 +143,10 @@ type Severity struct {
|
|||
Score string `gorm:"column:score;not null"`
|
||||
|
||||
// Source is the name of the source of the severity score (e.g. "nvd@nist.gov" or "security-advisories@github.com")
|
||||
Source string `gorm:"column:source"`
|
||||
Source *string `gorm:"column:source"`
|
||||
|
||||
// Priority is a free-form organizational field to convey priority over other severities (e.g. primary vs secondary or authoritative vs unverified)
|
||||
Priority string `gorm:"column:priority"` // TODO: naming is hard...
|
||||
Priority *string `gorm:"column:priority"` // TODO: naming is hard...
|
||||
}
|
||||
|
||||
type Reference struct {
|
||||
|
@ -210,10 +210,10 @@ type AffectedSeverity struct {
|
|||
ID int64 `gorm:"column:id;primaryKey"`
|
||||
AffectedID int64 `gorm:"column:affected_id;not null"`
|
||||
|
||||
Type string `gorm:"column:type;not null"`
|
||||
Score string `gorm:"column:score;not null"`
|
||||
Source string `gorm:"column:source"`
|
||||
Tag string `gorm:"column:tag"`
|
||||
Type string `gorm:"column:type;not null"`
|
||||
Score string `gorm:"column:score;not null"`
|
||||
Source *string `gorm:"column:source"`
|
||||
Priority *string `gorm:"column:priority"` // TODO: naming is hard...
|
||||
}
|
||||
|
||||
type AffectedVersion struct {
|
||||
|
@ -235,35 +235,48 @@ type Range struct {
|
|||
AffectedID int64 `gorm:"column:affected_id;not null"`
|
||||
|
||||
Type string `gorm:"column:type;not null"`
|
||||
Repo string `gorm:"column:repo"`
|
||||
Events *[]RangeEvent `gorm:"foreignKey:RangeID"`
|
||||
Repo *string `gorm:"column:repo"`
|
||||
Events *[]RangeEvent `gorm:"many2many:range_range_events"`
|
||||
}
|
||||
|
||||
type RangeEvent struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
RangeID int64 `gorm:"column:range_id;not null"`
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
|
||||
Introduced string `gorm:"column:introduced"`
|
||||
Fixed string `gorm:"column:fixed"`
|
||||
LastAffected string `gorm:"column:last_affected"`
|
||||
Limit string `gorm:"column:limit"`
|
||||
Introduced *string `gorm:"column:introduced;index:idx_range_event,unique"`
|
||||
Fixed *string `gorm:"column:fixed;index:idx_range_event,unique"`
|
||||
LastAffected *string `gorm:"column:last_affected;index:idx_range_event,unique"`
|
||||
Limit *string `gorm:"column:range_limit;index:idx_range_event,unique"` // limit is a keyword in sql, so it's easier to just use range_limit instead
|
||||
|
||||
// non OSV...
|
||||
State string `gorm:"column:state"` // TODO: this could be db specific since there will be multiple ways to represent/interpret this
|
||||
State string `gorm:"column:state;index:idx_range_event,unique"` // TODO: this could be db specific since there will be multiple ways to represent/interpret this
|
||||
|
||||
RangeEventMetadata []RangeEventMetadata `gorm:"foreignKey:RangeEventID"`
|
||||
// if deduplicating these, then this can't be associated
|
||||
//RangeEventMetadata *[]RangeEventMetadata `gorm:"foreignKey:RangeEventID"`
|
||||
}
|
||||
|
||||
type RangeEventMetadata struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
RangeEventID int64 `gorm:"column:range_event_id;not null"`
|
||||
func (re *RangeEvent) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
//tx = tx.Session(&gorm.Session{Logger: loggerIgnoreRecordNotFound{tx.Logger}})
|
||||
|
||||
GitCommit string `gorm:"column:git_commit"`
|
||||
PullRequestURL string `gorm:"column:pull_request_url"`
|
||||
Timestamp string `gorm:"column:timestamp"`
|
||||
// TODO: what else here?
|
||||
// if the event already exist in the table then we should not insert a new record
|
||||
var existing RangeEvent
|
||||
result := tx.Where("introduced = ? AND fixed = ? AND last_affected = ? AND range_limit = ? AND state = ?", re.Introduced, re.Fixed, re.LastAffected, re.Limit, re.State).First(&existing)
|
||||
if result.Error == nil {
|
||||
// if the record already exists, then we should use the existing record
|
||||
*re = existing
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//type RangeEventMetadata struct {
|
||||
// ID int64 `gorm:"primaryKey"`
|
||||
// RangeEventID int64 `gorm:"column:range_event_id;not null"`
|
||||
//
|
||||
// GitCommit *string `gorm:"column:git_commit"`
|
||||
// PullRequestURL *string `gorm:"column:pull_request_url"`
|
||||
// Timestamp *string `gorm:"column:timestamp"`
|
||||
// // TODO: what else here?
|
||||
//}
|
||||
|
||||
// primary package identifiers (search entrypoints)
|
||||
|
||||
type Cpe struct {
|
||||
|
@ -271,17 +284,28 @@ type Cpe struct {
|
|||
|
||||
ID int64 `gorm:"column:id;primaryKey"`
|
||||
|
||||
Schema string `gorm:"column:schema;not null"` // effectively the CPE version
|
||||
Type string `gorm:"column:type;not null"`
|
||||
Vendor string `gorm:"column:vendor"`
|
||||
Product string `gorm:"column:product;not null"`
|
||||
Version string `gorm:"column:version"`
|
||||
Update string `gorm:"column:update"`
|
||||
TargetSoftware string `gorm:"column:target_software"`
|
||||
Schema string `gorm:"column:schema;not null;index:idx_cpe"` // effectively the CPE version
|
||||
Type string `gorm:"column:type;not null;index:idx_cpe"`
|
||||
Vendor *string `gorm:"column:vendor;index:idx_cpe"`
|
||||
Product string `gorm:"column:product;not null;index:idx_cpe"`
|
||||
Version *string `gorm:"column:version;index:idx_cpe"`
|
||||
Update *string `gorm:"column:version_update;index:idx_cpe"` // update is a SQL keyword
|
||||
TargetSoftware *string `gorm:"column:target_software;index:idx_cpe"`
|
||||
|
||||
// TODO: should we also have the remaining CPE fields here?
|
||||
}
|
||||
|
||||
func (c *Cpe) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
// if the name, major version, and minor version already exist in the table then we should not insert a new record
|
||||
var existing Cpe
|
||||
result := tx.Where("schema = ? AND type = ? AND vendor = ? AND product = ? AND version = ? AND version_update = ? AND target_software = ?", c.Schema, c.Type, c.Vendor, c.Product, c.Version, c.Update, c.TargetSoftware).First(&existing)
|
||||
if result.Error == nil {
|
||||
// if the record already exists, then we should use the existing record
|
||||
*c = existing
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Digest represents arbitrary digests that can be associated with a vulnerability such that if found the material can be considered to be affected by this vulnerability
|
||||
type Digest struct {
|
||||
ID int64 `gorm:"column:id;primaryKey"`
|
||||
|
@ -296,7 +320,7 @@ type Package struct {
|
|||
ID int64 `gorm:"column:id;primaryKey"`
|
||||
|
||||
// TODO: break purl out into fields here
|
||||
Ecosystem string `gorm:"column:ecosystem"`
|
||||
Ecosystem string `gorm:"column:ecosystem"` // TODO: NVD doesn't have this, should this be nullable?
|
||||
PackageName string `gorm:"column:package_name;index:package_name"`
|
||||
|
||||
OperatingSystemID *int64 `gorm:"column:operating_system_id"`
|
||||
|
@ -316,12 +340,12 @@ type Package struct {
|
|||
type Purl struct {
|
||||
ID int64 `gorm:"column:id;primaryKey"`
|
||||
|
||||
Scheme string `gorm:"column:scheme"`
|
||||
Type string `gorm:"column:type"`
|
||||
Namespace string `gorm:"column:namespace"`
|
||||
Name string `gorm:"column:name"`
|
||||
Version string `gorm:"column:version"`
|
||||
SubPath string `gorm:"column:subpath"`
|
||||
Scheme string `gorm:"column:scheme"`
|
||||
Type string `gorm:"column:type"`
|
||||
Namespace *string `gorm:"column:namespace"`
|
||||
Name string `gorm:"column:name"`
|
||||
Version string `gorm:"column:version"`
|
||||
SubPath *string `gorm:"column:subpath"`
|
||||
|
||||
Qualifiers *[]Qualifier `gorm:"many2many:purl_qualifiers"`
|
||||
}
|
||||
|
@ -346,11 +370,11 @@ type OperatingSystem struct {
|
|||
|
||||
func (os *OperatingSystem) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
// if the name, major version, and minor version already exist in the table then we should not insert a new record
|
||||
var existingOs OperatingSystem
|
||||
result := tx.Where("name = ? AND major_version = ? AND minor_version = ?", os.Name, os.MajorVersion, os.MinorVersion).First(&existingOs)
|
||||
var existing OperatingSystem
|
||||
result := tx.Where("name = ? AND major_version = ? AND minor_version = ?", os.Name, os.MajorVersion, os.MinorVersion).First(&existing)
|
||||
if result.Error == nil {
|
||||
// if the record already exists, then we should use the existing record
|
||||
*os = existingOs
|
||||
*os = existing
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -4,22 +4,64 @@ import (
|
|||
"fmt"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
db *gorm.DB
|
||||
preloadCache *preloadCache
|
||||
destination string
|
||||
write bool
|
||||
useMem bool
|
||||
}
|
||||
|
||||
//type loggerIgnoreRecordNotFound struct {
|
||||
// logger.Interface
|
||||
//}
|
||||
//
|
||||
//func (c loggerIgnoreRecordNotFound) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||
// if msg == "record not found" {
|
||||
// // Suppress "record not found" messages
|
||||
// return
|
||||
// }
|
||||
// c.Interface.Error(ctx, msg, data...)
|
||||
//}
|
||||
//
|
||||
//func (c loggerIgnoreRecordNotFound) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||
// if msg == "record not found" {
|
||||
// // Suppress "record not found" messages
|
||||
// return
|
||||
// }
|
||||
// c.Interface.Warn(ctx, msg, data...)
|
||||
//}
|
||||
|
||||
func newState(dbFilePath string, overwrite bool) (*state, error) {
|
||||
//db, err := gormadapter.Open(dbFilePath, overwrite)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(dbFilePath), &gorm.Config{})
|
||||
lgr := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
LogLevel: logger.Warn,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: true,
|
||||
})
|
||||
|
||||
useMem := true
|
||||
location := dbFilePath
|
||||
if useMem {
|
||||
location = "file::memory:?cache=shared"
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(location), &gorm.Config{
|
||||
Logger: lgr,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Failed to connect to database:", err)
|
||||
return nil, err
|
||||
|
@ -31,21 +73,34 @@ func newState(dbFilePath string, overwrite bool) (*state, error) {
|
|||
if err := db.AutoMigrate(All()...); err != nil {
|
||||
return nil, fmt.Errorf("unable to migrate: %w", err)
|
||||
}
|
||||
} else {
|
||||
// optimize read-only access
|
||||
db.Exec("PRAGMA synchronous = OFF")
|
||||
db.Exec("PRAGMA journal_mode = OFF")
|
||||
db.Exec("PRAGMA temp_store = MEMORY")
|
||||
db.Exec("PRAGMA cache_size = 100000")
|
||||
db.Exec("PRAGMA mmap_size = 268435456") // 256 MB
|
||||
}
|
||||
|
||||
db.Exec("PRAGMA synchronous = OFF")
|
||||
db.Exec("PRAGMA journal_mode = OFF")
|
||||
db.Exec("PRAGMA temp_store = MEMORY")
|
||||
db.Exec("PRAGMA cache_size = 100000")
|
||||
db.Exec("PRAGMA mmap_size = 268435456") // 256 MB
|
||||
db.Exec("PRAGMA auto_vacuum = NONE")
|
||||
|
||||
return &state{
|
||||
db: db,
|
||||
preloadCache: newPreloadCache(),
|
||||
destination: dbFilePath,
|
||||
write: overwrite,
|
||||
useMem: useMem,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *state) close() error {
|
||||
if s.write {
|
||||
if s.useMem {
|
||||
return s.db.Exec(fmt.Sprintf("VACUUM main into %q", s.destination)).Error
|
||||
}
|
||||
return s.db.Exec("VACUUM").Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *state) getPreloadableFields(model interface{}) []string {
|
||||
t := reflect.TypeOf(model).Elem()
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package v6
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"io"
|
||||
"path/filepath"
|
||||
)
|
||||
|
@ -55,6 +57,7 @@ func (c *StoreConfig) DBFilePath() string {
|
|||
// New creates a new instance of the Store.
|
||||
func New(cfg StoreConfig) (Store, error) {
|
||||
return &store{
|
||||
cfg: &cfg,
|
||||
ProviderStore: NewProviderStore(&cfg),
|
||||
OperatingSystemStore: NewOperatingSystemStore(&cfg),
|
||||
AffectedStore: NewAffectedStore(&cfg),
|
||||
|
@ -64,5 +67,7 @@ func New(cfg StoreConfig) (Store, error) {
|
|||
}
|
||||
|
||||
func (s store) Close() error {
|
||||
return nil
|
||||
log.Debug("closing store")
|
||||
st := s.cfg.state()
|
||||
return st.db.Exec(fmt.Sprintf("VACUUM main into %q", st.destination)).Error
|
||||
}
|
||||
|
|
|
@ -41,16 +41,193 @@ func (s *vulnerabilityStore) GetVulnerability(id string, loadAuxInfo bool) ([]Vu
|
|||
}
|
||||
|
||||
func (s *vulnerabilityStore) AddVulnerabilities(vulnerabilities ...*Vulnerability) error {
|
||||
for _, h := range []func([]*Vulnerability) error{
|
||||
s.handleOSs,
|
||||
s.handleRangeEvents,
|
||||
s.handleCPEs,
|
||||
} {
|
||||
if err := h(vulnerabilities); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return s.db.CreateInBatches(vulnerabilities, s.BatchSize).Error
|
||||
}
|
||||
|
||||
func (s *vulnerabilityStore) handleCPEs(vulns []*Vulnerability) error {
|
||||
// ensure unique operating systems
|
||||
uniqueOSList, err := ensureUniqueOperatingSystems(s.db, vulnerabilities)
|
||||
unique, err := ensureUniqueCPEs(s.db, vulns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update vulnerabilities with operating system IDs
|
||||
updateVulnerabilitiesWithOperatingSystems(vulnerabilities, uniqueOSList)
|
||||
updateVulnerabilitiesWithCPEs(vulns, unique)
|
||||
|
||||
return s.db.CreateInBatches(vulnerabilities, 100).Error
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateVulnerabilitiesWithCPEs(vulns []*Vulnerability, uniqueCPEs []*Cpe) {
|
||||
cpeMap := make(map[string]int64)
|
||||
for _, c := range uniqueCPEs {
|
||||
cpeKey := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", c.Schema, c.Type, strVal(c.Vendor), c.Product, strVal(c.Version), strVal(c.Update), strVal(c.TargetSoftware))
|
||||
cpeMap[cpeKey] = c.ID
|
||||
}
|
||||
|
||||
for i, v := range vulns {
|
||||
if v.Affected == nil {
|
||||
continue
|
||||
}
|
||||
for j, a := range *v.Affected {
|
||||
if a.Package.Cpes == nil {
|
||||
continue
|
||||
}
|
||||
for k, c := range *a.Package.Cpes {
|
||||
cpeKey := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", c.Schema, c.Type, strVal(c.Vendor), c.Product, strVal(c.Version), strVal(c.Update), strVal(c.TargetSoftware))
|
||||
val := cpeMap[cpeKey]
|
||||
(*(*vulns[i].Affected)[j].Package.Cpes)[k].ID = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureUniqueCPEs(db *gorm.DB, vulns []*Vulnerability) ([]*Cpe, error) {
|
||||
cpeMap := make(map[string]Cpe)
|
||||
for _, v := range vulns {
|
||||
if v.Affected == nil {
|
||||
continue
|
||||
}
|
||||
for _, a := range *v.Affected {
|
||||
if a.Package.Cpes == nil {
|
||||
continue
|
||||
}
|
||||
for _, c := range *a.Package.Cpes {
|
||||
cpeKey := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", c.Schema, c.Type, strVal(c.Vendor), c.Product, strVal(c.Version), strVal(c.Update), strVal(c.TargetSoftware))
|
||||
cpeMap[cpeKey] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extract unique CPEs
|
||||
var uniqueCPEs []*Cpe
|
||||
for i := range cpeMap {
|
||||
c := cpeMap[i]
|
||||
uniqueCPEs = append(uniqueCPEs, &c)
|
||||
}
|
||||
|
||||
// insert unique CPEs into the database or fetch existing ones
|
||||
for i, c := range uniqueCPEs {
|
||||
var existing Cpe
|
||||
err := db.Where("schema = ? AND type = ? AND vendor = ? AND product = ? AND version = ? AND version_update = ? AND target_software = ?", c.Schema, c.Type, c.Vendor, c.Product, c.Version, c.Update, c.TargetSoftware).
|
||||
FirstOrCreate(&existing, c).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uniqueCPEs[i] = &existing
|
||||
}
|
||||
|
||||
return uniqueCPEs, nil
|
||||
}
|
||||
|
||||
func (s *vulnerabilityStore) handleRangeEvents(vulns []*Vulnerability) error {
|
||||
// ensure unique operating systems
|
||||
unique, err := ensureUniqueRangeEvents(s.db, vulns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update vulnerabilities with operating system IDs
|
||||
updateVulnerabilitiesWithRanges(vulns, unique)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureUniqueRangeEvents(db *gorm.DB, vulns []*Vulnerability) ([]*RangeEvent, error) {
|
||||
reMap := make(map[string]RangeEvent)
|
||||
for _, v := range vulns {
|
||||
for _, a := range *v.Affected {
|
||||
if a.Range == nil {
|
||||
continue
|
||||
}
|
||||
for _, r := range *a.Range {
|
||||
if r.Events == nil {
|
||||
continue
|
||||
}
|
||||
for _, re := range *r.Events {
|
||||
reKey := fmt.Sprintf("%s:%s:%s:%s:%s", strVal(re.Introduced), strVal(re.Fixed), strVal(re.LastAffected), strVal(re.Limit), re.State)
|
||||
reMap[reKey] = re
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extract unique range events
|
||||
var uniqueRangeEvents []*RangeEvent
|
||||
for i := range reMap {
|
||||
re := reMap[i]
|
||||
uniqueRangeEvents = append(uniqueRangeEvents, &re)
|
||||
}
|
||||
|
||||
// insert unique range events into the database or fetch existing ones
|
||||
for i, re := range uniqueRangeEvents {
|
||||
var existing RangeEvent
|
||||
err := db.Where("introduced = ? AND fixed = ? AND last_affected = ? AND range_limit = ? AND state = ?", re.Introduced, re.Fixed, re.LastAffected, re.Limit, re.State).
|
||||
FirstOrCreate(&existing, re).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uniqueRangeEvents[i] = &existing
|
||||
|
||||
}
|
||||
|
||||
return uniqueRangeEvents, nil
|
||||
}
|
||||
|
||||
func updateVulnerabilitiesWithRanges(vulns []*Vulnerability, uniqueRangeEvents []*RangeEvent) {
|
||||
reMap := make(map[string]int64)
|
||||
for _, re := range uniqueRangeEvents {
|
||||
reKey := fmt.Sprintf("%s:%s:%s:%s:%s", strVal(re.Introduced), strVal(re.Fixed), strVal(re.LastAffected), strVal(re.Limit), re.State)
|
||||
reMap[reKey] = re.ID
|
||||
}
|
||||
|
||||
for i, v := range vulns {
|
||||
for j, a := range *v.Affected {
|
||||
if a.Range == nil {
|
||||
continue
|
||||
}
|
||||
for k, r := range *a.Range {
|
||||
if r.Events == nil {
|
||||
continue
|
||||
}
|
||||
for l, re := range *r.Events {
|
||||
reKey := fmt.Sprintf("%s:%s:%s:%s:%s", strVal(re.Introduced), strVal(re.Fixed), strVal(re.LastAffected), strVal(re.Limit), re.State)
|
||||
val := reMap[reKey]
|
||||
(*(*(*vulns[i].Affected)[j].Range)[k].Events)[l].ID = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func strVal(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func (s *vulnerabilityStore) handleOSs(vulns []*Vulnerability) error {
|
||||
// ensure unique operating systems
|
||||
uniqueOSList, err := ensureUniqueOperatingSystems(s.db, vulns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update vulnerabilities with operating system IDs
|
||||
updateVulnerabilitiesWithOperatingSystems(vulns, uniqueOSList)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureUniqueOperatingSystems(db *gorm.DB, vulns []*Vulnerability) ([]*OperatingSystem, error) {
|
||||
|
@ -95,6 +272,9 @@ func updateVulnerabilitiesWithOperatingSystems(vulns []*Vulnerability, uniqueOSL
|
|||
|
||||
for i, v := range vulns {
|
||||
for j, a := range *v.Affected {
|
||||
if a.Package == nil || a.Package.OperatingSystem == nil {
|
||||
continue
|
||||
}
|
||||
osKey := fmt.Sprintf("%s:%s:%s", a.Package.OperatingSystem.Name, a.Package.OperatingSystem.MajorVersion, a.Package.OperatingSystem.MinorVersion)
|
||||
val := osMap[osKey]
|
||||
(*vulns[i].Affected)[j].Package.OperatingSystemID = &val
|
||||
|
|
Loading…
Reference in a new issue