Skip to content

Commit 27f3a75

Browse files
Merge pull request #6907 from devtron-labs/optimize-vul-summary-oss
misc: Refactor vulnerability query implementation and cleanup unused code
2 parents 17c5b99 + 4f35537 commit 27f3a75

File tree

5 files changed

+158
-177
lines changed

5 files changed

+158
-177
lines changed

api/restHandler/ImageScanRestHandler.go

Lines changed: 47 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -428,99 +428,55 @@ func (impl ImageScanRestHandlerImpl) VulnerabilitySummary(w http.ResponseWriter,
428428
return
429429
}
430430

431-
// Create ImageScanRequest with filters for fetching deploy info
432-
request := &securityBean.ImageScanRequest{
433-
ImageScanFilter: bean.ImageScanFilter{
434-
EnvironmentIds: summaryRequest.EnvironmentIds,
435-
ClusterIds: summaryRequest.ClusterIds,
436-
},
437-
}
438-
439-
deployInfoList, err := impl.imageScanService.FetchAllDeployInfo(request)
440-
if err != nil {
441-
impl.logger.Errorw("service err, VulnerabilitySummary", "err", err)
442-
if util.IsErrNoRows(err) {
443-
emptySummary := &securityBean.VulnerabilitySummary{
444-
TotalVulnerabilities: 0,
445-
SeverityCount: &securityBean.SeverityCount{
446-
Critical: 0,
447-
High: 0,
448-
Medium: 0,
449-
Low: 0,
450-
Unknown: 0,
451-
},
452-
FixableVulnerabilities: 0,
453-
NotFixableVulnerabilities: 0,
454-
}
455-
common.WriteJsonResp(w, nil, emptySummary, http.StatusOK)
456-
} else {
457-
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
458-
}
459-
return
460-
}
461-
462-
filteredDeployInfoList, err := impl.imageScanService.FilterDeployInfoByScannedArtifactsDeployedInEnv(deployInfoList)
463-
if err != nil {
464-
impl.logger.Errorw("request err, FilterDeployInfoListForScannedArtifacts", "err", err)
465-
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
466-
return
467-
}
468-
469-
_, rbacSpan := otel.Tracer("imageScanRestHandler").Start(ctx, "RBACProcessing")
431+
// Check if user is super admin first - this determines the optimization path
432+
_, rbacSpan := otel.Tracer("imageScanRestHandler").Start(ctx, "RBACCheck")
470433
token := r.Header.Get("token")
471-
isSuperAdmin := false
472-
if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); ok {
473-
isSuperAdmin = true
474-
}
434+
isSuperAdmin := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*")
435+
rbacSpan.End()
436+
475437
var ids []int
438+
476439
if isSuperAdmin {
477-
ids = sliceUtil.NewSliceFromFuncExec(filteredDeployInfoList, func(item *security2.ImageScanDeployInfo) int {
478-
return item.Id
479-
})
440+
// OPTIMIZATION: For super-admin users, skip deploy info fetching and filtering entirely
441+
// The GetVulnerabilityRawData query already handles all filtering (env, cluster, app) at DB level
442+
// When ids is empty, it doesn't apply RBAC filtering, which is correct for super-admins
443+
ids = nil
480444
} else {
445+
// OPTIMIZATION: For non-super-admin users, use optimized single query
446+
_, fetchSpan := otel.Tracer("imageScanRestHandler").Start(ctx, "FetchScannedDeployInfo")
447+
filteredDeployInfoList, err := impl.imageScanService.FetchScannedDeployInfoWithFilters(ctx, summaryRequest.EnvironmentIds, summaryRequest.ClusterIds)
448+
fetchSpan.End()
449+
if err != nil {
450+
impl.logger.Errorw("service err, VulnerabilitySummary", "err", err)
451+
if util.IsErrNoRows(err) {
452+
common.WriteJsonResp(w, nil, impl.getEmptyVulnerabilitySummary(), http.StatusOK)
453+
} else {
454+
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
455+
}
456+
return
457+
}
458+
459+
// Apply RBAC filtering
460+
_, rbacProcessSpan := otel.Tracer("imageScanRestHandler").Start(ctx, "RBACProcessing")
481461
ids, err = impl.getAuthorisedImageScanDeployInfoIds(token, filteredDeployInfoList)
462+
rbacProcessSpan.End()
482463
if err != nil {
483464
impl.logger.Errorw("error in getting authorised image scan deploy info ids", "err", err)
484465
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
485466
return
486467
}
487-
}
488-
rbacSpan.End()
489468

