Skip to content

Commit

Permalink
Add affected CPE store (#2258)
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman authored Nov 14, 2024
1 parent 2067035 commit 4e6f371
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 29 deletions.
106 changes: 106 additions & 0 deletions grype/db/v6/affected_cpe_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package v6

import (
"encoding/json"
"fmt"

"gorm.io/gorm"

"github.com/anchore/grype/internal/log"
)

type AffectedCPEStoreWriter interface {
AddAffectedCPEs(packages ...*AffectedCPEHandle) error
}

type AffectedCPEStoreReader interface {
GetCPEsByProduct(packageName string, config *GetAffectedCPEOptions) ([]AffectedCPEHandle, error)
}

type GetAffectedCPEOptions struct {
PreloadCPE bool
PreloadBlob bool
}

type affectedCPEStore struct {
db *gorm.DB
blobStore *blobStore
}

func newAffectedCPEStore(db *gorm.DB, bs *blobStore) *affectedCPEStore {
return &affectedCPEStore{
db: db,
blobStore: bs,
}
}

// AddAffectedCPEs adds one or more affected CPEs to the store
func (s *affectedCPEStore) AddAffectedCPEs(packages ...*AffectedCPEHandle) error {
for _, pkg := range packages {
if err := s.blobStore.addBlobable(pkg); err != nil {
return fmt.Errorf("unable to add affected package blob: %w", err)
}

if err := s.db.Create(pkg).Error; err != nil {
return fmt.Errorf("unable to add affected CPE: %w", err)
}
}
return nil
}

// GetCPEsByProduct retrieves a single AffectedCPEHandle by product name
func (s *affectedCPEStore) GetCPEsByProduct(packageName string, config *GetAffectedCPEOptions) ([]AffectedCPEHandle, error) {
if config == nil {
config = &GetAffectedCPEOptions{}
}

log.WithFields("product", packageName).Trace("fetching AffectedCPE record")

var pkgs []AffectedCPEHandle
query := s.db.
Joins("JOIN cpes ON cpes.id = affected_cpe_handles.cpe_id").
Where("cpes.product = ?", packageName)

query = s.handlePreload(query, *config)

err := query.Find(&pkgs).Error
if err != nil {
return nil, fmt.Errorf("unable to fetch affected package record: %w", err)
}

if config.PreloadBlob {
for i := range pkgs {
err := s.attachBlob(&pkgs[i])
if err != nil {
return nil, fmt.Errorf("unable to attach blob %#v: %w", pkgs[i], err)
}
}
}

return pkgs, nil
}

func (s *affectedCPEStore) handlePreload(query *gorm.DB, config GetAffectedCPEOptions) *gorm.DB {
if config.PreloadCPE {
query = query.Preload("CPE")
}

return query
}

// attachBlob attaches the BlobValue to the AffectedCPEHandle
func (s *affectedCPEStore) attachBlob(cpe *AffectedCPEHandle) error {
var blobValue *AffectedPackageBlob

rawValue, err := s.blobStore.getBlobValue(cpe.BlobID)
if err != nil {
return fmt.Errorf("unable to fetch blob value for affected CPE: %w", err)
}

if err := json.Unmarshal([]byte(rawValue), &blobValue); err != nil {
return fmt.Errorf("unable to unmarshal blob value: %w", err)
}

cpe.BlobValue = blobValue
return nil
}
100 changes: 100 additions & 0 deletions grype/db/v6/affected_cpe_store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package v6

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAffectedCPEStore_AddAffectedCPEs(t *testing.T) {
db := setupTestDB(t)
bw := newBlobStore(db)
s := newAffectedCPEStore(db, bw)

cpe1 := &AffectedCPEHandle{
VulnerabilityID: 1,
CpeID: 1,
CPE: &Cpe{
Type: "a",
Vendor: "vendor-1",
Product: "product-1",
Edition: "edition-1",
},
BlobValue: &AffectedPackageBlob{
CVEs: []string{"CVE-2023-5678"},
},
}

cpe2 := testAffectedCPEHandle()

err := s.AddAffectedCPEs(cpe1, cpe2)
require.NoError(t, err)

var result1 AffectedCPEHandle
err = db.Where("cpe_id = ?", 1).First(&result1).Error
require.NoError(t, err)
assert.Equal(t, cpe1.VulnerabilityID, result1.VulnerabilityID)
assert.Equal(t, cpe1.ID, result1.ID)
assert.Equal(t, cpe1.BlobID, result1.BlobID)
assert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch

var result2 AffectedCPEHandle
err = db.Where("cpe_id = ?", 2).First(&result2).Error
require.NoError(t, err)
assert.Equal(t, cpe2.VulnerabilityID, result2.VulnerabilityID)
assert.Equal(t, cpe2.ID, result2.ID)
assert.Equal(t, cpe2.BlobID, result2.BlobID)
assert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch
}

func TestAffectedCPEStore_GetCPEsByProduct(t *testing.T) {
db := setupTestDB(t)
bw := newBlobStore(db)
s := newAffectedCPEStore(db, bw)

cpe := testAffectedCPEHandle()
err := s.AddAffectedCPEs(cpe)
require.NoError(t, err)

results, err := s.GetCPEsByProduct(cpe.CPE.Product, nil)
require.NoError(t, err)

expected := []AffectedCPEHandle{*cpe}
require.Len(t, results, len(expected))
result := results[0]
assert.Equal(t, cpe.CpeID, result.CpeID)
assert.Equal(t, cpe.ID, result.ID)
assert.Equal(t, cpe.BlobID, result.BlobID)
require.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch

// fetch again with blob & cpe preloaded
results, err = s.GetCPEsByProduct(cpe.CPE.Product, &GetAffectedCPEOptions{PreloadCPE: true, PreloadBlob: true})
require.NoError(t, err)
require.Len(t, results, len(expected))
result = results[0]
assert.NotNil(t, result.BlobValue)
if d := cmp.Diff(*cpe, result); d != "" {
t.Errorf("unexpected result (-want +got):\n%s", d)
}
}

func testAffectedCPEHandle() *AffectedCPEHandle {
return &AffectedCPEHandle{
CPE: &Cpe{
Type: "application",
Vendor: "vendor",
Product: "product",
Edition: "edition",
Language: "language",
SoftwareEdition: "software_edition",
TargetHardware: "target_hardware",
TargetSoftware: "target_software",
Other: "other",
},
BlobValue: &AffectedPackageBlob{
CVEs: []string{"CVE-2024-4321"},
},
}
}
28 changes: 14 additions & 14 deletions grype/db/v6/affected_package_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
var NoDistroSpecified = &DistroSpecifier{}
var AnyDistroSpecified *DistroSpecifier

type GetAffectedOptions struct {
type GetAffectedPackageOptions struct {
PreloadOS bool
PreloadPackage bool
PreloadBlob bool
Expand All @@ -32,7 +32,7 @@ type AffectedPackageStoreWriter interface {
}

type AffectedPackageStoreReader interface {
GetAffectedPackagesByName(packageName string, config *GetAffectedOptions) ([]AffectedPackageHandle, error)
GetAffectedPackagesByName(packageName string, config *GetAffectedPackageOptions) ([]AffectedPackageHandle, error)
}

type affectedPackageStore struct {
Expand Down Expand Up @@ -68,9 +68,9 @@ func (s *affectedPackageStore) AddAffectedPackages(packages ...*AffectedPackageH
return nil
}

func (s *affectedPackageStore) GetAffectedPackagesByName(packageName string, config *GetAffectedOptions) ([]AffectedPackageHandle, error) {
func (s *affectedPackageStore) GetAffectedPackagesByName(packageName string, config *GetAffectedPackageOptions) ([]AffectedPackageHandle, error) {
if config == nil {
config = &GetAffectedOptions{}
config = &GetAffectedPackageOptions{}
}

log.WithFields("name", packageName, "distro", distroDisplay(config.Distro)).Trace("fetching AffectedPackage record")
Expand All @@ -82,16 +82,16 @@ func (s *affectedPackageStore) GetAffectedPackagesByName(packageName string, con
return s.getNonDistroPackageByName(packageName, *config)
}

func (s *affectedPackageStore) getNonDistroPackageByName(packageName string, config GetAffectedOptions) ([]AffectedPackageHandle, error) {
func (s *affectedPackageStore) getNonDistroPackageByName(packageName string, config GetAffectedPackageOptions) ([]AffectedPackageHandle, error) {
var pkgs []AffectedPackageHandle
query := s.db.Joins("JOIN packages ON affected_package_handles.package_id = packages.id")

if config.Distro != AnyDistroSpecified {
query = query.Where("operating_system_id IS NULL")
}

query = handlePacakge(query, packageName, config)
query = handlePreload(query, config)
query = s.handlePacakge(query, packageName, config)
query = s.handlePreload(query, config)

err := query.Find(&pkgs).Error

Expand All @@ -111,14 +111,14 @@ func (s *affectedPackageStore) getNonDistroPackageByName(packageName string, con
return pkgs, nil
}

func (s *affectedPackageStore) getPackageByNameAndDistro(packageName string, config GetAffectedOptions) ([]AffectedPackageHandle, error) {
func (s *affectedPackageStore) getPackageByNameAndDistro(packageName string, config GetAffectedPackageOptions) ([]AffectedPackageHandle, error) {
var pkgs []AffectedPackageHandle
query := s.db.Joins("JOIN packages ON affected_package_handles.package_id = packages.id").
Joins("JOIN operating_systems ON affected_package_handles.operating_system_id = operating_systems.id")

query = handlePacakge(query, packageName, config)
query = handleDistro(query, config.Distro)
query = handlePreload(query, config)
query = s.handlePacakge(query, packageName, config)
query = s.handleDistro(query, config.Distro)
query = s.handlePreload(query, config)

err := query.Find(&pkgs).Error
if err != nil {
Expand All @@ -137,7 +137,7 @@ func (s *affectedPackageStore) getPackageByNameAndDistro(packageName string, con
return pkgs, nil
}

func handlePacakge(query *gorm.DB, packageName string, config GetAffectedOptions) *gorm.DB {
func (s *affectedPackageStore) handlePacakge(query *gorm.DB, packageName string, config GetAffectedPackageOptions) *gorm.DB {
query = query.Where("packages.name = ?", packageName)

if config.PackageType != "" {
Expand All @@ -146,7 +146,7 @@ func handlePacakge(query *gorm.DB, packageName string, config GetAffectedOptions
return query
}

func handleDistro(query *gorm.DB, d *DistroSpecifier) *gorm.DB {
func (s *affectedPackageStore) handleDistro(query *gorm.DB, d *DistroSpecifier) *gorm.DB {
if d == AnyDistroSpecified {
return query
}
Expand All @@ -169,7 +169,7 @@ func handleDistro(query *gorm.DB, d *DistroSpecifier) *gorm.DB {
return query
}

func handlePreload(query *gorm.DB, config GetAffectedOptions) *gorm.DB {
func (s *affectedPackageStore) handlePreload(query *gorm.DB, config GetAffectedPackageOptions) *gorm.DB {
if config.PreloadPackage {
query = query.Preload("Package")
}
Expand Down
16 changes: 8 additions & 8 deletions grype/db/v6/affected_package_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestAffectedPackageStore_AddAffectedPackages(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, pkg1.PackageID, result1.PackageID)
assert.Equal(t, pkg1.BlobID, result1.BlobID)
assert.Nil(t, result1.BlobValue) // no preloading on fetch
require.Nil(t, result1.BlobValue) // no preloading on fetch

var result2 AffectedPackageHandle
err = db.Where("package_id = ?", pkg2.PackageID).First(&result2).Error
Expand All @@ -55,13 +55,13 @@ func TestAffectedPackageStore_GetAffectedPackagesByName(t *testing.T) {
tests := []struct {
name string
packageName string
options *GetAffectedOptions
options *GetAffectedPackageOptions
expected []AffectedPackageHandle
}{
{
name: "specific distro",
packageName: pkg2d1.Package.Name,
options: &GetAffectedOptions{
options: &GetAffectedPackageOptions{
Distro: &DistroSpecifier{
Name: "ubuntu",
MajorVersion: "20",
Expand All @@ -73,7 +73,7 @@ func TestAffectedPackageStore_GetAffectedPackagesByName(t *testing.T) {
{
name: "distro major version",
packageName: pkg2d1.Package.Name,
options: &GetAffectedOptions{
options: &GetAffectedPackageOptions{
Distro: &DistroSpecifier{
Name: "ubuntu",
MajorVersion: "20",
Expand All @@ -84,7 +84,7 @@ func TestAffectedPackageStore_GetAffectedPackagesByName(t *testing.T) {
{
name: "distro codename",
packageName: pkg2d1.Package.Name,
options: &GetAffectedOptions{
options: &GetAffectedPackageOptions{
Distro: &DistroSpecifier{
Name: "ubuntu",
Codename: "groovy",
Expand All @@ -95,23 +95,23 @@ func TestAffectedPackageStore_GetAffectedPackagesByName(t *testing.T) {
{
name: "no distro",
packageName: pkg2.Package.Name,
options: &GetAffectedOptions{
options: &GetAffectedPackageOptions{
Distro: NoDistroSpecified,
},
expected: []AffectedPackageHandle{*pkg2},
},
{
name: "any distro",
packageName: pkg2d1.Package.Name,
options: &GetAffectedOptions{
options: &GetAffectedPackageOptions{
Distro: AnyDistroSpecified,
},
expected: []AffectedPackageHandle{*pkg2d1, *pkg2, *pkg2d2},
},
{
name: "package type",
packageName: pkg2.Package.Name,
options: &GetAffectedOptions{
options: &GetAffectedPackageOptions{
PackageType: "type2",
},
expected: []AffectedPackageHandle{*pkg2},
Expand Down
2 changes: 2 additions & 0 deletions grype/db/v6/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ type Reader interface {
ProviderStoreReader
VulnerabilityStoreReader
AffectedPackageStoreReader
AffectedCPEStoreReader
}

type Writer interface {
DBMetadataStoreWriter
ProviderStoreWriter
VulnerabilityStoreWriter
AffectedPackageStoreWriter
AffectedCPEStoreWriter
io.Closer
}

Expand Down
Loading

0 comments on commit 4e6f371

Please sign in to comment.