@skyhook-io/k8s-ui 1.10.4 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/package.json +5 -5
  2. package/src/components/applications/ApplicationDetail.test.tsx +46 -0
  3. package/src/components/applications/ApplicationDetail.tsx +14 -15
  4. package/src/components/issues/IssuesView.tsx +9 -0
  5. package/src/components/issues/diagnostic.ts +3 -0
  6. package/src/components/issues/issues.test.ts +11 -0
  7. package/src/components/issues/severity.ts +3 -0
  8. package/src/components/issues/types.ts +3 -0
  9. package/src/components/resources/ResourcesSidebar.tsx +1 -1
  10. package/src/components/resources/ResourcesView.tsx +259 -30
  11. package/src/components/resources/index.ts +2 -0
  12. package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
  13. package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
  14. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
  15. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
  16. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
  17. package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
  18. package/src/components/resources/renderers/EventRenderer.tsx +10 -18
  19. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
  20. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
  21. package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
  22. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
  23. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
  24. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
  25. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
  26. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
  27. package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
  28. package/src/components/resources/renderers/index.ts +4 -0
  29. package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
  30. package/src/components/resources/renderers/velero-cells.tsx +128 -9
  31. package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
  32. package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
  33. package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
  34. package/src/components/resources/resource-utils-cnpg.ts +373 -56
  35. package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
  36. package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
  37. package/src/components/resources/resource-utils-velero.test.ts +237 -0
  38. package/src/components/resources/resource-utils-velero.ts +214 -22
  39. package/src/components/resources/resource-utils.ts +70 -3
  40. package/src/components/shared/ResourceActionsBar.tsx +3 -1
  41. package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
  42. package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
  43. package/src/components/topology/K8sResourceNode.tsx +11 -0
  44. package/src/components/topology/TopologyGraph.tsx +85 -5
  45. package/src/components/trace/ReachabilityGraph.tsx +517 -0
  46. package/src/components/trace/ReachabilityView.tsx +1173 -0
  47. package/src/components/trace/TracePanel.test.ts +172 -0
  48. package/src/components/trace/TracePanel.tsx +558 -0
  49. package/src/components/trace/TraceSummary.test.ts +213 -0
  50. package/src/components/trace/TraceSummary.tsx +168 -0
  51. package/src/components/trace/inClusterSummary.test.ts +98 -0
  52. package/src/components/trace/inClusterSummary.ts +81 -0
  53. package/src/components/trace/index.ts +19 -0
  54. package/src/components/trace/podReach.test.ts +71 -0
  55. package/src/components/trace/podReach.ts +37 -0
  56. package/src/components/trace/probe-display.test.ts +107 -0
  57. package/src/components/trace/probe-display.ts +37 -0
  58. package/src/components/trace/problemRows.test.ts +107 -0
  59. package/src/components/trace/reachFixtures.ts +261 -0
  60. package/src/components/trace/reachGraphModel.test.ts +1423 -0
  61. package/src/components/trace/reachGraphModel.ts +1521 -0
  62. package/src/components/trace/reachInspector.test.ts +892 -0
  63. package/src/components/trace/reachInspector.ts +797 -0
  64. package/src/components/trace/reachMarks.test.ts +698 -0
  65. package/src/components/trace/reachMarks.ts +623 -0
  66. package/src/components/trace/reachOrigins.test.ts +392 -0
  67. package/src/components/trace/reachOrigins.ts +449 -0
  68. package/src/components/trace/reachVerdict.test.ts +537 -0
  69. package/src/components/trace/reachVerdict.ts +601 -0
  70. package/src/components/trace/traceFingerprint.test.ts +214 -0
  71. package/src/components/trace/traceFingerprint.ts +122 -0
  72. package/src/components/trace/traceToSubgraph.test.ts +623 -0
  73. package/src/components/trace/traceToSubgraph.ts +463 -0
  74. package/src/components/trace/types.ts +414 -0
  75. package/src/components/ui/Badge.tsx +36 -0
  76. package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
  77. package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
  78. package/src/components/ui/InClusterConsentDialog.tsx +141 -0
  79. package/src/components/ui/index.ts +1 -0
  80. package/src/components/workload/WorkloadView.tsx +58 -5
  81. package/src/components/workload/index.ts +1 -1
  82. package/src/components/workload/reachabilityTab.test.ts +47 -0
  83. package/src/index.ts +5 -0
  84. package/src/theme/components.css +64 -0
  85. package/src/theme/variables.css +5 -0
  86. package/src/types/core.ts +8 -0
  87. package/src/utils/api-resources.ts +2 -0
  88. package/src/utils/application-history.test.ts +1 -1
  89. package/src/utils/applications.test.ts +63 -1
  90. package/src/utils/applications.ts +27 -0
  91. package/src/utils/inClusterConsent.test.ts +109 -0
  92. package/src/utils/inClusterConsent.ts +72 -0
  93. package/src/utils/index.ts +1 -0
  94. package/src/utils/navigation.test.ts +61 -0
  95. package/src/utils/navigation.ts +11 -9
  96. package/src/utils/pluralize.test.ts +80 -1
  97. package/src/utils/pluralize.ts +34 -0
  98. package/src/utils/resource-icons.test.ts +33 -0
  99. package/src/utils/resource-icons.ts +65 -2