490-
if len(ids) == 0 {
491-
emptySummary := &securityBean.VulnerabilitySummary{
492-
TotalVulnerabilities: 0,
493-
SeverityCount: &securityBean.SeverityCount{
494-
Critical: 0,
495-
High: 0,
496-
Medium: 0,
497-
Low: 0,
498-
Unknown: 0,
499-
},
500-
FixableVulnerabilities: 0,
501-
NotFixableVulnerabilities: 0,
469+
if len(ids) == 0 {
470+
common.WriteJsonResp(w, nil, impl.getEmptyVulnerabilitySummary(), http.StatusOK)
471+
return
502472
}
503-
common.WriteJsonResp(w, nil, emptySummary, http.StatusOK)
504-
return
505473
}
506474

507475
summary, err := impl.imageScanService.FetchVulnerabilitySummary(ctx, summaryRequest, ids)
508476
if err != nil {
509477
impl.logger.Errorw("service err, VulnerabilitySummary", "err", err)
510478
if util.IsErrNoRows(err) {
511-
emptySummary := &securityBean.VulnerabilitySummary{
512-
TotalVulnerabilities: 0,
513-
SeverityCount: &securityBean.SeverityCount{
514-
Critical: 0,
515-
High: 0,
516-
Medium: 0,
517-
Low: 0,
518-
Unknown: 0,
519-
},
520-
FixableVulnerabilities: 0,
521-
NotFixableVulnerabilities: 0,
522-
}
523-
common.WriteJsonResp(w, nil, emptySummary, http.StatusOK)
479+
common.WriteJsonResp(w, nil, impl.getEmptyVulnerabilitySummary(), http.StatusOK)
524480
} else {
525481
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
526482
}
@@ -529,6 +485,22 @@ func (impl ImageScanRestHandlerImpl) VulnerabilitySummary(w http.ResponseWriter,
529485
common.WriteJsonResp(w, err, summary, http.StatusOK)
530486
}
531487

488+
// getEmptyVulnerabilitySummary returns an empty vulnerability summary response
489+
func (impl ImageScanRestHandlerImpl) getEmptyVulnerabilitySummary() *securityBean.VulnerabilitySummary {
490+
return &securityBean.VulnerabilitySummary{
491+
TotalVulnerabilities: 0,
492+
SeverityCount: &securityBean.SeverityCount{
493+
Critical: 0,
494+
High: 0,
495+
Medium: 0,
496+
Low: 0,
497+
Unknown: 0,
498+
},
499+
FixableVulnerabilities: 0,
500+
NotFixableVulnerabilities: 0,
501+
}
502+
}
503+
532504
func (impl ImageScanRestHandlerImpl) VulnerabilityListing(w http.ResponseWriter, r *http.Request) {
533505
ctx, span := otel.Tracer("imageScanRestHandler").Start(r.Context(), "VulnerabilityListing")
534506
defer span.End()

pkg/overview/SecurityOverviewService.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (service *SecurityOverviewServiceImpl) GetSecurityOverview(ctx context.Cont
6868
service.logger.Infow("GetSecurityOverview called", "request", request)
6969

7070
// Fetch all vulnerabilities with fixed_version in a single query
71-
vulnerabilities, err := service.imageScanResultRepository.GetVulnerabilitiesWithFixedVersionByFilters(request.EnvIds, request.ClusterIds, request.AppIds)
71+
vulnerabilities, err := service.imageScanResultRepository.GetVulnerabilityRawData("", nil, request.EnvIds, request.ClusterIds, request.AppIds, nil)
7272
if err != nil {
7373
service.logger.Errorw("error fetching vulnerabilities", "err", err)
7474
return nil, fmt.Errorf("failed to fetch vulnerabilities: %w", err)

pkg/policyGovernance/security/imageScanning/ImageScanService.go

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ type ImageScanService interface {
6060
// resource scanning functions below
6161
GetScanResults(resourceScanQueryParams *bean3.ResourceScanQueryParams) (parser.ResourceScanResponseDto, error)
6262
FilterDeployInfoByScannedArtifactsDeployedInEnv(deployInfoList []*repository3.ImageScanDeployInfo) ([]*repository3.ImageScanDeployInfo, error)
63+
64+
// Optimized method for vulnerability summary - combines filtering with scanned artifact check in single query
65+
FetchScannedDeployInfoWithFilters(ctx context.Context, envIds, clusterIds []int) ([]*repository3.ImageScanDeployInfo, error)
6366
}
6467

6568
type ImageScanServiceImpl struct {
@@ -928,6 +931,21 @@ func (impl ImageScanServiceImpl) fetchLatestArtifactMetadataDeployedOnAllEnvsAcr
928931
return appEnvToCiArtifactMap, ciArtifactIdToScannedMap, nil
929932
}
930933

934+
// FetchScannedDeployInfoWithFilters returns deploy info for scanned artifacts that are currently deployed
935+
// This is an optimized method that combines the logic of FetchAllDeployInfo + FilterDeployInfoByScannedArtifactsDeployedInEnv
936+
// into a single database query, significantly improving performance for vulnerability summary API
937+
func (impl *ImageScanServiceImpl) FetchScannedDeployInfoWithFilters(ctx context.Context, envIds, clusterIds []int) ([]*repository3.ImageScanDeployInfo, error) {
938+
_, span := otel.Tracer("imageScanService").Start(ctx, "FetchScannedDeployInfoWithFilters")
939+
defer span.End()
940+
941+
deployInfoList, err := impl.imageScanDeployInfoRepository.FindScannedDeployInfoWithFilters(envIds, clusterIds)
942+
if err != nil {
943+
impl.Logger.Errorw("error in FetchScannedDeployInfoWithFilters", "err", err, "envIds", envIds, "clusterIds", clusterIds)
944+
return nil, err
945+
}
946+
return deployInfoList, nil
947+
}
948+
931949
// FetchVulnerabilitySummary fetches the vulnerability summary for the given filters
932950
// Same filters as VulnerabilityListing: Environment, Cluster, Application, Severity, Fix Availability, Vulnerability Age
933951
// ids parameter contains RBAC-filtered deploy info IDs that the user has access to
@@ -959,41 +977,23 @@ func (impl *ImageScanServiceImpl) FetchVulnerabilitySummary(ctx context.Context,
959977
}, nil
960978
}
961979

962-
// Build vulnerability map (deduplicate by CVE + App + Env + Package + Version + FixedVersion)
963-
// Same logic as VulnerabilityListing
964-
vulnerabilityMap := make(map[string]*bean3.VulnerabilityDetail)
980+
vulnerabilities := make([]*bean3.VulnerabilityDetail, 0)
965981
for _, data := range rawData {
966-
// Create unique key for this CVE+App+Env+Package+Version+FixedVersion combination
967-
key := fmt.Sprintf("%s|%d|%d|%s|%s|%s", data.CveStoreName, data.AppId, data.EnvId, data.Package, data.CurrentVersion, data.FixedVersion)
968-
969982
// Convert severity int to string
970983
severityStr := impl.convertSeverityEnumToString(data.Severity)
984+
vulnerabilities = append(vulnerabilities, &bean3.VulnerabilityDetail{
985+
CVEName: data.CveStoreName,
986+
Severity: severityStr,
987+
AppName: data.AppName,
988+
AppId: data.AppId,
989+
EnvName: data.EnvName,
990+
EnvId: data.EnvId,
991+
DiscoveredAt: data.ExecutionTime,
992+
Package: data.Package,
993+
CurrentVersion: data.CurrentVersion,
994+
FixedVersion: data.FixedVersion,
995+
})
971996

972-
if existing, exists := vulnerabilityMap[key]; exists {
973-
// Keep the earliest discovery time
974-
if data.ExecutionTime.Before(existing.DiscoveredAt) {
975-
existing.DiscoveredAt = data.ExecutionTime
976-
}
977-
} else {
978-
vulnerabilityMap[key] = &bean3.VulnerabilityDetail{
979-
CVEName: data.CveStoreName,
980-
Severity: severityStr,
981-
AppName: data.AppName,
982-
AppId: data.AppId,
983-
EnvName: data.EnvName,
984-
EnvId: data.EnvId,
985-
DiscoveredAt: data.ExecutionTime,
986-
Package: data.Package,
987-
CurrentVersion: data.CurrentVersion,
988-
FixedVersion: data.FixedVersion,
989-
}
990-
}
991-
}
992-
993-
// Convert map to slice
994-
vulnerabilities := make([]*bean3.VulnerabilityDetail, 0, len(vulnerabilityMap))
995-
for _, vuln := range vulnerabilityMap {
996-
vulnerabilities = append(vulnerabilities, vuln)
997997
}
998998

999999
// Apply code-level filters (FixAvailable and VulnAge)

pkg/policyGovernance/security/imageScanning/repository/ImageScanDeployInfoRepository.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ type ImageScanDeployInfoRepository interface {
8787
GetActiveDeploymentScannedUnscannedCountByFilters(envIds, clusterIds, appIds []int) (*DeploymentScannedCount, error)
8888
GetNonScannedAppEnvCombinations(request *repoBean.ImageScanFilter, size int, offset int, deployInfoIds []int) ([]*ImageScanDeployInfo, error)
8989
GetNonScannedAppEnvCombinationsCount(request *repoBean.ImageScanFilter, deployInfoIds []int) (int, error)
90+
91+
// Optimized method for vulnerability summary - combines filtering with scanned artifact check in single query
92+
FindScannedDeployInfoWithFilters(envIds, clusterIds []int) ([]*ImageScanDeployInfo, error)
9093
}
9194

9295
type ImageScanDeployInfoRepositoryImpl struct {
@@ -633,3 +636,79 @@ func (impl ImageScanDeployInfoRepositoryImpl) buildNonScannedAppEnvQuery(request
633636

634637
return query, queryParams
635638
}
639+
640+
// FindScannedDeployInfoWithFilters returns deploy info for scanned artifacts that are currently deployed
641+
// This combines the logic of FindAll + FilterDeployInfoByScannedArtifactsDeployedInEnv into a single optimized query
642+
// It only returns deploy info where:
643+
// 1. The artifact is currently deployed (latest deployment per app+env)
644+
// 2. The artifact has been scanned (exists in image_scan_deploy_info with valid scan history)
645+
// 3. The deployed image matches the scanned image
646+
func (impl ImageScanDeployInfoRepositoryImpl) FindScannedDeployInfoWithFilters(envIds, clusterIds []int) ([]*ImageScanDeployInfo, error) {
647+
var results []*ImageScanDeployInfo
648+
649+
// This query:
650+
// 1. Gets the latest deployment per app+env from cd_workflow_runner
651+
// 2. Joins with image_scan_deploy_info to get only scanned deployments
652+
// 3. Verifies the deployed image matches the scanned image
653+
// 4. Applies environment and cluster filters
654+
query := `
655+
WITH LatestDeployments AS (
656+
SELECT
657+
p.app_id,
658+
p.environment_id,
659+
env.cluster_id,
660+
cia.image,
661+
ROW_NUMBER() OVER (PARTITION BY p.app_id, p.environment_id ORDER BY cwr.id DESC) AS rn
662+
FROM cd_workflow_runner cwr
663+
INNER JOIN cd_workflow cw ON cw.id = cwr.cd_workflow_id
664+
INNER JOIN pipeline p ON p.id = cw.pipeline_id
665+
INNER JOIN environment env ON env.id = p.environment_id
666+
INNER JOIN ci_artifact cia ON cia.id = cw.ci_artifact_id
667+
WHERE cwr.workflow_type = 'DEPLOY'
668+
AND p.deleted = false
669+
AND env.active = true
670+
`
671+
672+
var queryParams []interface{}
673+
674+
// Add filters to CTE
675+
if len(envIds) > 0 {
676+
query += " AND p.environment_id = ANY(?)"
677+
queryParams = append(queryParams, pg.Array(envIds))
678+
}
679+
680+
if len(clusterIds) > 0 {
681+
query += " AND env.cluster_id = ANY(?)"
682+
queryParams = append(queryParams, pg.Array(clusterIds))
683+
}
684+
685+
// Complete the CTE and join with image_scan_deploy_info
686+
query += `
687+
)
688+
SELECT
689+
isdi.id,
690+
isdi.image_scan_execution_history_id,
691+
isdi.scan_object_meta_id,
692+
isdi.object_type,
693+
isdi.env_id,
694+
isdi.cluster_id
695+
FROM LatestDeployments ld
696+
INNER JOIN image_scan_deploy_info isdi
697+
ON isdi.scan_object_meta_id = ld.app_id
698+
AND isdi.env_id = ld.environment_id
699+
AND isdi.object_type = 'app'
700+
AND isdi.image_scan_execution_history_id[1] != -1
701+
INNER JOIN image_scan_execution_history iseh
702+
ON iseh.id = isdi.image_scan_execution_history_id[1]
703+
AND iseh.image = ld.image
704+
WHERE ld.rn = 1
705+
`
706+
707+
_, err := impl.dbConnection.Query(&results, query, queryParams...)
708+
if err != nil {
709+
impl.logger.Errorw("error in FindScannedDeployInfoWithFilters", "err", err, "envIds", envIds, "clusterIds", clusterIds)
710+
return nil, err
711+
}
712+
713+
return results, nil
714+
}

0 commit comments

Comments
 (0)