diff --git a/internal/api/graphql/graph/baseResolver/image.go b/internal/api/graphql/graph/baseResolver/image.go index 290c1980..e6e58907 100644 --- a/internal/api/graphql/graph/baseResolver/image.go +++ b/internal/api/graphql/graph/baseResolver/image.go @@ -33,14 +33,13 @@ func ImageBaseResolver(app app.Heureka, ctx context.Context, filter *model.Image } opt := GetListOptions(requestedFields) - // Set default ordering - if lo.Contains(requestedFields, "edges.node.vulnerabilities") { - opt.Order = append(opt.Order, entity.Order{By: entity.CriticalCount, Direction: entity.OrderDirectionDesc}) - opt.Order = append(opt.Order, entity.Order{By: entity.HighCount, Direction: entity.OrderDirectionDesc}) - opt.Order = append(opt.Order, entity.Order{By: entity.MediumCount, Direction: entity.OrderDirectionDesc}) - opt.Order = append(opt.Order, entity.Order{By: entity.LowCount, Direction: entity.OrderDirectionDesc}) - opt.Order = append(opt.Order, entity.Order{By: entity.NoneCount, Direction: entity.OrderDirectionDesc}) - } + // Set default ordering by vulnerability counts + // Secondary ordering by repository name + opt.Order = append(opt.Order, entity.Order{By: entity.CriticalCount, Direction: entity.OrderDirectionDesc}) + opt.Order = append(opt.Order, entity.Order{By: entity.HighCount, Direction: entity.OrderDirectionDesc}) + opt.Order = append(opt.Order, entity.Order{By: entity.MediumCount, Direction: entity.OrderDirectionDesc}) + opt.Order = append(opt.Order, entity.Order{By: entity.LowCount, Direction: entity.OrderDirectionDesc}) + opt.Order = append(opt.Order, entity.Order{By: entity.NoneCount, Direction: entity.OrderDirectionDesc}) opt.Order = append(opt.Order, entity.Order{ By: entity.ComponentRepository, Direction: entity.OrderDirectionAsc, diff --git a/internal/database/mariadb/component.go b/internal/database/mariadb/component.go index 6548a5b7..0c94e5d2 100644 --- a/internal/database/mariadb/component.go +++ b/internal/database/mariadb/component.go @@ -88,14 +88,15 @@ func (s *SqlDatabase) needSingleComponentByServiceVulnerabilityCounts(filter *en orderByCount := lo.ContainsBy(order, func(o entity.Order) bool { return o.By == entity.CriticalCount || o.By == entity.HighCount || o.By == entity.MediumCount || o.By == entity.LowCount || o.By == entity.NoneCount }) - return orderByCount && (len(filter.Id) > 0 && (len(filter.ServiceCCRN) > 0)) + + return orderByCount && len(filter.ServiceCCRN) > 0 } func (s *SqlDatabase) needAllComponentByServiceVulnerabilityCounts(filter *entity.ComponentFilter, order []entity.Order) bool { orderByCount := lo.ContainsBy(order, func(o entity.Order) bool { return o.By == entity.CriticalCount || o.By == entity.HighCount || o.By == entity.MediumCount || o.By == entity.LowCount || o.By == entity.NoneCount }) - return orderByCount && (len(filter.Id) == 0 && (len(filter.ServiceCCRN) > 0)) + return !orderByCount && (len(filter.Id) == 0 && (len(filter.ServiceCCRN) > 0)) } func (s *SqlDatabase) getComponentColumns(order []entity.Order) string { diff --git a/internal/e2e/image_query_test.go b/internal/e2e/image_query_test.go index d767525f..35ff0e39 100644 --- a/internal/e2e/image_query_test.go +++ b/internal/e2e/image_query_test.go @@ -57,6 +57,47 @@ var _ = Describe("Getting Images via API", Label("e2e", "Images"), func() { Expect(err).ToNot(HaveOccurred()) Expect(*respData.Images.Counts).To(Equal(imgTest.counts)) }) + It("returns images sorted by vulnerability severity counts then by repository name", func() { + respData, err := e2e_common.ExecuteGqlQueryFromFile[struct { + Images model.ImageConnection `json:"Images"` + }]( + imgTest.port, + "../api/graphql/graph/queryCollection/image/query.graphql", + map[string]interface{}{ + "filter": map[string]any{ + "service": lo.Map(imgTest.services, func(item mariadb.BaseServiceRow, index int) string { return item.CCRN.String }), + }, + "first": 10, + "after": "", + }) + + Expect(err).ToNot(HaveOccurred()) + Expect(respData.Images.Edges).To(HaveLen(5), "Should return all 5 images") + + // Verify images are sorted by vulnerability counts in descending order + // The test data setup creates images with different vulnerability counts + // We expect them to be ordered by: critical, high, medium, low, none counts (descending) + // then by repository name (ascending) as tiebreaker + // TODO: make sure there is a case with same vulnerability counts but different repository names to verify the secondary ordering as well + + // Extract the vulnerability counts for comparison + var previousCounts model.SeverityCounts + for i, edge := range respData.Images.Edges { + counts := *edge.Node.VulnerabilityCounts + + if i > 0 { + comparison := e2e_common.CompareSeverityCounts(counts, previousCounts) + Expect(comparison).To(BeNumerically("<=", 0), + fmt.Sprintf("Image %d (%s) should have equal or lower severity than image %d (%s). Counts: %v vs %v", + i, *edge.Node.Repository, i-1, *respData.Images.Edges[i-1].Node.Repository, counts, previousCounts)) + } + + previousCounts = counts + + Expect(edge.Node.Repository).ToNot(BeNil(), "Image should have repository") + Expect(edge.Node.VulnerabilityCounts).ToNot(BeNil(), "Image should have vulnerability counts") + } + }) It("returns the expected content and the expected PageInfo when filtered using repository", func() { service := imgTest.services[0] componentInstances := lo.Filter(imgTest.componentInstances, func(ci mariadb.ComponentInstanceRow, _ int) bool {