From 99087de3452ce4430252447441ffa429f47856db Mon Sep 17 00:00:00 2001 From: Valiantsin Tsimoshyk Date: Tue, 7 Apr 2026 13:49:46 +0200 Subject: [PATCH 1/2] fix(image): sort images by vulnerability count by default --- internal/api/graphql/graph/baseResolver/image.go | 15 +++++++-------- internal/database/mariadb/component.go | 5 +++-- 2 files changed, 10 insertions(+), 10 deletions(-) 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 { From 9875a62f51e4807c4add854035227293af21fc09 Mon Sep 17 00:00:00 2001 From: Valiantsin Tsimoshyk Date: Tue, 7 Apr 2026 16:54:21 +0200 Subject: [PATCH 2/2] fix(image): ordering test --- internal/e2e/image_query_test.go | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) 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 {