diff --git a/go.mod b/go.mod index 0dd2b21b6..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-20260528080657-d4f49e5737da + 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 08b8ab991..b8a7356bf 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-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= diff --git a/integration_tests/issues_for_team.lua b/integration_tests/issues_for_team.lua index 6fe9ec3a9..105c6369c 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 IMMEDIATE and 3 HIGH risk-tier vulnerabilities", + severity = "CRITICAL", critical = 5, riskScore = 250, workload = { 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..69fe7b795 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 @@ -517,14 +518,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 { @@ -926,6 +932,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 @@ -1025,8 +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 @@ -1055,15 +1076,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 { @@ -5131,6 +5156,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 @@ -5145,6 +5191,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 @@ -6612,6 +6672,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 @@ -7004,6 +7113,34 @@ 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 + } + + 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 @@ -7018,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 @@ -7144,6 +7288,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 @@ -22748,6 +22920,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 +22948,18 @@ type ExternalIngressCriticalVulnerabilityIssue implements Issue & Node { ingresses: [String!]! } +"Raised when a workload with external ingresses has one or more IMMEDIATE risk-tier 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! @@ -30587,6 +30772,7 @@ enum CVEOrderField { SEVERITY CVSS_SCORE AFFECTED_WORKLOADS_COUNT + PRIORITY } extend interface Workload { @@ -30822,6 +31008,18 @@ type ImageVulnerabilitySummary { "Number of vulnerabilities with severity UNASSIGNED." unassigned: Int! + "Number of vulnerabilities with risk tier IMMEDIATE." + priorityActNow: Int! + + "Number of vulnerabilities with risk tier HIGH." + priorityHigh: Int! + + "Number of vulnerabilities with risk tier ELEVATED." + priorityElevated: Int! + + "Number of vulnerabilities with risk tier MONITOR." + priorityMonitor: Int! + "Timestamp of the last update of the vulnerability summary." lastUpdated: Time @@ -30900,6 +31098,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." @@ -30910,6 +31111,29 @@ 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 { + "Vulnerability is known to be actively exploited and requires immediate action." + IMMEDIATE + "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 { @@ -30934,6 +31158,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." @@ -31044,6 +31283,7 @@ enum ImageVulnerabilityOrderField { PACKAGE STATE SUPPRESSED + PRIORITY } type WorkloadVulnerabilitySummary implements Node { @@ -31086,29 +31326,37 @@ 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. + """ + VULNERABILITY_PRIORITY_ACT_NOW + """ + Order by HIGH risk-tier count. + """ + VULNERABILITY_PRIORITY_HIGH } type TenantVulnerabilitySummary { @@ -32378,6 +32626,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) } @@ -33116,6 +33374,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": @@ -33124,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) } @@ -33184,6 +33452,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/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/gengql/vulnerability.generated.go b/internal/graph/gengql/vulnerability.generated.go index c2a7a9a1c..5f0840d47 100644 --- a/internal/graph/gengql/vulnerability.generated.go +++ b/internal/graph/gengql/vulnerability.generated.go @@ -258,6 +258,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, @@ -820,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, @@ -921,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, @@ -1320,6 +1550,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, @@ -3243,6 +3565,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 @@ -3702,6 +4043,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": @@ -3713,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)) } @@ -3958,6 +4315,26 @@ func (ec *executionContext) _ImageVulnerabilitySummary(ctx context.Context, sel if out.Values[i] == graphql.Null { out.Invalids++ } + case "priorityActNow": + out.Values[i] = ec._ImageVulnerabilitySummary_priorityActNow(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "priorityHigh": + out.Values[i] = ec._ImageVulnerabilitySummary_priorityHigh(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "priorityElevated": + out.Values[i] = ec._ImageVulnerabilitySummary_priorityElevated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "priorityMonitor": + out.Values[i] = ec._ImageVulnerabilitySummary_priorityMonitor(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "lastUpdated": out.Values[i] = ec._ImageVulnerabilitySummary_lastUpdated(ctx, field, obj) case "staleImageTag": @@ -4879,6 +5256,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 0301eec5e..c55a111de 100644 --- a/internal/graph/issues.resolvers.go +++ b/internal/graph/issues.resolvers.go @@ -40,6 +40,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) { + return team.GetTeamEnvironment(ctx, obj.TeamSlug, obj.EnvironmentName) +} + +func (r *externalIngressActNowVulnerabilityIssueResolver) Workload(ctx context.Context, obj *issue.ExternalIngressActNowVulnerabilityIssue) (workload.Workload, error) { + return getWorkloadByResourceType(ctx, obj.TeamSlug, obj.EnvironmentName, obj.ResourceName, obj.ResourceType) +} + func (r *externalIngressCriticalVulnerabilityIssueResolver) TeamEnvironment(ctx context.Context, obj *issue.ExternalIngressCriticalVulnerabilityIssue) (*team.TeamEnvironment, error) { return team.GetTeamEnvironment(ctx, obj.TeamSlug, obj.EnvironmentName) } @@ -157,6 +165,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 +219,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..11b41ee1f 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 IMMEDIATE risk-tier 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/schema/vulnerability.graphqls b/internal/graph/schema/vulnerability.graphqls index 37645d575..9f98b965b 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 risk tier IMMEDIATE." + priorityActNow: Int! + + "Number of vulnerabilities with risk tier HIGH." + priorityHigh: Int! + + "Number of vulnerabilities with risk tier ELEVATED." + priorityElevated: Int! + + "Number of vulnerabilities with risk tier MONITOR." + priorityMonitor: Int! + "Timestamp of the last update of the vulnerability summary." lastUpdated: Time @@ -368,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." @@ -378,6 +394,29 @@ 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 { + "Vulnerability is known to be actively exploited and requires immediate action." + IMMEDIATE + "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 { @@ -402,6 +441,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 +566,7 @@ enum ImageVulnerabilityOrderField { PACKAGE STATE SUPPRESSED + PRIORITY } type WorkloadVulnerabilitySummary implements Node { @@ -554,29 +609,37 @@ 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. + """ + VULNERABILITY_PRIORITY_ACT_NOW + """ + Order by HIGH risk-tier count. + """ + VULNERABILITY_PRIORITY_HIGH } type TenantVulnerabilitySummary { diff --git a/internal/issue/checker/workload_v13s.go b/internal/issue/checker/workload_v13s.go index 1fc65db5d..5fc5f30ff 100644 --- a/internal/issue/checker/workload_v13s.go +++ b/internal/issue/checker/workload_v13s.go @@ -40,6 +40,8 @@ func (f fakeV13sClient) ListVulnerabilitySummaries(ctx context.Context, opts ... VulnerabilitySummary: &vulnerabilities.Summary{ Critical: 5, RiskScore: 250, + ActNow: 2, + HighRisk: 3, }, SbomStatus: &vulnerabilities.SbomStatusInfo{ Status: vulnerabilities.SbomStatus_SBOM_STATUS_READY, @@ -72,6 +74,8 @@ func (f fakeV13sClient) ListVulnerabilitySummaries(ctx context.Context, opts ... VulnerabilitySummary: &vulnerabilities.Summary{ Critical: 5, RiskScore: 250, + ActNow: 2, + HighRisk: 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.ActNow > 0 || summary.HighRisk > 0) { + severity := issue.SeverityWarning + if summary.ActNow > 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 IMMEDIATE and %d HIGH risk-tier vulnerabilities", node.Workload.ImageName, - node.VulnerabilitySummary.Critical, - node.VulnerabilitySummary.RiskScore, + summary.ActNow, + summary.HighRisk, ), 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.ActNow == 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 IMMEDIATE risk-tier vulnerabilities", + strings.Join(externalIngresses, ", "), + node.VulnerabilitySummary.ActNow, + ), + IssueDetails: issue.ExternalIngressActNowVulnerabilityIssueDetails{ + PriorityActNow: int(node.VulnerabilitySummary.ActNow), + 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..10eb6329b 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()), + ActNow: vulnFactor, + HighRisk: vulnFactor * 2, + ElevatedRisk: vulnFactor * 3, + Monitor: vulnFactor * 4, } if name == "no-errors" { @@ -120,38 +124,43 @@ func createWorkloadSummary(env, team, workloadType, name, image string, vulnFact func createVulnerabilities(w *vulnerabilities.WorkloadSummary) []*vulnerabilities.Vulnerability { findings := make([]*vulnerabilities.Vulnerability, 0) + epssScore := 0.85 + epssPercentile := 0.973 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), &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), 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), 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), 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), nil, nil, false, false)) } return findings } -func createVulnerability(severity vulnerabilities.Severity, componentName string) *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), 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, + 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..e82214391 100644 --- a/internal/vulnerability/models.go +++ b/internal/vulnerability/models.go @@ -46,8 +46,13 @@ 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"` SeveritySince *time.Time `json:"severitySince"` Suppression *ImageVulnerabilitySuppression `json:"suppression"` VulnerabilityDetailsLink string `json:"vulnerabilityDetailsLink"` @@ -71,15 +76,19 @@ 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"` + StaleImageTag *string `json:"staleImageTag"` + PriorityActNow int `json:"priorityActNow"` + PriorityHigh int `json:"priorityHigh"` + PriorityElevated int `json:"priorityElevated"` + PriorityMonitor int `json:"priorityMonitor"` } type ImageVulnerabilityOrderField string @@ -222,6 +231,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{ @@ -234,11 +245,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 @@ -413,14 +426,57 @@ type VulnerabilityFixSample struct { TotalWorkloads int `json:"totalWorkloads"` } +type CVEPriority string + +const ( + CVEPriorityImmediate CVEPriority = "IMMEDIATE" + CVEPriorityHigh CVEPriority = "HIGH" + CVEPriorityElevated CVEPriority = "ELEVATED" + CVEPriorityMonitor CVEPriority = "MONITOR" +) + +func (e CVEPriority) IsValid() bool { + switch e { + case CVEPriorityImmediate, 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. @@ -477,6 +533,7 @@ const ( CVEOrderFieldSeverity CVEOrderField = "SEVERITY" CVEOrderFieldCVSSScore CVEOrderField = "CVSS_SCORE" CVEOrderFieldAffectedWorkloadsCount CVEOrderField = "AFFECTED_WORKLOADS_COUNT" + CVEOrderFieldPriority CVEOrderField = "PRIORITY" ) var AllCVEOrderField = []CVEOrderField{ @@ -484,11 +541,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..f72395647 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 } @@ -699,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) } @@ -721,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) { diff --git a/internal/vulnerability/sortfilter.go b/internal/vulnerability/sortfilter.go index cad3014d3..a682a60c7 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.OrderByActNow, + "VULNERABILITY_PRIORITY_HIGH": vulnerabilities.OrderByHighRisk, } 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 { diff --git a/internal/vulnerability/transform.go b/internal/vulnerability/transform.go index 000f50ca4..6aad4e9f3 100644 --- a/internal/vulnerability/transform.go +++ b/internal/vulnerability/transform.go @@ -26,8 +26,13 @@ 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, SeveritySince: severitySince, VulnerabilityDetailsLink: v.Cve.Link, } @@ -61,26 +66,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.ActNow), + PriorityHigh: int(v13sSummary.HighRisk), + PriorityElevated: int(v13sSummary.ElevatedRisk), + PriorityMonitor: int(v13sSummary.Monitor), } 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{ @@ -92,13 +106,38 @@ 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), + EpssScore: cve.EpssScore, + EpssPercentile: cve.EpssPercentile, + HasKevEntry: cve.HasKevEntry, + KnownRansomwareUse: cve.KnownRansomwareUse, + } +} + +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 + } + + if (cve.GetSeverity() == vulnerabilities.Severity_CRITICAL || cve.GetSeverity() == vulnerabilities.Severity_HIGH) && cve.GetEpssPercentile() >= 0.50 { + return CVEPriorityElevated + } + + return CVEPriorityMonitor } func mapVulnerabilitySeverity(severity ImageVulnerabilitySeverity) vulnerabilities.Severity { 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) + } + }) + } +}