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
2 changes: 2 additions & 0 deletions internal/api/graphql/gqlgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ models:
resolver: false
service:
resolver: true
region:
resolver: true
IssueMatchFilterValue:
fields:
status:
Expand Down
1 change: 1 addition & 0 deletions internal/api/graphql/graph/baseResolver/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ var (
VulnerabilityFilterSupportGroup string = "supportGroup"
VulnerabilityFilterSeverity string = "severity"
VulnerabilityFilterService string = "service"
VulnerabilityFilterRegion string = "region"
)

type ResolverError struct {
Expand Down
1 change: 1 addition & 0 deletions internal/api/graphql/graph/baseResolver/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ func IssueCountsBaseResolver(app app.Heureka, ctx context.Context, filter *model
Paginated: entity.Paginated{},
ServiceCCRN: filter.ServiceCcrn,
SupportGroupCCRN: filter.SupportGroupCcrn,
Region: filter.Region,
PrimaryName: filter.PrimaryName,
Type: lo.Map(filter.IssueType, func(item *model.IssueTypes, _ int) *string { return pointer.String(item.String()) }),
Search: filter.Search,
Expand Down
4 changes: 4 additions & 0 deletions internal/api/graphql/graph/baseResolver/vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func VulnerabilityBaseResolver(app app.Heureka, ctx context.Context, filter *mod
f := &entity.IssueFilter{
Paginated: entity.Paginated{First: first, After: after},
SupportGroupCCRN: filter.SupportGroup,
Region: filter.Region,
ServiceCCRN: filter.Service,
Type: []*string{lo.ToPtr(entity.IssueTypeVulnerability.String())},
Search: filter.Search,
Expand Down Expand Up @@ -101,15 +102,18 @@ func VulnerabilityBaseResolver(app app.Heureka, ctx context.Context, filter *mod
IssueMatchStatus: []*model.IssueMatchStatusValues{lo.ToPtr(model.IssueMatchStatusValuesNew)},
Search: filter.Search,
ServiceCcrn: filter.Service,
Region: filter.Region,
ComponentVersionID: util.ConvertIntToStrSlice(f.ComponentVersionId),
AllServices: lo.ToPtr(true),
}

severityCounts, err := IssueCountsBaseResolver(app, ctx, icFilter, &model.NodeParent{
ParentName: model.VulnerabilityNodeName,
})
if err != nil {
return nil, NewResolverError("VulnerabilityBaseResolver", err.Error())
}

connection.Counts = severityCounts
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

query{
VulnerabilityFilterValues{
region {
displayName
filterName
values
}
}
}
20 changes: 20 additions & 0 deletions internal/api/graphql/graph/resolver/vulnerability_filter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/api/graphql/graph/schema/issue.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ enum IssueStatusValues {
input IssueFilter {
serviceCcrn: [String],
supportGroupCcrn: [String],
region: [String],
primaryName: [String],
issueMatchStatus: [IssueMatchStatusValues],
issueType: [IssueTypes],
Expand Down
2 changes: 1 addition & 1 deletion internal/api/graphql/graph/schema/vulnerability.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ enum VulnerabilityStatus {
all
}


input VulnerabilityFilter {
severity: [SeverityValues]
service: [String]
supportGroup: [String]
region: [String]
name: [String]
search: [String]
status: VulnerabilityStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ type VulnerabilityFilterValue {
supportGroup: FilterItem
severity: FilterItem
service: FilterItem
region: FilterItem
}
11 changes: 5 additions & 6 deletions internal/database/mariadb/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var issueObject = DbObject{
FilterProperties: []*FilterProperty{
NewFilterProperty("S.service_ccrn = ?", WrapRetSlice(func(filter *entity.IssueFilter) []*string { return filter.ServiceCCRN })),
NewFilterProperty("CI.componentinstance_service_id = ?", WrapRetSlice(func(filter *entity.IssueFilter) []*int64 { return filter.ServiceId })),
NewFilterProperty("CI.componentinstance_region = ?", WrapRetSlice(func(filter *entity.IssueFilter) []*string { return filter.Region })),
NewFilterProperty("I.issue_id = ?", WrapRetSlice(func(filter *entity.IssueFilter) []*int64 { return filter.Id })),
NewFilterProperty("IM.issuematch_status = ?", WrapRetSlice(func(filter *entity.IssueFilter) []*string { return filter.IssueMatchStatus })),
NewFilterProperty("IM.issuematch_rating = ?", WrapRetSlice(func(filter *entity.IssueFilter) []*string { return filter.IssueMatchSeverity })),
Expand Down Expand Up @@ -75,13 +76,15 @@ func getIssueJoins(filter *entity.IssueFilter, order []entity.Order) string {
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 ||
len(filter.Region) > 0 {
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 ||
len(filter.Region) > 0 || filter.AllServices {

joins = fmt.Sprintf("%s\n%s", joins, `
LEFT JOIN ComponentInstance CI ON CI.componentinstance_id = IM.issuematch_component_instance_id
Expand Down Expand Up @@ -255,10 +258,6 @@ func (s *SqlDatabase) GetIssuesWithAggregations(filter *entity.IssueFilter, orde
GROUP BY I.issue_id %s ORDER BY %s LIMIT ?
`

// count(distinct activity_id) as agg_activities,
// LEFT JOIN ActivityHasIssue AHI on I.issue_id = AHI.activityhasissue_issue_id
// LEFT JOIN Activity A on AHI.activityhasissue_activity_id = A.activity_id~

baseAggQuery := `
SELECT I.*,
count(distinct issuematch_id) as agg_issue_matches,
Expand Down
21 changes: 21 additions & 0 deletions internal/database/mariadb/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,27 @@ var _ = Describe("Issue", Label("database", "Issue"), func() {
Expect(entries).To(BeEmpty())
})
})
It("can filter by a region", func() {
region := "test-de-1"

filter := &entity.IssueFilter{
Region: []*string{
&region,
},
}

entries, err := db.GetIssues(filter, nil)

By("throwing no error", func() {
Expect(err).To(BeNil())
})

for _, entry := range entries {
for _, im := range entry.Issue.IssueMatches {
Expect(im.ComponentInstance.Region).To(Equal(region))
Copy link
Collaborator

Choose a reason for hiding this comment

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

The database doesn't resolve the relations, so this check will never be executed. You need to collect the entities from the seedCollection

}
}
})
It("can filter by multiple existing service names", func() {
serviceCcrns := make([]*string, len(seedCollection.ServiceRows))
var expectedIssues []mariadb.IssueRow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ ALTER EVENT refresh_mvCountIssueRatingsServiceId
ON SCHEDULE EVERY 2 HOUR;

ALTER EVENT refresh_mvCountIssueRatingsOther
ON SCHEDULE EVERY 2 HOUR;
ON SCHEDULE EVERY 2 HOUR;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
-- SPDX-License-Identifier: Apache-2.0

DROP EVENT IF EXISTS refresh_mvCountIssueRatingsRegion;

DROP PROCEDURE IF EXISTS refresh_mvCountIssueRatingsRegion_proc;

DROP TABLE IF EXISTS mvCountIssueRatingsRegion;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-- SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
-- SPDX-License-Identifier: Apache-2.0

CREATE TABLE IF NOT EXISTS mvCountIssueRatingsRegion (
region VARCHAR(1024) UNIQUE,
critical_count INT DEFAULT 0,
high_count INT DEFAULT 0,
medium_count INT DEFAULT 0,
low_count INT DEFAULT 0,
none_count INT DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1
);

CREATE PROCEDURE IF NOT EXISTS refresh_mvCountIssueRatingsRegion_proc()
BEGIN
UPDATE mvCountIssueRatingsRegion
SET is_active = 0
WHERE is_active = 1;

INSERT INTO mvCountIssueRatingsRegion
(region, critical_count, high_count, medium_count, low_count, none_count, is_active)
SELECT
DISTINCT CI.componentinstance_region,
COUNT(DISTINCT CASE WHEN IV.issuevariant_rating = 'Critical' THEN IV.issuevariant_issue_id END),
COUNT(DISTINCT CASE WHEN IV.issuevariant_rating = 'High' THEN IV.issuevariant_issue_id END),
COUNT(DISTINCT CASE WHEN IV.issuevariant_rating = 'Medium' THEN IV.issuevariant_issue_id END),
COUNT(DISTINCT CASE WHEN IV.issuevariant_rating = 'Low' THEN IV.issuevariant_issue_id END),
COUNT(DISTINCT CASE WHEN IV.issuevariant_rating = 'None' THEN IV.issuevariant_issue_id END),
1
FROM Issue I
LEFT JOIN IssueVariant IV ON IV.issuevariant_issue_id = I.issue_id
RIGHT JOIN IssueMatch IM ON I.issue_id = IM.issuematch_issue_id
LEFT JOIN ComponentInstance CI ON CI.componentinstance_id = IM.issuematch_component_instance_id
WHERE I.issue_deleted_at IS NULL
GROUP BY CI.componentinstance_region
ON DUPLICATE KEY UPDATE
critical_count = VALUES(critical_count),
high_count = VALUES(high_count),
medium_count = VALUES(medium_count),
low_count = VALUES(low_count),
none_count = VALUES(none_count),
is_active = 1;

DELETE FROM mvCountIssueRatingsRegion
WHERE is_active = 0;
END;

CREATE EVENT IF NOT EXISTS refresh_mvCountIssueRatingsRegion
ON SCHEDULE EVERY 2 HOUR
DO
CALL refresh_mvCountIssueRatingsRegion_proc();
7 changes: 7 additions & 0 deletions internal/database/mariadb/mv_vulnerabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func getCountTable(filter *entity.IssueFilter) string {
} else if len(filter.ServiceCCRN) > 0 || len(filter.ServiceId) > 0 {
// Count issues that appear in single service
return "mvCountIssueRatingsServiceId"
} else if len(filter.Region) > 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The region filter can be used by itself to get the number of vulnerabilities, but most likely it'll be used in combination with the support group filter. This combination is currently not possible. In order to support this, we need an additional mvCountIssueRatingsSupportGroupPerRegion table

return "mvCountIssueRatingsRegion"
} else {
// Total count of issues
return "mvCountIssueRatingsOther"
Expand Down Expand Up @@ -76,6 +78,11 @@ func (s *SqlDatabase) CountIssueRatings(filter *entity.IssueFilter) (*entity.Iss
fl = append(fl, buildFilterQuery(filter.SupportGroupCCRN, "CIR.supportgroup_ccrn = ?", OP_OR))
}

if len(filter.Region) > 0 {
filterParameters = buildQueryParameters(filterParameters, filter.Region)
fl = append(fl, buildFilterQuery(filter.Region, "CIR.region = ?", OP_OR))
}

filterStr := combineFilterQueries(fl, OP_AND)
if filterStr != "" {
query = fmt.Sprintf("%s WHERE %s", query, filterStr)
Expand Down
17 changes: 17 additions & 0 deletions internal/database/mariadb/mv_vulnerabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ var _ = Describe("Counting Issues by Severity", Label("IssueCounts"), func() {
testIssueSeverityCount(filter, counts)
}

testRegionTotalCount := func(counts map[string]entity.IssueSeverityCounts) {
for _, cir := range seedCollection.ComponentInstanceRows {
filter := &entity.IssueFilter{
Region: []*string{&cir.Region.String},
}

testIssueSeverityCount(filter, counts[cir.Region.String])
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test is not working as intended. All items in ComponentInstanceRows have an empty region. counts[cir.Region.String] will always return 0 for all counts. Same will be returned from the db for an empty region.

}
}

insertRemediation := func(serviceRow *mariadb.BaseServiceRow, componentRow *mariadb.ComponentRow, expirationDate time.Time) *entity.Remediation {
remediation := test.NewFakeRemediation()
if serviceRow != nil {
Expand Down Expand Up @@ -488,6 +498,13 @@ var _ = Describe("Counting Issues by Severity", Label("IssueCounts"), func() {

testServicesTotalCount(totalCount)
})

It("return the total count with region filter", func() {
severityCounts, err := test.LoadRegionIssueCounts(test.GetTestDataPath("../mariadb/testdata/issue_counts/issue_counts_per_region.json"))
Expect(err).ToNot(HaveOccurred())

testRegionTotalCount(severityCounts)
})
})
When("there is an active remediation for a vulnerability only in one service", Label("WithRemediations"), func() {
BeforeEach(func() {
Expand Down
18 changes: 11 additions & 7 deletions internal/database/mariadb/test/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ func loadIssueCountsFromFile(filename string, idKey string) (map[string]entity.I
return nil, err
}

var tempIssueCounts []map[string]int64
var tempIssueCounts []map[string]any
if err := json.Unmarshal(data, &tempIssueCounts); err != nil {
return nil, err
}
Expand All @@ -328,12 +328,12 @@ func loadIssueCountsFromFile(filename string, idKey string) (map[string]entity.I
for _, tic := range tempIssueCounts {
id := fmt.Sprintf("%v", tic[idKey])
issueCounts[id] = entity.IssueSeverityCounts{
Critical: tic["critical"],
High: tic["high"],
Medium: tic["medium"],
Low: tic["low"],
None: tic["none"],
Total: tic["total"],
Critical: int64(tic["critical"].(float64)),
High: int64(tic["high"].(float64)),
Medium: int64(tic["medium"].(float64)),
Low: int64(tic["low"].(float64)),
None: int64(tic["none"].(float64)),
Total: int64(tic["total"].(float64)),
}
}

Expand All @@ -352,6 +352,10 @@ func LoadSupportGroupIssueCounts(filename string) (map[string]entity.IssueSeveri
return loadIssueCountsFromFile(filename, "support_group_id")
}

func LoadRegionIssueCounts(filename string) (map[string]entity.IssueSeverityCounts, error) {
return loadIssueCountsFromFile(filename, "region")
}

func LoadIssueCounts(filename string) (entity.IssueSeverityCounts, error) {
data, err := os.ReadFile(filename)
var issueCounts entity.IssueSeverityCounts
Expand Down
8 changes: 8 additions & 0 deletions internal/database/mariadb/test/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,8 @@ func (s *DatabaseSeeder) SeedComponentVersions(num int, components []mariadb.Com
}

func (s *DatabaseSeeder) SeedComponentInstances(num int, componentVersions []mariadb.ComponentVersionRow, services []mariadb.BaseServiceRow) []mariadb.ComponentInstanceRow {
regions := []string{"test-de-1", "test-de-2", "test-us-1", "test-jp-2", "test-jp-1"}

var componentInstances []mariadb.ComponentInstanceRow
for i := 0; i < num; i++ {
componentInstance := NewFakeComponentInstance()
Expand All @@ -730,6 +732,11 @@ func (s *DatabaseSeeder) SeedComponentInstances(num int, componentVersions []mar
componentInstance.Id = sql.NullInt64{Int64: componentInstanceId, Valid: true}
componentInstances = append(componentInstances, componentInstance)
}

componentInstance.Region = sql.NullString{
String: regions[i%len(regions)],
Valid: true,
}
}
return componentInstances
}
Expand Down Expand Up @@ -1692,6 +1699,7 @@ func (s *DatabaseSeeder) RefreshCountIssueRatings() error {
CALL refresh_mvCountIssueRatingsComponentVersion_proc();
CALL refresh_mvCountIssueRatingsServiceId_proc();
CALL refresh_mvCountIssueRatingsOther_proc();
CALL refresh_mvCountIssueRatingsRegion_proc();
`)
return err
}
Expand Down
Loading
Loading