From d5aaca567335d0d83ac2291d1306602ca41af239 Mon Sep 17 00:00:00 2001 From: AJ Heflin Date: Fri, 27 Mar 2026 13:29:54 -0400 Subject: [PATCH] ROX-30352: add vmcve view layer for severity aggregations Add a SQL view layer for VM CVE data following the imagecve/nodecve pattern. Provides Count, CountBySeverity, Get, and GetVMIDs methods that aggregate CVE data across VMs using the VirtualMachineCVEV2 schema. The direct vm_v2_id FK on the CVE table enables efficient VM-scoped queries without joining through scans/components. Partially generated by AI. Co-Authored-By: Claude Opus 4.6 (1M context) --- central/views/vmcve/db_response.go | 131 ++++++++++++++++ central/views/vmcve/mocks/types.go | 241 +++++++++++++++++++++++++++++ central/views/vmcve/singleton.go | 31 ++++ central/views/vmcve/types.go | 35 +++++ central/views/vmcve/view_impl.go | 185 ++++++++++++++++++++++ 5 files changed, 623 insertions(+) create mode 100644 central/views/vmcve/db_response.go create mode 100644 central/views/vmcve/mocks/types.go create mode 100644 central/views/vmcve/singleton.go create mode 100644 central/views/vmcve/types.go create mode 100644 central/views/vmcve/view_impl.go diff --git a/central/views/vmcve/db_response.go b/central/views/vmcve/db_response.go new file mode 100644 index 0000000000000..0f8582475be85 --- /dev/null +++ b/central/views/vmcve/db_response.go @@ -0,0 +1,131 @@ +package vmcve + +import ( + "time" + + "github.com/stackrox/rox/central/views/common" +) + +type vmCVECoreResponse struct { + CVE string `db:"cve"` + CVEIDs []string `db:"cve_id"` + VMsWithCriticalSeverity int `db:"critical_severity_count"` + FixableVMsWithCriticalSev int `db:"fixable_critical_severity_count"` + VMsWithImportantSeverity int `db:"important_severity_count"` + FixableVMsWithImportantSev int `db:"fixable_important_severity_count"` + VMsWithModerateSeverity int `db:"moderate_severity_count"` + FixableVMsWithModerateSev int `db:"fixable_moderate_severity_count"` + VMsWithLowSeverity int `db:"low_severity_count"` + FixableVMsWithLowSev int `db:"fixable_low_severity_count"` + VMsWithUnknownSeverity int `db:"unknown_severity_count"` + FixableVMsWithUnknownSev int `db:"fixable_unknown_severity_count"` + TopCVSS *float32 `db:"cvss_max"` + AffectedVMCount int `db:"virtual_machine_id_count"` + FirstDiscoveredInSystem *time.Time `db:"cve_created_time_min"` + Published *time.Time `db:"cve_published_on_min"` + EPSSProbabilityMax *float32 `db:"epss_probability_max"` +} + +func (c *vmCVECoreResponse) GetCVE() string { + return c.CVE +} + +func (c *vmCVECoreResponse) GetCVEIDs() []string { + return c.CVEIDs +} + +func (c *vmCVECoreResponse) GetVMsBySeverity() common.ResourceCountByCVESeverity { + return &resourceCountByVMCVESeverity{ + CriticalSeverityCount: c.VMsWithCriticalSeverity, + FixableCriticalSeverityCount: c.FixableVMsWithCriticalSev, + ImportantSeverityCount: c.VMsWithImportantSeverity, + FixableImportantSeverityCount: c.FixableVMsWithImportantSev, + ModerateSeverityCount: c.VMsWithModerateSeverity, + FixableModerateSeverityCount: c.FixableVMsWithModerateSev, + LowSeverityCount: c.VMsWithLowSeverity, + FixableLowSeverityCount: c.FixableVMsWithLowSev, + UnknownSeverityCount: c.VMsWithUnknownSeverity, + FixableUnknownSeverityCount: c.FixableVMsWithUnknownSev, + } +} + +func (c *vmCVECoreResponse) GetTopCVSS() float32 { + if c.TopCVSS == nil { + return 0.0 + } + return *c.TopCVSS +} + +func (c *vmCVECoreResponse) GetAffectedVMCount() int { + return c.AffectedVMCount +} + +func (c *vmCVECoreResponse) GetFirstDiscoveredInSystem() *time.Time { + return c.FirstDiscoveredInSystem +} + +func (c *vmCVECoreResponse) GetPublishDate() *time.Time { + return c.Published +} + +func (c *vmCVECoreResponse) GetEPSSProbability() float32 { + if c.EPSSProbabilityMax == nil { + return 0.0 + } + return *c.EPSSProbabilityMax +} + +type vmCVECoreCount struct { + CVECount int `db:"cve_count"` +} + +type vmIDResponse struct { + VMID string `db:"virtual_machine_id"` +} + +// resourceCountByVMCVESeverity contains the counts of VMs by CVE severity. +type resourceCountByVMCVESeverity struct { + CriticalSeverityCount int `db:"critical_severity_count"` + FixableCriticalSeverityCount int `db:"fixable_critical_severity_count"` + ImportantSeverityCount int `db:"important_severity_count"` + FixableImportantSeverityCount int `db:"fixable_important_severity_count"` + ModerateSeverityCount int `db:"moderate_severity_count"` + FixableModerateSeverityCount int `db:"fixable_moderate_severity_count"` + LowSeverityCount int `db:"low_severity_count"` + FixableLowSeverityCount int `db:"fixable_low_severity_count"` + UnknownSeverityCount int `db:"unknown_severity_count"` + FixableUnknownSeverityCount int `db:"fixable_unknown_severity_count"` +} + +func (r *resourceCountByVMCVESeverity) GetCriticalSeverityCount() common.ResourceCountByFixability { + return &resourceCountByFixability{total: r.CriticalSeverityCount, fixable: r.FixableCriticalSeverityCount} +} + +func (r *resourceCountByVMCVESeverity) GetImportantSeverityCount() common.ResourceCountByFixability { + return &resourceCountByFixability{total: r.ImportantSeverityCount, fixable: r.FixableImportantSeverityCount} +} + +func (r *resourceCountByVMCVESeverity) GetModerateSeverityCount() common.ResourceCountByFixability { + return &resourceCountByFixability{total: r.ModerateSeverityCount, fixable: r.FixableModerateSeverityCount} +} + +func (r *resourceCountByVMCVESeverity) GetLowSeverityCount() common.ResourceCountByFixability { + return &resourceCountByFixability{total: r.LowSeverityCount, fixable: r.FixableLowSeverityCount} +} + +func (r *resourceCountByVMCVESeverity) GetUnknownSeverityCount() common.ResourceCountByFixability { + return &resourceCountByFixability{total: r.UnknownSeverityCount, fixable: r.FixableUnknownSeverityCount} +} + +type resourceCountByFixability struct { + total int + fixable int +} + +func (r *resourceCountByFixability) GetTotal() int { + return r.total +} + +func (r *resourceCountByFixability) GetFixable() int { + return r.fixable +} diff --git a/central/views/vmcve/mocks/types.go b/central/views/vmcve/mocks/types.go new file mode 100644 index 0000000000000..a4a0a440dbe74 --- /dev/null +++ b/central/views/vmcve/mocks/types.go @@ -0,0 +1,241 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: types.go +// +// Generated by this command: +// +// mockgen -package mocks -destination mocks/types.go -source types.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + time "time" + + common "github.com/stackrox/rox/central/views/common" + vmcve "github.com/stackrox/rox/central/views/vmcve" + v1 "github.com/stackrox/rox/generated/api/v1" + gomock "go.uber.org/mock/gomock" +) + +// MockCveCore is a mock of CveCore interface. +type MockCveCore struct { + ctrl *gomock.Controller + recorder *MockCveCoreMockRecorder + isgomock struct{} +} + +// MockCveCoreMockRecorder is the mock recorder for MockCveCore. +type MockCveCoreMockRecorder struct { + mock *MockCveCore +} + +// NewMockCveCore creates a new mock instance. +func NewMockCveCore(ctrl *gomock.Controller) *MockCveCore { + mock := &MockCveCore{ctrl: ctrl} + mock.recorder = &MockCveCoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCveCore) EXPECT() *MockCveCoreMockRecorder { + return m.recorder +} + +// GetAffectedVMCount mocks base method. +func (m *MockCveCore) GetAffectedVMCount() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAffectedVMCount") + ret0, _ := ret[0].(int) + return ret0 +} + +// GetAffectedVMCount indicates an expected call of GetAffectedVMCount. +func (mr *MockCveCoreMockRecorder) GetAffectedVMCount() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAffectedVMCount", reflect.TypeOf((*MockCveCore)(nil).GetAffectedVMCount)) +} + +// GetCVE mocks base method. +func (m *MockCveCore) GetCVE() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCVE") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetCVE indicates an expected call of GetCVE. +func (mr *MockCveCoreMockRecorder) GetCVE() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCVE", reflect.TypeOf((*MockCveCore)(nil).GetCVE)) +} + +// GetCVEIDs mocks base method. +func (m *MockCveCore) GetCVEIDs() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCVEIDs") + ret0, _ := ret[0].([]string) + return ret0 +} + +// GetCVEIDs indicates an expected call of GetCVEIDs. +func (mr *MockCveCoreMockRecorder) GetCVEIDs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCVEIDs", reflect.TypeOf((*MockCveCore)(nil).GetCVEIDs)) +} + +// GetEPSSProbability mocks base method. +func (m *MockCveCore) GetEPSSProbability() float32 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEPSSProbability") + ret0, _ := ret[0].(float32) + return ret0 +} + +// GetEPSSProbability indicates an expected call of GetEPSSProbability. +func (mr *MockCveCoreMockRecorder) GetEPSSProbability() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEPSSProbability", reflect.TypeOf((*MockCveCore)(nil).GetEPSSProbability)) +} + +// GetFirstDiscoveredInSystem mocks base method. +func (m *MockCveCore) GetFirstDiscoveredInSystem() *time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFirstDiscoveredInSystem") + ret0, _ := ret[0].(*time.Time) + return ret0 +} + +// GetFirstDiscoveredInSystem indicates an expected call of GetFirstDiscoveredInSystem. +func (mr *MockCveCoreMockRecorder) GetFirstDiscoveredInSystem() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFirstDiscoveredInSystem", reflect.TypeOf((*MockCveCore)(nil).GetFirstDiscoveredInSystem)) +} + +// GetPublishDate mocks base method. +func (m *MockCveCore) GetPublishDate() *time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPublishDate") + ret0, _ := ret[0].(*time.Time) + return ret0 +} + +// GetPublishDate indicates an expected call of GetPublishDate. +func (mr *MockCveCoreMockRecorder) GetPublishDate() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublishDate", reflect.TypeOf((*MockCveCore)(nil).GetPublishDate)) +} + +// GetTopCVSS mocks base method. +func (m *MockCveCore) GetTopCVSS() float32 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTopCVSS") + ret0, _ := ret[0].(float32) + return ret0 +} + +// GetTopCVSS indicates an expected call of GetTopCVSS. +func (mr *MockCveCoreMockRecorder) GetTopCVSS() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTopCVSS", reflect.TypeOf((*MockCveCore)(nil).GetTopCVSS)) +} + +// GetVMsBySeverity mocks base method. +func (m *MockCveCore) GetVMsBySeverity() common.ResourceCountByCVESeverity { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVMsBySeverity") + ret0, _ := ret[0].(common.ResourceCountByCVESeverity) + return ret0 +} + +// GetVMsBySeverity indicates an expected call of GetVMsBySeverity. +func (mr *MockCveCoreMockRecorder) GetVMsBySeverity() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVMsBySeverity", reflect.TypeOf((*MockCveCore)(nil).GetVMsBySeverity)) +} + +// MockCveView is a mock of CveView interface. +type MockCveView struct { + ctrl *gomock.Controller + recorder *MockCveViewMockRecorder + isgomock struct{} +} + +// MockCveViewMockRecorder is the mock recorder for MockCveView. +type MockCveViewMockRecorder struct { + mock *MockCveView +} + +// NewMockCveView creates a new mock instance. +func NewMockCveView(ctrl *gomock.Controller) *MockCveView { + mock := &MockCveView{ctrl: ctrl} + mock.recorder = &MockCveViewMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCveView) EXPECT() *MockCveViewMockRecorder { + return m.recorder +} + +// Count mocks base method. +func (m *MockCveView) Count(ctx context.Context, q *v1.Query) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Count", ctx, q) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Count indicates an expected call of Count. +func (mr *MockCveViewMockRecorder) Count(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockCveView)(nil).Count), ctx, q) +} + +// CountBySeverity mocks base method. +func (m *MockCveView) CountBySeverity(ctx context.Context, q *v1.Query) (common.ResourceCountByCVESeverity, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CountBySeverity", ctx, q) + ret0, _ := ret[0].(common.ResourceCountByCVESeverity) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CountBySeverity indicates an expected call of CountBySeverity. +func (mr *MockCveViewMockRecorder) CountBySeverity(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountBySeverity", reflect.TypeOf((*MockCveView)(nil).CountBySeverity), ctx, q) +} + +// Get mocks base method. +func (m *MockCveView) Get(ctx context.Context, q *v1.Query) ([]vmcve.CveCore, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, q) + ret0, _ := ret[0].([]vmcve.CveCore) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockCveViewMockRecorder) Get(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCveView)(nil).Get), ctx, q) +} + +// GetVMIDs mocks base method. +func (m *MockCveView) GetVMIDs(ctx context.Context, q *v1.Query) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVMIDs", ctx, q) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetVMIDs indicates an expected call of GetVMIDs. +func (mr *MockCveViewMockRecorder) GetVMIDs(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVMIDs", reflect.TypeOf((*MockCveView)(nil).GetVMIDs), ctx, q) +} diff --git a/central/views/vmcve/singleton.go b/central/views/vmcve/singleton.go new file mode 100644 index 0000000000000..af93b47a0084c --- /dev/null +++ b/central/views/vmcve/singleton.go @@ -0,0 +1,31 @@ +package vmcve + +import ( + "github.com/stackrox/rox/central/globaldb" + "github.com/stackrox/rox/pkg/postgres" + "github.com/stackrox/rox/pkg/postgres/schema" + "github.com/stackrox/rox/pkg/sync" +) + +var ( + once sync.Once + + vmCVEView CveView +) + +// NewCVEView returns the interface CveView +// that provides searching VM CVEs stored in the database. +func NewCVEView(db postgres.DB) CveView { + return &vmCVECoreViewImpl{ + db: db, + schema: schema.VirtualMachineCvev2Schema, + } +} + +// Singleton provides the interface to search VM CVEs stored in the database. +func Singleton() CveView { + once.Do(func() { + vmCVEView = NewCVEView(globaldb.GetPostgres()) + }) + return vmCVEView +} diff --git a/central/views/vmcve/types.go b/central/views/vmcve/types.go new file mode 100644 index 0000000000000..79f076f167c6a --- /dev/null +++ b/central/views/vmcve/types.go @@ -0,0 +1,35 @@ +package vmcve + +import ( + "context" + "time" + + "github.com/stackrox/rox/central/views/common" + v1 "github.com/stackrox/rox/generated/api/v1" +) + +// CveCore is an interface to get VM CVE properties. +// +//go:generate mockgen-wrapper +type CveCore interface { + GetCVE() string + GetCVEIDs() []string + GetVMsBySeverity() common.ResourceCountByCVESeverity + GetTopCVSS() float32 + GetAffectedVMCount() int + GetFirstDiscoveredInSystem() *time.Time + GetPublishDate() *time.Time + GetEPSSProbability() float32 +} + +// CveView interface is like a SQL view that provides functionality to fetch VM CVE data +// irrespective of the data model. One CVE can have multiple database entries if that CVE +// impacts multiple VMs or components. However, the core information is the same. +// +//go:generate mockgen-wrapper +type CveView interface { + Count(ctx context.Context, q *v1.Query) (int, error) + CountBySeverity(ctx context.Context, q *v1.Query) (common.ResourceCountByCVESeverity, error) + Get(ctx context.Context, q *v1.Query) ([]CveCore, error) + GetVMIDs(ctx context.Context, q *v1.Query) ([]string, error) +} diff --git a/central/views/vmcve/view_impl.go b/central/views/vmcve/view_impl.go new file mode 100644 index 0000000000000..178f84665d7e7 --- /dev/null +++ b/central/views/vmcve/view_impl.go @@ -0,0 +1,185 @@ +package vmcve + +import ( + "context" + "sort" + + "github.com/stackrox/rox/central/views/common" + v1 "github.com/stackrox/rox/generated/api/v1" + "github.com/stackrox/rox/pkg/contextutil" + "github.com/stackrox/rox/pkg/env" + "github.com/stackrox/rox/pkg/postgres" + "github.com/stackrox/rox/pkg/postgres/walker" + "github.com/stackrox/rox/pkg/search" + "github.com/stackrox/rox/pkg/search/paginated" + pgSearch "github.com/stackrox/rox/pkg/search/postgres" + "github.com/stackrox/rox/pkg/search/postgres/aggregatefunc" +) + +var ( + queryTimeout = env.PostgresVMStatementTimeout.DurationSetting() +) + +type vmCVECoreViewImpl struct { + schema *walker.Schema + db postgres.DB +} + +func (v *vmCVECoreViewImpl) Count(ctx context.Context, q *v1.Query) (int, error) { + if err := common.ValidateQuery(q); err != nil { + return 0, err + } + + queryCtx, cancel := contextutil.ContextWithTimeoutIfNotExists(ctx, queryTimeout) + defer cancel() + + result, err := pgSearch.RunSelectOneForSchema[vmCVECoreCount](queryCtx, v.db, v.schema, common.WithCountQuery(q, search.CVE)) + if err != nil { + return 0, err + } + if result == nil { + return 0, nil + } + return result.CVECount, nil +} + +func (v *vmCVECoreViewImpl) CountBySeverity(ctx context.Context, q *v1.Query) (common.ResourceCountByCVESeverity, error) { + if err := common.ValidateQuery(q); err != nil { + return nil, err + } + + queryCtx, cancel := contextutil.ContextWithTimeoutIfNotExists(ctx, queryTimeout) + defer cancel() + + result, err := pgSearch.RunSelectOneForSchema[resourceCountByVMCVESeverity](queryCtx, v.db, v.schema, common.WithCountBySeverityAndFixabilityQuery(q, search.VirtualMachineID)) + if err != nil { + return nil, err + } + if result == nil { + return &resourceCountByVMCVESeverity{}, nil + } + return result, nil +} + +func (v *vmCVECoreViewImpl) Get(ctx context.Context, q *v1.Query) ([]CveCore, error) { + if err := common.ValidateQuery(q); err != nil { + return nil, err + } + + cloned := q.CloneVT() + cloned = common.UpdateSortAggs(cloned) + + var cveIDsToFilter []string + var err error + if cloned.GetPagination().GetLimit() > 0 || cloned.GetPagination().GetOffset() > 0 { + cveIDsToFilter, err = v.getFilteredCVEs(ctx, cloned) + if err != nil { + return nil, err + } + + if cloned.GetPagination() != nil && cloned.GetPagination().GetSortOptions() != nil { + cloned.Pagination = &v1.QueryPagination{SortOptions: cloned.GetPagination().GetSortOptions()} + } + } + + queryCtx, cancel := contextutil.ContextWithTimeoutIfNotExists(ctx, queryTimeout) + defer cancel() + + ret := make([]CveCore, 0, paginated.GetLimit(q.GetPagination().GetLimit(), 100)) + err = pgSearch.RunSelectRequestForSchemaFn[vmCVECoreResponse](queryCtx, v.db, v.schema, withSelectCVECoreResponseQuery(cloned, cveIDsToFilter), func(r *vmCVECoreResponse) error { + sort.SliceStable(r.CVEIDs, func(i, j int) bool { + return r.CVEIDs[i] < r.CVEIDs[j] + }) + ret = append(ret, r) + return nil + }) + if err != nil { + return nil, err + } + return ret, nil +} + +func (v *vmCVECoreViewImpl) GetVMIDs(ctx context.Context, q *v1.Query) ([]string, error) { + q.Selects = []*v1.QuerySelect{ + search.NewQuerySelect(search.VirtualMachineID).Distinct().Proto(), + } + + queryCtx, cancel := contextutil.ContextWithTimeoutIfNotExists(ctx, queryTimeout) + defer cancel() + + ret := make([]string, 0, paginated.GetLimit(q.GetPagination().GetLimit(), 100)) + err := pgSearch.RunSelectRequestForSchemaFn[vmIDResponse](queryCtx, v.db, v.schema, q, func(r *vmIDResponse) error { + ret = append(ret, r.VMID) + return nil + }) + if err != nil { + return nil, err + } + if len(ret) == 0 { + return nil, nil + } + return ret, nil +} + +func withSelectCVEIdentifiersQuery(q *v1.Query) *v1.Query { + cloned := q.CloneVT() + cloned.Selects = []*v1.QuerySelect{ + search.NewQuerySelect(search.CVEID).Distinct().Proto(), + } + cloned.GroupBy = &v1.QueryGroupBy{ + Fields: []string{search.CVE.String()}, + } + + if common.IsSortBySeverityCounts(cloned) { + cloned.Selects = append(cloned.Selects, + common.WithCountBySeverityAndFixabilityQuery(q, search.VirtualMachineID).GetSelects()..., + ) + } + + return cloned +} + +func withSelectCVECoreResponseQuery(q *v1.Query, cveIDsToFilter []string) *v1.Query { + cloned := q.CloneVT() + if len(cveIDsToFilter) > 0 { + cloned = search.ConjunctionQuery(cloned, search.NewQueryBuilder().AddDocIDs(cveIDsToFilter...).ProtoQuery()) + cloned.Pagination = q.GetPagination() + } + + cloned.Selects = []*v1.QuerySelect{ + search.NewQuerySelect(search.CVE).Proto(), + search.NewQuerySelect(search.CVEID).Distinct().Proto(), + } + cloned.Selects = append(cloned.Selects, + common.WithCountBySeverityAndFixabilityQuery(q, search.VirtualMachineID).GetSelects()..., + ) + cloned.Selects = append(cloned.Selects, + search.NewQuerySelect(search.CVSS).AggrFunc(aggregatefunc.Max).Proto(), + search.NewQuerySelect(search.VirtualMachineID).AggrFunc(aggregatefunc.Count).Distinct().Proto(), + search.NewQuerySelect(search.CVECreatedTime).AggrFunc(aggregatefunc.Min).Proto(), + search.NewQuerySelect(search.CVEPublishedOn).AggrFunc(aggregatefunc.Min).Proto(), + search.NewQuerySelect(search.EPSSProbablity).AggrFunc(aggregatefunc.Max).Proto(), + ) + cloned.GroupBy = &v1.QueryGroupBy{ + Fields: []string{search.CVE.String()}, + } + + return cloned +} + +func (v *vmCVECoreViewImpl) getFilteredCVEs(ctx context.Context, q *v1.Query) ([]string, error) { + var cveIDsToFilter []string + + queryCtx, cancel := contextutil.ContextWithTimeoutIfNotExists(ctx, queryTimeout) + defer cancel() + + err := pgSearch.RunSelectRequestForSchemaFn[vmCVECoreResponse](queryCtx, v.db, v.schema, withSelectCVEIdentifiersQuery(q), func(r *vmCVECoreResponse) error { + cveIDsToFilter = append(cveIDsToFilter, r.CVEIDs...) + return nil + }) + if err != nil { + return nil, err + } + + return cveIDsToFilter, nil +}