diff --git a/grype/db/v6/vulnerability_store.go b/grype/db/v6/vulnerability_store.go index 800ac259a21..8b0a74aa923 100644 --- a/grype/db/v6/vulnerability_store.go +++ b/grype/db/v6/vulnerability_store.go @@ -2,6 +2,7 @@ package v6 import ( "encoding/json" + "errors" "fmt" "strings" "time" @@ -118,7 +119,40 @@ func (s *vulnerabilityStore) AddVulnerabilities(vulnerabilities ...*Vulnerabilit } } - // write the vulnerability handle to the DB + if err := s.addUniqueVulnerability(v); err != nil { + return err + } + } + return nil +} + +func (s *vulnerabilityStore) addUniqueVulnerability(v *VulnerabilityHandle) error { + // if this is a unique (name, status, published, modified, withdrawn, provider_id, blob_id) then it will be created + // otherwise do not create the new entry (this is to prevent duplicates) + query := s.db.Where("name = ? AND status = ? AND provider_id = ? AND blob_id = ?", v.Name, v.Status, v.ProviderID, v.BlobID) + + if v.PublishedDate != nil { + query = query.Where("published_date = ?", *v.PublishedDate) + } else { + query = query.Where("published_date IS NULL") + } + + if v.ModifiedDate != nil { + query = query.Where("modified_date = ?", *v.ModifiedDate) + } else { + query = query.Where("modified_date IS NULL") + } + + if v.WithdrawnDate != nil { + query = query.Where("withdrawn_date = ?", *v.WithdrawnDate) + } else { + query = query.Where("withdrawn_date IS NULL") + } + + if err := query.First(v).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } if err := s.db.Create(v).Error; err != nil { return err } diff --git a/grype/db/v6/vulnerability_store_test.go b/grype/db/v6/vulnerability_store_test.go index f5e6b8f5e88..73cf933bd47 100644 --- a/grype/db/v6/vulnerability_store_test.go +++ b/grype/db/v6/vulnerability_store_test.go @@ -49,6 +49,40 @@ func TestVulnerabilityStore_AddVulnerabilities(t *testing.T) { assert.Nil(t, result1.Provider) // since we're not preloading any fields on the fetch } +func TestVulnerabilityStore_NoDuplicateVulnerabilities(t *testing.T) { + db := setupTestStore(t).db + bw := newBlobStore(db) + s := newVulnerabilityStore(db, bw) + + vuln := VulnerabilityHandle{ + Name: "CVE-1234-5678", + BlobValue: &VulnerabilityBlob{ + ID: "CVE-1234-5678", + }, + Provider: &Provider{ + ID: "provider!", + }, + } + + err := s.AddVulnerabilities(&vuln) + require.NoError(t, err) + + err = s.AddVulnerabilities(&vuln) + require.NoError(t, err) + + var results []VulnerabilityHandle + err = db.Where("name = ?", "CVE-1234-5678").Preload("Provider").Find(&results).Error + require.NoError(t, err) + require.Len(t, results, 1, "expected exactly one vulnerability handle to be added") + + result := results[0] + assert.NotEmpty(t, result.ProviderID) + assert.NotEmpty(t, result.BlobID) + if d := cmp.Diff(vuln, result, cmpopts.IgnoreFields(VulnerabilityHandle{}, "BlobValue")); d != "" { + t.Errorf("unexpected result (-want +got):\n%s", d) + } +} + func TestVulnerabilityStore_AddVulnerabilities_missingModifiedDate(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db)