@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.
- package/package.json +5 -5
- package/src/components/applications/ApplicationDetail.test.tsx +46 -0
- package/src/components/applications/ApplicationDetail.tsx +14 -15
- package/src/components/issues/IssuesView.tsx +9 -0
- package/src/components/issues/diagnostic.ts +3 -0
- package/src/components/issues/issues.test.ts +11 -0
- package/src/components/issues/severity.ts +3 -0
- package/src/components/issues/types.ts +3 -0
- package/src/components/resources/ResourcesSidebar.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +259 -30
- package/src/components/resources/index.ts +2 -0
- package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
- package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
- package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/EventRenderer.tsx +10 -18
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
- package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
- package/src/components/resources/renderers/velero-cells.tsx +128 -9
- package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
- package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
- package/src/components/resources/resource-utils-cnpg.ts +373 -56
- package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
- package/src/components/resources/resource-utils-velero.test.ts +237 -0
- package/src/components/resources/resource-utils-velero.ts +214 -22
- package/src/components/resources/resource-utils.ts +70 -3
- package/src/components/shared/ResourceActionsBar.tsx +3 -1
- package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
- package/src/components/topology/K8sResourceNode.tsx +11 -0
- package/src/components/topology/TopologyGraph.tsx +85 -5
- package/src/components/trace/ReachabilityGraph.tsx +517 -0
- package/src/components/trace/ReachabilityView.tsx +1173 -0
- package/src/components/trace/TracePanel.test.ts +172 -0
- package/src/components/trace/TracePanel.tsx +558 -0
- package/src/components/trace/TraceSummary.test.ts +213 -0
- package/src/components/trace/TraceSummary.tsx +168 -0
- package/src/components/trace/inClusterSummary.test.ts +98 -0
- package/src/components/trace/inClusterSummary.ts +81 -0
- package/src/components/trace/index.ts +19 -0
- package/src/components/trace/podReach.test.ts +71 -0
- package/src/components/trace/podReach.ts +37 -0
- package/src/components/trace/probe-display.test.ts +107 -0
- package/src/components/trace/probe-display.ts +37 -0
- package/src/components/trace/problemRows.test.ts +107 -0
- package/src/components/trace/reachFixtures.ts +261 -0
- package/src/components/trace/reachGraphModel.test.ts +1423 -0
- package/src/components/trace/reachGraphModel.ts +1521 -0
- package/src/components/trace/reachInspector.test.ts +892 -0
- package/src/components/trace/reachInspector.ts +797 -0
- package/src/components/trace/reachMarks.test.ts +698 -0
- package/src/components/trace/reachMarks.ts +623 -0
- package/src/components/trace/reachOrigins.test.ts +392 -0
- package/src/components/trace/reachOrigins.ts +449 -0
- package/src/components/trace/reachVerdict.test.ts +537 -0
- package/src/components/trace/reachVerdict.ts +601 -0
- package/src/components/trace/traceFingerprint.test.ts +214 -0
- package/src/components/trace/traceFingerprint.ts +122 -0
- package/src/components/trace/traceToSubgraph.test.ts +623 -0
- package/src/components/trace/traceToSubgraph.ts +463 -0
- package/src/components/trace/types.ts +414 -0
- package/src/components/ui/Badge.tsx +36 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
- package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
- package/src/components/ui/InClusterConsentDialog.tsx +141 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +58 -5
- package/src/components/workload/index.ts +1 -1
- package/src/components/workload/reachabilityTab.test.ts +47 -0
- package/src/index.ts +5 -0
- package/src/theme/components.css +64 -0
- package/src/theme/variables.css +5 -0
- package/src/types/core.ts +8 -0
- package/src/utils/api-resources.ts +2 -0
- package/src/utils/application-history.test.ts +1 -1
- package/src/utils/applications.test.ts +63 -1
- package/src/utils/applications.ts +27 -0
- package/src/utils/inClusterConsent.test.ts +109 -0
- package/src/utils/inClusterConsent.ts +72 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +61 -0
- package/src/utils/navigation.ts +11 -9
- package/src/utils/pluralize.test.ts +80 -1
- package/src/utils/pluralize.ts +34 -0
- package/src/utils/resource-icons.test.ts +33 -0
- 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
|
+
}
|