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
13 changes: 7 additions & 6 deletions docs/ordering.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,13 @@ The `By` field is the database column name and is defined as a constant:

```go
var DbColumnNameMap = map[DbColumnName]string{
ComponentInstanceCcrn: "componentinstance_ccrn",
IssuePrimaryName: "issue_primary_name",
IssueMatchId: "issuematch_id",
IssueMatchRating: "issuematch_rating",
IssueMatchTargetRemediationDate: "issuematch_target_remediation_date",
SupportGroupName: "supportgroup_name",
ComponentInstanceCcrn: "componentinstance_ccrn",
IssuePrimaryName: "issue_primary_name",
IssueMatchId: "issuematch_id",
IssueMatchRating: "issuematch_rating",
IssueEarliestTargetRemediationDate: "agg_earliest_target_remediation_date",
IssueMatchTargetRemediationDate: "issuematch_target_remediation_date",
SupportGroupName: "supportgroup_name",
}
```

Expand Down
6 changes: 5 additions & 1 deletion internal/api/graphql/graph/baseResolver/vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// It's designed for the Vulnerability List View in the UI
// - Only returns Issues of type Vulnerability
// - Only returns Issues with at least one IssueMatch with status "new"
// - Default ordering is by IssueVariantRating (descending) and IssuePrimaryName (ascending)
// - Default ordering is by IssueVariantRating (descending), EarliestTargetRemediationDate (descending) and IssuePrimaryName (ascending)
func VulnerabilityBaseResolver(app app.Heureka, ctx context.Context, filter *model.VulnerabilityFilter, first *int, after *string, parent *model.NodeParent) (*model.VulnerabilityConnection, error) {
requestedFields := GetPreloads(ctx)
logrus.WithFields(logrus.Fields{
Expand Down Expand Up @@ -63,6 +63,10 @@ func VulnerabilityBaseResolver(app app.Heureka, ctx context.Context, filter *mod
By: entity.IssueVariantRating,
Direction: entity.OrderDirectionDesc,
})
opt.Order = append(opt.Order, entity.Order{
By: entity.IssueEarliestTargetRemediationDate,
Direction: entity.OrderDirectionDesc,
})
opt.Order = append(opt.Order, entity.Order{
By: entity.IssuePrimaryName,
Direction: entity.OrderDirectionAsc,
Expand Down
7 changes: 4 additions & 3 deletions internal/app/issue/issue_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"math"
"strconv"
"testing"
"time"

"github.com/cloudoperators/heureka/internal/app/common"
"github.com/cloudoperators/heureka/internal/app/event"
Expand Down Expand Up @@ -213,20 +214,20 @@ var _ = Describe("When listing Issues", Label("app", "ListIssues"), func() {
filter.First = &pageSize
issues := []entity.IssueResult{}
for _, i := range test.NNewFakeIssueEntities(resElements) {
cursor, _ := mariadb.EncodeCursor(mariadb.WithIssue([]entity.Order{}, i, 0))
cursor, _ := mariadb.EncodeCursor(mariadb.WithIssue([]entity.Order{}, i, 0, time.Time{}))
issues = append(issues, entity.IssueResult{WithCursor: entity.WithCursor{Value: cursor}, Issue: lo.ToPtr(i)})
}

cursors := lo.Map(issues, func(ir entity.IssueResult, _ int) string {
cursor, _ := mariadb.EncodeCursor(mariadb.WithIssue([]entity.Order{}, *ir.Issue, 0))
cursor, _ := mariadb.EncodeCursor(mariadb.WithIssue([]entity.Order{}, *ir.Issue, 0, time.Time{}))
return cursor
})

var i int64 = 0
for len(cursors) < dbElements {
i++
issue := test.NewFakeIssueEntity()
c, _ := mariadb.EncodeCursor(mariadb.WithIssue([]entity.Order{}, issue, 0))
c, _ := mariadb.EncodeCursor(mariadb.WithIssue([]entity.Order{}, issue, 0, time.Time{}))
cursors = append(cursors, c)
}
db.On("GetIssues", filter, []entity.Order{}).Return(issues, nil)
Expand Down
5 changes: 4 additions & 1 deletion internal/database/mariadb/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"time"

"github.com/cloudoperators/heureka/internal/entity"
)
Expand Down Expand Up @@ -217,7 +218,7 @@ func WithComponentVersion(order []entity.Order, cv entity.ComponentVersion, isc
}
}

func WithIssue(order []entity.Order, issue entity.Issue, ivRating int64) NewCursor {
func WithIssue(order []entity.Order, issue entity.Issue, ivRating int64, earliestTargetRemediationDate time.Time) NewCursor {
return func(cursors *cursors) error {
order = GetDefaultOrder(order, entity.IssueId, entity.OrderDirectionAsc)
for _, o := range order {
Expand All @@ -228,6 +229,8 @@ func WithIssue(order []entity.Order, issue entity.Issue, ivRating int64) NewCurs
cursors.fields = append(cursors.fields, Field{Name: entity.IssuePrimaryName, Value: issue.PrimaryName, Order: o.Direction})
case entity.IssueVariantRating:
cursors.fields = append(cursors.fields, Field{Name: entity.IssueVariantRating, Value: ivRating, Order: o.Direction})
case entity.IssueEarliestTargetRemediationDate:
cursors.fields = append(cursors.fields, Field{Name: entity.IssueEarliestTargetRemediationDate, Value: earliestTargetRemediationDate, Order: o.Direction})
default:
continue
}
Expand Down
67 changes: 42 additions & 25 deletions internal/database/mariadb/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package mariadb
import (
"errors"
"fmt"
"time"

"github.com/go-sql-driver/mysql"
"github.com/samber/lo"
Expand Down Expand Up @@ -69,27 +70,39 @@ func ensureIssueFilter(filter *entity.IssueFilter) *entity.IssueFilter {
}

func getIssueJoins(filter *entity.IssueFilter, order []entity.Order) string {
return getIssueJoinsExtended(filter, order, false)
}

func getIssueJoinsExtended(filter *entity.IssueFilter, order []entity.Order, includeAggJoins bool) string {
joins := ""
orderByRating := lo.ContainsBy(order, func(o entity.Order) bool {
return o.By == entity.IssueVariantRating
})
orderByIssueMatch := lo.ContainsBy(order, func(o entity.Order) bool {
return o.By == entity.IssueEarliestTargetRemediationDate ||
o.By == entity.IssueMatchId ||
o.By == entity.IssueMatchRating ||
o.By == entity.IssueMatchTargetRemediationDate
})

if filter.AllServices || filter.HasIssueMatches {
joins = fmt.Sprintf("%s\n%s", joins, `
RIGHT JOIN IssueMatch IM ON I.issue_id = IM.issuematch_issue_id
`)
} else if len(filter.IssueMatchStatus) > 0 || len(filter.ServiceId) > 0 || len(filter.ServiceCCRN) > 0 ||
len(filter.IssueMatchId) > 0 || len(filter.SupportGroupCCRN) > 0 || len(filter.IssueMatchSeverity) > 0 {
len(filter.IssueMatchId) > 0 || len(filter.SupportGroupCCRN) > 0 || len(filter.IssueMatchSeverity) > 0 ||
orderByIssueMatch || includeAggJoins {
joins = fmt.Sprintf("%s\n%s", joins, `
LEFT JOIN IssueMatch IM ON I.issue_id = IM.issuematch_issue_id
`)
}

if len(filter.ServiceId) > 0 || len(filter.ServiceCCRN) > 0 || len(filter.SupportGroupCCRN) > 0 || filter.AllServices {
if len(filter.ServiceId) > 0 || len(filter.ServiceCCRN) > 0 || len(filter.SupportGroupCCRN) > 0 || filter.AllServices || includeAggJoins {

joins = fmt.Sprintf("%s\n%s", joins, `
LEFT JOIN ComponentInstance CI ON CI.componentinstance_id = IM.issuematch_component_instance_id
`)
if len(filter.ServiceCCRN) > 0 || filter.AllServices {
if len(filter.ServiceCCRN) > 0 || filter.AllServices || includeAggJoins {
joins = fmt.Sprintf("%s\n%s", joins, `
LEFT JOIN ComponentVersion CV ON CI.componentinstance_component_version_id = CV.componentversion_id
LEFT JOIN Service S ON S.service_id = CI.componentinstance_service_id
Expand All @@ -103,7 +116,7 @@ func getIssueJoins(filter *entity.IssueFilter, order []entity.Order) string {
}
}

if len(filter.ComponentVersionId) > 0 || len(filter.ComponentId) > 0 {
if len(filter.ComponentVersionId) > 0 || len(filter.ComponentId) > 0 || includeAggJoins {
joins = fmt.Sprintf("%s\n%s", joins, `
LEFT JOIN ComponentVersionIssue CVI ON I.issue_id = CVI.componentversionissue_issue_id
`)
Expand Down Expand Up @@ -141,6 +154,8 @@ func getIssueColumns(order []entity.Order) string {
switch o.By {
case entity.IssueVariantRating:
columns = fmt.Sprintf("%s, MAX(CAST(IV.issuevariant_rating AS UNSIGNED)) AS issuevariant_rating_num", columns)
case entity.IssueEarliestTargetRemediationDate:
columns = fmt.Sprintf("%s, MIN(IM.issuematch_target_remediation_date) AS agg_earliest_target_remediation_date", columns)
}
}
return columns
Expand Down Expand Up @@ -250,9 +265,7 @@ func (s *SqlDatabase) GetIssuesWithAggregations(filter *entity.IssueFilter, orde
})

baseCiQuery := `
SELECT I.*, SUM(CI.componentinstance_count) AS agg_affected_component_instances %s FROM Issue I
LEFT JOIN IssueMatch IM on I.issue_id = IM.issuematch_issue_id
LEFT JOIN ComponentInstance CI on IM.issuematch_component_instance_id = CI.componentinstance_id
SELECT I.issue_id, SUM(CI.componentinstance_count) AS agg_affected_component_instances %s FROM Issue I
%s
%s
GROUP BY I.issue_id %s ORDER BY %s LIMIT ?
Expand All @@ -271,11 +284,6 @@ func (s *SqlDatabase) GetIssuesWithAggregations(filter *entity.IssueFilter, orde
min(issuematch_created_at) agg_earliest_discovery_date
%s
FROM Issue I
LEFT JOIN IssueMatch IM on I.issue_id = IM.issuematch_issue_id
LEFT JOIN ComponentInstance CI ON CI.componentinstance_id = IM.issuematch_component_instance_id
LEFT JOIN ComponentVersion CV ON CI.componentinstance_component_version_id = CV.componentversion_id
LEFT JOIN Service S ON S.service_id = CI.componentinstance_service_id
LEFT JOIN ComponentVersionIssue CVI ON I.issue_id = CVI.componentversionissue_issue_id
%s
%s
GROUP BY I.issue_id %s ORDER BY %s LIMIT ?
Expand All @@ -288,33 +296,34 @@ func (s *SqlDatabase) GetIssuesWithAggregations(filter *entity.IssueFilter, orde
Aggs AS (
%s
)
SELECT A.*, CIC.*
SELECT A.*, CIC.agg_affected_component_instances
FROM ComponentInstanceCounts CIC
JOIN Aggs A ON CIC.issue_id = A.issue_id;
JOIN Aggs A ON CIC.issue_id = A.issue_id
ORDER BY %s;
`

filter = ensureIssueFilter(filter)
joins := getIssueJoins(filter, order)
joins := getIssueJoinsExtended(filter, order, true)
cursorFields, err := DecodeCursor(filter.Paginated.After)
if err != nil {
return nil, err
}

columns := getIssueColumns(order)
aggOrder := lo.Filter(order, func(o entity.Order, _ int) bool {
return o.By != entity.IssueEarliestTargetRemediationDate
})
aggColumns := getIssueColumns(aggOrder)
defaultOrder := GetDefaultOrder(order, entity.IssueId, entity.OrderDirectionAsc)
orderStr := CreateOrderString(defaultOrder)

whereClause := getIssueFilterWhereClause(filter)

cursorQuery := CreateCursorQuery("", cursorFields)
filterStr := issueObject.GetFilterQuery(filter)
if filterStr != "" && cursorQuery != "" {
cursorQuery = fmt.Sprintf(" AND (%s)", cursorQuery)
}
cursorQuery := getIssueCursorQuery(filter, cursorFields)

ciQuery := fmt.Sprintf(baseCiQuery, columns, joins, whereClause, cursorQuery, orderStr)
aggQuery := fmt.Sprintf(baseAggQuery, columns, joins, whereClause, cursorQuery, orderStr)
query := fmt.Sprintf(baseQuery, ciQuery, aggQuery)
aggQuery := fmt.Sprintf(baseAggQuery, aggColumns, joins, whereClause, cursorQuery, orderStr)
query := fmt.Sprintf(baseQuery, ciQuery, aggQuery, orderStr)

stmt, err := s.db.Preparex(query)
if err != nil {
Expand Down Expand Up @@ -351,7 +360,7 @@ func (s *SqlDatabase) GetIssuesWithAggregations(filter *entity.IssueFilter, orde
ivRating = e.IssueVariantRow.RatingNumerical.Int64
}

cursor, _ := EncodeCursor(WithIssue(defaultOrder, issue.Issue, ivRating))
cursor, _ := EncodeCursor(WithIssue(defaultOrder, issue.Issue, ivRating, issue.EarliestTargetRemediationDate))

sr := entity.IssueResult{
WithCursor: entity.WithCursor{
Expand Down Expand Up @@ -469,8 +478,12 @@ func (s *SqlDatabase) GetAllIssueCursors(filter *entity.IssueFilter, order []ent
if row.IssueVariantRow != nil {
ivRating = row.IssueVariantRow.RatingNumerical.Int64
}
var etrd time.Time
if row.IssueAggregationsRow != nil {
etrd = GetTimeValue(row.IssueAggregationsRow.EarliestTargetRemediationDate)
}

cursor, _ := EncodeCursor(WithIssue(order, issue, ivRating))
cursor, _ := EncodeCursor(WithIssue(order, issue, ivRating, etrd))

return cursor
}), nil
Expand Down Expand Up @@ -508,8 +521,12 @@ func (s *SqlDatabase) GetIssues(filter *entity.IssueFilter, order []entity.Order
if e.IssueVariantRow != nil {
ivRating = e.IssueVariantRow.RatingNumerical.Int64
}
var etrd time.Time
if e.IssueAggregationsRow != nil {
etrd = GetTimeValue(e.IssueAggregationsRow.EarliestTargetRemediationDate)
}

cursor, _ := EncodeCursor(WithIssue(order, issue, ivRating))
cursor, _ := EncodeCursor(WithIssue(order, issue, ivRating, etrd))

sr := entity.IssueResult{
WithCursor: entity.WithCursor{
Expand Down
80 changes: 79 additions & 1 deletion internal/database/mariadb/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ var _ = Describe("Issue", Label("database", "Issue"), func() {
},
[]entity.Order{},
func(entries []entity.IssueResult) string {
after, _ := mariadb.EncodeCursor(mariadb.WithIssue([]entity.Order{}, *entries[len(entries)-1].Issue, 0))
after, _ := mariadb.EncodeCursor(mariadb.WithIssue([]entity.Order{}, *entries[len(entries)-1].Issue, 0, time.Time{}))
return after
},
len(seedCollection.IssueRows),
Expand Down Expand Up @@ -1129,5 +1129,83 @@ var _ = Describe("Ordering Issues", Label("IssueOrder"), func() {
}
})
})

It("can order by earliest target remediation date", func() {
issue1Row := test.NewFakeIssue()
issue1Row.PrimaryName = sql.NullString{String: "CVE-2024-53068", Valid: true}
issue1Id, _ := seeder.InsertFakeIssue(issue1Row)
issue1Row.Id = sql.NullInt64{Int64: issue1Id, Valid: true}

issue2Row := test.NewFakeIssue()
issue2Row.PrimaryName = sql.NullString{String: "CVE-2024-53061", Valid: true}
issue2Id, _ := seeder.InsertFakeIssue(issue2Row)
issue2Row.Id = sql.NullInt64{Int64: issue2Id, Valid: true}

issue3Row := test.NewFakeIssue()
issue3Row.PrimaryName = sql.NullString{String: "CVE-2024-53059", Valid: true}
issue3Id, _ := seeder.InsertFakeIssue(issue3Row)
issue3Row.Id = sql.NullInt64{Int64: issue3Id, Valid: true}

date1, _ := time.Parse(time.RFC3339, "2026-02-17T01:10:57Z")
date2, _ := time.Parse(time.RFC3339, "2026-02-26T08:37:53Z")
date3, _ := time.Parse(time.RFC3339, "2026-02-17T07:15:47Z")

userId := seedCollection.UserRows[0].Id
ciId := seedCollection.ComponentInstanceRows[0].Id

im1 := test.NewFakeIssueMatch()
im1.IssueId = issue1Row.Id
im1.TargetRemediationDate = sql.NullTime{Time: date1, Valid: true}
im1.UserId = userId
im1.ComponentInstanceId = ciId
seeder.InsertFakeIssueMatch(im1)

im2 := test.NewFakeIssueMatch()
im2.IssueId = issue2Row.Id
im2.TargetRemediationDate = sql.NullTime{Time: date2, Valid: true}
im2.UserId = userId
im2.ComponentInstanceId = ciId
seeder.InsertFakeIssueMatch(im2)

im3 := test.NewFakeIssueMatch()
im3.IssueId = issue3Row.Id
im3.TargetRemediationDate = sql.NullTime{Time: date3, Valid: true}
im3.UserId = userId
im3.ComponentInstanceId = ciId
seeder.InsertFakeIssueMatch(im3)

order := []entity.Order{
{By: entity.IssueEarliestTargetRemediationDate, Direction: entity.OrderDirectionDesc},
{By: entity.IssuePrimaryName, Direction: entity.OrderDirectionAsc},
}

res, err := db.GetIssuesWithAggregations(nil, order)
Expect(err).Should(BeNil())

// Expected order (DESC Date):
// 1. Issue 2 (Feb 26)
// 2. Issue 3 (Feb 17 07:15)
// 3. Issue 1 (Feb 17 01:10)

var idx1, idx2, idx3 int = -1, -1, -1
for i, r := range res {
if r.Issue.Id == issue1Row.Id.Int64 {
idx1 = i
}
if r.Issue.Id == issue2Row.Id.Int64 {
idx2 = i
}
if r.Issue.Id == issue3Row.Id.Int64 {
idx3 = i
}
}

Expect(idx1).ToNot(Equal(-1))
Expect(idx2).ToNot(Equal(-1))
Expect(idx3).ToNot(Equal(-1))

Expect(idx2 < idx3).To(BeTrue(), "Issue 2 (Feb 26) should come before Issue 3 (Feb 17 07:15)")
Expect(idx3 < idx1).To(BeTrue(), "Issue 3 (Feb 17 07:15) should come before Issue 1 (Feb 17 01:10)")
})
})
})
2 changes: 2 additions & 0 deletions internal/database/mariadb/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ func ColumnName(f entity.OrderByField) string {
return "issuematch_rating"
case entity.IssueMatchTargetRemediationDate:
return "issuematch_target_remediation_date"
case entity.IssueEarliestTargetRemediationDate:
return "agg_earliest_target_remediation_date"
case entity.SupportGroupId:
return "supportgroup_id"
case entity.SupportGroupCcrn:
Expand Down
1 change: 1 addition & 0 deletions internal/entity/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const (
IssueMatchId
IssueMatchRating
IssueMatchTargetRemediationDate
IssueEarliestTargetRemediationDate

CriticalCount
HighCount
Expand Down
Loading