Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions internal/api/graphql/graph/baseResolver/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions internal/database/mariadb/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic error in needAllComponentByServiceVulnerabilityCounts: the condition was changed from orderByCount && to !orderByCount &&, which inverts the logic. This means the materialized view will now be joined when NOT ordering by vulnerability counts, which is the opposite of the intended behavior. When vulnerability count ordering is applied globally in ImageBaseResolver, this join will never be used, causing potentially incorrect results.

Suggested change
return !orderByCount && (len(filter.Id) == 0 && (len(filter.ServiceCCRN) > 0))
return orderByCount && (len(filter.Id) == 0 && (len(filter.ServiceCCRN) > 0))

Copilot uses AI. Check for mistakes.
}

func (s *SqlDatabase) getComponentColumns(order []entity.Order) string {
Expand Down
41 changes: 41 additions & 0 deletions internal/e2e/image_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Comment on lines +78 to +99
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test has a TODO comment acknowledging that it doesn't verify secondary ordering (by repository name) when vulnerability counts are equal. Consider adding test data with images having the same vulnerability counts but different repository names to fully validate the secondary ordering behavior.

Suggested change
// 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")
}
// and by repository name in ascending order as a tiebreaker.
var previousCounts model.SeverityCounts
var previousRepository string
hadEqualCountsPair := false
for i, edge := range respData.Images.Edges {
Expect(edge.Node.Repository).ToNot(BeNil(), "Image should have repository")
Expect(edge.Node.VulnerabilityCounts).ToNot(BeNil(), "Image should have vulnerability counts")
counts := *edge.Node.VulnerabilityCounts
repository := *edge.Node.Repository
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, repository, i-1, previousRepository, counts, previousCounts))
if comparison == 0 {
hadEqualCountsPair = true
Expect(previousRepository <= repository).To(BeTrue(),
fmt.Sprintf("Image %d (%s) should sort after image %d (%s) when severity counts are equal. Counts: %v",
i, repository, i-1, previousRepository, counts))
}
}
previousCounts = counts
previousRepository = repository
}
Expect(hadEqualCountsPair).To(BeTrue(),
"Test setup should include at least one pair of images with equal vulnerability counts but different repository names to verify secondary ordering")

Copilot uses AI. Check for mistakes.
})
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 {
Expand Down
Loading