@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.
Files changed (39) hide show
  1. package/package.json +2 -1
  2. package/src/components/applications/ApplicationsList.tsx +131 -115
  3. package/src/components/applications/index.ts +3 -1
  4. package/src/components/audit/AuditCard.tsx +1 -1
  5. package/src/components/charts/AreaChart.tsx +52 -18
  6. package/src/components/charts/MetricsSummary.tsx +12 -3
  7. package/src/components/charts/saturation.test.ts +34 -0
  8. package/src/components/charts/saturation.ts +1 -0
  9. package/src/components/charts/types.ts +5 -4
  10. package/src/components/checks/ChecksView.tsx +12 -23
  11. package/src/components/checks/severity.ts +11 -11
  12. package/src/components/gitops/GitOpsTableView.tsx +298 -274
  13. package/src/components/issues/severity.ts +19 -11
  14. package/src/components/resources/ResourcesSidebar.tsx +3 -1
  15. package/src/components/resources/ResourcesView.tsx +13 -0
  16. package/src/components/resources/index.ts +1 -0
  17. package/src/components/resources/renderers/HPARenderer.test.tsx +91 -0
  18. package/src/components/resources/renderers/HPARenderer.tsx +93 -86
  19. package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
  20. package/src/components/resources/renderers/WorkloadRenderer.tsx +73 -4
  21. package/src/components/resources/resource-utils-hpa.test.ts +30 -0
  22. package/src/components/resources/resource-utils-hpa.ts +106 -0
  23. package/src/components/resources/resource-utils.ts +2 -10
  24. package/src/components/shared/ResourceRendererDispatch.tsx +10 -2
  25. package/src/components/ui/DistributionBar.tsx +88 -0
  26. package/src/components/ui/Facet.tsx +130 -0
  27. package/src/components/ui/PageHeader.tsx +43 -0
  28. package/src/components/ui/SearchPillInput.tsx +237 -0
  29. package/src/components/ui/SortableTh.tsx +58 -0
  30. package/src/components/ui/SummaryTile.tsx +60 -0
  31. package/src/components/ui/drawer-components.tsx +56 -17
  32. package/src/components/ui/index.ts +12 -0
  33. package/src/components/workload/WorkloadView.tsx +18 -1
  34. package/src/theme/tailwind-theme.css +1 -0
  35. package/src/theme/variables.css +7 -3
  36. package/src/types/core.ts +54 -2
  37. package/src/utils/rbac-blast-radius.test.ts +48 -3
  38. package/src/utils/rbac-blast-radius.ts +14 -4
  39. 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', 'audit', 'gitops'.
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' | 'audit' | 'gitops' | 'issues'
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 verb wildcards', () => {
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
- rules: [{ verbs: ['*'], resources: ['pods'], apiGroups: [''] }],
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: ['pods'], apiGroups: [''] },
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. The current set is the calibrated minimum
57
- * for "the Pod's identity is genuinely god-mode".
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
- if (verbs.includes('*')) {
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
  }
package/tsconfig.json CHANGED
@@ -9,6 +9,7 @@
9
9
  "isolatedModules": true,
10
10
  "noEmit": true,
11
11
  "jsx": "react-jsx",
12
+ "types": ["node"],
12
13
  "strict": true,
13
14
  "noUnusedLocals": true,
14
15
  "noUnusedParameters": true,