@skyhook-io/k8s-ui 1.3.2 → 1.4.1
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 -2
- package/src/components/audit/AuditAlerts.tsx +88 -0
- package/src/components/audit/AuditCard.tsx +130 -0
- package/src/components/audit/AuditFindingsTable.tsx +526 -0
- package/src/components/audit/index.ts +3 -0
- package/src/components/dock/DockContext.tsx +5 -2
- package/src/components/dock/LocalTerminalTab.tsx +11 -0
- package/src/components/resources/ResourcesView.tsx +126 -49
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
- package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +445 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +160 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +17 -57
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/PodRenderer.tsx +22 -74
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
- package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
- package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
- package/src/components/resources/resource-utils-cnpg.ts +47 -0
- package/src/components/resources/resource-utils-karpenter.ts +6 -0
- package/src/components/resources/resource-utils-prometheus.ts +50 -6
- package/src/components/resources/resource-utils.ts +131 -33
- package/src/components/shared/CreateResourceDialog.tsx +236 -0
- package/src/components/shared/EditableYamlView.tsx +16 -1
- package/src/components/shared/ResourceActionsBar.tsx +9 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -6
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +11 -4
- package/src/components/topology/K8sResourceNode.tsx +15 -0
- package/src/components/topology/TopologyControls.tsx +21 -1
- package/src/components/topology/TopologyGraph.tsx +1 -1
- package/src/components/ui/Badge.tsx +8 -0
- package/src/components/ui/drawer-components.tsx +68 -5
- package/src/components/workload/WorkloadView.tsx +55 -29
- package/src/index.ts +3 -0
- package/src/theme/variables.css +1 -1
- package/src/types/core.ts +2 -1
- package/src/types/gitops.ts +8 -0
- package/src/utils/badge-colors.ts +7 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/k8s-errors.ts +142 -0
- package/src/utils/navigation.test.ts +29 -2
- package/src/utils/navigation.ts +45 -24
- package/src/utils/resource-icons.ts +3 -0
- package/src/utils/skeleton-yaml.ts +302 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { Shield, ArrowDownToLine, ArrowUpFromLine, Ban, Lock } from 'lucide-react'
|
|
2
|
+
import { Section, LabelSelectorDisplay } from '../../ui/drawer-components'
|
|
3
|
+
|
|
4
|
+
interface CiliumNetworkPolicyRendererProps {
|
|
5
|
+
data: any
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function CiliumNetworkPolicyRenderer({ data }: CiliumNetworkPolicyRendererProps) {
|
|
9
|
+
const spec = data.spec || data.specs || {}
|
|
10
|
+
const endpointSelector = spec.endpointSelector || {}
|
|
11
|
+
const ingress: any[] | undefined = spec.ingress
|
|
12
|
+
const ingressDeny: any[] | undefined = spec.ingressDeny
|
|
13
|
+
const egress: any[] | undefined = spec.egress
|
|
14
|
+
const egressDeny: any[] | undefined = spec.egressDeny
|
|
15
|
+
const description = spec.description || ''
|
|
16
|
+
const enableDefaultDeny = spec.enableDefaultDeny
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<Section title="Target" icon={Shield}>
|
|
21
|
+
{description && (
|
|
22
|
+
<p className="text-xs text-theme-text-secondary mb-2">{description}</p>
|
|
23
|
+
)}
|
|
24
|
+
<div className="mt-2">
|
|
25
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Endpoint Selector</div>
|
|
26
|
+
<LabelSelectorDisplay selector={endpointSelector} emptyText="All endpoints in namespace" />
|
|
27
|
+
</div>
|
|
28
|
+
{spec.nodeSelector && (
|
|
29
|
+
<div className="mt-2">
|
|
30
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Node Selector</div>
|
|
31
|
+
<LabelSelectorDisplay selector={spec.nodeSelector} emptyText="All nodes" />
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
{enableDefaultDeny && (
|
|
35
|
+
<div className="mt-2">
|
|
36
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Default Deny</div>
|
|
37
|
+
<div className="flex flex-wrap gap-1">
|
|
38
|
+
{enableDefaultDeny.ingress !== undefined && (
|
|
39
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
40
|
+
Ingress: {enableDefaultDeny.ingress ? 'Yes' : 'No'}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
{enableDefaultDeny.egress !== undefined && (
|
|
44
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
45
|
+
Egress: {enableDefaultDeny.egress ? 'Yes' : 'No'}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
</Section>
|
|
52
|
+
|
|
53
|
+
{ingress && ingress.length > 0 && (
|
|
54
|
+
<Section title="Ingress Allow" icon={ArrowDownToLine} defaultExpanded>
|
|
55
|
+
<div className="space-y-3">
|
|
56
|
+
{ingress.map((rule: any, i: number) => (
|
|
57
|
+
<CiliumRuleCard key={i} rule={rule} />
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</Section>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{ingressDeny && ingressDeny.length > 0 && (
|
|
64
|
+
<Section title="Ingress Deny" icon={Ban} defaultExpanded>
|
|
65
|
+
<div className="space-y-3">
|
|
66
|
+
{ingressDeny.map((rule: any, i: number) => (
|
|
67
|
+
<CiliumRuleCard key={i} rule={rule} />
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</Section>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{egress && egress.length > 0 && (
|
|
74
|
+
<Section title="Egress Allow" icon={ArrowUpFromLine} defaultExpanded>
|
|
75
|
+
<div className="space-y-3">
|
|
76
|
+
{egress.map((rule: any, i: number) => (
|
|
77
|
+
<CiliumRuleCard key={i} rule={rule} />
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</Section>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{egressDeny && egressDeny.length > 0 && (
|
|
84
|
+
<Section title="Egress Deny" icon={Ban} defaultExpanded>
|
|
85
|
+
<div className="space-y-3">
|
|
86
|
+
{egressDeny.map((rule: any, i: number) => (
|
|
87
|
+
<CiliumRuleCard key={i} rule={rule} />
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
</Section>
|
|
91
|
+
)}
|
|
92
|
+
</>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function CiliumRuleCard({ rule }: { rule: any }) {
|
|
97
|
+
const fromEndpoints: any[] = rule.fromEndpoints || []
|
|
98
|
+
const fromEntities: string[] = rule.fromEntities || []
|
|
99
|
+
const fromCIDR: string[] = rule.fromCIDR || []
|
|
100
|
+
const fromCIDRSet: any[] = rule.fromCIDRSet || []
|
|
101
|
+
const fromFQDNs: any[] = rule.fromFQDNs || []
|
|
102
|
+
const fromNodes: any[] = rule.fromNodes || []
|
|
103
|
+
const fromGroups: any[] = rule.fromGroups || []
|
|
104
|
+
const toEndpoints: any[] = rule.toEndpoints || []
|
|
105
|
+
const toEntities: string[] = rule.toEntities || []
|
|
106
|
+
const toCIDR: string[] = rule.toCIDR || []
|
|
107
|
+
const toCIDRSet: any[] = rule.toCIDRSet || []
|
|
108
|
+
const toFQDNs: any[] = rule.toFQDNs || []
|
|
109
|
+
const toServices: any[] = rule.toServices || []
|
|
110
|
+
const toNodes: any[] = rule.toNodes || []
|
|
111
|
+
const toGroups: any[] = rule.toGroups || []
|
|
112
|
+
const toPorts: any[] = rule.toPorts || []
|
|
113
|
+
const icmps: any[] = rule.icmps || []
|
|
114
|
+
const authentication = rule.authentication
|
|
115
|
+
|
|
116
|
+
const hasFrom = fromEndpoints.length > 0 || fromEntities.length > 0 || fromCIDR.length > 0 ||
|
|
117
|
+
fromCIDRSet.length > 0 || fromFQDNs.length > 0 || fromNodes.length > 0 || fromGroups.length > 0
|
|
118
|
+
const hasTo = toEndpoints.length > 0 || toEntities.length > 0 || toCIDR.length > 0 ||
|
|
119
|
+
toCIDRSet.length > 0 || toFQDNs.length > 0 || toServices.length > 0 || toNodes.length > 0 || toGroups.length > 0
|
|
120
|
+
const hasRules = hasFrom || hasTo || toPorts.length > 0 || icmps.length > 0
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="card-inner-lg">
|
|
124
|
+
{authentication && (
|
|
125
|
+
<div className="mb-2 flex items-center gap-1.5">
|
|
126
|
+
<Lock className="h-3 w-3 text-yellow-400" />
|
|
127
|
+
<span className="text-xs text-yellow-400">Authentication: {authentication.mode || 'required'}</span>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{fromEndpoints.length > 0 && (
|
|
132
|
+
<div className="mb-2">
|
|
133
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From Endpoints</div>
|
|
134
|
+
<div className="space-y-1">
|
|
135
|
+
{fromEndpoints.map((ep: any, j: number) => (
|
|
136
|
+
<LabelSelectorDisplay key={j} selector={ep} emptyText="all endpoints" inline />
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{fromEntities.length > 0 && (
|
|
143
|
+
<div className="mb-2">
|
|
144
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From Entities</div>
|
|
145
|
+
<div className="flex flex-wrap gap-1">
|
|
146
|
+
{fromEntities.map((e, j) => (
|
|
147
|
+
<span key={j} className="badge bg-purple-500/20 text-purple-400 border-purple-500/30">{e}</span>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{(fromCIDR.length > 0 || fromCIDRSet.length > 0) && (
|
|
154
|
+
<div className="mb-2">
|
|
155
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From CIDR</div>
|
|
156
|
+
<div className="flex flex-wrap gap-1">
|
|
157
|
+
{fromCIDR.map((c, j) => (
|
|
158
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">{c}</span>
|
|
159
|
+
))}
|
|
160
|
+
{fromCIDRSet.map((cs: any, j: number) => (
|
|
161
|
+
<span key={`set-${j}`} className="badge bg-theme-elevated text-theme-text-secondary">{cs.cidr}</span>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{fromFQDNs.length > 0 && (
|
|
168
|
+
<div className="mb-2">
|
|
169
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From FQDNs</div>
|
|
170
|
+
<div className="flex flex-wrap gap-1">
|
|
171
|
+
{fromFQDNs.map((fqdn: any, j: number) => (
|
|
172
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
173
|
+
{fqdn.matchName || fqdn.matchPattern || JSON.stringify(fqdn)}
|
|
174
|
+
</span>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{fromNodes.length > 0 && (
|
|
181
|
+
<div className="mb-2">
|
|
182
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From Nodes</div>
|
|
183
|
+
<div className="space-y-1">
|
|
184
|
+
{fromNodes.map((node: any, j: number) => (
|
|
185
|
+
<LabelSelectorDisplay key={j} selector={node} />
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{fromGroups.length > 0 && (
|
|
192
|
+
<div className="mb-2">
|
|
193
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From Groups</div>
|
|
194
|
+
<div className="flex flex-wrap gap-1">
|
|
195
|
+
{fromGroups.map((g: any, j: number) => (
|
|
196
|
+
<CloudGroupEntry key={j} group={g} />
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{toEndpoints.length > 0 && (
|
|
203
|
+
<div className="mb-2">
|
|
204
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Endpoints</div>
|
|
205
|
+
<div className="space-y-1">
|
|
206
|
+
{toEndpoints.map((ep: any, j: number) => (
|
|
207
|
+
<LabelSelectorDisplay key={j} selector={ep} emptyText="all endpoints" inline />
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
{toEntities.length > 0 && (
|
|
214
|
+
<div className="mb-2">
|
|
215
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Entities</div>
|
|
216
|
+
<div className="flex flex-wrap gap-1">
|
|
217
|
+
{toEntities.map((e, j) => (
|
|
218
|
+
<span key={j} className="badge bg-purple-500/20 text-purple-400 border-purple-500/30">{e}</span>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{(toCIDR.length > 0 || toCIDRSet.length > 0) && (
|
|
225
|
+
<div className="mb-2">
|
|
226
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To CIDR</div>
|
|
227
|
+
<div className="flex flex-wrap gap-1">
|
|
228
|
+
{toCIDR.map((c, j) => (
|
|
229
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">{c}</span>
|
|
230
|
+
))}
|
|
231
|
+
{toCIDRSet.map((cs: any, j: number) => (
|
|
232
|
+
<span key={`set-${j}`} className="badge bg-theme-elevated text-theme-text-secondary">{cs.cidr}</span>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{toFQDNs.length > 0 && (
|
|
239
|
+
<div className="mb-2">
|
|
240
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To FQDNs</div>
|
|
241
|
+
<div className="flex flex-wrap gap-1">
|
|
242
|
+
{toFQDNs.map((fqdn: any, j: number) => (
|
|
243
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
244
|
+
{fqdn.matchName || fqdn.matchPattern || JSON.stringify(fqdn)}
|
|
245
|
+
</span>
|
|
246
|
+
))}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{toServices.length > 0 && (
|
|
252
|
+
<div className="mb-2">
|
|
253
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Services</div>
|
|
254
|
+
<div className="flex flex-wrap gap-1">
|
|
255
|
+
{toServices.map((svc: any, j: number) => {
|
|
256
|
+
const k8s = svc.k8sService
|
|
257
|
+
const sel = svc.k8sServiceSelector
|
|
258
|
+
if (k8s) {
|
|
259
|
+
const ns = k8s.namespace ? `${k8s.namespace}/` : ''
|
|
260
|
+
return (
|
|
261
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
262
|
+
{ns}{k8s.serviceName}
|
|
263
|
+
</span>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
if (sel) {
|
|
267
|
+
return <LabelSelectorDisplay key={j} selector={sel.selector || sel} />
|
|
268
|
+
}
|
|
269
|
+
return (
|
|
270
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
271
|
+
{JSON.stringify(svc)}
|
|
272
|
+
</span>
|
|
273
|
+
)
|
|
274
|
+
})}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{toNodes.length > 0 && (
|
|
280
|
+
<div className="mb-2">
|
|
281
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Nodes</div>
|
|
282
|
+
<div className="space-y-1">
|
|
283
|
+
{toNodes.map((node: any, j: number) => (
|
|
284
|
+
<LabelSelectorDisplay key={j} selector={node} />
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{toGroups.length > 0 && (
|
|
291
|
+
<div className="mb-2">
|
|
292
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Groups</div>
|
|
293
|
+
<div className="flex flex-wrap gap-1">
|
|
294
|
+
{toGroups.map((g: any, j: number) => (
|
|
295
|
+
<CloudGroupEntry key={j} group={g} />
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{toPorts.length > 0 && (
|
|
302
|
+
<div className="mb-2">
|
|
303
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Ports</div>
|
|
304
|
+
<div className="flex flex-wrap gap-1">
|
|
305
|
+
{toPorts.map((p: any, j: number) => {
|
|
306
|
+
const ports = p.ports || []
|
|
307
|
+
return ports.map((port: any, k: number) => (
|
|
308
|
+
<span key={`${j}-${k}`} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
309
|
+
{port.protocol || 'TCP'}/{port.port}
|
|
310
|
+
</span>
|
|
311
|
+
))
|
|
312
|
+
})}
|
|
313
|
+
</div>
|
|
314
|
+
{toPorts.some((p: any) => p.rules) && (
|
|
315
|
+
<div className="mt-2">
|
|
316
|
+
{toPorts.map((p: any, j: number) => {
|
|
317
|
+
if (!p.rules) return null
|
|
318
|
+
return <L7Rules key={j} rules={p.rules} ports={p.ports} />
|
|
319
|
+
})}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{icmps.length > 0 && (
|
|
326
|
+
<div className="mb-2">
|
|
327
|
+
<div className="text-xs text-theme-text-tertiary mb-1">ICMP</div>
|
|
328
|
+
<div className="flex flex-wrap gap-1">
|
|
329
|
+
{icmps.map((icmp: any, j: number) => {
|
|
330
|
+
const fields = icmp.fields || []
|
|
331
|
+
return fields.map((f: any, k: number) => (
|
|
332
|
+
<span key={`${j}-${k}`} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
333
|
+
{f.family || 'IPv4'} type {f.type}
|
|
334
|
+
</span>
|
|
335
|
+
))
|
|
336
|
+
})}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
{!hasRules && (
|
|
342
|
+
<div className="text-xs text-theme-text-tertiary">Allow all</div>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function L7Rules({ rules, ports }: { rules: any; ports?: any[] }) {
|
|
349
|
+
const portLabel = ports?.map((p: any) => `${p.protocol || 'TCP'}/${p.port}`).join(', ')
|
|
350
|
+
const httpRules: any[] = rules.http || []
|
|
351
|
+
const dnsRules: any[] = rules.dns || []
|
|
352
|
+
const kafkaRules: any[] = rules.kafka || []
|
|
353
|
+
|
|
354
|
+
if (httpRules.length === 0 && dnsRules.length === 0 && kafkaRules.length === 0) return null
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<div className="card-inner text-xs space-y-2">
|
|
358
|
+
{portLabel && (
|
|
359
|
+
<div className="text-theme-text-tertiary">L7 rules for {portLabel}</div>
|
|
360
|
+
)}
|
|
361
|
+
{httpRules.length > 0 && (
|
|
362
|
+
<div>
|
|
363
|
+
<div className="text-theme-text-tertiary mb-1">HTTP</div>
|
|
364
|
+
<div className="space-y-1">
|
|
365
|
+
{httpRules.map((r: any, i: number) => (
|
|
366
|
+
<div key={i} className="flex flex-wrap items-center gap-1">
|
|
367
|
+
{r.method && (
|
|
368
|
+
<span className="badge badge-sm bg-blue-500/20 text-blue-400 border-blue-500/30">
|
|
369
|
+
{r.method}
|
|
370
|
+
</span>
|
|
371
|
+
)}
|
|
372
|
+
{r.path && (
|
|
373
|
+
<span className="text-theme-text-secondary font-mono">{r.path}</span>
|
|
374
|
+
)}
|
|
375
|
+
{r.headers && r.headers.map((h: any, hi: number) => (
|
|
376
|
+
<span key={hi} className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
377
|
+
{Object.entries(h).map(([k, v]) => `${k}: ${v}`).join(', ')}
|
|
378
|
+
</span>
|
|
379
|
+
))}
|
|
380
|
+
</div>
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
{dnsRules.length > 0 && (
|
|
386
|
+
<div>
|
|
387
|
+
<div className="text-theme-text-tertiary mb-1">DNS</div>
|
|
388
|
+
<div className="flex flex-wrap gap-1">
|
|
389
|
+
{dnsRules.map((r: any, i: number) => (
|
|
390
|
+
<span key={i} className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
391
|
+
{r.matchName || r.matchPattern || JSON.stringify(r)}
|
|
392
|
+
</span>
|
|
393
|
+
))}
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
{kafkaRules.length > 0 && (
|
|
398
|
+
<div>
|
|
399
|
+
<div className="text-theme-text-tertiary mb-1">Kafka</div>
|
|
400
|
+
<div className="space-y-1">
|
|
401
|
+
{kafkaRules.map((r: any, i: number) => (
|
|
402
|
+
<div key={i} className="flex flex-wrap gap-1">
|
|
403
|
+
{r.apiKey && (
|
|
404
|
+
<span className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
405
|
+
{r.apiKey}
|
|
406
|
+
</span>
|
|
407
|
+
)}
|
|
408
|
+
{r.topic && (
|
|
409
|
+
<span className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
410
|
+
topic: {r.topic}
|
|
411
|
+
</span>
|
|
412
|
+
)}
|
|
413
|
+
{r.role && (
|
|
414
|
+
<span className="badge badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
415
|
+
role: {r.role}
|
|
416
|
+
</span>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
))}
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function CloudGroupEntry({ group }: { group: any }) {
|
|
428
|
+
const aws = group.aws
|
|
429
|
+
if (aws) {
|
|
430
|
+
const parts: string[] = []
|
|
431
|
+
if (aws.region) parts.push(aws.region)
|
|
432
|
+
if (aws.securityGroupsIds) parts.push(...aws.securityGroupsIds)
|
|
433
|
+
if (aws.securityGroupsNames) parts.push(...aws.securityGroupsNames)
|
|
434
|
+
return (
|
|
435
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
436
|
+
AWS: {parts.join(', ') || JSON.stringify(aws)}
|
|
437
|
+
</span>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
return (
|
|
441
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
442
|
+
{JSON.stringify(group)}
|
|
443
|
+
</span>
|
|
444
|
+
)
|
|
445
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { ShieldCheck, ChevronDown, ChevronRight, CheckCircle2, XCircle } from 'lucide-react'
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import { ShieldCheck, ChevronDown, ChevronRight, CheckCircle2, XCircle, Search } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
|
|
5
5
|
import { formatAge } from '../resource-utils'
|
|
@@ -12,6 +12,8 @@ interface ClusterComplianceReportRendererProps {
|
|
|
12
12
|
export function ClusterComplianceReportRenderer({ data }: ClusterComplianceReportRendererProps) {
|
|
13
13
|
const [expandedSection, setExpandedSection] = useState(true)
|
|
14
14
|
const [expandedControls, setExpandedControls] = useState<Set<string>>(new Set())
|
|
15
|
+
const [searchTerm, setSearchTerm] = useState('')
|
|
16
|
+
const [showFailedOnly, setShowFailedOnly] = useState(false)
|
|
15
17
|
|
|
16
18
|
const compliance = data.spec?.compliance || {}
|
|
17
19
|
const status = data.status || {}
|
|
@@ -24,18 +26,40 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
24
26
|
const total = passCount + failCount
|
|
25
27
|
|
|
26
28
|
// Build a map of control definitions for descriptions
|
|
27
|
-
const controlMap =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const controlMap = useMemo(() => {
|
|
30
|
+
const map = new Map<string, any>()
|
|
31
|
+
for (const ctrl of controls) {
|
|
32
|
+
if (ctrl.id) map.set(ctrl.id, ctrl)
|
|
33
|
+
}
|
|
34
|
+
return map
|
|
35
|
+
}, [controls])
|
|
31
36
|
|
|
32
37
|
// Sort: failed first by severity, then passed
|
|
33
|
-
const sortedChecks = [...controlChecks].sort((a: any, b: any) => {
|
|
38
|
+
const sortedChecks = useMemo(() => [...controlChecks].sort((a: any, b: any) => {
|
|
34
39
|
const aFail = (a.totalFail || 0) > 0
|
|
35
40
|
const bFail = (b.totalFail || 0) > 0
|
|
36
41
|
if (aFail !== bFail) return aFail ? -1 : 1
|
|
37
42
|
return (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99)
|
|
38
|
-
})
|
|
43
|
+
}), [controlChecks])
|
|
44
|
+
|
|
45
|
+
const filteredChecks = useMemo(() => {
|
|
46
|
+
let result = sortedChecks
|
|
47
|
+
if (showFailedOnly) {
|
|
48
|
+
result = result.filter((c: any) => (c.totalFail || 0) > 0)
|
|
49
|
+
}
|
|
50
|
+
if (searchTerm) {
|
|
51
|
+
const term = searchTerm.toLowerCase()
|
|
52
|
+
result = result.filter((c: any) => {
|
|
53
|
+
const def = controlMap.get(c.id)
|
|
54
|
+
return (c.name || '').toLowerCase().includes(term) ||
|
|
55
|
+
(c.id || '').toLowerCase().includes(term) ||
|
|
56
|
+
(def?.description || '').toLowerCase().includes(term)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
return result
|
|
60
|
+
}, [sortedChecks, searchTerm, showFailedOnly, controlMap])
|
|
61
|
+
|
|
62
|
+
const hasActiveFilter = searchTerm !== '' || showFailedOnly
|
|
39
63
|
|
|
40
64
|
const toggleControl = (id: string) => {
|
|
41
65
|
setExpandedControls(prev => {
|
|
@@ -82,6 +106,11 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
82
106
|
<span className="text-sm font-medium text-red-400">{failCount}</span>
|
|
83
107
|
<span className="text-xs text-theme-text-tertiary">failed</span>
|
|
84
108
|
</div>
|
|
109
|
+
{total > 0 && (
|
|
110
|
+
<div className="ml-auto text-sm font-medium text-theme-text-secondary">
|
|
111
|
+
{Math.round((passCount / total) * 100)}% compliant
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
85
114
|
</div>
|
|
86
115
|
{total > 0 && (
|
|
87
116
|
<div className="h-3 rounded overflow-hidden flex">
|
|
@@ -102,8 +131,45 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
102
131
|
{sortedChecks.length} controls
|
|
103
132
|
</button>
|
|
104
133
|
{expandedSection && (
|
|
134
|
+
<>
|
|
135
|
+
{/* Search and filter */}
|
|
136
|
+
<div className="flex items-center gap-2 mb-2">
|
|
137
|
+
<div className="relative flex-1">
|
|
138
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
139
|
+
<input
|
|
140
|
+
type="text"
|
|
141
|
+
placeholder="Filter by control name or ID..."
|
|
142
|
+
value={searchTerm}
|
|
143
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
144
|
+
className="w-full pl-6 pr-2 py-1 text-xs bg-theme-bg border border-theme-border rounded text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => setShowFailedOnly(!showFailedOnly)}
|
|
149
|
+
className={clsx(
|
|
150
|
+
'flex items-center gap-1 px-2 py-1 text-xs rounded border transition-colors whitespace-nowrap',
|
|
151
|
+
showFailedOnly
|
|
152
|
+
? 'bg-red-500/15 border-red-500/30 text-red-400'
|
|
153
|
+
: 'bg-theme-bg border-theme-border text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
<XCircle className="w-3 h-3" />
|
|
157
|
+
Failed
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
{hasActiveFilter && (
|
|
161
|
+
<div className="flex items-center gap-1 mb-2 text-xs text-theme-text-tertiary">
|
|
162
|
+
Showing {filteredChecks.length} of {sortedChecks.length}
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => { setSearchTerm(''); setShowFailedOnly(false) }}
|
|
165
|
+
className="ml-1 text-blue-400 hover:text-blue-300 hover:underline"
|
|
166
|
+
>
|
|
167
|
+
Clear
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
105
171
|
<div className="space-y-0.5">
|
|
106
|
-
{
|
|
172
|
+
{filteredChecks.map((check: any) => {
|
|
107
173
|
const hasFail = (check.totalFail || 0) > 0
|
|
108
174
|
const controlDef = controlMap.get(check.id)
|
|
109
175
|
const isExpanded = expandedControls.has(check.id)
|
|
@@ -155,7 +221,13 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
155
221
|
</div>
|
|
156
222
|
)
|
|
157
223
|
})}
|
|
224
|
+
{filteredChecks.length === 0 && (
|
|
225
|
+
<div className="py-4 text-center text-xs text-theme-text-tertiary">
|
|
226
|
+
No controls match the current filter.
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
158
229
|
</div>
|
|
230
|
+
</>
|
|
159
231
|
)}
|
|
160
232
|
</Section>
|
|
161
233
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Globe, AlertTriangle } from 'lucide-react'
|
|
2
|
-
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
3
|
import {
|
|
4
4
|
getClusterExternalSecretStatus,
|
|
5
5
|
getClusterExternalSecretProvisionedNamespaces,
|
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
|
|
11
11
|
interface ClusterExternalSecretRendererProps {
|
|
12
12
|
data: any
|
|
13
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function ClusterExternalSecretRenderer({ data }: ClusterExternalSecretRendererProps) {
|
|
16
|
+
export function ClusterExternalSecretRenderer({ data, onNavigate }: ClusterExternalSecretRendererProps) {
|
|
16
17
|
const status = data.status || {}
|
|
17
18
|
const conditions = status.conditions || []
|
|
18
19
|
|
|
@@ -26,6 +27,8 @@ export function ClusterExternalSecretRenderer({ data }: ClusterExternalSecretRen
|
|
|
26
27
|
const esSpec = data.spec?.externalSecretSpec
|
|
27
28
|
const refreshInterval = esSpec?.refreshInterval || '-'
|
|
28
29
|
const storeRef = esSpec?.secretStoreRef
|
|
30
|
+
const storeName = storeRef?.name
|
|
31
|
+
const storeKindSingular = storeRef?.kind || 'SecretStore'
|
|
29
32
|
const dataCount = (esSpec?.data || []).length
|
|
30
33
|
const dataFromCount = (esSpec?.dataFrom || []).length
|
|
31
34
|
|
|
@@ -119,7 +122,11 @@ export function ClusterExternalSecretRenderer({ data }: ClusterExternalSecretRen
|
|
|
119
122
|
<Property label="Refresh Interval" value={refreshInterval} />
|
|
120
123
|
{storeRef && (
|
|
121
124
|
<>
|
|
122
|
-
<Property label="Store Name" value={
|
|
125
|
+
<Property label="Store Name" value={
|
|
126
|
+
storeName && storeKindSingular === 'ClusterSecretStore'
|
|
127
|
+
? <ResourceLink name={storeName} kind={storeKindSingular} namespace="" onNavigate={onNavigate} />
|
|
128
|
+
: storeName || '-'
|
|
129
|
+
} />
|
|
123
130
|
<Property label="Store Kind" value={storeRef.kind || 'SecretStore'} />
|
|
124
131
|
</>
|
|
125
132
|
)}
|