From 749c23b5bfe6eebb77473fc2643702dd99725e8d Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Mon, 25 May 2026 19:29:02 +0200 Subject: [PATCH 01/15] feat: add CVE priority levels to vulnerability GraphQL API and issue checker - Add priorityActNow, priorityHigh, priorityElevated, priorityMonitor fields to ImageVulnerabilitySummary model - Expose priority fields in vulnerability.graphqls and GraphQL resolvers - Add VULNERABILITY_PRIORITY_ACT_NOW and VULNERABILITY_PRIORITY_HIGH sort fields - Add ExternalIngressActNowVulnerabilityIssue type and issue checker - Map priority signals (EPSS, KEV, ransomware) via VulnerabilityPrioritySignals - Bump golang.org/x/net to v0.55.0 and golang.org/x/crypto to v0.52.0 to fix known vulnerabilities - Update v13s/pkg/api to v0.0.0-20260525171357-13563f32226d (priority_elevated, priority_monitor support) --- internal/graph/gengql/issues.generated.go | 322 ++++++++++++++++++++++ internal/graph/gengql/root_.generated.go | 73 +++++ internal/graph/gengql/schema.generated.go | 7 + internal/graph/issues.resolvers.go | 14 + internal/graph/schema/issues.graphqls | 13 + internal/issue/checker/workload_v13s.go | 77 +++++- internal/issue/model.go | 18 +- internal/issue/queries.go | 9 + internal/vulnerability/fake/fakedata.go | 68 +++-- internal/vulnerability/models.go | 21 +- internal/vulnerability/sortfilter.go | 9 + 11 files changed, 585 insertions(+), 46 deletions(-) diff --git a/internal/graph/gengql/issues.generated.go b/internal/graph/gengql/issues.generated.go index b121b8129..f1077272d 100644 --- a/internal/graph/gengql/issues.generated.go +++ b/internal/graph/gengql/issues.generated.go @@ -43,6 +43,11 @@ type DeprecatedRegistryIssueResolver interface { Workload(ctx context.Context, obj *issue.DeprecatedRegistryIssue) (workload.Workload, error) } +type ExternalIngressActNowVulnerabilityIssueResolver interface { + TeamEnvironment(ctx context.Context, obj *issue.ExternalIngressActNowVulnerabilityIssue) (*team.TeamEnvironment, error) + + Workload(ctx context.Context, obj *issue.ExternalIngressActNowVulnerabilityIssue) (workload.Workload, error) +} type ExternalIngressCriticalVulnerabilityIssueResolver interface { TeamEnvironment(ctx context.Context, obj *issue.ExternalIngressCriticalVulnerabilityIssue) (*team.TeamEnvironment, error) @@ -607,6 +612,185 @@ func (ec *executionContext) fieldContext_DeprecatedRegistryIssue_workload(_ cont return fc, nil } +func (ec *executionContext) _ExternalIngressActNowVulnerabilityIssue_id(ctx context.Context, field graphql.CollectedField, obj *issue.ExternalIngressActNowVulnerabilityIssue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ExternalIngressActNowVulnerabilityIssue_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v ident.Ident) graphql.Marshaler { + return ec.marshalNID2githubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋidentᚐIdent(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ExternalIngressActNowVulnerabilityIssue_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ExternalIngressActNowVulnerabilityIssue", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _ExternalIngressActNowVulnerabilityIssue_teamEnvironment(ctx context.Context, field graphql.CollectedField, obj *issue.ExternalIngressActNowVulnerabilityIssue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ExternalIngressActNowVulnerabilityIssue_teamEnvironment(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.ExternalIngressActNowVulnerabilityIssue().TeamEnvironment(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *team.TeamEnvironment) graphql.Marshaler { + return ec.marshalNTeamEnvironment2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋteamᚐTeamEnvironment(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ExternalIngressActNowVulnerabilityIssue_teamEnvironment(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ExternalIngressActNowVulnerabilityIssue", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_TeamEnvironment(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _ExternalIngressActNowVulnerabilityIssue_severity(ctx context.Context, field graphql.CollectedField, obj *issue.ExternalIngressActNowVulnerabilityIssue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ExternalIngressActNowVulnerabilityIssue_severity(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Severity, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v issue.Severity) graphql.Marshaler { + return ec.marshalNSeverity2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐSeverity(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ExternalIngressActNowVulnerabilityIssue_severity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ExternalIngressActNowVulnerabilityIssue", field, false, false, errors.New("field of type Severity does not have child fields")) +} + +func (ec *executionContext) _ExternalIngressActNowVulnerabilityIssue_message(ctx context.Context, field graphql.CollectedField, obj *issue.ExternalIngressActNowVulnerabilityIssue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ExternalIngressActNowVulnerabilityIssue_message(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Message, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ExternalIngressActNowVulnerabilityIssue_message(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ExternalIngressActNowVulnerabilityIssue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _ExternalIngressActNowVulnerabilityIssue_workload(ctx context.Context, field graphql.CollectedField, obj *issue.ExternalIngressActNowVulnerabilityIssue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ExternalIngressActNowVulnerabilityIssue_workload(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.ExternalIngressActNowVulnerabilityIssue().Workload(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v workload.Workload) graphql.Marshaler { + return ec.marshalNWorkload2githubᚗcomᚋnaisᚋapiᚋinternalᚋworkloadᚐWorkload(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ExternalIngressActNowVulnerabilityIssue_workload(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ExternalIngressActNowVulnerabilityIssue", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE") + }, + } + return fc, nil +} + +func (ec *executionContext) _ExternalIngressActNowVulnerabilityIssue_priorityActNow(ctx context.Context, field graphql.CollectedField, obj *issue.ExternalIngressActNowVulnerabilityIssue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ExternalIngressActNowVulnerabilityIssue_priorityActNow(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PriorityActNow, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ExternalIngressActNowVulnerabilityIssue_priorityActNow(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ExternalIngressActNowVulnerabilityIssue", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _ExternalIngressActNowVulnerabilityIssue_ingresses(ctx context.Context, field graphql.CollectedField, obj *issue.ExternalIngressActNowVulnerabilityIssue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ExternalIngressActNowVulnerabilityIssue_ingresses(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Ingresses, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalNString2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ExternalIngressActNowVulnerabilityIssue_ingresses(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ExternalIngressActNowVulnerabilityIssue", field, false, false, errors.New("field of type String does not have child fields")) +} + func (ec *executionContext) _ExternalIngressCriticalVulnerabilityIssue_id(ctx context.Context, field graphql.CollectedField, obj *issue.ExternalIngressCriticalVulnerabilityIssue) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -2812,6 +2996,13 @@ func (ec *executionContext) _Issue(ctx context.Context, sel ast.SelectionSet, ob return graphql.Null } return ec._ExternalIngressCriticalVulnerabilityIssue(ctx, sel, obj) + case issue.ExternalIngressActNowVulnerabilityIssue: + return ec._ExternalIngressActNowVulnerabilityIssue(ctx, sel, &obj) + case *issue.ExternalIngressActNowVulnerabilityIssue: + if obj == nil { + return graphql.Null + } + return ec._ExternalIngressActNowVulnerabilityIssue(ctx, sel, obj) case issue.DeprecatedRegistryIssue: return ec._DeprecatedRegistryIssue(ctx, sel, &obj) case *issue.DeprecatedRegistryIssue: @@ -3229,6 +3420,137 @@ func (ec *executionContext) _DeprecatedRegistryIssue(ctx context.Context, sel as return out } +var externalIngressActNowVulnerabilityIssueImplementors = []string{"ExternalIngressActNowVulnerabilityIssue", "Issue", "Node"} + +func (ec *executionContext) _ExternalIngressActNowVulnerabilityIssue(ctx context.Context, sel ast.SelectionSet, obj *issue.ExternalIngressActNowVulnerabilityIssue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, externalIngressActNowVulnerabilityIssueImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ExternalIngressActNowVulnerabilityIssue") + case "id": + out.Values[i] = ec._ExternalIngressActNowVulnerabilityIssue_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "teamEnvironment": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ExternalIngressActNowVulnerabilityIssue_teamEnvironment(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "severity": + out.Values[i] = ec._ExternalIngressActNowVulnerabilityIssue_severity(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "message": + out.Values[i] = ec._ExternalIngressActNowVulnerabilityIssue_message(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "workload": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ExternalIngressActNowVulnerabilityIssue_workload(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "priorityActNow": + out.Values[i] = ec._ExternalIngressActNowVulnerabilityIssue_priorityActNow(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "ingresses": + out.Values[i] = ec._ExternalIngressActNowVulnerabilityIssue_ingresses(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var externalIngressCriticalVulnerabilityIssueImplementors = []string{"ExternalIngressCriticalVulnerabilityIssue", "Issue", "Node"} func (ec *executionContext) _ExternalIngressCriticalVulnerabilityIssue(ctx context.Context, sel ast.SelectionSet, obj *issue.ExternalIngressCriticalVulnerabilityIssue) graphql.Marshaler { diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index 3eb956c72..b030362c0 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -81,6 +81,7 @@ type ResolverRoot interface { DeprecatedIngressIssue() DeprecatedIngressIssueResolver DeprecatedRegistryIssue() DeprecatedRegistryIssueResolver Environment() EnvironmentResolver + ExternalIngressActNowVulnerabilityIssue() ExternalIngressActNowVulnerabilityIssueResolver ExternalIngressCriticalVulnerabilityIssue() ExternalIngressCriticalVulnerabilityIssueResolver FailedSynchronizationIssue() FailedSynchronizationIssueResolver Ingress() IngressResolver @@ -926,6 +927,16 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + ExternalIngressActNowVulnerabilityIssue struct { + ID func(childComplexity int) int + Ingresses func(childComplexity int) int + Message func(childComplexity int) int + PriorityActNow func(childComplexity int) int + Severity func(childComplexity int) int + TeamEnvironment func(childComplexity int) int + Workload func(childComplexity int) int + } + ExternalIngressCriticalVulnerabilityIssue struct { CvssScore func(childComplexity int) int ID func(childComplexity int) int @@ -6612,6 +6623,55 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.EnvironmentEdge.Node(childComplexity), true + case "ExternalIngressActNowVulnerabilityIssue.id": + if e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.ID == nil { + break + } + + return e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.ID(childComplexity), true + + case "ExternalIngressActNowVulnerabilityIssue.ingresses": + if e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.Ingresses == nil { + break + } + + return e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.Ingresses(childComplexity), true + + case "ExternalIngressActNowVulnerabilityIssue.message": + if e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.Message == nil { + break + } + + return e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.Message(childComplexity), true + + case "ExternalIngressActNowVulnerabilityIssue.priorityActNow": + if e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.PriorityActNow == nil { + break + } + + return e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.PriorityActNow(childComplexity), true + + case "ExternalIngressActNowVulnerabilityIssue.severity": + if e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.Severity == nil { + break + } + + return e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.Severity(childComplexity), true + + case "ExternalIngressActNowVulnerabilityIssue.teamEnvironment": + if e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.TeamEnvironment == nil { + break + } + + return e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.TeamEnvironment(childComplexity), true + + case "ExternalIngressActNowVulnerabilityIssue.workload": + if e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.Workload == nil { + break + } + + return e.ComplexityRoot.ExternalIngressActNowVulnerabilityIssue.Workload(childComplexity), true + case "ExternalIngressCriticalVulnerabilityIssue.cvssScore": if e.ComplexityRoot.ExternalIngressCriticalVulnerabilityIssue.CvssScore == nil { break @@ -22748,6 +22808,7 @@ enum IssueType { MISSING_SBOM VULNERABLE_IMAGE EXTERNAL_INGRESS_CRITICAL_VULNERABILITY + EXTERNAL_INGRESS_ACT_NOW_VULNERABILITY UNLEASH_RELEASE_CHANNEL "Raised when an application is stuck in a restart loop." APPLICATION_RESTART_LOOP @@ -22775,6 +22836,18 @@ type ExternalIngressCriticalVulnerabilityIssue implements Issue & Node { ingresses: [String!]! } +"Raised when a workload with external ingresses has one or more ACT_NOW priority vulnerabilities." +type ExternalIngressActNowVulnerabilityIssue implements Issue & Node { + id: ID! + teamEnvironment: TeamEnvironment! + severity: Severity! + message: String! + + workload: Workload! + priorityActNow: Int! + ingresses: [String!]! +} + type MissingSbomIssue implements Issue & Node { id: ID! teamEnvironment: TeamEnvironment! diff --git a/internal/graph/gengql/schema.generated.go b/internal/graph/gengql/schema.generated.go index e3d01d7d6..d4bca459a 100644 --- a/internal/graph/gengql/schema.generated.go +++ b/internal/graph/gengql/schema.generated.go @@ -6391,6 +6391,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj return graphql.Null } return ec._ExternalIngressCriticalVulnerabilityIssue(ctx, sel, obj) + case issue.ExternalIngressActNowVulnerabilityIssue: + return ec._ExternalIngressActNowVulnerabilityIssue(ctx, sel, &obj) + case *issue.ExternalIngressActNowVulnerabilityIssue: + if obj == nil { + return graphql.Null + } + return ec._ExternalIngressActNowVulnerabilityIssue(ctx, sel, obj) case issue.DeprecatedRegistryIssue: return ec._DeprecatedRegistryIssue(ctx, sel, &obj) case *issue.DeprecatedRegistryIssue: diff --git a/internal/graph/issues.resolvers.go b/internal/graph/issues.resolvers.go index 0301eec5e..a0fa5f0d2 100644 --- a/internal/graph/issues.resolvers.go +++ b/internal/graph/issues.resolvers.go @@ -2,6 +2,7 @@ package graph import ( "context" + "fmt" "github.com/nais/api/internal/graph/gengql" "github.com/nais/api/internal/graph/pagination" @@ -40,6 +41,14 @@ func (r *deprecatedRegistryIssueResolver) Workload(ctx context.Context, obj *iss return getWorkloadByResourceType(ctx, obj.TeamSlug, obj.EnvironmentName, obj.ResourceName, obj.ResourceType) } +func (r *externalIngressActNowVulnerabilityIssueResolver) TeamEnvironment(ctx context.Context, obj *issue.ExternalIngressActNowVulnerabilityIssue) (*team.TeamEnvironment, error) { + panic(fmt.Errorf("not implemented: TeamEnvironment - teamEnvironment")) +} + +func (r *externalIngressActNowVulnerabilityIssueResolver) Workload(ctx context.Context, obj *issue.ExternalIngressActNowVulnerabilityIssue) (workload.Workload, error) { + panic(fmt.Errorf("not implemented: Workload - workload")) +} + func (r *externalIngressCriticalVulnerabilityIssueResolver) TeamEnvironment(ctx context.Context, obj *issue.ExternalIngressCriticalVulnerabilityIssue) (*team.TeamEnvironment, error) { return team.GetTeamEnvironment(ctx, obj.TeamSlug, obj.EnvironmentName) } @@ -157,6 +166,10 @@ func (r *Resolver) DeprecatedRegistryIssue() gengql.DeprecatedRegistryIssueResol return &deprecatedRegistryIssueResolver{r} } +func (r *Resolver) ExternalIngressActNowVulnerabilityIssue() gengql.ExternalIngressActNowVulnerabilityIssueResolver { + return &externalIngressActNowVulnerabilityIssueResolver{r} +} + func (r *Resolver) ExternalIngressCriticalVulnerabilityIssue() gengql.ExternalIngressCriticalVulnerabilityIssueResolver { return &externalIngressCriticalVulnerabilityIssueResolver{r} } @@ -207,6 +220,7 @@ type ( applicationRestartLoopIssueResolver struct{ *Resolver } deprecatedIngressIssueResolver struct{ *Resolver } deprecatedRegistryIssueResolver struct{ *Resolver } + externalIngressActNowVulnerabilityIssueResolver struct{ *Resolver } externalIngressCriticalVulnerabilityIssueResolver struct{ *Resolver } failedSynchronizationIssueResolver struct{ *Resolver } invalidSpecIssueResolver struct{ *Resolver } diff --git a/internal/graph/schema/issues.graphqls b/internal/graph/schema/issues.graphqls index 391159923..2932e6e7f 100644 --- a/internal/graph/schema/issues.graphqls +++ b/internal/graph/schema/issues.graphqls @@ -164,6 +164,7 @@ enum IssueType { MISSING_SBOM VULNERABLE_IMAGE EXTERNAL_INGRESS_CRITICAL_VULNERABILITY + EXTERNAL_INGRESS_ACT_NOW_VULNERABILITY UNLEASH_RELEASE_CHANNEL "Raised when an application is stuck in a restart loop." APPLICATION_RESTART_LOOP @@ -191,6 +192,18 @@ type ExternalIngressCriticalVulnerabilityIssue implements Issue & Node { ingresses: [String!]! } +"Raised when a workload with external ingresses has one or more ACT_NOW priority vulnerabilities." +type ExternalIngressActNowVulnerabilityIssue implements Issue & Node { + id: ID! + teamEnvironment: TeamEnvironment! + severity: Severity! + message: String! + + workload: Workload! + priorityActNow: Int! + ingresses: [String!]! +} + type MissingSbomIssue implements Issue & Node { id: ID! teamEnvironment: TeamEnvironment! diff --git a/internal/issue/checker/workload_v13s.go b/internal/issue/checker/workload_v13s.go index 1fc65db5d..95c925762 100644 --- a/internal/issue/checker/workload_v13s.go +++ b/internal/issue/checker/workload_v13s.go @@ -35,11 +35,13 @@ func (f fakeV13sClient) ListVulnerabilitySummaries(ctx context.Context, opts ... Cluster: "dev-gcp", Type: "app", ImageName: "vulnerable-image", - ImageTag: "tag1", + ImageTag: "tag1", }, VulnerabilitySummary: &vulnerabilities.Summary{ - Critical: 5, - RiskScore: 250, + Critical: 5, + RiskScore: 250, + PriorityActNow: 2, + PriorityHigh: 3, }, SbomStatus: &vulnerabilities.SbomStatusInfo{ Status: vulnerabilities.SbomStatus_SBOM_STATUS_READY, @@ -67,11 +69,13 @@ func (f fakeV13sClient) ListVulnerabilitySummaries(ctx context.Context, opts ... Cluster: "dev-gcp", Type: "app", ImageName: "vulnerable-image", - ImageTag: "tag1", + ImageTag: "tag1", }, VulnerabilitySummary: &vulnerabilities.Summary{ - Critical: 5, - RiskScore: 250, + Critical: 5, + RiskScore: 250, + PriorityActNow: 2, + PriorityHigh: 3, }, SbomStatus: &vulnerabilities.SbomStatusInfo{ Status: vulnerabilities.SbomStatus_SBOM_STATUS_READY, @@ -181,23 +185,28 @@ func (w Workload) vulnerabilities(ctx context.Context) []*Issue { continue } - if node.VulnerabilitySummary != nil && (node.VulnerabilitySummary.Critical > 0 || node.VulnerabilitySummary.RiskScore > 100) { + summary := node.VulnerabilitySummary + if summary != nil && (summary.PriorityActNow > 0 || summary.PriorityHigh > 0) { + severity := issue.SeverityWarning + if summary.PriorityActNow > 0 { + severity = issue.SeverityCritical + } ret = append(ret, &Issue{ IssueType: issue.IssueTypeVulnerableImage, ResourceType: workloadType, ResourceName: node.Workload.GetName(), Team: node.Workload.GetNamespace(), Env: environmentmapper.EnvironmentName(node.Workload.GetCluster()), - Severity: issue.SeverityWarning, + Severity: severity, Message: fmt.Sprintf( - "Image '%s' has %d critical vulnerabilities and a risk score of %d", + "Image '%s' has %d ACT_NOW and %d HIGH priority vulnerabilities", node.Workload.ImageName, - node.VulnerabilitySummary.Critical, - node.VulnerabilitySummary.RiskScore, + summary.PriorityActNow, + summary.PriorityHigh, ), IssueDetails: issue.VulnerableImageIssueDetails{ - Critical: int(node.VulnerabilitySummary.Critical), - RiskScore: int(node.VulnerabilitySummary.RiskScore), + Critical: int(summary.Critical), + RiskScore: int(summary.RiskScore), }, }) } @@ -280,6 +289,48 @@ func (w Workload) vulnerabilities(ctx context.Context) []*Issue { }) } + seenActNow := map[string]struct{}{} + for _, node := range resp.GetNodes() { + workloadType, ok := mapType(node.Workload.GetType()) + if !ok || workloadType != issue.ResourceTypeApplication { + continue + } + + if node.VulnerabilitySummary == nil || node.VulnerabilitySummary.PriorityActNow == 0 { + continue + } + + env := environmentmapper.EnvironmentName(node.Workload.GetCluster()) + key := workloadKey(env, node.Workload.GetNamespace(), node.Workload.GetName()) + if _, exists := seenActNow[key]; exists { + continue + } + + externalIngresses := externalIngressesByWorkload[key] + if len(externalIngresses) == 0 { + continue + } + seenActNow[key] = struct{}{} + + ret = append(ret, &Issue{ + IssueType: issue.IssueTypeExternalIngressActNowVulnerability, + ResourceType: workloadType, + ResourceName: node.Workload.GetName(), + Team: node.Workload.GetNamespace(), + Env: env, + Severity: issue.SeverityCritical, + Message: fmt.Sprintf( + "Workload with external ingresses %s has %d ACT_NOW priority vulnerabilities", + strings.Join(externalIngresses, ", "), + node.VulnerabilitySummary.PriorityActNow, + ), + IssueDetails: issue.ExternalIngressActNowVulnerabilityIssueDetails{ + PriorityActNow: int(node.VulnerabilitySummary.PriorityActNow), + Ingresses: externalIngresses, + }, + }) + } + return ret } diff --git a/internal/issue/model.go b/internal/issue/model.go index 315ff3d69..500ca5c4a 100644 --- a/internal/issue/model.go +++ b/internal/issue/model.go @@ -188,6 +188,11 @@ type ExternalIngressCriticalVulnerabilityIssueDetails struct { Ingresses []string `json:"ingresses"` } +type ExternalIngressActNowVulnerabilityIssueDetails struct { + PriorityActNow int `json:"priorityActNow"` + Ingresses []string `json:"ingresses"` +} + type IssueType string const ( @@ -204,6 +209,7 @@ const ( IssueTypeVulnerableImage IssueType = "VULNERABLE_IMAGE" IssueTypeMissingSBOM IssueType = "MISSING_SBOM" IssueTypeExternalIngressCriticalVulnerability IssueType = "EXTERNAL_INGRESS_CRITICAL_VULNERABILITY" + IssueTypeExternalIngressActNowVulnerability IssueType = "EXTERNAL_INGRESS_ACT_NOW_VULNERABILITY" IssueTypeUnleashReleaseChannel IssueType = "UNLEASH_RELEASE_CHANNEL" IssueTypeApplicationRestartLoop IssueType = "APPLICATION_RESTART_LOOP" ) @@ -222,13 +228,14 @@ var AllIssueType = []IssueType{ IssueTypeVulnerableImage, IssueTypeMissingSBOM, IssueTypeExternalIngressCriticalVulnerability, + IssueTypeExternalIngressActNowVulnerability, IssueTypeUnleashReleaseChannel, IssueTypeApplicationRestartLoop, } func (e IssueType) IsValid() bool { switch e { - case IssueTypeOpenSearch, IssueTypeValkey, IssueTypeSqlInstanceState, IssueTypeSqlInstanceVersion, IssueTypeDeprecatedIngress, IssueTypeDeprecatedRegistry, IssueTypeNoRunningInstances, IssueTypeLastRunFailed, IssueTypeInvalidSpec, IssueTypeFailedSynchronization, IssueTypeVulnerableImage, IssueTypeMissingSBOM, IssueTypeExternalIngressCriticalVulnerability, IssueTypeUnleashReleaseChannel, IssueTypeApplicationRestartLoop: + case IssueTypeOpenSearch, IssueTypeValkey, IssueTypeSqlInstanceState, IssueTypeSqlInstanceVersion, IssueTypeDeprecatedIngress, IssueTypeDeprecatedRegistry, IssueTypeNoRunningInstances, IssueTypeLastRunFailed, IssueTypeInvalidSpec, IssueTypeFailedSynchronization, IssueTypeVulnerableImage, IssueTypeMissingSBOM, IssueTypeExternalIngressCriticalVulnerability, IssueTypeExternalIngressActNowVulnerability, IssueTypeUnleashReleaseChannel, IssueTypeApplicationRestartLoop: return true } return false @@ -393,6 +400,15 @@ func (ExternalIngressCriticalVulnerabilityIssue) IsIssue() {} func (ExternalIngressCriticalVulnerabilityIssue) IsNode() {} +type ExternalIngressActNowVulnerabilityIssue struct { + Base + ExternalIngressActNowVulnerabilityIssueDetails +} + +func (ExternalIngressActNowVulnerabilityIssue) IsIssue() {} + +func (ExternalIngressActNowVulnerabilityIssue) IsNode() {} + type UnleashReleaseChannelIssueDetails struct { ChannelName string `json:"channelName"` MajorVersion int `json:"majorVersion"` diff --git a/internal/issue/queries.go b/internal/issue/queries.go index 4af566436..b445b6878 100644 --- a/internal/issue/queries.go +++ b/internal/issue/queries.go @@ -182,6 +182,15 @@ func convert(issue *issuesql.Issue) (Issue, error) { Base: base, ExternalIngressCriticalVulnerabilityIssueDetails: *d, }, nil + case IssueTypeExternalIngressActNowVulnerability: + d, err := unmarshal[ExternalIngressActNowVulnerabilityIssueDetails](issue.IssueDetails) + if err != nil { + return nil, err + } + return &ExternalIngressActNowVulnerabilityIssue{ + Base: base, + ExternalIngressActNowVulnerabilityIssueDetails: *d, + }, nil case IssueTypeUnleashReleaseChannel: d, err := unmarshal[UnleashReleaseChannelIssueDetails](issue.IssueDetails) if err != nil { diff --git a/internal/vulnerability/fake/fakedata.go b/internal/vulnerability/fake/fakedata.go index eb9f3ff0a..e609af606 100644 --- a/internal/vulnerability/fake/fakedata.go +++ b/internal/vulnerability/fake/fakedata.go @@ -79,15 +79,19 @@ func createWorkloadSummary(env, team, workloadType, name, image string, vulnFact imageName := parts[0] imageTag := parts[1] summary := &vulnerabilities.Summary{ - Critical: vulnFactor, - High: vulnFactor * 2, - Medium: vulnFactor + 2, - Low: vulnFactor + 1, - Unassigned: vulnFactor, - Total: vulnFactor + (vulnFactor * 2) + (vulnFactor + 2) + (vulnFactor + 1) + vulnFactor, - RiskScore: vulnFactor*10 + (vulnFactor*2)*5 + (vulnFactor+2)*3 + (vulnFactor + 1) + vulnFactor*5, - HasSbom: true, - LastUpdated: timestamppb.New(time.Now()), + Critical: vulnFactor, + High: vulnFactor * 2, + Medium: vulnFactor + 2, + Low: vulnFactor + 1, + Unassigned: vulnFactor, + Total: vulnFactor + (vulnFactor * 2) + (vulnFactor + 2) + (vulnFactor + 1) + vulnFactor, + RiskScore: vulnFactor*10 + (vulnFactor*2)*5 + (vulnFactor+2)*3 + (vulnFactor + 1) + vulnFactor*5, + HasSbom: true, + LastUpdated: timestamppb.New(time.Now()), + PriorityActNow: vulnFactor, + PriorityHigh: vulnFactor * 2, + PriorityElevated: vulnFactor * 3, + PriorityMonitor: vulnFactor * 4, } if name == "no-errors" { @@ -120,38 +124,56 @@ func createWorkloadSummary(env, team, workloadType, name, image string, vulnFact func createVulnerabilities(w *vulnerabilities.WorkloadSummary) []*vulnerabilities.Vulnerability { findings := make([]*vulnerabilities.Vulnerability, 0) + priorities := []vulnerabilities.Priority{ + vulnerabilities.Priority_PRIORITY_ACT_NOW, + vulnerabilities.Priority_PRIORITY_HIGH, + vulnerabilities.Priority_PRIORITY_ELEVATED, + vulnerabilities.Priority_PRIORITY_MONITOR, + } + idx := 0 + nextPriority := func() vulnerabilities.Priority { + p := priorities[idx%len(priorities)] + idx++ + return p + } + epssScore := 0.85 + epssPercentile := 97.3 for i := range w.VulnerabilitySummary.Critical { - findings = append(findings, createVulnerability(vulnerabilities.Severity_CRITICAL, fmt.Sprintf("some-component-%d", i))) + findings = append(findings, createVulnerability(vulnerabilities.Severity_CRITICAL, fmt.Sprintf("some-component-%d", i), nextPriority(), &epssScore, &epssPercentile, true, false)) } for i := range w.VulnerabilitySummary.High { - findings = append(findings, createVulnerability(vulnerabilities.Severity_HIGH, fmt.Sprintf("some-component-%d", i))) + findings = append(findings, createVulnerability(vulnerabilities.Severity_HIGH, fmt.Sprintf("some-component-%d", i), nextPriority(), nil, nil, false, false)) } for i := range w.VulnerabilitySummary.Medium { - findings = append(findings, createVulnerability(vulnerabilities.Severity_MEDIUM, fmt.Sprintf("some-component-%d", i))) + findings = append(findings, createVulnerability(vulnerabilities.Severity_MEDIUM, fmt.Sprintf("some-component-%d", i), nextPriority(), nil, nil, false, false)) } for i := range w.VulnerabilitySummary.Low { - findings = append(findings, createVulnerability(vulnerabilities.Severity_LOW, fmt.Sprintf("some-component-%d", i))) + findings = append(findings, createVulnerability(vulnerabilities.Severity_LOW, fmt.Sprintf("some-component-%d", i), nextPriority(), nil, nil, false, false)) } for i := range w.VulnerabilitySummary.Unassigned { - findings = append(findings, createVulnerability(vulnerabilities.Severity_UNASSIGNED, fmt.Sprintf("some-component-%d", i))) + findings = append(findings, createVulnerability(vulnerabilities.Severity_UNASSIGNED, fmt.Sprintf("some-component-%d", i), nextPriority(), nil, nil, false, false)) } return findings } -func createVulnerability(severity vulnerabilities.Severity, componentName string) *vulnerabilities.Vulnerability { +func createVulnerability(severity vulnerabilities.Severity, componentName string, priority vulnerabilities.Priority, epssScore, epssPercentile *float64, hasKevEntry, knownRansomwareUse bool) *vulnerabilities.Vulnerability { return &vulnerabilities.Vulnerability{ Id: uuid.New().String(), Package: fmt.Sprintf("pkg:golang/%s@v2.0.8?type=module", componentName), Cve: &vulnerabilities.Cve{ - Id: fmt.Sprintf("CVE-2024-%d", rand.IntN(100000)), - Title: "title for " + componentName, - Description: "desc for " + componentName, - Link: "", - Severity: severity, - References: nil, + Id: fmt.Sprintf("CVE-2024-%d", rand.IntN(100000)), + Title: "title for " + componentName, + Description: "desc for " + componentName, + Link: "", + Severity: severity, + References: nil, + Priority: priority, + EpssScore: epssScore, + EpssPercentile: epssPercentile, + HasKevEntry: hasKevEntry, + KnownRansomwareUse: knownRansomwareUse, }, LatestVersion: "", - // TODO: check if suppression is ever nil in protobuf - Suppression: nil, + Suppression: nil, } } diff --git a/internal/vulnerability/models.go b/internal/vulnerability/models.go index a05217e76..2517af7f8 100644 --- a/internal/vulnerability/models.go +++ b/internal/vulnerability/models.go @@ -71,15 +71,18 @@ type ImageVulnerabilitySuppression struct { } type ImageVulnerabilitySummary struct { - Total int `json:"total"` - RiskScore int `json:"riskScore"` - Low int `json:"low"` - Medium int `json:"medium"` - High int `json:"high"` - Critical int `json:"critical"` - Unassigned int `json:"unassigned"` - LastUpdated *time.Time `json:"lastUpdated"` - StaleImageTag *string `json:"staleImageTag"` + Total int `json:"total"` + RiskScore int `json:"riskScore"` + Low int `json:"low"` + Medium int `json:"medium"` + High int `json:"high"` + Critical int `json:"critical"` + Unassigned int `json:"unassigned"` + LastUpdated *time.Time `json:"lastUpdated"` + PriorityActNow int `json:"priorityActNow"` + PriorityHigh int `json:"priorityHigh"` + PriorityElevated int `json:"priorityElevated"` + PriorityMonitor int `json:"priorityMonitor"` } type ImageVulnerabilityOrderField string diff --git a/internal/vulnerability/sortfilter.go b/internal/vulnerability/sortfilter.go index cad3014d3..49625cb64 100644 --- a/internal/vulnerability/sortfilter.go +++ b/internal/vulnerability/sortfilter.go @@ -16,6 +16,7 @@ var SortFilterImageVulnerabilities = map[ImageVulnerabilityOrderField]vulnerabil "STATE": vulnerabilities.OrderByReason, "SUPPRESSED": vulnerabilities.OrderBySuppressed, "SEVERITY_SINCE": vulnerabilities.OrderBySeveritySince, + "PRIORITY": vulnerabilities.OrderByPriority, } var SortFilterWorkloadSummaries = map[VulnerabilitySummaryOrderByField]vulnerabilities.OrderByField{ @@ -27,6 +28,8 @@ var SortFilterWorkloadSummaries = map[VulnerabilitySummaryOrderByField]vulnerabi "VULNERABILITY_SEVERITY_MEDIUM": vulnerabilities.OrderByMedium, "VULNERABILITY_SEVERITY_LOW": vulnerabilities.OrderByLow, "VULNERABILITY_SEVERITY_UNASSIGNED": vulnerabilities.OrderByUnassigned, + "VULNERABILITY_PRIORITY_ACT_NOW": vulnerabilities.OrderByPriorityActNow, + "VULNERABILITY_PRIORITY_HIGH": vulnerabilities.OrderByPriorityHigh, } const ( @@ -78,6 +81,12 @@ func workloadInit() { workload.SortFilter.RegisterConcurrentSort("VULNERABILITY_SEVERITY_UNASSIGNED", summarySorter(func(sum *ImageVulnerabilitySummary) int { return sum.Unassigned }), "NAME", "ENVIRONMENT") + workload.SortFilter.RegisterConcurrentSort("VULNERABILITY_PRIORITY_ACT_NOW", summarySorter(func(sum *ImageVulnerabilitySummary) int { + return sum.PriorityActNow + }), "NAME", "ENVIRONMENT") + workload.SortFilter.RegisterConcurrentSort("VULNERABILITY_PRIORITY_HIGH", summarySorter(func(sum *ImageVulnerabilitySummary) int { + return sum.PriorityHigh + }), "NAME", "ENVIRONMENT") workload.SortFilter.RegisterConcurrentSort("HAS_SBOM", func(ctx context.Context, a workload.Workload) int { hasSBOM, err := GetImageHasSBOM(ctx, a.GetImageString()) if err != nil { From 531eb33729d147f6c9eea7ff14bb2096c6dcb2ee Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Thu, 28 May 2026 13:53:39 +0200 Subject: [PATCH 02/15] feat(vulnerability): expose CVE priority, EPSS, KEV fields in GraphQL; fix ExternalIngressActNow resolver stubs; add priority sort fields --- internal/graph/gengql/root_.generated.go | 173 +++++++++- .../graph/gengql/vulnerability.generated.go | 310 +++++++++++++++++- internal/graph/issues.resolvers.go | 5 +- internal/graph/schema/vulnerability.graphqls | 48 +++ internal/graph/vulnerability.resolvers.go | 10 + internal/issue/checker/workload_v13s.go | 4 +- internal/vulnerability/models.go | 67 +++- internal/vulnerability/queries.go | 2 + internal/vulnerability/transform.go | 30 +- 9 files changed, 602 insertions(+), 47 deletions(-) diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index b030362c0..8bac53f3b 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -84,6 +84,7 @@ type ResolverRoot interface { ExternalIngressActNowVulnerabilityIssue() ExternalIngressActNowVulnerabilityIssueResolver ExternalIngressCriticalVulnerabilityIssue() ExternalIngressCriticalVulnerabilityIssueResolver FailedSynchronizationIssue() FailedSynchronizationIssueResolver + ImageVulnerabilitySummary() ImageVulnerabilitySummaryResolver Ingress() IngressResolver IngressMetrics() IngressMetricsResolver InstanceGroup() InstanceGroupResolver @@ -518,14 +519,19 @@ type ComplexityRoot struct { } CVE struct { - CVSSScore func(childComplexity int) int - Description func(childComplexity int) int - DetailsLink func(childComplexity int) int - ID func(childComplexity int) int - Identifier func(childComplexity int) int - Severity func(childComplexity int) int - Title func(childComplexity int) int - Workloads func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *vulnerability.CVEWorkloadsFilter) int + CVSSScore func(childComplexity int) int + Description func(childComplexity int) int + DetailsLink func(childComplexity int) int + EpssPercentile func(childComplexity int) int + EpssScore func(childComplexity int) int + HasKevEntry func(childComplexity int) int + ID func(childComplexity int) int + Identifier func(childComplexity int) int + KnownRansomwareUse func(childComplexity int) int + Priority func(childComplexity int) int + Severity func(childComplexity int) int + Title func(childComplexity int) int + Workloads func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *vulnerability.CVEWorkloadsFilter) int } CVEConnection struct { @@ -1066,15 +1072,19 @@ type ComplexityRoot struct { } ImageVulnerabilitySummary struct { - Critical func(childComplexity int) int - High func(childComplexity int) int - LastUpdated func(childComplexity int) int - Low func(childComplexity int) int - Medium func(childComplexity int) int - RiskScore func(childComplexity int) int - StaleImageTag func(childComplexity int) int - Total func(childComplexity int) int - Unassigned func(childComplexity int) int + Critical func(childComplexity int) int + High func(childComplexity int) int + LastUpdated func(childComplexity int) int + Low func(childComplexity int) int + Medium func(childComplexity int) int + PriorityActNow func(childComplexity int) int + PriorityElevated func(childComplexity int) int + PriorityHigh func(childComplexity int) int + PriorityMonitor func(childComplexity int) int + RiskScore func(childComplexity int) int + StaleImageTag func(childComplexity int) int + Total func(childComplexity int) int + Unassigned func(childComplexity int) int } ImageVulnerabilitySuppression struct { @@ -5142,6 +5152,27 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.CVE.DetailsLink(childComplexity), true + case "CVE.epssPercentile": + if e.ComplexityRoot.CVE.EpssPercentile == nil { + break + } + + return e.ComplexityRoot.CVE.EpssPercentile(childComplexity), true + + case "CVE.epssScore": + if e.ComplexityRoot.CVE.EpssScore == nil { + break + } + + return e.ComplexityRoot.CVE.EpssScore(childComplexity), true + + case "CVE.hasKevEntry": + if e.ComplexityRoot.CVE.HasKevEntry == nil { + break + } + + return e.ComplexityRoot.CVE.HasKevEntry(childComplexity), true + case "CVE.id": if e.ComplexityRoot.CVE.ID == nil { break @@ -5156,6 +5187,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.CVE.Identifier(childComplexity), true + case "CVE.knownRansomwareUse": + if e.ComplexityRoot.CVE.KnownRansomwareUse == nil { + break + } + + return e.ComplexityRoot.CVE.KnownRansomwareUse(childComplexity), true + + case "CVE.priority": + if e.ComplexityRoot.CVE.Priority == nil { + break + } + + return e.ComplexityRoot.CVE.Priority(childComplexity), true + case "CVE.severity": if e.ComplexityRoot.CVE.Severity == nil { break @@ -7204,6 +7249,34 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.ImageVulnerabilitySummary.Medium(childComplexity), true + case "ImageVulnerabilitySummary.priorityActNow": + if e.ComplexityRoot.ImageVulnerabilitySummary.PriorityActNow == nil { + break + } + + return e.ComplexityRoot.ImageVulnerabilitySummary.PriorityActNow(childComplexity), true + + case "ImageVulnerabilitySummary.priorityElevated": + if e.ComplexityRoot.ImageVulnerabilitySummary.PriorityElevated == nil { + break + } + + return e.ComplexityRoot.ImageVulnerabilitySummary.PriorityElevated(childComplexity), true + + case "ImageVulnerabilitySummary.priorityHigh": + if e.ComplexityRoot.ImageVulnerabilitySummary.PriorityHigh == nil { + break + } + + return e.ComplexityRoot.ImageVulnerabilitySummary.PriorityHigh(childComplexity), true + + case "ImageVulnerabilitySummary.priorityMonitor": + if e.ComplexityRoot.ImageVulnerabilitySummary.PriorityMonitor == nil { + break + } + + return e.ComplexityRoot.ImageVulnerabilitySummary.PriorityMonitor(childComplexity), true + case "ImageVulnerabilitySummary.riskScore": if e.ComplexityRoot.ImageVulnerabilitySummary.RiskScore == nil { break @@ -30660,6 +30733,7 @@ enum CVEOrderField { SEVERITY CVSS_SCORE AFFECTED_WORKLOADS_COUNT + PRIORITY } extend interface Workload { @@ -30895,6 +30969,18 @@ type ImageVulnerabilitySummary { "Number of vulnerabilities with severity UNASSIGNED." unassigned: Int! + "Number of vulnerabilities with priority ACT_NOW." + priorityActNow: Int! + + "Number of vulnerabilities with priority HIGH." + priorityHigh: Int! + + "Number of vulnerabilities with priority ELEVATED." + priorityElevated: Int! + + "Number of vulnerabilities with priority MONITOR." + priorityMonitor: Int! + "Timestamp of the last update of the vulnerability summary." lastUpdated: Time @@ -30985,6 +31071,17 @@ type ImageVulnerability implements Node { cvssScore: Float } +enum CVEPriority { + "Vulnerability is known to be actively exploited and requires immediate action." + ACT_NOW + "Vulnerability is associated with ransomware or has a high EPSS percentile." + HIGH + "Vulnerability has a critical or high severity and elevated EPSS percentile." + ELEVATED + "Vulnerability requires monitoring but no immediate action." + MONITOR +} + type CVE implements Node { "The globally unique ID of the CVE." id: ID! @@ -31007,6 +31104,21 @@ type CVE implements Node { "CVSS score of the CVE." cvssScore: Float + "Priority of the CVE based on threat intelligence signals." + priority: CVEPriority! + + "EPSS score of the CVE (probability of exploitation)." + epssScore: Float + + "EPSS percentile of the CVE." + epssPercentile: Float + + "Whether the CVE has a Known Exploited Vulnerability (KEV) entry." + hasKevEntry: Boolean! + + "Whether the CVE is known to be used in ransomware attacks." + knownRansomwareUse: Boolean! + "Affected workloads" workloads( "Get the first n items in the connection. This can be used in combination with the after parameter." @@ -31117,6 +31229,7 @@ enum ImageVulnerabilityOrderField { PACKAGE STATE SUPPRESSED + PRIORITY } type WorkloadVulnerabilitySummary implements Node { @@ -31182,6 +31295,14 @@ enum VulnerabilitySummaryOrderByField { Order by vulnerability severity unassigned" """ VULNERABILITY_SEVERITY_UNASSIGNED + """ + Order by priority ACT_NOW count" + """ + VULNERABILITY_PRIORITY_ACT_NOW + """ + Order by priority HIGH count" + """ + VULNERABILITY_PRIORITY_HIGH } type TenantVulnerabilitySummary { @@ -32451,6 +32572,16 @@ func (ec *executionContext) childFields_CVE(ctx context.Context, field graphql.C return ec.fieldContext_CVE_detailsLink(ctx, field) case "cvssScore": return ec.fieldContext_CVE_cvssScore(ctx, field) + case "priority": + return ec.fieldContext_CVE_priority(ctx, field) + case "epssScore": + return ec.fieldContext_CVE_epssScore(ctx, field) + case "epssPercentile": + return ec.fieldContext_CVE_epssPercentile(ctx, field) + case "hasKevEntry": + return ec.fieldContext_CVE_hasKevEntry(ctx, field) + case "knownRansomwareUse": + return ec.fieldContext_CVE_knownRansomwareUse(ctx, field) case "workloads": return ec.fieldContext_CVE_workloads(ctx, field) } @@ -33257,6 +33388,14 @@ func (ec *executionContext) childFields_ImageVulnerabilitySummary(ctx context.Co return ec.fieldContext_ImageVulnerabilitySummary_critical(ctx, field) case "unassigned": return ec.fieldContext_ImageVulnerabilitySummary_unassigned(ctx, field) + case "priorityActNow": + return ec.fieldContext_ImageVulnerabilitySummary_priorityActNow(ctx, field) + case "priorityHigh": + return ec.fieldContext_ImageVulnerabilitySummary_priorityHigh(ctx, field) + case "priorityElevated": + return ec.fieldContext_ImageVulnerabilitySummary_priorityElevated(ctx, field) + case "priorityMonitor": + return ec.fieldContext_ImageVulnerabilitySummary_priorityMonitor(ctx, field) case "lastUpdated": return ec.fieldContext_ImageVulnerabilitySummary_lastUpdated(ctx, field) case "staleImageTag": diff --git a/internal/graph/gengql/vulnerability.generated.go b/internal/graph/gengql/vulnerability.generated.go index c2a7a9a1c..c80f86fbe 100644 --- a/internal/graph/gengql/vulnerability.generated.go +++ b/internal/graph/gengql/vulnerability.generated.go @@ -32,6 +32,9 @@ type ContainerImageSBOMResolver interface { type ContainerImageWorkloadReferenceResolver interface { Workload(ctx context.Context, obj *vulnerability.ContainerImageWorkloadReference) (workload.Workload, error) } +type ImageVulnerabilitySummaryResolver interface { + StaleImageTag(ctx context.Context, obj *vulnerability.ImageVulnerabilitySummary) (*string, error) +} type TeamVulnerabilitySummaryResolver interface { RiskScoreTrend(ctx context.Context, obj *vulnerability.TeamVulnerabilitySummary) (vulnerability.TeamVulnerabilityRiskScoreTrend, error) } @@ -258,6 +261,121 @@ func (ec *executionContext) fieldContext_CVE_cvssScore(_ context.Context, field return graphql.NewScalarFieldContext("CVE", field, false, false, errors.New("field of type Float does not have child fields")) } +func (ec *executionContext) _CVE_priority(ctx context.Context, field graphql.CollectedField, obj *vulnerability.CVE) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_CVE_priority(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Priority, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v vulnerability.CVEPriority) graphql.Marshaler { + return ec.marshalNCVEPriority2githubᚗcomᚋnaisᚋapiᚋinternalᚋvulnerabilityᚐCVEPriority(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_CVE_priority(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("CVE", field, false, false, errors.New("field of type CVEPriority does not have child fields")) +} + +func (ec *executionContext) _CVE_epssScore(ctx context.Context, field graphql.CollectedField, obj *vulnerability.CVE) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_CVE_epssScore(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EpssScore, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_CVE_epssScore(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("CVE", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _CVE_epssPercentile(ctx context.Context, field graphql.CollectedField, obj *vulnerability.CVE) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_CVE_epssPercentile(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EpssPercentile, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_CVE_epssPercentile(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("CVE", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _CVE_hasKevEntry(ctx context.Context, field graphql.CollectedField, obj *vulnerability.CVE) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_CVE_hasKevEntry(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.HasKevEntry, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_CVE_hasKevEntry(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("CVE", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _CVE_knownRansomwareUse(ctx context.Context, field graphql.CollectedField, obj *vulnerability.CVE) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_CVE_knownRansomwareUse(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.KnownRansomwareUse, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_CVE_knownRansomwareUse(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("CVE", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + func (ec *executionContext) _CVE_workloads(ctx context.Context, field graphql.CollectedField, obj *vulnerability.CVE) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -1320,6 +1438,98 @@ func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_unassigned(_ return graphql.NewScalarFieldContext("ImageVulnerabilitySummary", field, false, false, errors.New("field of type Int does not have child fields")) } +func (ec *executionContext) _ImageVulnerabilitySummary_priorityActNow(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerabilitySummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageVulnerabilitySummary_priorityActNow(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PriorityActNow, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_priorityActNow(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageVulnerabilitySummary", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _ImageVulnerabilitySummary_priorityHigh(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerabilitySummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageVulnerabilitySummary_priorityHigh(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PriorityHigh, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_priorityHigh(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageVulnerabilitySummary", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _ImageVulnerabilitySummary_priorityElevated(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerabilitySummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageVulnerabilitySummary_priorityElevated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PriorityElevated, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_priorityElevated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageVulnerabilitySummary", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _ImageVulnerabilitySummary_priorityMonitor(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerabilitySummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageVulnerabilitySummary_priorityMonitor(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PriorityMonitor, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_priorityMonitor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageVulnerabilitySummary", field, false, false, errors.New("field of type Int does not have child fields")) +} + func (ec *executionContext) _ImageVulnerabilitySummary_lastUpdated(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerabilitySummary) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -1352,7 +1562,7 @@ func (ec *executionContext) _ImageVulnerabilitySummary_staleImageTag(ctx context return ec.fieldContext_ImageVulnerabilitySummary_staleImageTag(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.StaleImageTag, nil + return ec.Resolvers.ImageVulnerabilitySummary().StaleImageTag(ctx, obj) }, nil, func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { @@ -1363,7 +1573,7 @@ func (ec *executionContext) _ImageVulnerabilitySummary_staleImageTag(ctx context ) } func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_staleImageTag(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("ImageVulnerabilitySummary", field, false, false, errors.New("field of type String does not have child fields")) + return graphql.NewScalarFieldContext("ImageVulnerabilitySummary", field, true, true, errors.New("field of type String does not have child fields")) } func (ec *executionContext) _ImageVulnerabilitySuppression_state(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerabilitySuppression) (ret graphql.Marshaler) { @@ -3243,6 +3453,25 @@ func (ec *executionContext) _CVE(ctx context.Context, sel ast.SelectionSet, obj } case "cvssScore": out.Values[i] = ec._CVE_cvssScore(ctx, field, obj) + case "priority": + out.Values[i] = ec._CVE_priority(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "epssScore": + out.Values[i] = ec._CVE_epssScore(ctx, field, obj) + case "epssPercentile": + out.Values[i] = ec._CVE_epssPercentile(ctx, field, obj) + case "hasKevEntry": + out.Values[i] = ec._CVE_hasKevEntry(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "knownRansomwareUse": + out.Values[i] = ec._CVE_knownRansomwareUse(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "workloads": field := field @@ -3926,42 +4155,93 @@ func (ec *executionContext) _ImageVulnerabilitySummary(ctx context.Context, sel case "total": out.Values[i] = ec._ImageVulnerabilitySummary_total(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "riskScore": out.Values[i] = ec._ImageVulnerabilitySummary_riskScore(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "low": out.Values[i] = ec._ImageVulnerabilitySummary_low(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "medium": out.Values[i] = ec._ImageVulnerabilitySummary_medium(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "high": out.Values[i] = ec._ImageVulnerabilitySummary_high(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "critical": out.Values[i] = ec._ImageVulnerabilitySummary_critical(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "unassigned": out.Values[i] = ec._ImageVulnerabilitySummary_unassigned(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) + } + case "priorityActNow": + out.Values[i] = ec._ImageVulnerabilitySummary_priorityActNow(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "priorityHigh": + out.Values[i] = ec._ImageVulnerabilitySummary_priorityHigh(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "priorityElevated": + out.Values[i] = ec._ImageVulnerabilitySummary_priorityElevated(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "priorityMonitor": + out.Values[i] = ec._ImageVulnerabilitySummary_priorityMonitor(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) } case "lastUpdated": out.Values[i] = ec._ImageVulnerabilitySummary_lastUpdated(ctx, field, obj) case "staleImageTag": - out.Values[i] = ec._ImageVulnerabilitySummary_staleImageTag(ctx, field, obj) + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ImageVulnerabilitySummary_staleImageTag(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -4879,6 +5159,16 @@ func (ec *executionContext) marshalNCVEOrderField2githubᚗcomᚋnaisᚋapiᚋin return v } +func (ec *executionContext) unmarshalNCVEPriority2githubᚗcomᚋnaisᚋapiᚋinternalᚋvulnerabilityᚐCVEPriority(ctx context.Context, v any) (vulnerability.CVEPriority, error) { + var res vulnerability.CVEPriority + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNCVEPriority2githubᚗcomᚋnaisᚋapiᚋinternalᚋvulnerabilityᚐCVEPriority(ctx context.Context, sel ast.SelectionSet, v vulnerability.CVEPriority) graphql.Marshaler { + return v +} + func (ec *executionContext) marshalNContainerImageSBOM2githubᚗcomᚋnaisᚋapiᚋinternalᚋvulnerabilityᚐContainerImageSBOM(ctx context.Context, sel ast.SelectionSet, v vulnerability.ContainerImageSBOM) graphql.Marshaler { return ec._ContainerImageSBOM(ctx, sel, &v) } diff --git a/internal/graph/issues.resolvers.go b/internal/graph/issues.resolvers.go index a0fa5f0d2..c55a111de 100644 --- a/internal/graph/issues.resolvers.go +++ b/internal/graph/issues.resolvers.go @@ -2,7 +2,6 @@ package graph import ( "context" - "fmt" "github.com/nais/api/internal/graph/gengql" "github.com/nais/api/internal/graph/pagination" @@ -42,11 +41,11 @@ func (r *deprecatedRegistryIssueResolver) Workload(ctx context.Context, obj *iss } func (r *externalIngressActNowVulnerabilityIssueResolver) TeamEnvironment(ctx context.Context, obj *issue.ExternalIngressActNowVulnerabilityIssue) (*team.TeamEnvironment, error) { - panic(fmt.Errorf("not implemented: TeamEnvironment - teamEnvironment")) + return team.GetTeamEnvironment(ctx, obj.TeamSlug, obj.EnvironmentName) } func (r *externalIngressActNowVulnerabilityIssueResolver) Workload(ctx context.Context, obj *issue.ExternalIngressActNowVulnerabilityIssue) (workload.Workload, error) { - panic(fmt.Errorf("not implemented: Workload - workload")) + return getWorkloadByResourceType(ctx, obj.TeamSlug, obj.EnvironmentName, obj.ResourceName, obj.ResourceType) } func (r *externalIngressCriticalVulnerabilityIssueResolver) TeamEnvironment(ctx context.Context, obj *issue.ExternalIngressCriticalVulnerabilityIssue) (*team.TeamEnvironment, error) { diff --git a/internal/graph/schema/vulnerability.graphqls b/internal/graph/schema/vulnerability.graphqls index 37645d575..fd8ef3a19 100644 --- a/internal/graph/schema/vulnerability.graphqls +++ b/internal/graph/schema/vulnerability.graphqls @@ -55,6 +55,7 @@ enum CVEOrderField { SEVERITY CVSS_SCORE AFFECTED_WORKLOADS_COUNT + PRIORITY } extend interface Workload { @@ -290,6 +291,18 @@ type ImageVulnerabilitySummary { "Number of vulnerabilities with severity UNASSIGNED." unassigned: Int! + "Number of vulnerabilities with priority ACT_NOW." + priorityActNow: Int! + + "Number of vulnerabilities with priority HIGH." + priorityHigh: Int! + + "Number of vulnerabilities with priority ELEVATED." + priorityElevated: Int! + + "Number of vulnerabilities with priority MONITOR." + priorityMonitor: Int! + "Timestamp of the last update of the vulnerability summary." lastUpdated: Time @@ -380,6 +393,17 @@ type ImageVulnerability implements Node { cvssScore: Float } +enum CVEPriority { + "Vulnerability is known to be actively exploited and requires immediate action." + ACT_NOW + "Vulnerability is associated with ransomware or has a high EPSS percentile." + HIGH + "Vulnerability has a critical or high severity and elevated EPSS percentile." + ELEVATED + "Vulnerability requires monitoring but no immediate action." + MONITOR +} + type CVE implements Node { "The globally unique ID of the CVE." id: ID! @@ -402,6 +426,21 @@ type CVE implements Node { "CVSS score of the CVE." cvssScore: Float + "Priority of the CVE based on threat intelligence signals." + priority: CVEPriority! + + "EPSS score of the CVE (probability of exploitation)." + epssScore: Float + + "EPSS percentile of the CVE." + epssPercentile: Float + + "Whether the CVE has a Known Exploited Vulnerability (KEV) entry." + hasKevEntry: Boolean! + + "Whether the CVE is known to be used in ransomware attacks." + knownRansomwareUse: Boolean! + "Affected workloads" workloads( "Get the first n items in the connection. This can be used in combination with the after parameter." @@ -512,6 +551,7 @@ enum ImageVulnerabilityOrderField { PACKAGE STATE SUPPRESSED + PRIORITY } type WorkloadVulnerabilitySummary implements Node { @@ -577,6 +617,14 @@ enum VulnerabilitySummaryOrderByField { Order by vulnerability severity unassigned" """ VULNERABILITY_SEVERITY_UNASSIGNED + """ + Order by priority ACT_NOW count" + """ + VULNERABILITY_PRIORITY_ACT_NOW + """ + Order by priority HIGH count" + """ + VULNERABILITY_PRIORITY_HIGH } type TenantVulnerabilitySummary { diff --git a/internal/graph/vulnerability.resolvers.go b/internal/graph/vulnerability.resolvers.go index fbc3c706e..7529d8d23 100644 --- a/internal/graph/vulnerability.resolvers.go +++ b/internal/graph/vulnerability.resolvers.go @@ -2,6 +2,7 @@ package graph import ( "context" + "fmt" "time" "github.com/nais/api/internal/auth/authz" @@ -80,6 +81,10 @@ func (r *containerImageWorkloadReferenceResolver) Workload(ctx context.Context, return getWorkload(ctx, obj.Reference, obj.TeamSlug, environmentmapper.EnvironmentName(obj.EnvironmentName)) } +func (r *imageVulnerabilitySummaryResolver) StaleImageTag(ctx context.Context, obj *vulnerability.ImageVulnerabilitySummary) (*string, error) { + panic(fmt.Errorf("not implemented: StaleImageTag - staleImageTag")) +} + func (r *jobResolver) ImageVulnerabilityHistory(ctx context.Context, obj *job.Job, from scalar.Date) (*vulnerability.ImageVulnerabilityHistory, error) { return vulnerability.GetWorkloadVulnerabilityHistoryForWorkload(ctx, obj, from.Time()) } @@ -173,6 +178,10 @@ func (r *Resolver) ContainerImageWorkloadReference() gengql.ContainerImageWorklo return &containerImageWorkloadReferenceResolver{r} } +func (r *Resolver) ImageVulnerabilitySummary() gengql.ImageVulnerabilitySummaryResolver { + return &imageVulnerabilitySummaryResolver{r} +} + func (r *Resolver) TeamVulnerabilitySummary() gengql.TeamVulnerabilitySummaryResolver { return &teamVulnerabilitySummaryResolver{r} } @@ -185,6 +194,7 @@ type ( cVEResolver struct{ *Resolver } containerImageSBOMResolver struct{ *Resolver } containerImageWorkloadReferenceResolver struct{ *Resolver } + imageVulnerabilitySummaryResolver struct{ *Resolver } teamVulnerabilitySummaryResolver struct{ *Resolver } workloadVulnerabilitySummaryResolver struct{ *Resolver } ) diff --git a/internal/issue/checker/workload_v13s.go b/internal/issue/checker/workload_v13s.go index 95c925762..827a8d994 100644 --- a/internal/issue/checker/workload_v13s.go +++ b/internal/issue/checker/workload_v13s.go @@ -35,7 +35,7 @@ func (f fakeV13sClient) ListVulnerabilitySummaries(ctx context.Context, opts ... Cluster: "dev-gcp", Type: "app", ImageName: "vulnerable-image", - ImageTag: "tag1", + ImageTag: "tag1", }, VulnerabilitySummary: &vulnerabilities.Summary{ Critical: 5, @@ -69,7 +69,7 @@ func (f fakeV13sClient) ListVulnerabilitySummaries(ctx context.Context, opts ... Cluster: "dev-gcp", Type: "app", ImageName: "vulnerable-image", - ImageTag: "tag1", + ImageTag: "tag1", }, VulnerabilitySummary: &vulnerabilities.Summary{ Critical: 5, diff --git a/internal/vulnerability/models.go b/internal/vulnerability/models.go index 2517af7f8..982781c97 100644 --- a/internal/vulnerability/models.go +++ b/internal/vulnerability/models.go @@ -225,6 +225,8 @@ const ( VulnerabilitySummaryOrderByFieldVulnerabilitySeverityLow VulnerabilitySummaryOrderByField = "VULNERABILITY_SEVERITY_LOW" VulnerabilitySummaryOrderByFieldVulnerabilitySeverityUnassigned VulnerabilitySummaryOrderByField = "VULNERABILITY_SEVERITY_UNASSIGNED" VulnerabilitySummaryOrderByFieldVulnerabilityLastScanned VulnerabilitySummaryOrderByField = "VULNERABILITY_LAST_SCANNED" + VulnerabilitySummaryOrderByFieldVulnerabilityPriorityActNow VulnerabilitySummaryOrderByField = "VULNERABILITY_PRIORITY_ACT_NOW" + VulnerabilitySummaryOrderByFieldVulnerabilityPriorityHigh VulnerabilitySummaryOrderByField = "VULNERABILITY_PRIORITY_HIGH" ) var AllVulnerabilitySummaryOrderByField = []VulnerabilitySummaryOrderByField{ @@ -237,11 +239,13 @@ var AllVulnerabilitySummaryOrderByField = []VulnerabilitySummaryOrderByField{ VulnerabilitySummaryOrderByFieldVulnerabilitySeverityLow, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityUnassigned, VulnerabilitySummaryOrderByFieldVulnerabilityLastScanned, + VulnerabilitySummaryOrderByFieldVulnerabilityPriorityActNow, + VulnerabilitySummaryOrderByFieldVulnerabilityPriorityHigh, } func (e VulnerabilitySummaryOrderByField) IsValid() bool { switch e { - case VulnerabilitySummaryOrderByFieldName, VulnerabilitySummaryOrderByFieldEnvironment, VulnerabilitySummaryOrderByFieldVulnerabilityRiskScore, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityCritical, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityHigh, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityMedium, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityLow, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityUnassigned, VulnerabilitySummaryOrderByFieldVulnerabilityLastScanned: + case VulnerabilitySummaryOrderByFieldName, VulnerabilitySummaryOrderByFieldEnvironment, VulnerabilitySummaryOrderByFieldVulnerabilityRiskScore, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityCritical, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityHigh, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityMedium, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityLow, VulnerabilitySummaryOrderByFieldVulnerabilitySeverityUnassigned, VulnerabilitySummaryOrderByFieldVulnerabilityLastScanned, VulnerabilitySummaryOrderByFieldVulnerabilityPriorityActNow, VulnerabilitySummaryOrderByFieldVulnerabilityPriorityHigh: return true } return false @@ -416,14 +420,57 @@ type VulnerabilityFixSample struct { TotalWorkloads int `json:"totalWorkloads"` } +type CVEPriority string + +const ( + CVEPriorityActNow CVEPriority = "ACT_NOW" + CVEPriorityHigh CVEPriority = "HIGH" + CVEPriorityElevated CVEPriority = "ELEVATED" + CVEPriorityMonitor CVEPriority = "MONITOR" +) + +func (e CVEPriority) IsValid() bool { + switch e { + case CVEPriorityActNow, CVEPriorityHigh, CVEPriorityElevated, CVEPriorityMonitor: + return true + } + return false +} + +func (e CVEPriority) String() string { + return string(e) +} + +func (e *CVEPriority) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = CVEPriority(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid CVEPriority", str) + } + return nil +} + +func (e CVEPriority) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + type CVE struct { - Identifier string `json:"identifier"` - Severity ImageVulnerabilitySeverity `json:"severity"` - Title string `json:"title"` - Description string `json:"description"` - SeveritySince *time.Time `json:"severitySince,omitempty"` - DetailsLink string `json:"detailsLink"` - CVSSScore *float64 `json:"cvssScore,omitempty"` + Identifier string `json:"identifier"` + Severity ImageVulnerabilitySeverity `json:"severity"` + Title string `json:"title"` + Description string `json:"description"` + SeveritySince *time.Time `json:"severitySince,omitempty"` + DetailsLink string `json:"detailsLink"` + CVSSScore *float64 `json:"cvssScore,omitempty"` + Priority CVEPriority `json:"priority"` + EpssScore *float64 `json:"epssScore,omitempty"` + EpssPercentile *float64 `json:"epssPercentile,omitempty"` + HasKevEntry bool `json:"hasKevEntry"` + KnownRansomwareUse bool `json:"knownRansomwareUse"` // AffectedWorkloads is used to short circuit counting affected workloads in resolvers, // if the only field requested of the workloads field is the total count. @@ -480,6 +527,7 @@ const ( CVEOrderFieldSeverity CVEOrderField = "SEVERITY" CVEOrderFieldCVSSScore CVEOrderField = "CVSS_SCORE" CVEOrderFieldAffectedWorkloadsCount CVEOrderField = "AFFECTED_WORKLOADS_COUNT" + CVEOrderFieldPriority CVEOrderField = "PRIORITY" ) var AllCVEOrderField = []CVEOrderField{ @@ -487,11 +535,12 @@ var AllCVEOrderField = []CVEOrderField{ CVEOrderFieldSeverity, CVEOrderFieldCVSSScore, CVEOrderFieldAffectedWorkloadsCount, + CVEOrderFieldPriority, } func (e CVEOrderField) IsValid() bool { switch e { - case CVEOrderFieldIdentifier, CVEOrderFieldSeverity, CVEOrderFieldCVSSScore, CVEOrderFieldAffectedWorkloadsCount: + case CVEOrderFieldIdentifier, CVEOrderFieldSeverity, CVEOrderFieldCVSSScore, CVEOrderFieldAffectedWorkloadsCount, CVEOrderFieldPriority: return true } return false diff --git a/internal/vulnerability/queries.go b/internal/vulnerability/queries.go index 86992af64..8ab7e1859 100644 --- a/internal/vulnerability/queries.go +++ b/internal/vulnerability/queries.go @@ -655,6 +655,8 @@ func ListCVEs(ctx context.Context, page *pagination.Pagination, orderBy *CVEOrde field = vulnerabilities.OrderByCvssScore case CVEOrderFieldAffectedWorkloadsCount: field = vulnerabilities.OrderByAffectedWorkloads + case CVEOrderFieldPriority: + field = vulnerabilities.OrderByPriority default: field = vulnerabilities.OrderByCvssScore } diff --git a/internal/vulnerability/transform.go b/internal/vulnerability/transform.go index 000f50ca4..a9a318d42 100644 --- a/internal/vulnerability/transform.go +++ b/internal/vulnerability/transform.go @@ -92,12 +92,30 @@ func toWorkloadVulnerabilitySummary(w *vulnerabilities.WorkloadSummary) *Workloa func toCVE(cve *vulnerabilities.Cve) *CVE { return &CVE{ - Identifier: cve.Id, - Title: cve.Title, - Description: cve.Description, - DetailsLink: cve.Link, - CVSSScore: cve.CvssScore, - Severity: ImageVulnerabilitySeverity(cve.Severity.String()), + Identifier: cve.Id, + Title: cve.Title, + Description: cve.Description, + DetailsLink: cve.Link, + CVSSScore: cve.CvssScore, + Severity: ImageVulnerabilitySeverity(cve.Severity.String()), + Priority: parseCVEPriority(cve.Priority), + EpssScore: cve.EpssScore, + EpssPercentile: cve.EpssPercentile, + HasKevEntry: cve.HasKevEntry, + KnownRansomwareUse: cve.KnownRansomwareUse, + } +} + +func parseCVEPriority(p vulnerabilities.Priority) CVEPriority { + switch p { + case vulnerabilities.Priority_PRIORITY_ACT_NOW: + return CVEPriorityActNow + case vulnerabilities.Priority_PRIORITY_HIGH: + return CVEPriorityHigh + case vulnerabilities.Priority_PRIORITY_ELEVATED: + return CVEPriorityElevated + default: + return CVEPriorityMonitor } } From 6fbbfaf9024db07292c43b2cdb682c3214d4dc56 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Thu, 28 May 2026 14:19:23 +0200 Subject: [PATCH 03/15] chore: bump v13s to v0.0.0-20260528121134-739c7136ac8e (cve-priority rebase onto main) --- go.mod | 6 +++++- go.sum | 21 +++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0dd2b21b6..c86ba9bd6 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/nais/pgrator/pkg/api v0.0.0-20260219115817-cf954d58c04e github.com/nais/tester v0.1.1 github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe - github.com/nais/v13s/pkg/api v0.0.0-20260528080657-d4f49e5737da + github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pressly/goose/v3 v3.27.0 github.com/prometheus/client_golang v1.23.2 @@ -80,7 +80,11 @@ require ( golang.org/x/text v0.37.0 golang.org/x/tools v0.44.0 google.golang.org/api v0.280.0 +<<<<<<< HEAD google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 +======= + google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 +>>>>>>> 99537aa0 (chore: bump v13s to v0.0.0-20260528121134-739c7136ac8e (cve-priority rebase onto main)) google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 k8s.io/api v0.35.1 diff --git a/go.sum b/go.sum index 08b8ab991..26c3952e1 100644 --- a/go.sum +++ b/go.sum @@ -813,8 +813,8 @@ github.com/nais/tester v0.1.1 h1:tpJ5HKpu3mEIWX/mec0Yj0xLHEpt+MwTAsj282n0Py0= github.com/nais/tester v0.1.1/go.mod h1:NCQMcgftHz/EXorob1XwDTOqkQmImDqr51YQ2Uea9Pc= github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe h1:CdRVopOihru4tXVwKZjhg6C8SbPLCQYOhJKpjBZYhjg= github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe/go.mod h1:Tiz/1If3WgcfvNhmsO5DiQC+L+1XhBG3KWbIfbjx4EU= -github.com/nais/v13s/pkg/api v0.0.0-20260528080657-d4f49e5737da h1:59leNz7qKRctGQS6xUnPzVUqa2NnEzVlwMDAWyhUwJs= -github.com/nais/v13s/pkg/api v0.0.0-20260528080657-d4f49e5737da/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= +github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e h1:7fut4nxp6NlX7xS5SnwLkdyAhWanqmcpuClssn/cT8Q= +github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= github.com/ncruces/go-sqlite3 v0.32.0 h1:hNBUXp88LrfQCsuyXLqWTbTUG35sUuktDsqhhgHvU20= github.com/ncruces/go-sqlite3 v0.32.0/go.mod h1:MIWTK60ONDl0oVY073zYvJP21C3Dly6P9bxVpgkLwdQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -1416,6 +1416,11 @@ gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +<<<<<<< HEAD +======= +google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk= +google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= +>>>>>>> 99537aa0 (chore: bump v13s to v0.0.0-20260528121134-739c7136ac8e (cve-priority rebase onto main)) google.golang.org/api v0.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk= google.golang.org/api v0.280.0/go.mod h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -1428,8 +1433,15 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +<<<<<<< HEAD google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +======= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +>>>>>>> 99537aa0 (chore: bump v13s to v0.0.0-20260528121134-739c7136ac8e (cve-priority rebase onto main)) google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -1438,6 +1450,11 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +<<<<<<< HEAD +======= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +>>>>>>> 99537aa0 (chore: bump v13s to v0.0.0-20260528121134-739c7136ac8e (cve-priority rebase onto main)) google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= From 7ad3ba3cdb48220149a7a9d09841642bb5d6ace4 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Thu, 28 May 2026 16:21:18 +0200 Subject: [PATCH 04/15] =?UTF-8?q?refactor:=20simplify=20toWorkloadVulnerab?= =?UTF-8?q?ilitySummary=20=E2=80=94=20v13s=20now=20zeroes=20counts=20for?= =?UTF-8?q?=20terminal=20states?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/vulnerability/transform.go | 41 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/internal/vulnerability/transform.go b/internal/vulnerability/transform.go index a9a318d42..6b60cb8e6 100644 --- a/internal/vulnerability/transform.go +++ b/internal/vulnerability/transform.go @@ -61,26 +61,35 @@ func toWorkloadVulnerabilitySummary(w *vulnerabilities.WorkloadSummary) *Workloa wType = workload.TypeJob } - summary := &ImageVulnerabilitySummary{} - if s := w.GetVulnerabilitySummary(); s != nil { - var lastUpdated *time.Time - if ts := s.GetLastUpdated(); ts != nil { - t := ts.AsTime() - lastUpdated = &t - } - summary.Critical = int(s.Critical) - summary.High = int(s.High) - summary.Medium = int(s.Medium) - summary.Low = int(s.Low) - summary.Unassigned = int(s.Unassigned) - summary.Total = int(s.Total) - summary.RiskScore = int(s.RiskScore) - summary.LastUpdated = lastUpdated + v13sSummary := w.GetVulnerabilitySummary() + if v13sSummary == nil { + v13sSummary = &vulnerabilities.Summary{} + } + + var lastUpdated *time.Time + if ts := v13sSummary.GetLastUpdated(); ts != nil { + t := ts.AsTime() + lastUpdated = &t + } + + summary := &ImageVulnerabilitySummary{ + Critical: int(v13sSummary.Critical), + High: int(v13sSummary.High), + Medium: int(v13sSummary.Medium), + Low: int(v13sSummary.Low), + Unassigned: int(v13sSummary.Unassigned), + Total: int(v13sSummary.Total), + RiskScore: int(v13sSummary.RiskScore), + LastUpdated: lastUpdated, + PriorityActNow: int(v13sSummary.PriorityActNow), + PriorityHigh: int(v13sSummary.PriorityHigh), + PriorityElevated: int(v13sSummary.PriorityElevated), + PriorityMonitor: int(v13sSummary.PriorityMonitor), } return &WorkloadVulnerabilitySummary{ Summary: summary, - HasSbom: w.GetSbomStatus().GetStatus() == vulnerabilities.SbomStatus_SBOM_STATUS_READY, + HasSbom: v13sSummary.GetHasSbom(), TeamSlug: slug.Slug(w.GetWorkload().GetNamespace()), EnvironmentName: environmentmapper.EnvironmentName(w.GetWorkload().GetCluster()), WorkloadReference: &workload.Reference{ From dd0729eb1051bf7f37e9d145e84f6eb4b0e3886f Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:43:29 +0200 Subject: [PATCH 05/15] fix: resolve leftover go.mod/go.sum conflict markers --- go.mod | 4 ---- go.sum | 17 ----------------- 2 files changed, 21 deletions(-) diff --git a/go.mod b/go.mod index c86ba9bd6..ac87ef35b 100644 --- a/go.mod +++ b/go.mod @@ -80,11 +80,7 @@ require ( golang.org/x/text v0.37.0 golang.org/x/tools v0.44.0 google.golang.org/api v0.280.0 -<<<<<<< HEAD google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 -======= - google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 ->>>>>>> 99537aa0 (chore: bump v13s to v0.0.0-20260528121134-739c7136ac8e (cve-priority rebase onto main)) google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 k8s.io/api v0.35.1 diff --git a/go.sum b/go.sum index 26c3952e1..43198cc52 100644 --- a/go.sum +++ b/go.sum @@ -1416,11 +1416,6 @@ gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -<<<<<<< HEAD -======= -google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk= -google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= ->>>>>>> 99537aa0 (chore: bump v13s to v0.0.0-20260528121134-739c7136ac8e (cve-priority rebase onto main)) google.golang.org/api v0.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk= google.golang.org/api v0.280.0/go.mod h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -1433,15 +1428,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= -<<<<<<< HEAD google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -======= -google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= -google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= ->>>>>>> 99537aa0 (chore: bump v13s to v0.0.0-20260528121134-739c7136ac8e (cve-priority rebase onto main)) google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -1450,11 +1438,6 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -<<<<<<< HEAD -======= -google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= -google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= ->>>>>>> 99537aa0 (chore: bump v13s to v0.0.0-20260528121134-739c7136ac8e (cve-priority rebase onto main)) google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= From 058e2a7ca29c4a2ae97be3d1399ced2eab939a80 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:58:49 +0200 Subject: [PATCH 06/15] fix: address gosec int32 cast and update issue expectation --- integration_tests/issues_for_team.lua | 4 ++-- internal/vulnerability/models.go | 1 + internal/vulnerability/queries.go | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/integration_tests/issues_for_team.lua b/integration_tests/issues_for_team.lua index 6fe9ec3a9..8b6575e11 100644 --- a/integration_tests/issues_for_team.lua +++ b/integration_tests/issues_for_team.lua @@ -579,8 +579,8 @@ Test.gql("VulnerableImageIssue", function(t) nodes = { { __typename = "VulnerableImageIssue", - message = "Image 'vulnerable-image' has 5 critical vulnerabilities and a risk score of 250", - severity = "WARNING", + message = "Image 'vulnerable-image' has 2 ACT_NOW and 3 HIGH priority vulnerabilities", + severity = "CRITICAL", critical = 5, riskScore = 250, workload = { diff --git a/internal/vulnerability/models.go b/internal/vulnerability/models.go index 982781c97..ae61dc0f3 100644 --- a/internal/vulnerability/models.go +++ b/internal/vulnerability/models.go @@ -79,6 +79,7 @@ type ImageVulnerabilitySummary struct { Critical int `json:"critical"` Unassigned int `json:"unassigned"` LastUpdated *time.Time `json:"lastUpdated"` + StaleImageTag *string `json:"staleImageTag"` PriorityActNow int `json:"priorityActNow"` PriorityHigh int `json:"priorityHigh"` PriorityElevated int `json:"priorityElevated"` diff --git a/internal/vulnerability/queries.go b/internal/vulnerability/queries.go index 8ab7e1859..f72395647 100644 --- a/internal/vulnerability/queries.go +++ b/internal/vulnerability/queries.go @@ -701,7 +701,7 @@ func GetWorkloadsByCVE(ctx context.Context, cve string, page *pagination.Paginat if err != nil { return nil, apierror.Errorf("list workloads for vulnerability by CVE: %v", err) } - return convertWorkloadNodes(ctx, resp.GetNodes(), page, int32(min(resp.GetPageInfo().GetTotalCount(), math.MaxInt32))) //nolint:gosec + return convertWorkloadNodes(ctx, resp.GetNodes(), page, safeInt32TotalCount(resp.GetPageInfo().GetTotalCount())) } return getWorkloadsByCVE(ctx, cve, page) } @@ -723,7 +723,17 @@ func getWorkloadsByCVE(ctx context.Context, cve string, page *pagination.Paginat if err != nil { return nil, apierror.Errorf("list workloads for vulnerability by CVE: %v", err) } - return convertWorkloadNodes(ctx, resp.GetNodes(), page, int32(min(resp.GetPageInfo().GetTotalCount(), math.MaxInt32))) //nolint:gosec + return convertWorkloadNodes(ctx, resp.GetNodes(), page, safeInt32TotalCount(resp.GetPageInfo().GetTotalCount())) +} + +func safeInt32TotalCount(totalCount int64) int32 { + if totalCount <= 0 { + return 0 + } + if totalCount > int64(math.MaxInt32) { + return math.MaxInt32 + } + return int32(totalCount) } func convertWorkloadNodes(ctx context.Context, nodes []*vulnerabilities.WorkloadForVulnerability, page *pagination.Pagination, totalCount int32) (*WorkloadWithVulnerabilityConnection, error) { From 89368f027949c64162647fde9174836fc26ac2e9 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:09:21 +0200 Subject: [PATCH 07/15] chore(deps): bump v13s api to af9d5e6 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ac87ef35b..3d2891d62 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/nais/pgrator/pkg/api v0.0.0-20260219115817-cf954d58c04e github.com/nais/tester v0.1.1 github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe - github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e + github.com/nais/v13s/pkg/api v0.0.0-20260602103534-af9d5e6d27d3 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pressly/goose/v3 v3.27.0 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index 43198cc52..8d2852525 100644 --- a/go.sum +++ b/go.sum @@ -815,6 +815,8 @@ github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe h1:CdRVopOihru4 github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe/go.mod h1:Tiz/1If3WgcfvNhmsO5DiQC+L+1XhBG3KWbIfbjx4EU= github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e h1:7fut4nxp6NlX7xS5SnwLkdyAhWanqmcpuClssn/cT8Q= github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= +github.com/nais/v13s/pkg/api v0.0.0-20260602103534-af9d5e6d27d3 h1:ei4Q9fAPa0dtBo7aBwCUuVrDRXA9jtipqsU6PhOcPYw= +github.com/nais/v13s/pkg/api v0.0.0-20260602103534-af9d5e6d27d3/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= github.com/ncruces/go-sqlite3 v0.32.0 h1:hNBUXp88LrfQCsuyXLqWTbTUG35sUuktDsqhhgHvU20= github.com/ncruces/go-sqlite3 v0.32.0/go.mod h1:MIWTK60ONDl0oVY073zYvJP21C3Dly6P9bxVpgkLwdQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= From 228161a079dddd0c8d7d890a8a7de16ec76586f5 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:09:24 +0200 Subject: [PATCH 08/15] fix(vulnerability): adapt API to v13s risk-tier model --- integration_tests/issues_for_team.lua | 2 +- internal/graph/gengql/root_.generated.go | 15 +++-- .../graph/gengql/vulnerability.generated.go | 62 +++++-------------- internal/graph/schema/vulnerability.graphqls | 14 ++--- internal/graph/vulnerability.resolvers.go | 10 --- internal/issue/checker/workload_v13s.go | 34 +++++----- internal/vulnerability/fake/fakedata.go | 58 +++++++++-------- internal/vulnerability/models.go | 10 +-- internal/vulnerability/queries.go | 2 +- internal/vulnerability/sortfilter.go | 6 +- internal/vulnerability/transform.go | 33 ++++++---- 11 files changed, 106 insertions(+), 140 deletions(-) diff --git a/integration_tests/issues_for_team.lua b/integration_tests/issues_for_team.lua index 8b6575e11..105c6369c 100644 --- a/integration_tests/issues_for_team.lua +++ b/integration_tests/issues_for_team.lua @@ -579,7 +579,7 @@ Test.gql("VulnerableImageIssue", function(t) nodes = { { __typename = "VulnerableImageIssue", - message = "Image 'vulnerable-image' has 2 ACT_NOW and 3 HIGH priority vulnerabilities", + message = "Image 'vulnerable-image' has 2 IMMEDIATE and 3 HIGH risk-tier vulnerabilities", severity = "CRITICAL", critical = 5, riskScore = 250, diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index 8bac53f3b..00c5b0db0 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -84,7 +84,6 @@ type ResolverRoot interface { ExternalIngressActNowVulnerabilityIssue() ExternalIngressActNowVulnerabilityIssueResolver ExternalIngressCriticalVulnerabilityIssue() ExternalIngressCriticalVulnerabilityIssueResolver FailedSynchronizationIssue() FailedSynchronizationIssueResolver - ImageVulnerabilitySummary() ImageVulnerabilitySummaryResolver Ingress() IngressResolver IngressMetrics() IngressMetricsResolver InstanceGroup() InstanceGroupResolver @@ -30969,16 +30968,16 @@ type ImageVulnerabilitySummary { "Number of vulnerabilities with severity UNASSIGNED." unassigned: Int! - "Number of vulnerabilities with priority ACT_NOW." + "Number of vulnerabilities with risk tier IMMEDIATE." priorityActNow: Int! - "Number of vulnerabilities with priority HIGH." + "Number of vulnerabilities with risk tier HIGH." priorityHigh: Int! - "Number of vulnerabilities with priority ELEVATED." + "Number of vulnerabilities with risk tier ELEVATED." priorityElevated: Int! - "Number of vulnerabilities with priority MONITOR." + "Number of vulnerabilities with risk tier MONITOR." priorityMonitor: Int! "Timestamp of the last update of the vulnerability summary." @@ -31073,7 +31072,7 @@ type ImageVulnerability implements Node { enum CVEPriority { "Vulnerability is known to be actively exploited and requires immediate action." - ACT_NOW + IMMEDIATE "Vulnerability is associated with ransomware or has a high EPSS percentile." HIGH "Vulnerability has a critical or high severity and elevated EPSS percentile." @@ -31296,11 +31295,11 @@ enum VulnerabilitySummaryOrderByField { """ VULNERABILITY_SEVERITY_UNASSIGNED """ - Order by priority ACT_NOW count" + Order by IMMEDIATE risk-tier count" """ VULNERABILITY_PRIORITY_ACT_NOW """ - Order by priority HIGH count" + Order by HIGH risk-tier count" """ VULNERABILITY_PRIORITY_HIGH } diff --git a/internal/graph/gengql/vulnerability.generated.go b/internal/graph/gengql/vulnerability.generated.go index c80f86fbe..e5dd92780 100644 --- a/internal/graph/gengql/vulnerability.generated.go +++ b/internal/graph/gengql/vulnerability.generated.go @@ -32,9 +32,6 @@ type ContainerImageSBOMResolver interface { type ContainerImageWorkloadReferenceResolver interface { Workload(ctx context.Context, obj *vulnerability.ContainerImageWorkloadReference) (workload.Workload, error) } -type ImageVulnerabilitySummaryResolver interface { - StaleImageTag(ctx context.Context, obj *vulnerability.ImageVulnerabilitySummary) (*string, error) -} type TeamVulnerabilitySummaryResolver interface { RiskScoreTrend(ctx context.Context, obj *vulnerability.TeamVulnerabilitySummary) (vulnerability.TeamVulnerabilityRiskScoreTrend, error) } @@ -1562,7 +1559,7 @@ func (ec *executionContext) _ImageVulnerabilitySummary_staleImageTag(ctx context return ec.fieldContext_ImageVulnerabilitySummary_staleImageTag(ctx, field) }, func(ctx context.Context) (any, error) { - return ec.Resolvers.ImageVulnerabilitySummary().StaleImageTag(ctx, obj) + return obj.StaleImageTag, nil }, nil, func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { @@ -1573,7 +1570,7 @@ func (ec *executionContext) _ImageVulnerabilitySummary_staleImageTag(ctx context ) } func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_staleImageTag(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("ImageVulnerabilitySummary", field, true, true, errors.New("field of type String does not have child fields")) + return graphql.NewScalarFieldContext("ImageVulnerabilitySummary", field, false, false, errors.New("field of type String does not have child fields")) } func (ec *executionContext) _ImageVulnerabilitySuppression_state(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerabilitySuppression) (ret graphql.Marshaler) { @@ -4155,93 +4152,62 @@ func (ec *executionContext) _ImageVulnerabilitySummary(ctx context.Context, sel case "total": out.Values[i] = ec._ImageVulnerabilitySummary_total(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "riskScore": out.Values[i] = ec._ImageVulnerabilitySummary_riskScore(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "low": out.Values[i] = ec._ImageVulnerabilitySummary_low(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "medium": out.Values[i] = ec._ImageVulnerabilitySummary_medium(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "high": out.Values[i] = ec._ImageVulnerabilitySummary_high(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "critical": out.Values[i] = ec._ImageVulnerabilitySummary_critical(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "unassigned": out.Values[i] = ec._ImageVulnerabilitySummary_unassigned(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "priorityActNow": out.Values[i] = ec._ImageVulnerabilitySummary_priorityActNow(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "priorityHigh": out.Values[i] = ec._ImageVulnerabilitySummary_priorityHigh(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "priorityElevated": out.Values[i] = ec._ImageVulnerabilitySummary_priorityElevated(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "priorityMonitor": out.Values[i] = ec._ImageVulnerabilitySummary_priorityMonitor(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "lastUpdated": out.Values[i] = ec._ImageVulnerabilitySummary_lastUpdated(ctx, field, obj) case "staleImageTag": - field := field - - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._ImageVulnerabilitySummary_staleImageTag(ctx, field, obj) - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + out.Values[i] = ec._ImageVulnerabilitySummary_staleImageTag(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/internal/graph/schema/vulnerability.graphqls b/internal/graph/schema/vulnerability.graphqls index fd8ef3a19..cb2441a05 100644 --- a/internal/graph/schema/vulnerability.graphqls +++ b/internal/graph/schema/vulnerability.graphqls @@ -291,16 +291,16 @@ type ImageVulnerabilitySummary { "Number of vulnerabilities with severity UNASSIGNED." unassigned: Int! - "Number of vulnerabilities with priority ACT_NOW." + "Number of vulnerabilities with risk tier IMMEDIATE." priorityActNow: Int! - "Number of vulnerabilities with priority HIGH." + "Number of vulnerabilities with risk tier HIGH." priorityHigh: Int! - "Number of vulnerabilities with priority ELEVATED." + "Number of vulnerabilities with risk tier ELEVATED." priorityElevated: Int! - "Number of vulnerabilities with priority MONITOR." + "Number of vulnerabilities with risk tier MONITOR." priorityMonitor: Int! "Timestamp of the last update of the vulnerability summary." @@ -395,7 +395,7 @@ type ImageVulnerability implements Node { enum CVEPriority { "Vulnerability is known to be actively exploited and requires immediate action." - ACT_NOW + IMMEDIATE "Vulnerability is associated with ransomware or has a high EPSS percentile." HIGH "Vulnerability has a critical or high severity and elevated EPSS percentile." @@ -618,11 +618,11 @@ enum VulnerabilitySummaryOrderByField { """ VULNERABILITY_SEVERITY_UNASSIGNED """ - Order by priority ACT_NOW count" + Order by IMMEDIATE risk-tier count" """ VULNERABILITY_PRIORITY_ACT_NOW """ - Order by priority HIGH count" + Order by HIGH risk-tier count" """ VULNERABILITY_PRIORITY_HIGH } diff --git a/internal/graph/vulnerability.resolvers.go b/internal/graph/vulnerability.resolvers.go index 7529d8d23..fbc3c706e 100644 --- a/internal/graph/vulnerability.resolvers.go +++ b/internal/graph/vulnerability.resolvers.go @@ -2,7 +2,6 @@ package graph import ( "context" - "fmt" "time" "github.com/nais/api/internal/auth/authz" @@ -81,10 +80,6 @@ func (r *containerImageWorkloadReferenceResolver) Workload(ctx context.Context, return getWorkload(ctx, obj.Reference, obj.TeamSlug, environmentmapper.EnvironmentName(obj.EnvironmentName)) } -func (r *imageVulnerabilitySummaryResolver) StaleImageTag(ctx context.Context, obj *vulnerability.ImageVulnerabilitySummary) (*string, error) { - panic(fmt.Errorf("not implemented: StaleImageTag - staleImageTag")) -} - func (r *jobResolver) ImageVulnerabilityHistory(ctx context.Context, obj *job.Job, from scalar.Date) (*vulnerability.ImageVulnerabilityHistory, error) { return vulnerability.GetWorkloadVulnerabilityHistoryForWorkload(ctx, obj, from.Time()) } @@ -178,10 +173,6 @@ func (r *Resolver) ContainerImageWorkloadReference() gengql.ContainerImageWorklo return &containerImageWorkloadReferenceResolver{r} } -func (r *Resolver) ImageVulnerabilitySummary() gengql.ImageVulnerabilitySummaryResolver { - return &imageVulnerabilitySummaryResolver{r} -} - func (r *Resolver) TeamVulnerabilitySummary() gengql.TeamVulnerabilitySummaryResolver { return &teamVulnerabilitySummaryResolver{r} } @@ -194,7 +185,6 @@ type ( cVEResolver struct{ *Resolver } containerImageSBOMResolver struct{ *Resolver } containerImageWorkloadReferenceResolver struct{ *Resolver } - imageVulnerabilitySummaryResolver struct{ *Resolver } teamVulnerabilitySummaryResolver struct{ *Resolver } workloadVulnerabilitySummaryResolver struct{ *Resolver } ) diff --git a/internal/issue/checker/workload_v13s.go b/internal/issue/checker/workload_v13s.go index 827a8d994..5fc5f30ff 100644 --- a/internal/issue/checker/workload_v13s.go +++ b/internal/issue/checker/workload_v13s.go @@ -38,10 +38,10 @@ func (f fakeV13sClient) ListVulnerabilitySummaries(ctx context.Context, opts ... ImageTag: "tag1", }, VulnerabilitySummary: &vulnerabilities.Summary{ - Critical: 5, - RiskScore: 250, - PriorityActNow: 2, - PriorityHigh: 3, + Critical: 5, + RiskScore: 250, + ActNow: 2, + HighRisk: 3, }, SbomStatus: &vulnerabilities.SbomStatusInfo{ Status: vulnerabilities.SbomStatus_SBOM_STATUS_READY, @@ -72,10 +72,10 @@ func (f fakeV13sClient) ListVulnerabilitySummaries(ctx context.Context, opts ... ImageTag: "tag1", }, VulnerabilitySummary: &vulnerabilities.Summary{ - Critical: 5, - RiskScore: 250, - PriorityActNow: 2, - PriorityHigh: 3, + Critical: 5, + RiskScore: 250, + ActNow: 2, + HighRisk: 3, }, SbomStatus: &vulnerabilities.SbomStatusInfo{ Status: vulnerabilities.SbomStatus_SBOM_STATUS_READY, @@ -186,9 +186,9 @@ func (w Workload) vulnerabilities(ctx context.Context) []*Issue { } summary := node.VulnerabilitySummary - if summary != nil && (summary.PriorityActNow > 0 || summary.PriorityHigh > 0) { + if summary != nil && (summary.ActNow > 0 || summary.HighRisk > 0) { severity := issue.SeverityWarning - if summary.PriorityActNow > 0 { + if summary.ActNow > 0 { severity = issue.SeverityCritical } ret = append(ret, &Issue{ @@ -199,10 +199,10 @@ func (w Workload) vulnerabilities(ctx context.Context) []*Issue { Env: environmentmapper.EnvironmentName(node.Workload.GetCluster()), Severity: severity, Message: fmt.Sprintf( - "Image '%s' has %d ACT_NOW and %d HIGH priority vulnerabilities", + "Image '%s' has %d IMMEDIATE and %d HIGH risk-tier vulnerabilities", node.Workload.ImageName, - summary.PriorityActNow, - summary.PriorityHigh, + summary.ActNow, + summary.HighRisk, ), IssueDetails: issue.VulnerableImageIssueDetails{ Critical: int(summary.Critical), @@ -296,7 +296,7 @@ func (w Workload) vulnerabilities(ctx context.Context) []*Issue { continue } - if node.VulnerabilitySummary == nil || node.VulnerabilitySummary.PriorityActNow == 0 { + if node.VulnerabilitySummary == nil || node.VulnerabilitySummary.ActNow == 0 { continue } @@ -320,12 +320,12 @@ func (w Workload) vulnerabilities(ctx context.Context) []*Issue { Env: env, Severity: issue.SeverityCritical, Message: fmt.Sprintf( - "Workload with external ingresses %s has %d ACT_NOW priority vulnerabilities", + "Workload with external ingresses %s has %d IMMEDIATE risk-tier vulnerabilities", strings.Join(externalIngresses, ", "), - node.VulnerabilitySummary.PriorityActNow, + node.VulnerabilitySummary.ActNow, ), IssueDetails: issue.ExternalIngressActNowVulnerabilityIssueDetails{ - PriorityActNow: int(node.VulnerabilitySummary.PriorityActNow), + PriorityActNow: int(node.VulnerabilitySummary.ActNow), Ingresses: externalIngresses, }, }) diff --git a/internal/vulnerability/fake/fakedata.go b/internal/vulnerability/fake/fakedata.go index e609af606..85b333635 100644 --- a/internal/vulnerability/fake/fakedata.go +++ b/internal/vulnerability/fake/fakedata.go @@ -79,19 +79,19 @@ func createWorkloadSummary(env, team, workloadType, name, image string, vulnFact imageName := parts[0] imageTag := parts[1] summary := &vulnerabilities.Summary{ - Critical: vulnFactor, - High: vulnFactor * 2, - Medium: vulnFactor + 2, - Low: vulnFactor + 1, - Unassigned: vulnFactor, - Total: vulnFactor + (vulnFactor * 2) + (vulnFactor + 2) + (vulnFactor + 1) + vulnFactor, - RiskScore: vulnFactor*10 + (vulnFactor*2)*5 + (vulnFactor+2)*3 + (vulnFactor + 1) + vulnFactor*5, - HasSbom: true, - LastUpdated: timestamppb.New(time.Now()), - PriorityActNow: vulnFactor, - PriorityHigh: vulnFactor * 2, - PriorityElevated: vulnFactor * 3, - PriorityMonitor: vulnFactor * 4, + Critical: vulnFactor, + High: vulnFactor * 2, + Medium: vulnFactor + 2, + Low: vulnFactor + 1, + Unassigned: vulnFactor, + Total: vulnFactor + (vulnFactor * 2) + (vulnFactor + 2) + (vulnFactor + 1) + vulnFactor, + RiskScore: vulnFactor*10 + (vulnFactor*2)*5 + (vulnFactor+2)*3 + (vulnFactor + 1) + vulnFactor*5, + HasSbom: true, + LastUpdated: timestamppb.New(time.Now()), + ActNow: vulnFactor, + HighRisk: vulnFactor * 2, + ElevatedRisk: vulnFactor * 3, + Monitor: vulnFactor * 4, } if name == "no-errors" { @@ -124,39 +124,44 @@ func createWorkloadSummary(env, team, workloadType, name, image string, vulnFact func createVulnerabilities(w *vulnerabilities.WorkloadSummary) []*vulnerabilities.Vulnerability { findings := make([]*vulnerabilities.Vulnerability, 0) - priorities := []vulnerabilities.Priority{ - vulnerabilities.Priority_PRIORITY_ACT_NOW, - vulnerabilities.Priority_PRIORITY_HIGH, - vulnerabilities.Priority_PRIORITY_ELEVATED, - vulnerabilities.Priority_PRIORITY_MONITOR, + tiers := []vulnerabilities.RiskTier{ + vulnerabilities.RiskTier_ACT_NOW, + vulnerabilities.RiskTier_HIGH_RISK, + vulnerabilities.RiskTier_ELEVATED_RISK, + vulnerabilities.RiskTier_MONITOR, } idx := 0 - nextPriority := func() vulnerabilities.Priority { - p := priorities[idx%len(priorities)] + nextRiskTier := func() vulnerabilities.RiskTier { + p := tiers[idx%len(tiers)] idx++ return p } epssScore := 0.85 epssPercentile := 97.3 for i := range w.VulnerabilitySummary.Critical { - findings = append(findings, createVulnerability(vulnerabilities.Severity_CRITICAL, fmt.Sprintf("some-component-%d", i), nextPriority(), &epssScore, &epssPercentile, true, false)) + _ = nextRiskTier() + findings = append(findings, createVulnerability(vulnerabilities.Severity_CRITICAL, fmt.Sprintf("some-component-%d", i), &epssScore, &epssPercentile, true, false)) } for i := range w.VulnerabilitySummary.High { - findings = append(findings, createVulnerability(vulnerabilities.Severity_HIGH, fmt.Sprintf("some-component-%d", i), nextPriority(), nil, nil, false, false)) + _ = nextRiskTier() + findings = append(findings, createVulnerability(vulnerabilities.Severity_HIGH, fmt.Sprintf("some-component-%d", i), nil, nil, false, false)) } for i := range w.VulnerabilitySummary.Medium { - findings = append(findings, createVulnerability(vulnerabilities.Severity_MEDIUM, fmt.Sprintf("some-component-%d", i), nextPriority(), nil, nil, false, false)) + _ = nextRiskTier() + findings = append(findings, createVulnerability(vulnerabilities.Severity_MEDIUM, fmt.Sprintf("some-component-%d", i), nil, nil, false, false)) } for i := range w.VulnerabilitySummary.Low { - findings = append(findings, createVulnerability(vulnerabilities.Severity_LOW, fmt.Sprintf("some-component-%d", i), nextPriority(), nil, nil, false, false)) + _ = nextRiskTier() + findings = append(findings, createVulnerability(vulnerabilities.Severity_LOW, fmt.Sprintf("some-component-%d", i), nil, nil, false, false)) } for i := range w.VulnerabilitySummary.Unassigned { - findings = append(findings, createVulnerability(vulnerabilities.Severity_UNASSIGNED, fmt.Sprintf("some-component-%d", i), nextPriority(), nil, nil, false, false)) + _ = nextRiskTier() + findings = append(findings, createVulnerability(vulnerabilities.Severity_UNASSIGNED, fmt.Sprintf("some-component-%d", i), nil, nil, false, false)) } return findings } -func createVulnerability(severity vulnerabilities.Severity, componentName string, priority vulnerabilities.Priority, epssScore, epssPercentile *float64, hasKevEntry, knownRansomwareUse bool) *vulnerabilities.Vulnerability { +func createVulnerability(severity vulnerabilities.Severity, componentName string, epssScore, epssPercentile *float64, hasKevEntry, knownRansomwareUse bool) *vulnerabilities.Vulnerability { return &vulnerabilities.Vulnerability{ Id: uuid.New().String(), Package: fmt.Sprintf("pkg:golang/%s@v2.0.8?type=module", componentName), @@ -167,7 +172,6 @@ func createVulnerability(severity vulnerabilities.Severity, componentName string Link: "", Severity: severity, References: nil, - Priority: priority, EpssScore: epssScore, EpssPercentile: epssPercentile, HasKevEntry: hasKevEntry, diff --git a/internal/vulnerability/models.go b/internal/vulnerability/models.go index ae61dc0f3..4286aeb22 100644 --- a/internal/vulnerability/models.go +++ b/internal/vulnerability/models.go @@ -424,15 +424,15 @@ type VulnerabilityFixSample struct { type CVEPriority string const ( - CVEPriorityActNow CVEPriority = "ACT_NOW" - CVEPriorityHigh CVEPriority = "HIGH" - CVEPriorityElevated CVEPriority = "ELEVATED" - CVEPriorityMonitor CVEPriority = "MONITOR" + CVEPriorityImmediate CVEPriority = "IMMEDIATE" + CVEPriorityHigh CVEPriority = "HIGH" + CVEPriorityElevated CVEPriority = "ELEVATED" + CVEPriorityMonitor CVEPriority = "MONITOR" ) func (e CVEPriority) IsValid() bool { switch e { - case CVEPriorityActNow, CVEPriorityHigh, CVEPriorityElevated, CVEPriorityMonitor: + case CVEPriorityImmediate, CVEPriorityHigh, CVEPriorityElevated, CVEPriorityMonitor: return true } return false diff --git a/internal/vulnerability/queries.go b/internal/vulnerability/queries.go index f72395647..ecc676248 100644 --- a/internal/vulnerability/queries.go +++ b/internal/vulnerability/queries.go @@ -656,7 +656,7 @@ func ListCVEs(ctx context.Context, page *pagination.Pagination, orderBy *CVEOrde case CVEOrderFieldAffectedWorkloadsCount: field = vulnerabilities.OrderByAffectedWorkloads case CVEOrderFieldPriority: - field = vulnerabilities.OrderByPriority + field = vulnerabilities.OrderByTopRiskTier default: field = vulnerabilities.OrderByCvssScore } diff --git a/internal/vulnerability/sortfilter.go b/internal/vulnerability/sortfilter.go index 49625cb64..2ff475293 100644 --- a/internal/vulnerability/sortfilter.go +++ b/internal/vulnerability/sortfilter.go @@ -16,7 +16,7 @@ var SortFilterImageVulnerabilities = map[ImageVulnerabilityOrderField]vulnerabil "STATE": vulnerabilities.OrderByReason, "SUPPRESSED": vulnerabilities.OrderBySuppressed, "SEVERITY_SINCE": vulnerabilities.OrderBySeveritySince, - "PRIORITY": vulnerabilities.OrderByPriority, + "PRIORITY": vulnerabilities.OrderByTopRiskTier, } var SortFilterWorkloadSummaries = map[VulnerabilitySummaryOrderByField]vulnerabilities.OrderByField{ @@ -28,8 +28,8 @@ var SortFilterWorkloadSummaries = map[VulnerabilitySummaryOrderByField]vulnerabi "VULNERABILITY_SEVERITY_MEDIUM": vulnerabilities.OrderByMedium, "VULNERABILITY_SEVERITY_LOW": vulnerabilities.OrderByLow, "VULNERABILITY_SEVERITY_UNASSIGNED": vulnerabilities.OrderByUnassigned, - "VULNERABILITY_PRIORITY_ACT_NOW": vulnerabilities.OrderByPriorityActNow, - "VULNERABILITY_PRIORITY_HIGH": vulnerabilities.OrderByPriorityHigh, + "VULNERABILITY_PRIORITY_ACT_NOW": vulnerabilities.OrderByActNow, + "VULNERABILITY_PRIORITY_HIGH": vulnerabilities.OrderByHighRisk, } const ( diff --git a/internal/vulnerability/transform.go b/internal/vulnerability/transform.go index 6b60cb8e6..b98f59118 100644 --- a/internal/vulnerability/transform.go +++ b/internal/vulnerability/transform.go @@ -81,10 +81,10 @@ func toWorkloadVulnerabilitySummary(w *vulnerabilities.WorkloadSummary) *Workloa Total: int(v13sSummary.Total), RiskScore: int(v13sSummary.RiskScore), LastUpdated: lastUpdated, - PriorityActNow: int(v13sSummary.PriorityActNow), - PriorityHigh: int(v13sSummary.PriorityHigh), - PriorityElevated: int(v13sSummary.PriorityElevated), - PriorityMonitor: int(v13sSummary.PriorityMonitor), + PriorityActNow: int(v13sSummary.ActNow), + PriorityHigh: int(v13sSummary.HighRisk), + PriorityElevated: int(v13sSummary.ElevatedRisk), + PriorityMonitor: int(v13sSummary.Monitor), } return &WorkloadVulnerabilitySummary{ @@ -107,7 +107,7 @@ func toCVE(cve *vulnerabilities.Cve) *CVE { DetailsLink: cve.Link, CVSSScore: cve.CvssScore, Severity: ImageVulnerabilitySeverity(cve.Severity.String()), - Priority: parseCVEPriority(cve.Priority), + Priority: parseCVEPriority(cve), EpssScore: cve.EpssScore, EpssPercentile: cve.EpssPercentile, HasKevEntry: cve.HasKevEntry, @@ -115,17 +115,24 @@ func toCVE(cve *vulnerabilities.Cve) *CVE { } } -func parseCVEPriority(p vulnerabilities.Priority) CVEPriority { - switch p { - case vulnerabilities.Priority_PRIORITY_ACT_NOW: - return CVEPriorityActNow - case vulnerabilities.Priority_PRIORITY_HIGH: +func parseCVEPriority(cve *vulnerabilities.Cve) CVEPriority { + if cve == nil { + return CVEPriorityMonitor + } + + if cve.GetHasKevEntry() { + return CVEPriorityImmediate + } + + if cve.GetKnownRansomwareUse() || cve.GetEpssPercentile() >= 0.90 { return CVEPriorityHigh - case vulnerabilities.Priority_PRIORITY_ELEVATED: + } + + if (cve.GetSeverity() == vulnerabilities.Severity_CRITICAL || cve.GetSeverity() == vulnerabilities.Severity_HIGH) && cve.GetEpssPercentile() >= 0.50 { return CVEPriorityElevated - default: - return CVEPriorityMonitor } + + return CVEPriorityMonitor } func mapVulnerabilitySeverity(severity ImageVulnerabilitySeverity) vulnerabilities.Severity { From 7b6aa5de9a9d955ce63f2230286c237baff2750f Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:23:31 +0200 Subject: [PATCH 09/15] test(vulnerability): cover CVE risk-tier derivation --- internal/graph/gengql/root_.generated.go | 2 +- internal/graph/schema/issues.graphqls | 2 +- internal/vulnerability/transform_test.go | 86 ++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 internal/vulnerability/transform_test.go diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index 00c5b0db0..fcae9dea0 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -22908,7 +22908,7 @@ type ExternalIngressCriticalVulnerabilityIssue implements Issue & Node { ingresses: [String!]! } -"Raised when a workload with external ingresses has one or more ACT_NOW priority vulnerabilities." +"Raised when a workload with external ingresses has one or more IMMEDIATE risk-tier vulnerabilities." type ExternalIngressActNowVulnerabilityIssue implements Issue & Node { id: ID! teamEnvironment: TeamEnvironment! diff --git a/internal/graph/schema/issues.graphqls b/internal/graph/schema/issues.graphqls index 2932e6e7f..11b41ee1f 100644 --- a/internal/graph/schema/issues.graphqls +++ b/internal/graph/schema/issues.graphqls @@ -192,7 +192,7 @@ type ExternalIngressCriticalVulnerabilityIssue implements Issue & Node { ingresses: [String!]! } -"Raised when a workload with external ingresses has one or more ACT_NOW priority vulnerabilities." +"Raised when a workload with external ingresses has one or more IMMEDIATE risk-tier vulnerabilities." type ExternalIngressActNowVulnerabilityIssue implements Issue & Node { id: ID! teamEnvironment: TeamEnvironment! diff --git a/internal/vulnerability/transform_test.go b/internal/vulnerability/transform_test.go new file mode 100644 index 000000000..3ecc35018 --- /dev/null +++ b/internal/vulnerability/transform_test.go @@ -0,0 +1,86 @@ +package vulnerability + +import ( + "testing" + + "github.com/nais/v13s/pkg/api/vulnerabilities" +) + +func TestParseCVEPriority(t *testing.T) { + tests := []struct { + name string + cve *vulnerabilities.Cve + want CVEPriority + }{ + { + name: "nil cve defaults to monitor", + cve: nil, + want: CVEPriorityMonitor, + }, + { + name: "kev is immediate", + cve: &vulnerabilities.Cve{ + HasKevEntry: true, + KnownRansomwareUse: true, + EpssPercentile: new(0.99), + Severity: vulnerabilities.Severity_CRITICAL, + }, + want: CVEPriorityImmediate, + }, + { + name: "ransomware use is high", + cve: &vulnerabilities.Cve{ + KnownRansomwareUse: true, + }, + want: CVEPriorityHigh, + }, + { + name: "epss percentile threshold is high", + cve: &vulnerabilities.Cve{ + EpssPercentile: new(0.90), + }, + want: CVEPriorityHigh, + }, + { + name: "critical severity and elevated epss is elevated", + cve: &vulnerabilities.Cve{ + Severity: vulnerabilities.Severity_CRITICAL, + EpssPercentile: new(0.50), + }, + want: CVEPriorityElevated, + }, + { + name: "high severity and elevated epss is elevated", + cve: &vulnerabilities.Cve{ + Severity: vulnerabilities.Severity_HIGH, + EpssPercentile: new(0.50), + }, + want: CVEPriorityElevated, + }, + { + name: "low severity and elevated epss stays monitor", + cve: &vulnerabilities.Cve{ + Severity: vulnerabilities.Severity_LOW, + EpssPercentile: new(0.70), + }, + want: CVEPriorityMonitor, + }, + { + name: "high severity below elevated epss stays monitor", + cve: &vulnerabilities.Cve{ + Severity: vulnerabilities.Severity_HIGH, + EpssPercentile: new(0.49), + }, + want: CVEPriorityMonitor, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseCVEPriority(tt.cve) + if got != tt.want { + t.Fatalf("parseCVEPriority() = %s, want %s", got, tt.want) + } + }) + } +} From 8de35a9847b7e2c73ddcaef627110bb2d3b811cf Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:18:27 +0200 Subject: [PATCH 10/15] feat(vulnerability): expose fixVersion on image vulnerabilities --- internal/graph/gengql/root_.generated.go | 13 ++++++++++ .../graph/gengql/vulnerability.generated.go | 25 +++++++++++++++++++ internal/graph/schema/vulnerability.graphqls | 3 +++ internal/vulnerability/models.go | 1 + internal/vulnerability/transform.go | 1 + 5 files changed, 43 insertions(+) diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index fcae9dea0..3be893e1e 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -1041,6 +1041,7 @@ type ComplexityRoot struct { ImageVulnerability struct { CvssScore func(childComplexity int) int Description func(childComplexity int) int + FixVersion func(childComplexity int) int ID func(childComplexity int) int Identifier func(childComplexity int) int Package func(childComplexity int) int @@ -7108,6 +7109,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.ImageVulnerability.Description(childComplexity), true + case "ImageVulnerability.fixVersion": + if e.ComplexityRoot.ImageVulnerability.FixVersion == nil { + break + } + + return e.ComplexityRoot.ImageVulnerability.FixVersion(childComplexity), true + case "ImageVulnerability.id": if e.ComplexityRoot.ImageVulnerability.ID == nil { break @@ -31058,6 +31066,9 @@ type ImageVulnerability implements Node { "Package name of the vulnerability." package: String! + "First known package version that contains a fix." + fixVersion: String + suppression: ImageVulnerabilitySuppression "Timestamp of when the vulnerability got its current severity." @@ -33319,6 +33330,8 @@ func (ec *executionContext) childFields_ImageVulnerability(ctx context.Context, return ec.fieldContext_ImageVulnerability_description(ctx, field) case "package": return ec.fieldContext_ImageVulnerability_package(ctx, field) + case "fixVersion": + return ec.fieldContext_ImageVulnerability_fixVersion(ctx, field) case "suppression": return ec.fieldContext_ImageVulnerability_suppression(ctx, field) case "severitySince": diff --git a/internal/graph/gengql/vulnerability.generated.go b/internal/graph/gengql/vulnerability.generated.go index e5dd92780..e892fa1af 100644 --- a/internal/graph/gengql/vulnerability.generated.go +++ b/internal/graph/gengql/vulnerability.generated.go @@ -935,6 +935,29 @@ func (ec *executionContext) fieldContext_ImageVulnerability_package(_ context.Co return graphql.NewScalarFieldContext("ImageVulnerability", field, false, false, errors.New("field of type String does not have child fields")) } +func (ec *executionContext) _ImageVulnerability_fixVersion(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerability) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageVulnerability_fixVersion(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.FixVersion, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_ImageVulnerability_fixVersion(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageVulnerability", field, false, false, errors.New("field of type String does not have child fields")) +} + func (ec *executionContext) _ImageVulnerability_suppression(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerability) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -3928,6 +3951,8 @@ func (ec *executionContext) _ImageVulnerability(ctx context.Context, sel ast.Sel if out.Values[i] == graphql.Null { out.Invalids++ } + case "fixVersion": + out.Values[i] = ec._ImageVulnerability_fixVersion(ctx, field, obj) case "suppression": out.Values[i] = ec._ImageVulnerability_suppression(ctx, field, obj) case "severitySince": diff --git a/internal/graph/schema/vulnerability.graphqls b/internal/graph/schema/vulnerability.graphqls index cb2441a05..2497e6bd1 100644 --- a/internal/graph/schema/vulnerability.graphqls +++ b/internal/graph/schema/vulnerability.graphqls @@ -381,6 +381,9 @@ type ImageVulnerability implements Node { "Package name of the vulnerability." package: String! + "First known package version that contains a fix." + fixVersion: String + suppression: ImageVulnerabilitySuppression "Timestamp of when the vulnerability got its current severity." diff --git a/internal/vulnerability/models.go b/internal/vulnerability/models.go index 4286aeb22..800acb60c 100644 --- a/internal/vulnerability/models.go +++ b/internal/vulnerability/models.go @@ -48,6 +48,7 @@ type ImageVulnerability struct { CvssScore *float64 `json:"cvssScore"` Description string `json:"description"` Package string `json:"package"` + FixVersion *string `json:"fixVersion,omitempty"` SeveritySince *time.Time `json:"severitySince"` Suppression *ImageVulnerabilitySuppression `json:"suppression"` VulnerabilityDetailsLink string `json:"vulnerabilityDetailsLink"` diff --git a/internal/vulnerability/transform.go b/internal/vulnerability/transform.go index b98f59118..e2eaade37 100644 --- a/internal/vulnerability/transform.go +++ b/internal/vulnerability/transform.go @@ -28,6 +28,7 @@ func toImageVulnerability(v *vulnerabilities.Vulnerability) *ImageVulnerability CvssScore: v.GetCve().CvssScore, Description: description, Package: v.Package, + FixVersion: v.FixVersion, SeveritySince: severitySince, VulnerabilityDetailsLink: v.Cve.Link, } From 3a773f3675afdf3655075018b30ba2d5db550cb1 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:43:27 +0200 Subject: [PATCH 11/15] fix(vulnerability): handle RISK_TIER_UNSPECIFIED in risk-tier mapping --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 3d2891d62..a0fbce8b8 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/nais/pgrator/pkg/api v0.0.0-20260219115817-cf954d58c04e github.com/nais/tester v0.1.1 github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe - github.com/nais/v13s/pkg/api v0.0.0-20260602103534-af9d5e6d27d3 + github.com/nais/v13s/pkg/api v0.0.0-20260603094115-97cc35c043d9 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pressly/goose/v3 v3.27.0 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index 8d2852525..44ce5d8e6 100644 --- a/go.sum +++ b/go.sum @@ -817,6 +817,8 @@ github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e h1:7fut4nxp6NlX7 github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= github.com/nais/v13s/pkg/api v0.0.0-20260602103534-af9d5e6d27d3 h1:ei4Q9fAPa0dtBo7aBwCUuVrDRXA9jtipqsU6PhOcPYw= github.com/nais/v13s/pkg/api v0.0.0-20260602103534-af9d5e6d27d3/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= +github.com/nais/v13s/pkg/api v0.0.0-20260603094115-97cc35c043d9 h1:lwdg5KcNrjpBhqhFQtD1YK/GQmVBve1inZanhEDgGco= +github.com/nais/v13s/pkg/api v0.0.0-20260603094115-97cc35c043d9/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= github.com/ncruces/go-sqlite3 v0.32.0 h1:hNBUXp88LrfQCsuyXLqWTbTUG35sUuktDsqhhgHvU20= github.com/ncruces/go-sqlite3 v0.32.0/go.mod h1:MIWTK60ONDl0oVY073zYvJP21C3Dly6P9bxVpgkLwdQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= From c341041596bbaaabe95bc7b3b160e0a81d161738 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:52:25 +0200 Subject: [PATCH 12/15] feat(vulnerability): expose KEV and EPSS fields on ImageVulnerability --- internal/graph/gengql/root_.generated.go | 52 +++++++++ .../graph/gengql/vulnerability.generated.go | 106 ++++++++++++++++++ internal/graph/schema/vulnerability.graphqls | 12 ++ internal/vulnerability/models.go | 4 + internal/vulnerability/transform.go | 4 + 5 files changed, 178 insertions(+) diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index 3be893e1e..43db117cc 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -1041,9 +1041,13 @@ type ComplexityRoot struct { ImageVulnerability struct { CvssScore func(childComplexity int) int Description func(childComplexity int) int + EpssPercentile func(childComplexity int) int + EpssScore func(childComplexity int) int FixVersion func(childComplexity int) int + HasKevEntry func(childComplexity int) int ID func(childComplexity int) int Identifier func(childComplexity int) int + KnownRansomwareUse func(childComplexity int) int Package func(childComplexity int) int Severity func(childComplexity int) int SeveritySince func(childComplexity int) int @@ -7109,6 +7113,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.ImageVulnerability.Description(childComplexity), true + case "ImageVulnerability.epssPercentile": + if e.ComplexityRoot.ImageVulnerability.EpssPercentile == nil { + break + } + + return e.ComplexityRoot.ImageVulnerability.EpssPercentile(childComplexity), true + + case "ImageVulnerability.epssScore": + if e.ComplexityRoot.ImageVulnerability.EpssScore == nil { + break + } + + return e.ComplexityRoot.ImageVulnerability.EpssScore(childComplexity), true + case "ImageVulnerability.fixVersion": if e.ComplexityRoot.ImageVulnerability.FixVersion == nil { break @@ -7116,6 +7134,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.ImageVulnerability.FixVersion(childComplexity), true + case "ImageVulnerability.hasKevEntry": + if e.ComplexityRoot.ImageVulnerability.HasKevEntry == nil { + break + } + + return e.ComplexityRoot.ImageVulnerability.HasKevEntry(childComplexity), true + case "ImageVulnerability.id": if e.ComplexityRoot.ImageVulnerability.ID == nil { break @@ -7130,6 +7155,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.ImageVulnerability.Identifier(childComplexity), true + case "ImageVulnerability.knownRansomwareUse": + if e.ComplexityRoot.ImageVulnerability.KnownRansomwareUse == nil { + break + } + + return e.ComplexityRoot.ImageVulnerability.KnownRansomwareUse(childComplexity), true + case "ImageVulnerability.package": if e.ComplexityRoot.ImageVulnerability.Package == nil { break @@ -31079,6 +31111,18 @@ type ImageVulnerability implements Node { "CVSS score of the vulnerability." cvssScore: Float + + "EPSS score of the vulnerability." + epssScore: Float + + "EPSS percentile of the vulnerability (0-1)." + epssPercentile: Float + + "Whether the vulnerability has a CISA KEV entry." + hasKevEntry: Boolean! + + "Whether the vulnerability has known ransomware use." + knownRansomwareUse: Boolean! } enum CVEPriority { @@ -33340,6 +33384,14 @@ func (ec *executionContext) childFields_ImageVulnerability(ctx context.Context, return ec.fieldContext_ImageVulnerability_vulnerabilityDetailsLink(ctx, field) case "cvssScore": return ec.fieldContext_ImageVulnerability_cvssScore(ctx, field) + case "epssScore": + return ec.fieldContext_ImageVulnerability_epssScore(ctx, field) + case "epssPercentile": + return ec.fieldContext_ImageVulnerability_epssPercentile(ctx, field) + case "hasKevEntry": + return ec.fieldContext_ImageVulnerability_hasKevEntry(ctx, field) + case "knownRansomwareUse": + return ec.fieldContext_ImageVulnerability_knownRansomwareUse(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageVulnerability", field.Name) } diff --git a/internal/graph/gengql/vulnerability.generated.go b/internal/graph/gengql/vulnerability.generated.go index e892fa1af..5f0840d47 100644 --- a/internal/graph/gengql/vulnerability.generated.go +++ b/internal/graph/gengql/vulnerability.generated.go @@ -1059,6 +1059,98 @@ func (ec *executionContext) fieldContext_ImageVulnerability_cvssScore(_ context. return graphql.NewScalarFieldContext("ImageVulnerability", field, false, false, errors.New("field of type Float does not have child fields")) } +func (ec *executionContext) _ImageVulnerability_epssScore(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerability) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageVulnerability_epssScore(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EpssScore, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_ImageVulnerability_epssScore(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageVulnerability", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _ImageVulnerability_epssPercentile(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerability) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageVulnerability_epssPercentile(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.EpssPercentile, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_ImageVulnerability_epssPercentile(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageVulnerability", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _ImageVulnerability_hasKevEntry(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerability) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageVulnerability_hasKevEntry(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.HasKevEntry, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageVulnerability_hasKevEntry(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageVulnerability", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _ImageVulnerability_knownRansomwareUse(ctx context.Context, field graphql.CollectedField, obj *vulnerability.ImageVulnerability) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageVulnerability_knownRansomwareUse(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.KnownRansomwareUse, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageVulnerability_knownRansomwareUse(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageVulnerability", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + func (ec *executionContext) _ImageVulnerabilityConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *pagination.Connection[*vulnerability.ImageVulnerability]) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -3964,6 +4056,20 @@ func (ec *executionContext) _ImageVulnerability(ctx context.Context, sel ast.Sel } case "cvssScore": out.Values[i] = ec._ImageVulnerability_cvssScore(ctx, field, obj) + case "epssScore": + out.Values[i] = ec._ImageVulnerability_epssScore(ctx, field, obj) + case "epssPercentile": + out.Values[i] = ec._ImageVulnerability_epssPercentile(ctx, field, obj) + case "hasKevEntry": + out.Values[i] = ec._ImageVulnerability_hasKevEntry(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "knownRansomwareUse": + out.Values[i] = ec._ImageVulnerability_knownRansomwareUse(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/internal/graph/schema/vulnerability.graphqls b/internal/graph/schema/vulnerability.graphqls index 2497e6bd1..38ab9b061 100644 --- a/internal/graph/schema/vulnerability.graphqls +++ b/internal/graph/schema/vulnerability.graphqls @@ -394,6 +394,18 @@ type ImageVulnerability implements Node { "CVSS score of the vulnerability." cvssScore: Float + + "EPSS score of the vulnerability." + epssScore: Float + + "EPSS percentile of the vulnerability (0-1)." + epssPercentile: Float + + "Whether the vulnerability has a CISA KEV entry." + hasKevEntry: Boolean! + + "Whether the vulnerability has known ransomware use." + knownRansomwareUse: Boolean! } enum CVEPriority { diff --git a/internal/vulnerability/models.go b/internal/vulnerability/models.go index 800acb60c..e82214391 100644 --- a/internal/vulnerability/models.go +++ b/internal/vulnerability/models.go @@ -46,6 +46,10 @@ type ImageVulnerability struct { Identifier string `json:"identifier"` Severity ImageVulnerabilitySeverity `json:"severity"` CvssScore *float64 `json:"cvssScore"` + EpssScore *float64 `json:"epssScore"` + EpssPercentile *float64 `json:"epssPercentile"` + HasKevEntry bool `json:"hasKevEntry"` + KnownRansomwareUse bool `json:"knownRansomwareUse"` Description string `json:"description"` Package string `json:"package"` FixVersion *string `json:"fixVersion,omitempty"` diff --git a/internal/vulnerability/transform.go b/internal/vulnerability/transform.go index e2eaade37..6aad4e9f3 100644 --- a/internal/vulnerability/transform.go +++ b/internal/vulnerability/transform.go @@ -26,6 +26,10 @@ func toImageVulnerability(v *vulnerabilities.Vulnerability) *ImageVulnerability Identifier: v.Cve.Id, Severity: ImageVulnerabilitySeverity(v.Cve.Severity.String()), CvssScore: v.GetCve().CvssScore, + EpssScore: v.GetCve().EpssScore, + EpssPercentile: v.GetCve().EpssPercentile, + HasKevEntry: v.GetCve().HasKevEntry, + KnownRansomwareUse: v.GetCve().KnownRansomwareUse, Description: description, Package: v.Package, FixVersion: v.FixVersion, From ec24e2e29faa101107c5a5dbed6e3ba9c6accd60 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:12:06 +0200 Subject: [PATCH 13/15] fix(vulnerability): map PRIORITY sort to cve priority order --- internal/vulnerability/queries.go | 2 +- internal/vulnerability/sortfilter.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/vulnerability/queries.go b/internal/vulnerability/queries.go index ecc676248..f72395647 100644 --- a/internal/vulnerability/queries.go +++ b/internal/vulnerability/queries.go @@ -656,7 +656,7 @@ func ListCVEs(ctx context.Context, page *pagination.Pagination, orderBy *CVEOrde case CVEOrderFieldAffectedWorkloadsCount: field = vulnerabilities.OrderByAffectedWorkloads case CVEOrderFieldPriority: - field = vulnerabilities.OrderByTopRiskTier + field = vulnerabilities.OrderByPriority default: field = vulnerabilities.OrderByCvssScore } diff --git a/internal/vulnerability/sortfilter.go b/internal/vulnerability/sortfilter.go index 2ff475293..a682a60c7 100644 --- a/internal/vulnerability/sortfilter.go +++ b/internal/vulnerability/sortfilter.go @@ -16,7 +16,7 @@ var SortFilterImageVulnerabilities = map[ImageVulnerabilityOrderField]vulnerabil "STATE": vulnerabilities.OrderByReason, "SUPPRESSED": vulnerabilities.OrderBySuppressed, "SEVERITY_SINCE": vulnerabilities.OrderBySeveritySince, - "PRIORITY": vulnerabilities.OrderByTopRiskTier, + "PRIORITY": vulnerabilities.OrderByPriority, } var SortFilterWorkloadSummaries = map[VulnerabilitySummaryOrderByField]vulnerabilities.OrderByField{ From 957c6bf912e6e6a2fc01cb426172797aeccbd379 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:09:54 +0200 Subject: [PATCH 14/15] chore(deps): bump v13s api after exploitable removal --- go.mod | 2 +- go.sum | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index a0fbce8b8..d421fe07f 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/nais/pgrator/pkg/api v0.0.0-20260219115817-cf954d58c04e github.com/nais/tester v0.1.1 github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe - github.com/nais/v13s/pkg/api v0.0.0-20260603094115-97cc35c043d9 + github.com/nais/v13s/pkg/api v0.0.0-20260604080807-5ff2f400c716 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pressly/goose/v3 v3.27.0 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index 44ce5d8e6..b8a7356bf 100644 --- a/go.sum +++ b/go.sum @@ -813,12 +813,8 @@ github.com/nais/tester v0.1.1 h1:tpJ5HKpu3mEIWX/mec0Yj0xLHEpt+MwTAsj282n0Py0= github.com/nais/tester v0.1.1/go.mod h1:NCQMcgftHz/EXorob1XwDTOqkQmImDqr51YQ2Uea9Pc= github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe h1:CdRVopOihru4tXVwKZjhg6C8SbPLCQYOhJKpjBZYhjg= github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe/go.mod h1:Tiz/1If3WgcfvNhmsO5DiQC+L+1XhBG3KWbIfbjx4EU= -github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e h1:7fut4nxp6NlX7xS5SnwLkdyAhWanqmcpuClssn/cT8Q= -github.com/nais/v13s/pkg/api v0.0.0-20260528121134-739c7136ac8e/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= -github.com/nais/v13s/pkg/api v0.0.0-20260602103534-af9d5e6d27d3 h1:ei4Q9fAPa0dtBo7aBwCUuVrDRXA9jtipqsU6PhOcPYw= -github.com/nais/v13s/pkg/api v0.0.0-20260602103534-af9d5e6d27d3/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= -github.com/nais/v13s/pkg/api v0.0.0-20260603094115-97cc35c043d9 h1:lwdg5KcNrjpBhqhFQtD1YK/GQmVBve1inZanhEDgGco= -github.com/nais/v13s/pkg/api v0.0.0-20260603094115-97cc35c043d9/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= +github.com/nais/v13s/pkg/api v0.0.0-20260604080807-5ff2f400c716 h1:FpEOQH7TP50xuVCkkcMk+ZaSRnxhmHZmuhDVPGL56sU= +github.com/nais/v13s/pkg/api v0.0.0-20260604080807-5ff2f400c716/go.mod h1:KBuEYLBJOFM36G7D5RAZ5oRyUv0/IOK9JCgkUS1eqqY= github.com/ncruces/go-sqlite3 v0.32.0 h1:hNBUXp88LrfQCsuyXLqWTbTUG35sUuktDsqhhgHvU20= github.com/ncruces/go-sqlite3 v0.32.0/go.mod h1:MIWTK60ONDl0oVY073zYvJP21C3Dly6P9bxVpgkLwdQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= From 3d8ae289d0b1b157d9656e9d0f8836d2cb5edefc Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:34:57 +0200 Subject: [PATCH 15/15] fix(vulnerability): clean enum docs and normalize fake EPSS percentile --- internal/graph/gengql/root_.generated.go | 16 ++++++++-------- internal/graph/schema/vulnerability.graphqls | 16 ++++++++-------- internal/vulnerability/fake/fakedata.go | 19 +------------------ 3 files changed, 17 insertions(+), 34 deletions(-) diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index 43db117cc..69fe7b795 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -31326,35 +31326,35 @@ enum VulnerabilitySummaryOrderByField { """ ENVIRONMENT """ - Order by risk score" + Order by risk score. """ VULNERABILITY_RISK_SCORE """ - Order by vulnerability severity critical" + Order by vulnerability severity critical. """ VULNERABILITY_SEVERITY_CRITICAL """ - Order by vulnerability severity high" + Order by vulnerability severity high. """ VULNERABILITY_SEVERITY_HIGH """ - Order by vulnerability severity medium" + Order by vulnerability severity medium. """ VULNERABILITY_SEVERITY_MEDIUM """ - Order by vulnerability severity low" + Order by vulnerability severity low. """ VULNERABILITY_SEVERITY_LOW """ - Order by vulnerability severity unassigned" + Order by vulnerability severity unassigned. """ VULNERABILITY_SEVERITY_UNASSIGNED """ - Order by IMMEDIATE risk-tier count" + Order by IMMEDIATE risk-tier count. """ VULNERABILITY_PRIORITY_ACT_NOW """ - Order by HIGH risk-tier count" + Order by HIGH risk-tier count. """ VULNERABILITY_PRIORITY_HIGH } diff --git a/internal/graph/schema/vulnerability.graphqls b/internal/graph/schema/vulnerability.graphqls index 38ab9b061..9f98b965b 100644 --- a/internal/graph/schema/vulnerability.graphqls +++ b/internal/graph/schema/vulnerability.graphqls @@ -609,35 +609,35 @@ enum VulnerabilitySummaryOrderByField { """ ENVIRONMENT """ - Order by risk score" + Order by risk score. """ VULNERABILITY_RISK_SCORE """ - Order by vulnerability severity critical" + Order by vulnerability severity critical. """ VULNERABILITY_SEVERITY_CRITICAL """ - Order by vulnerability severity high" + Order by vulnerability severity high. """ VULNERABILITY_SEVERITY_HIGH """ - Order by vulnerability severity medium" + Order by vulnerability severity medium. """ VULNERABILITY_SEVERITY_MEDIUM """ - Order by vulnerability severity low" + Order by vulnerability severity low. """ VULNERABILITY_SEVERITY_LOW """ - Order by vulnerability severity unassigned" + Order by vulnerability severity unassigned. """ VULNERABILITY_SEVERITY_UNASSIGNED """ - Order by IMMEDIATE risk-tier count" + Order by IMMEDIATE risk-tier count. """ VULNERABILITY_PRIORITY_ACT_NOW """ - Order by HIGH risk-tier count" + Order by HIGH risk-tier count. """ VULNERABILITY_PRIORITY_HIGH } diff --git a/internal/vulnerability/fake/fakedata.go b/internal/vulnerability/fake/fakedata.go index 85b333635..10eb6329b 100644 --- a/internal/vulnerability/fake/fakedata.go +++ b/internal/vulnerability/fake/fakedata.go @@ -124,38 +124,21 @@ func createWorkloadSummary(env, team, workloadType, name, image string, vulnFact func createVulnerabilities(w *vulnerabilities.WorkloadSummary) []*vulnerabilities.Vulnerability { findings := make([]*vulnerabilities.Vulnerability, 0) - tiers := []vulnerabilities.RiskTier{ - vulnerabilities.RiskTier_ACT_NOW, - vulnerabilities.RiskTier_HIGH_RISK, - vulnerabilities.RiskTier_ELEVATED_RISK, - vulnerabilities.RiskTier_MONITOR, - } - idx := 0 - nextRiskTier := func() vulnerabilities.RiskTier { - p := tiers[idx%len(tiers)] - idx++ - return p - } epssScore := 0.85 - epssPercentile := 97.3 + epssPercentile := 0.973 for i := range w.VulnerabilitySummary.Critical { - _ = nextRiskTier() findings = append(findings, createVulnerability(vulnerabilities.Severity_CRITICAL, fmt.Sprintf("some-component-%d", i), &epssScore, &epssPercentile, true, false)) } for i := range w.VulnerabilitySummary.High { - _ = nextRiskTier() findings = append(findings, createVulnerability(vulnerabilities.Severity_HIGH, fmt.Sprintf("some-component-%d", i), nil, nil, false, false)) } for i := range w.VulnerabilitySummary.Medium { - _ = nextRiskTier() findings = append(findings, createVulnerability(vulnerabilities.Severity_MEDIUM, fmt.Sprintf("some-component-%d", i), nil, nil, false, false)) } for i := range w.VulnerabilitySummary.Low { - _ = nextRiskTier() findings = append(findings, createVulnerability(vulnerabilities.Severity_LOW, fmt.Sprintf("some-component-%d", i), nil, nil, false, false)) } for i := range w.VulnerabilitySummary.Unassigned { - _ = nextRiskTier() findings = append(findings, createVulnerability(vulnerabilities.Severity_UNASSIGNED, fmt.Sprintf("some-component-%d", i), nil, nil, false, false)) } return findings