@@ -0,0 +1,588 @@
1
+ // Kyverno modern CEL policy family (policies.kyverno.io) utility functions.
2
+ //
3
+ // Distinct from resource-utils-kyverno.ts, which serves the legacy kyverno.io
4
+ // Policy/ClusterPolicy family (deprecated in Kyverno 1.18, removal planned for
5
+ // 1.20). The two families share a vendor and nothing else: the legacy spec is
6
+ // `rules[]` with `validationFailureAction`, the modern one is VAP-shaped
7
+ // `matchConstraints` + CEL `validations[]` with `validationActions`. Keep the
8
+ // accessors apart — a shared one would have to guess which shape it holds.
9
+
10
+ import type { StatusBadge } from './resource-utils'
11
+ import { healthColors } from './resource-utils'
12
+
13
+ export const KYVERNO_MODERN_GROUP = 'policies.kyverno.io'
14
+
15
+ /**
16
+ * True when the resource belongs to the modern CEL family. Dispatch MUST gate
17
+ * on this rather than on the plural alone: `PolicyException` exists in both
18
+ * kyverno.io and policies.kyverno.io with the same Kind, the same plural and
19
+ * different spec shapes, and names like `validatingpolicies` are generic
20
+ * enough that another vendor could shadow them.
21
+ */
22
+ export function isModernKyvernoPolicy(resource: any): boolean {
23
+ return typeof resource?.apiVersion === 'string' && resource.apiVersion.startsWith(`${KYVERNO_MODERN_GROUP}/`)
24
+ }
25
+
26
+ /**
27
+ * Plurals served by policies.kyverno.io. Dispatch uses this together with
28
+ * isModernKyvernoPolicy so a foreign CRD sharing a generic plural like
29
+ * `validatingpolicies` falls through to generic rendering instead of being
30
+ * described in Kyverno's vocabulary.
31
+ */
32
+ export const KYVERNO_MODERN_PLURALS = new Set([
33
+ 'validatingpolicies',
34
+ 'namespacedvalidatingpolicies',
35
+ 'imagevalidatingpolicies',
36
+ 'namespacedimagevalidatingpolicies',
37
+ 'mutatingpolicies',
38
+ 'namespacedmutatingpolicies',
39
+ 'generatingpolicies',
40
+ 'namespacedgeneratingpolicies',
41
+ 'deletingpolicies',
42
+ 'namespaceddeletingpolicies',
43
+ ])
44
+
45
+ export type KyvernoPolicyFamily = 'validating' | 'imageValidating' | 'mutating' | 'generating' | 'deleting'
46
+
47
+ /**
48
+ * Maps a Kind to its behavioural family, collapsing each Namespaced* twin onto
49
+ * its cluster-scoped counterpart. The twins are the same spec with a narrower
50
+ * scope, so they render identically; scope is already conveyed by the
51
+ * namespace column and the resource header.
52
+ */
53
+ export function getKyvernoPolicyFamily(kind: string | undefined): KyvernoPolicyFamily | undefined {
54
+ switch ((kind || '').replace(/^Namespaced/, '')) {
55
+ case 'ValidatingPolicy':
56
+ return 'validating'
57
+ case 'ImageValidatingPolicy':
58
+ return 'imageValidating'
59
+ case 'MutatingPolicy':
60
+ return 'mutating'
61
+ case 'GeneratingPolicy':
62
+ return 'generating'
63
+ case 'DeletingPolicy':
64
+ return 'deleting'
65
+ default:
66
+ return undefined
67
+ }
68
+ }
69
+
70
+ // ============================================================================
71
+ // ENFORCEMENT POSTURE
72
+ // ============================================================================
73
+
74
+ export interface KyvernoEnforcementPosture {
75
+ /** Short label for the badge, e.g. "Deny", "Audit + Warn", "Deletes on schedule". */
76
+ label: string
77
+ level: 'healthy' | 'degraded' | 'alert' | 'unhealthy' | 'neutral' | 'unknown'
78
+ /** True only when a violating admission request is actually rejected. */
79
+ blocks: boolean
80
+ /**
81
+ * One sentence describing what this policy does to matched resources.
82
+ * Per-family, because "violations are recorded" is meaningless for a
83
+ * policy that mutates, generates, or deletes on a schedule.
84
+ */
85
+ summary: string
86
+ /**
87
+ * Set when the effective posture differs from what the declared fields
88
+ * suggest at a glance — the operator needs the reason, not just the verdict.
89
+ */
90
+ note?: string
91
+ }
92
+
93
+ /**
94
+ * Reads `spec.validationActions`, applying Kyverno's real default.
95
+ *
96
+ * An ABSENT validationActions means Deny. This is verified behaviour, not an
97
+ * inference: on Kyverno 1.18.2 a ValidatingPolicy with no validationActions
98
+ * rejected a violating request ("admission webhook vpol.validate.kyverno.svc-fail
99
+ * denied the request"). The legacy family defaults the analogous field to
100
+ * Audit, so carrying that assumption over would under-report the modern
101
+ * family's strictest state — the single worst error this surface could make.
102
+ */
103
+ export function getKyvernoValidationActions(resource: any): string[] {
104
+ const actions = resource?.spec?.validationActions
105
+ if (Array.isArray(actions) && actions.length > 0) {
106
+ return actions.filter((a: unknown): a is string => typeof a === 'string')
107
+ }
108
+ return ['Deny']
109
+ }
110
+
111
+ /** `spec.evaluation.admission.enabled`, defaulting to true per the CRD schema. */
112
+ export function isKyvernoAdmissionEnabled(resource: any): boolean {
113
+ return resource?.spec?.evaluation?.admission?.enabled !== false
114
+ }
115
+
116
+ /**
117
+ * `spec.evaluation.mutateExisting.enabled` (MutatingPolicy only), defaulting
118
+ * to FALSE per the CRD schema — the opposite default from admission/background.
119
+ *
120
+ * Load-bearing for the posture verdict: a MutatingPolicy with admission
121
+ * disabled still rewrites already-existing resources when this is on, so it is
122
+ * emphatically not inactive.
123
+ */
124
+ export function isKyvernoMutateExistingEnabled(resource: any): boolean {
125
+ return resource?.spec?.evaluation?.mutateExisting?.enabled === true
126
+ }
127
+
128
+ /** `spec.evaluation.background.enabled`, defaulting to true per the CRD schema. */
129
+ export function isKyvernoBackgroundEnabled(resource: any): boolean {
130
+ return resource?.spec?.evaluation?.background?.enabled !== false
131
+ }
132
+
133
+ /**
134
+ * `spec.evaluation.mode`, defaulting to "Kubernetes". A policy in "JSON" mode
135
+ * is evaluated against JSON payloads out-of-cluster and never participates in
136
+ * admission, so it cannot block regardless of its declared actions.
137
+ */
138
+ export function getKyvernoEvaluationMode(resource: any): string {
139
+ return resource?.spec?.evaluation?.mode || 'Kubernetes'
140
+ }
141
+
142
+ export function getKyvernoFailurePolicy(resource: any): string {
143
+ return resource?.spec?.failurePolicy || ''
144
+ }
145
+
146
+ /**
147
+ * Computes what the policy actually does, rather than echoing a single field.
148
+ *
149
+ * Three fields decide whether a validating policy blocks, and reading any one
150
+ * alone gets it wrong: `validationActions` (verified above to mean Deny when
151
+ * absent), `evaluation.admission.enabled`, and `evaluation.mode`. Verified on
152
+ * Kyverno 1.18.2: a policy with `validationActions: [Deny]` and
153
+ * `evaluation.admission.enabled: false` did NOT block a violating pod, while
154
+ * the same policy with admission enabled did. Rendering that first policy as a
155
+ * red "Deny" badge would tell an operator their cluster is protected when it
156
+ * is not.
157
+ *
158
+ * `failurePolicy` is deliberately NOT part of the blocks calculation — it
159
+ * governs what happens when the ENGINE errors, not when the policy fails, so
160
+ * it changes the failure mode rather than the enforcement posture.
161
+ */
162
+ export function getKyvernoEnforcementPosture(
163
+ resource: any,
164
+ family: KyvernoPolicyFamily | undefined
165
+ ): KyvernoEnforcementPosture {
166
+ const admissionEnabled = isKyvernoAdmissionEnabled(resource)
167
+ const mode = getKyvernoEvaluationMode(resource)
168
+ const backgroundEnabled = isKyvernoBackgroundEnabled(resource)
169
+ // Only the validating families carry validationActions; asking for them on a
170
+ // mutating/generating policy would read the Deny default off a field that
171
+ // kind never has.
172
+ const validating = family === 'validating' || family === 'imageValidating'
173
+ const declaresBlocking = validating && getKyvernoValidationActions(resource).includes('Deny')
174
+
175
+ if (family === 'deleting') {
176
+ const schedule = getKyvernoSchedule(resource)
177
+ return {
178
+ label: schedule ? `Deletes on schedule (${schedule})` : 'Deletes on schedule',
179
+ level: 'alert',
180
+ blocks: false,
181
+ summary: 'Matched resources are deleted when the schedule fires.',
182
+ note: schedule ? undefined : 'No schedule set — this policy will not run.',
183
+ }
184
+ }
185
+
186
+ if (mode === 'JSON') {
187
+ return {
188
+ label: 'JSON mode',
189
+ level: declaresBlocking ? 'alert' : 'neutral',
190
+ blocks: false,
191
+ summary: 'Evaluated outside the cluster; it does not take part in admission.',
192
+ note: 'Evaluated against JSON payloads outside the cluster; it does not take part in admission.',
193
+ }
194
+ }
195
+
196
+ if (family === 'generating') {
197
+ return {
198
+ label: admissionEnabled ? 'Generates resources' : 'Generates resources (background only)',
199
+ level: 'neutral',
200
+ blocks: false,
201
+ summary: 'Matched resources trigger generation of the resources below.',
202
+ }
203
+ }
204
+
205
+ if (family === 'mutating') {
206
+ const mutatesExisting = isKyvernoMutateExistingEnabled(resource)
207
+ if (admissionEnabled) {
208
+ return {
209
+ label: 'Mutates on admission',
210
+ level: 'degraded',
211
+ blocks: false,
212
+ summary: mutatesExisting
213
+ ? 'Matched resources are modified as they are admitted, and existing ones are rewritten too.'
214
+ : 'Matched resources are modified as they are admitted.',
215
+ }
216
+ }
217
+ // Admission off is NOT the same as inactive: mutateExisting rewrites
218
+ // resources that already exist, independently of the admission path.
219
+ // Calling this policy inactive would tell an operator nothing is being
220
+ // changed while it is actively rewriting their cluster.
221
+ if (mutatesExisting) {
222
+ return {
223
+ label: 'Mutates existing',
224
+ level: 'degraded',
225
+ blocks: false,
226
+ summary: 'Existing matched resources are rewritten by background scans.',
227
+ note: 'Admission evaluation is disabled, so nothing is modified as it is admitted — but mutateExisting is on.',
228
+ }
229
+ }
230
+ return {
231
+ label: 'Inactive',
232
+ level: 'alert',
233
+ blocks: false,
234
+ summary: 'This policy modifies nothing.',
235
+ note: 'Admission evaluation is disabled and mutateExisting is off, so this policy mutates nothing.',
236
+ }
237
+ }
238
+
239
+ // validating + imageValidating
240
+ const actions = getKyvernoValidationActions(resource)
241
+ const declaresDeny = actions.includes('Deny')
242
+
243
+ if (!admissionEnabled) {
244
+ return {
245
+ label: backgroundEnabled ? 'Background only' : 'Inactive',
246
+ // Tone marks the DISCREPANCY, not the severity of the action. A policy
247
+ // declaring Deny that evaluates nothing at admission is a gap between
248
+ // what an operator believes protects the cluster and what does — it must
249
+ // not read as calm. `alert` rather than `degraded` because `degraded` is
250
+ // already the deliberate Warn posture, and "I meant to audit" must not
251
+ // share a tone with "I meant to block and don't". A policy that never
252
+ // evaluates at all is a discrepancy whatever it declared.
253
+ level: !backgroundEnabled ? 'alert' : declaresBlocking ? 'alert' : 'neutral',
254
+ blocks: false,
255
+ summary: backgroundEnabled
256
+ ? 'Violations are recorded by background scans, not blocked.'
257
+ : 'This policy evaluates nothing.',
258
+ note: declaresDeny
259
+ ? backgroundEnabled
260
+ ? 'Declares Deny, but admission evaluation is disabled — violations are reported, never blocked.'
261
+ : 'Declares Deny, but both admission and background evaluation are disabled — this policy does nothing.'
262
+ : backgroundEnabled
263
+ ? 'Admission evaluation is disabled — violations are reported by background scans only.'
264
+ : 'Both admission and background evaluation are disabled — this policy does nothing.',
265
+ }
266
+ }
267
+
268
+ if (declaresDeny) {
269
+ return {
270
+ label: actions.join(' + '),
271
+ level: 'unhealthy',
272
+ blocks: true,
273
+ summary: 'Violating requests are rejected at admission.',
274
+ }
275
+ }
276
+ if (actions.includes('Warn')) {
277
+ return {
278
+ label: actions.join(' + '),
279
+ level: 'degraded',
280
+ blocks: false,
281
+ summary: 'Violations are surfaced as admission warnings, not blocked.',
282
+ }
283
+ }
284
+ return {
285
+ label: actions.join(' + ') || 'Audit',
286
+ level: 'neutral',
287
+ blocks: false,
288
+ summary: 'Violations are recorded in policy reports, not blocked.',
289
+ }
290
+ }
291
+
292
+ // ============================================================================
293
+ // MATCH SCOPE
294
+ // ============================================================================
295
+
296
+ export interface KyvernoResourceRule {
297
+ apiGroups: string[]
298
+ apiVersions: string[]
299
+ resources: string[]
300
+ operations: string[]
301
+ resourceNames: string[]
302
+ scope: string
303
+ }
304
+
305
+ function normalizeRule(raw: any): KyvernoResourceRule {
306
+ const list = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : [])
307
+ return {
308
+ apiGroups: list(raw?.apiGroups),
309
+ apiVersions: list(raw?.apiVersions),
310
+ resources: list(raw?.resources),
311
+ operations: list(raw?.operations),
312
+ resourceNames: list(raw?.resourceNames),
313
+ scope: typeof raw?.scope === 'string' ? raw.scope : '',
314
+ }
315
+ }
316
+
317
+ export function getKyvernoResourceRules(resource: any): KyvernoResourceRule[] {
318
+ const rules = resource?.spec?.matchConstraints?.resourceRules
319
+ return Array.isArray(rules) ? rules.map(normalizeRule) : []
320
+ }
321
+
322
+ export function getKyvernoExcludeResourceRules(resource: any): KyvernoResourceRule[] {
323
+ const rules = resource?.spec?.matchConstraints?.excludeResourceRules
324
+ return Array.isArray(rules) ? rules.map(normalizeRule) : []
325
+ }
326
+
327
+ export function getKyvernoNamespaceSelector(resource: any): any {
328
+ return resource?.spec?.matchConstraints?.namespaceSelector
329
+ }
330
+
331
+ export function getKyvernoObjectSelector(resource: any): any {
332
+ return resource?.spec?.matchConstraints?.objectSelector
333
+ }
334
+
335
+ /**
336
+ * Renders one resourceRule as a kubectl-ish string, e.g.
337
+ * "CREATE, UPDATE apps/v1 deployments". An empty apiGroup is the core group,
338
+ * which reads as "v1" rather than "/v1".
339
+ */
340
+ export function formatKyvernoResourceRule(rule: KyvernoResourceRule): string {
341
+ const groups = rule.apiGroups.length > 0 ? rule.apiGroups : ['']
342
+ const versions = rule.apiVersions.length > 0 ? rule.apiVersions.join(',') : '*'
343
+ const target = groups
344
+ .map((g) => (g === '' || g === '*' ? versions : `${g}/${versions}`))
345
+ .join(', ')
346
+ const resources = rule.resources.length > 0 ? rule.resources.join(', ') : '*'
347
+ const ops = rule.operations.length > 0 ? rule.operations.join(', ') : '*'
348
+ return `${ops} ${target} ${resources}`
349
+ }
350
+
351
+ /** Short "what does this apply to" summary for table columns. */
352
+ export function getKyvernoMatchSummary(resource: any): string {
353
+ const rules = getKyvernoResourceRules(resource)
354
+ if (rules.length === 0) return '-'
355
+ const resources = new Set<string>()
356
+ for (const rule of rules) {
357
+ for (const r of rule.resources) resources.add(r)
358
+ }
359
+ if (resources.size === 0) return '-'
360
+ const list = Array.from(resources)
361
+ return list.length <= 3 ? list.join(', ') : `${list.slice(0, 3).join(', ')} +${list.length - 3}`
362
+ }
363
+
364
+ // ============================================================================
365
+ // CEL EXPRESSIONS + PER-FAMILY SPEC ACCESSORS
366
+ // ============================================================================
367
+
368
+ export interface KyvernoCELValidation {
369
+ expression: string
370
+ message: string
371
+ messageExpression: string
372
+ reason: string
373
+ }
374
+
375
+ export function getKyvernoValidations(resource: any): KyvernoCELValidation[] {
376
+ const validations = resource?.spec?.validations
377
+ if (!Array.isArray(validations)) return []
378
+ return validations.map((v: any) => ({
379
+ expression: v?.expression || '',
380
+ message: v?.message || '',
381
+ messageExpression: v?.messageExpression || '',
382
+ reason: v?.reason || '',
383
+ }))
384
+ }
385
+
386
+ export interface KyvernoNamedExpression {
387
+ name: string
388
+ expression: string
389
+ }
390
+
391
+ function namedExpressions(raw: any): KyvernoNamedExpression[] {
392
+ if (!Array.isArray(raw)) return []
393
+ return raw.map((v: any) => ({ name: v?.name || '', expression: v?.expression || '' }))
394
+ }
395
+
396
+ export function getKyvernoVariables(resource: any): KyvernoNamedExpression[] {
397
+ return namedExpressions(resource?.spec?.variables)
398
+ }
399
+
400
+ export function getKyvernoMatchConditions(resource: any): KyvernoNamedExpression[] {
401
+ return namedExpressions(resource?.spec?.matchConditions)
402
+ }
403
+
404
+ /** DeletingPolicy: `spec.conditions[]` decides which matched objects are deleted. */
405
+ export function getKyvernoDeletionConditions(resource: any): KyvernoNamedExpression[] {
406
+ return namedExpressions(resource?.spec?.conditions)
407
+ }
408
+
409
+ export function getKyvernoSchedule(resource: any): string {
410
+ return resource?.spec?.schedule || ''
411
+ }
412
+
413
+ /**
414
+ * `status.lastExecutionTime` — when the schedule last fired.
415
+ *
416
+ * This, not readiness, is the health signal for a scheduled policy. Kyverno
417
+ * rejects an uncompilable DeletingPolicy at admission, so a broken one never
418
+ * exists to be flagged; the real failure is a policy that exists and never
419
+ * runs. Empty means it has not run since creation.
420
+ */
421
+ export function getKyvernoLastExecutionTime(resource: any): string {
422
+ return resource?.status?.lastExecutionTime || ''
423
+ }
424
+
425
+ export function getKyvernoDeletionPropagationPolicy(resource: any): string {
426
+ return resource?.spec?.deletionPropagationPolicy || ''
427
+ }
428
+
429
+ export function getKyvernoReinvocationPolicy(resource: any): string {
430
+ return resource?.spec?.reinvocationPolicy || ''
431
+ }
432
+
433
+ export interface KyvernoMutation {
434
+ patchType: string
435
+ expression: string
436
+ }
437
+
438
+ export function getKyvernoMutations(resource: any): KyvernoMutation[] {
439
+ const mutations = resource?.spec?.mutations
440
+ if (!Array.isArray(mutations)) return []
441
+ return mutations.map((m: any) => ({
442
+ patchType: m?.patchType || '',
443
+ // ApplyConfiguration and JSONPatch carry their CEL under different keys.
444
+ expression: m?.applyConfiguration?.expression || m?.jsonPatch?.expression || '',
445
+ }))
446
+ }
447
+
448
+ export function getKyvernoGenerations(resource: any): string[] {
449
+ const generate = resource?.spec?.generate
450
+ if (!Array.isArray(generate)) return []
451
+ return generate.map((g: any) => g?.expression || '').filter(Boolean)
452
+ }
453
+
454
+ /**
455
+ * GeneratingPolicy's evaluation block is shaped differently from the
456
+ * validating one: each key is an object with `.enabled` rather than a bare
457
+ * boolean, and the keys are generation-lifecycle concerns.
458
+ */
459
+ export function getKyvernoGenerationSettings(resource: any): {
460
+ generateExisting: boolean
461
+ synchronize: boolean
462
+ orphanDownstreamOnPolicyDelete: boolean
463
+ } {
464
+ const evaluation = resource?.spec?.evaluation || {}
465
+ return {
466
+ generateExisting: evaluation.generateExisting?.enabled === true,
467
+ synchronize: evaluation.synchronize?.enabled === true,
468
+ orphanDownstreamOnPolicyDelete: evaluation.orphanDownstreamOnPolicyDelete?.enabled === true,
469
+ }
470
+ }
471
+
472
+ // ============================================================================
473
+ // IMAGE VALIDATING POLICY (supply chain)
474
+ // ============================================================================
475
+
476
+ export interface KyvernoImageAttestor {
477
+ name: string
478
+ /** "cosign" | "notary" — which verification mechanism this attestor uses. */
479
+ kind: string
480
+ /** Human-readable identity summary: keyless issuer/subject, key ref, or cert. */
481
+ identity: string
482
+ }
483
+
484
+ export function getKyvernoAttestors(resource: any): KyvernoImageAttestor[] {
485
+ const attestors = resource?.spec?.attestors
486
+ if (!Array.isArray(attestors)) return []
487
+ return attestors.map((a: any) => {
488
+ if (a?.cosign) {
489
+ const identities = a.cosign.keyless?.identities
490
+ if (Array.isArray(identities) && identities.length > 0) {
491
+ const summary = identities
492
+ .map((id: any) => {
493
+ const issuer = id?.issuer || id?.issuerRegExp || '*'
494
+ const subject = id?.subject || id?.subjectRegExp || '*'
495
+ return `${subject} via ${issuer}`
496
+ })
497
+ .join('; ')
498
+ return { name: a?.name || '', kind: 'cosign', identity: `keyless: ${summary}` }
499
+ }
500
+ if (a.cosign.key) return { name: a?.name || '', kind: 'cosign', identity: 'public key' }
501
+ if (a.cosign.certificate) return { name: a?.name || '', kind: 'cosign', identity: 'certificate' }
502
+ return { name: a?.name || '', kind: 'cosign', identity: '' }
503
+ }
504
+ if (a?.notary) {
505
+ return { name: a?.name || '', kind: 'notary', identity: a.notary.certs ? 'certificate chain' : '' }
506
+ }
507
+ return { name: a?.name || '', kind: '', identity: '' }
508
+ })
509
+ }
510
+
511
+ export interface KyvernoImageAttestation {
512
+ name: string
513
+ /** "intoto" | "referrer" */
514
+ kind: string
515
+ type: string
516
+ }
517
+
518
+ export function getKyvernoAttestations(resource: any): KyvernoImageAttestation[] {
519
+ const attestations = resource?.spec?.attestations
520
+ if (!Array.isArray(attestations)) return []
521
+ return attestations.map((a: any) => {
522
+ if (a?.intoto) return { name: a?.name || '', kind: 'intoto', type: a.intoto.type || '' }
523
+ if (a?.referrer) return { name: a?.name || '', kind: 'referrer', type: a.referrer.type || '' }
524
+ return { name: a?.name || '', kind: '', type: '' }
525
+ })
526
+ }
527
+
528
+ /** `spec.matchImageReferences[]` — which image refs the policy scopes itself to. */
529
+ export function getKyvernoImageReferences(resource: any): string[] {
530
+ const refs = resource?.spec?.matchImageReferences
531
+ if (!Array.isArray(refs)) return []
532
+ return refs.map((r: any) => r?.glob || r?.expression || '').filter(Boolean)
533
+ }
534
+
535
+ /** `spec.images[]` — CEL expressions extracting image refs from the resource. */
536
+ export function getKyvernoImageExtractors(resource: any): KyvernoNamedExpression[] {
537
+ return namedExpressions(resource?.spec?.images)
538
+ }
539
+
540
+ // ============================================================================
541
+ // STATUS
542
+ // ============================================================================
543
+
544
+ /**
545
+ * Modern policies report readiness via `status.conditionStatus.ready` — the
546
+ * controller sets it false when the CEL fails to compile, which is the single
547
+ * most common "why isn't my policy working" cause.
548
+ */
549
+ export function getKyvernoPolicyReady(resource: any): boolean | undefined {
550
+ const ready = resource?.status?.conditionStatus?.ready
551
+ return typeof ready === 'boolean' ? ready : undefined
552
+ }
553
+
554
+ export function getKyvernoPolicyStatusMessage(resource: any): string {
555
+ return resource?.status?.conditionStatus?.message || ''
556
+ }
557
+
558
+ export function getKyvernoPolicyConditions(resource: any): any[] {
559
+ const conditions = resource?.status?.conditionStatus?.conditions
560
+ return Array.isArray(conditions) ? conditions : []
561
+ }
562
+
563
+ /**
564
+ * Table/drawer status badge. Readiness outranks posture: a policy that failed
565
+ * to compile enforces nothing, so surfacing its declared "Deny" would be
566
+ * actively misleading.
567
+ *
568
+ * This is why the eight admission-shaped kinds have no separate Ready column:
569
+ * readiness is not missing from their tables, it REPLACES the posture in this
570
+ * one cell. Upstream's own printer columns list READY separately, so the
571
+ * absence looks like an omission at a glance — it isn't. Adding a second Ready
572
+ * column beside Enforcement would show one irrelevant value on every row,
573
+ * because the two states are mutually exclusive by construction here.
574
+ *
575
+ * The Deleting kinds are the exception and DO carry their own `ready` column
576
+ * (see KNOWN_COLUMNS): their headline column is the cron schedule rather than
577
+ * a posture, so nothing else in the row would ever surface a failed compile.
578
+ */
579
+ export function getModernKyvernoPolicyStatus(resource: any): StatusBadge {
580
+ const ready = getKyvernoPolicyReady(resource)
581
+ if (ready === false) {
582
+ return { text: 'Not Ready', color: healthColors.unhealthy, level: 'unhealthy' }
583
+ }
584
+
585
+ const family = getKyvernoPolicyFamily(resource?.kind)
586
+ const posture = getKyvernoEnforcementPosture(resource, family)
587
+ return { text: posture.label, color: healthColors[posture.level], level: posture.level }
588
+ }