@skyhook-io/k8s-ui 1.7.15 → 1.8.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 +2 -1
- package/src/components/applications/ApplicationsList.tsx +131 -115
- package/src/components/applications/index.ts +3 -1
- package/src/components/audit/AuditCard.tsx +1 -1
- package/src/components/charts/AreaChart.tsx +52 -18
- package/src/components/charts/MetricsSummary.tsx +12 -3
- package/src/components/charts/saturation.test.ts +34 -0
- package/src/components/charts/saturation.ts +1 -0
- package/src/components/charts/types.ts +5 -4
- package/src/components/checks/ChecksView.tsx +12 -23
- package/src/components/checks/severity.ts +11 -11
- package/src/components/gitops/GitOpsTableView.tsx +298 -274
- package/src/components/issues/severity.ts +19 -11
- package/src/components/resources/ResourcesSidebar.tsx +3 -1
- package/src/components/resources/ResourcesView.tsx +13 -0
- package/src/components/resources/index.ts +1 -0
- package/src/components/resources/renderers/HPARenderer.test.tsx +91 -0
- package/src/components/resources/renderers/HPARenderer.tsx +93 -86
- package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +73 -4
- package/src/components/resources/resource-utils-hpa.test.ts +30 -0
- package/src/components/resources/resource-utils-hpa.ts +106 -0
- package/src/components/resources/resource-utils.ts +2 -10
- package/src/components/shared/ResourceRendererDispatch.tsx +10 -2
- package/src/components/ui/DistributionBar.tsx +88 -0
- package/src/components/ui/Facet.tsx +130 -0
- package/src/components/ui/PageHeader.tsx +43 -0
- package/src/components/ui/SearchPillInput.tsx +237 -0
- package/src/components/ui/SortableTh.tsx +58 -0
- package/src/components/ui/SummaryTile.tsx +60 -0
- package/src/components/ui/drawer-components.tsx +56 -17
- package/src/components/ui/index.ts +12 -0
- package/src/components/workload/WorkloadView.tsx +18 -1
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +7 -3
- package/src/types/core.ts +54 -2
- package/src/utils/rbac-blast-radius.test.ts +48 -3
- package/src/utils/rbac-blast-radius.ts +14 -4
- package/tsconfig.json +1 -0
package/src/types/core.ts
CHANGED
|
@@ -472,11 +472,63 @@ export interface SecretCertificateInfo {
|
|
|
472
472
|
certificates: CertificateInfo[]
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
export type HPADiagnosisState =
|
|
476
|
+
| 'ok'
|
|
477
|
+
| 'scaling_up'
|
|
478
|
+
| 'scaling_down'
|
|
479
|
+
| 'limited_max'
|
|
480
|
+
| 'limited_min'
|
|
481
|
+
| 'metrics_unavailable'
|
|
482
|
+
| 'metrics_incomplete'
|
|
483
|
+
| 'unable_to_scale'
|
|
484
|
+
| 'disabled'
|
|
485
|
+
| 'pinned'
|
|
486
|
+
| 'stale'
|
|
487
|
+
| 'stabilized'
|
|
488
|
+
| 'unknown'
|
|
489
|
+
|
|
490
|
+
export interface HPADiagnosis {
|
|
491
|
+
state: HPADiagnosisState
|
|
492
|
+
summary: string
|
|
493
|
+
target: {
|
|
494
|
+
apiVersion?: string
|
|
495
|
+
kind?: string
|
|
496
|
+
name?: string
|
|
497
|
+
}
|
|
498
|
+
bounds: {
|
|
499
|
+
min: number
|
|
500
|
+
max: number
|
|
501
|
+
current: number
|
|
502
|
+
desired: number
|
|
503
|
+
observedGeneration?: number
|
|
504
|
+
generation?: number
|
|
505
|
+
}
|
|
506
|
+
metrics?: HPAMetricSummary[]
|
|
507
|
+
reasons?: HPAReasonSummary[]
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export interface HPAReasonSummary {
|
|
511
|
+
id: string
|
|
512
|
+
message: string
|
|
513
|
+
detail?: string
|
|
514
|
+
conditionType?: string
|
|
515
|
+
conditionReason?: string
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export interface HPAMetricSummary {
|
|
519
|
+
type: string
|
|
520
|
+
name: string
|
|
521
|
+
current?: string
|
|
522
|
+
target?: string
|
|
523
|
+
status: string
|
|
524
|
+
}
|
|
525
|
+
|
|
475
526
|
// Resource with computed relationships and optional certificate info (API response wrapper)
|
|
476
527
|
export interface ResourceWithRelationships<T = unknown> {
|
|
477
528
|
resource: T
|
|
478
529
|
relationships?: Relationships
|
|
479
530
|
certificateInfo?: SecretCertificateInfo
|
|
531
|
+
hpaDiagnosis?: HPADiagnosis
|
|
480
532
|
}
|
|
481
533
|
|
|
482
534
|
// API Resource (from discovery endpoint)
|
|
@@ -947,11 +999,11 @@ export interface TrafficFilters {
|
|
|
947
999
|
timeRange: string
|
|
948
1000
|
}
|
|
949
1001
|
|
|
950
|
-
// Main view type now includes 'traffic', 'cost', '
|
|
1002
|
+
// Main view type now includes 'traffic', 'cost', 'checks', 'gitops'.
|
|
951
1003
|
// Library consumers (Radar Hub) get all GitOps surfaces — the package
|
|
952
1004
|
// IS the public surface, so adding new top-level views must extend
|
|
953
1005
|
// this type rather than rely on app-local extensions.
|
|
954
|
-
export type ExtendedMainView = MainView | 'traffic' | 'cost' | '
|
|
1006
|
+
export type ExtendedMainView = MainView | 'traffic' | 'cost' | 'checks' | 'gitops' | 'issues' | 'applications'
|
|
955
1007
|
|
|
956
1008
|
// ============================================================================
|
|
957
1009
|
// Image Filesystem Types
|
|
@@ -35,15 +35,59 @@ describe('detectBlastRadius', () => {
|
|
|
35
35
|
expect(reasons[0].reason).toMatch(/cluster-admin/)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
it('flags
|
|
38
|
+
it('flags an admin-shaped grant via ClusterRoleBinding', () => {
|
|
39
|
+
// apiGroups/resources/verbs all `*` bound cluster-wide is god-mode.
|
|
39
40
|
const b = binding({
|
|
40
|
-
|
|
41
|
+
binding: { kind: 'ClusterRoleBinding', namespace: '', name: 'admin-crb', roleRef: { kind: 'ClusterRole', namespace: '', name: 'admin' } },
|
|
42
|
+
rules: [{ verbs: ['*'], resources: ['*'], apiGroups: ['*'] }],
|
|
41
43
|
})
|
|
42
44
|
const reasons = detectBlastRadius(subject([b]))
|
|
43
45
|
expect(reasons).toHaveLength(1)
|
|
44
46
|
expect(reasons[0].reason).toMatch(/verb wildcard/)
|
|
45
47
|
})
|
|
46
48
|
|
|
49
|
+
it('does NOT flag the same admin-shaped grant via a namespace RoleBinding', () => {
|
|
50
|
+
// A RoleBinding is namespace-scoped by apiserver construction — even
|
|
51
|
+
// `*/*/*` is namespace-admin, not cluster takeover, so it stays collapsed.
|
|
52
|
+
const b = binding({
|
|
53
|
+
rules: [{ verbs: ['*'], resources: ['*'], apiGroups: ['*'] }],
|
|
54
|
+
})
|
|
55
|
+
expect(detectBlastRadius(subject([b]))).toHaveLength(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('flags verb wildcards on a cluster-wide binding (incl. cluster-wide secrets)', () => {
|
|
59
|
+
// Cluster-wide `verbs:['*']` on secrets is read-every-secret takeover and
|
|
60
|
+
// MUST stay loud even though the resource is named, not `*`.
|
|
61
|
+
const b = binding({
|
|
62
|
+
binding: { kind: 'ClusterRoleBinding', namespace: '', name: 'cluster-b', roleRef: { kind: 'ClusterRole', namespace: '', name: 'r' } },
|
|
63
|
+
rules: [{ verbs: ['*'], resources: ['secrets'], apiGroups: [''] }],
|
|
64
|
+
})
|
|
65
|
+
const reasons = detectBlastRadius(subject([b]))
|
|
66
|
+
expect(reasons).toHaveLength(1)
|
|
67
|
+
expect(reasons[0].reason).toMatch(/verb wildcard/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("does NOT flag a verb wildcard scoped to one operator's own CRDs", () => {
|
|
71
|
+
// `verbs:['*']` confined to a specific apiGroup+resources in a single
|
|
72
|
+
// namespace (RoleBinding) is the routine operator-owns-its-CRDs grant —
|
|
73
|
+
// expanding the Permissions section on every such SA is the over-fire
|
|
74
|
+
// this exclusion fixes.
|
|
75
|
+
const b = binding({
|
|
76
|
+
rules: [{ verbs: ['*'], resources: ['widgets'], apiGroups: ['acme.example.com'] }],
|
|
77
|
+
})
|
|
78
|
+
expect(detectBlastRadius(subject([b]))).toHaveLength(0)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('does NOT flag a namespace RoleBinding with resource-wildcard verb-wildcard over one group', () => {
|
|
82
|
+
// `verbs:['*']` over `resources:['*']` in one apiGroup, bound via a
|
|
83
|
+
// namespace RoleBinding, is namespace-scoped — broad within the namespace
|
|
84
|
+
// but not cluster takeover. Common operator shape; stays collapsed.
|
|
85
|
+
const b = binding({
|
|
86
|
+
rules: [{ verbs: ['*'], resources: ['*'], apiGroups: ['acme.example.com'] }],
|
|
87
|
+
})
|
|
88
|
+
expect(detectBlastRadius(subject([b]))).toHaveLength(0)
|
|
89
|
+
})
|
|
90
|
+
|
|
47
91
|
it('flags escalation verbs', () => {
|
|
48
92
|
const b = binding({
|
|
49
93
|
rules: [{ verbs: ['escalate'], resources: ['roles'], apiGroups: ['rbac.authorization.k8s.io'] }],
|
|
@@ -89,8 +133,9 @@ describe('detectBlastRadius', () => {
|
|
|
89
133
|
|
|
90
134
|
it('dedupes by binding identity when multiple risky rules match', () => {
|
|
91
135
|
const b = binding({
|
|
136
|
+
binding: { kind: 'ClusterRoleBinding', namespace: '', name: 'cluster-b', roleRef: { kind: 'ClusterRole', namespace: '', name: 'r' } },
|
|
92
137
|
rules: [
|
|
93
|
-
{ verbs: ['*'], resources: ['
|
|
138
|
+
{ verbs: ['*'], resources: ['secrets'], apiGroups: [''] },
|
|
94
139
|
{ verbs: ['escalate'], resources: ['roles'], apiGroups: ['rbac.authorization.k8s.io'] },
|
|
95
140
|
],
|
|
96
141
|
})
|
|
@@ -47,14 +47,19 @@ export interface BlastRadiusReason {
|
|
|
47
47
|
*
|
|
48
48
|
* 1. cluster-admin role-name match (covers the "bound my SA to cluster-admin
|
|
49
49
|
* for testing" pattern even when rules aren't loaded)
|
|
50
|
-
* 2. verb wildcards (`*`)
|
|
50
|
+
* 2. verb wildcards (`*`) granted by a cluster-wide binding (ClusterRoleBinding)
|
|
51
51
|
* 3. escalation verbs (`escalate` / `bind` / `impersonate`)
|
|
52
52
|
* 4. cluster-wide `create pods` (lateral-movement vector)
|
|
53
53
|
*
|
|
54
54
|
* Resource-only wildcards (e.g. `view`'s fluxcd CRD coverage) deliberately
|
|
55
55
|
* do NOT trigger — they fire on every authenticated SA and would train
|
|
56
|
-
* operators to ignore the alarm.
|
|
57
|
-
*
|
|
56
|
+
* operators to ignore the alarm. A verb wildcard granted by a RoleBinding is
|
|
57
|
+
* likewise excluded — a RoleBinding is namespace-scoped by apiserver
|
|
58
|
+
* construction, so even `verbs:['*']` over `resources:['*']` across every
|
|
59
|
+
* apiGroup (namespace-admin) is not cluster takeover, and operators routinely
|
|
60
|
+
* hold broad wildcards over their own CRDs. Only a cluster-wide binding counts
|
|
61
|
+
* as genuine god-mode. The current set is the calibrated minimum for "the
|
|
62
|
+
* Pod's identity is genuinely god-mode".
|
|
58
63
|
*
|
|
59
64
|
* Dedupes by binding identity so a binding with multiple risky rules
|
|
60
65
|
* counts once; first matching reason wins.
|
|
@@ -73,7 +78,12 @@ export function detectBlastRadius(rbacData: RBACSubjectResponse): BlastRadiusRea
|
|
|
73
78
|
for (const r of br.rules ?? []) {
|
|
74
79
|
const verbs = r.verbs ?? []
|
|
75
80
|
const resources = r.resources ?? []
|
|
76
|
-
|
|
81
|
+
// A verb wildcard is only cluster takeover when the binding is itself
|
|
82
|
+
// cluster-wide. A RoleBinding is namespace-scoped by apiserver
|
|
83
|
+
// construction no matter how broad its rule (apiGroups/resources `*`
|
|
84
|
+
// included), so namespace-admin SAs stay collapsed; only
|
|
85
|
+
// ClusterRoleBindings count.
|
|
86
|
+
if (verbs.includes('*') && br.binding.kind === 'ClusterRoleBinding') {
|
|
77
87
|
reasons.push({ binding: br, reason: 'grants verb wildcard (*) — every action on the listed resources' })
|
|
78
88
|
break
|
|
79
89
|
}
|