@skyhook-io/radar-app 1.14.2 → 1.14.3
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/api/client.ts +30 -0
- package/src/components/CloudConnectFlow.tsx +21 -7
- package/src/components/CloudFunnelButton.tsx +85 -8
- package/src/components/cloudConnectHandoff.test.ts +10 -1
- package/src/components/cloudConnectHandoff.ts +15 -1
- package/src/components/diagnose/AIMarkdown.tsx +87 -0
- package/src/components/diagnose/ActivityTurn.tsx +1244 -0
- package/src/components/diagnose/AgentControls.tsx +603 -0
- package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
- package/src/components/diagnose/AnalysisStory.tsx +13 -18
- package/src/components/diagnose/ApplyDialog.tsx +289 -0
- package/src/components/diagnose/AssessmentCard.tsx +1560 -0
- package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
- package/src/components/diagnose/EvidenceCard.tsx +611 -0
- package/src/components/diagnose/Home.test.tsx +1 -3
- package/src/components/diagnose/Home.tsx +2 -1
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
- package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
- package/src/components/diagnose/InvestigationView.tsx +11 -28
- package/src/components/diagnose/StoryExcerpt.tsx +118 -0
- package/src/components/diagnose/assessmentCopy.ts +169 -0
- package/src/components/diagnose/investigationCase.test.tsx +631 -2
- package/src/components/diagnose/investigationCase.ts +132 -24
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
- package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +134 -0
- package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
- package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
- package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
- package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
- package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
- package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
- package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
- package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
- package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
- package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
- package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
- package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
- package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
- package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
- package/src/components/diagnose/investigationEvidence/index.ts +1 -1
- package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +11 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
- package/src/components/diagnose/investigationEvidence/types.ts +97 -2
- package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
- package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
- package/src/components/diagnose/parts.test.tsx +3 -1
- package/src/components/diagnose/parts.tsx +26 -3904
- package/src/components/diagnose/target.ts +9 -3
- package/src/components/diagnose/toolCallLabel.test.ts +47 -1
- package/src/components/diagnose/toolCallLabel.ts +41 -2
- package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
- package/src/components/resources/PodFilePreview.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +12 -8
- package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
- package/src/components/ui/Omnibar.tsx +33 -10
- package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
- package/src/components/ui/Disclosure.tsx +0 -47
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { clsx } from "clsx";
|
|
3
|
+
import {
|
|
4
|
+
Badge,
|
|
5
|
+
StatusDot,
|
|
6
|
+
formatRelativeAgeTime,
|
|
7
|
+
mapHealthToTone,
|
|
8
|
+
ResourceLink,
|
|
9
|
+
} from "@skyhook-io/k8s-ui";
|
|
10
|
+
import { apiVersionToGroup } from "../../../../utils/navigation";
|
|
11
|
+
import { Tooltip } from "../../../ui/Tooltip";
|
|
12
|
+
import { EvidenceNavigationContext } from "../navigation";
|
|
13
|
+
import { type EvidenceDataOf, ResourceFact, severityBadge } from "../cardParts";
|
|
14
|
+
|
|
15
|
+
function helmStatusSeverity(status: string) {
|
|
16
|
+
const normalized = status.toLowerCase();
|
|
17
|
+
if (normalized === "deployed") return "success" as const;
|
|
18
|
+
if (normalized.includes("failed")) return "error" as const;
|
|
19
|
+
if (normalized.startsWith("pending") || normalized === "uninstalling")
|
|
20
|
+
return "info" as const;
|
|
21
|
+
return "warning" as const;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Scalars read as they are; a nested object shows the keys it holds, not its
|
|
25
|
+
// contents, which is enough to see what was overridden.
|
|
26
|
+
function helmValueText(value: unknown): string {
|
|
27
|
+
if (value === null) return "null";
|
|
28
|
+
if (typeof value !== "object") return String(value);
|
|
29
|
+
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
30
|
+
const keys = Object.keys(value as Record<string, unknown>);
|
|
31
|
+
return `{ ${keys.slice(0, 6).join(", ")}${keys.length > 6 ? ", …" : ""} }`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function HelmBody({ data }: { data: EvidenceDataOf<"helm"> }) {
|
|
35
|
+
const { onOpenResource } = useContext(EvidenceNavigationContext);
|
|
36
|
+
const { release } = data;
|
|
37
|
+
const operation = release.lastOperation;
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-3">
|
|
40
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
41
|
+
<Badge severity={helmStatusSeverity(release.status)} size="sm">
|
|
42
|
+
{release.status}
|
|
43
|
+
</Badge>
|
|
44
|
+
<Badge tone="structural" size="sm">
|
|
45
|
+
{release.chart}
|
|
46
|
+
{release.chartVersion ? ` ${release.chartVersion}` : ""}
|
|
47
|
+
</Badge>
|
|
48
|
+
<Badge tone="structural" size="sm">
|
|
49
|
+
revision {release.revision}
|
|
50
|
+
</Badge>
|
|
51
|
+
{release.appVersion ? (
|
|
52
|
+
<Badge tone="note" size="sm">
|
|
53
|
+
app {release.appVersion}
|
|
54
|
+
</Badge>
|
|
55
|
+
) : null}
|
|
56
|
+
{release.resourceHealth ? (
|
|
57
|
+
<Badge
|
|
58
|
+
severity={
|
|
59
|
+
mapHealthToTone(release.resourceHealth) === "healthy"
|
|
60
|
+
? "success"
|
|
61
|
+
: mapHealthToTone(release.resourceHealth) === "unhealthy"
|
|
62
|
+
? "error"
|
|
63
|
+
: "warning"
|
|
64
|
+
}
|
|
65
|
+
size="sm"
|
|
66
|
+
>
|
|
67
|
+
resources {release.resourceHealth}
|
|
68
|
+
</Badge>
|
|
69
|
+
) : null}
|
|
70
|
+
<Tooltip
|
|
71
|
+
content={new Date(release.updated).toLocaleString()}
|
|
72
|
+
delay={150}
|
|
73
|
+
position="left"
|
|
74
|
+
wrapperClassName="ml-auto"
|
|
75
|
+
>
|
|
76
|
+
<time
|
|
77
|
+
dateTime={release.updated}
|
|
78
|
+
className="text-xs text-theme-text-tertiary"
|
|
79
|
+
>
|
|
80
|
+
updated {formatRelativeAgeTime(release.updated)}
|
|
81
|
+
</time>
|
|
82
|
+
</Tooltip>
|
|
83
|
+
</div>
|
|
84
|
+
{release.healthIssue ? (
|
|
85
|
+
<p className="text-xs leading-relaxed text-warning-text">
|
|
86
|
+
{release.healthIssue}
|
|
87
|
+
</p>
|
|
88
|
+
) : null}
|
|
89
|
+
{release.healthSummary &&
|
|
90
|
+
release.healthSummary !== release.healthIssue ? (
|
|
91
|
+
<p className="text-xs leading-relaxed text-theme-text-secondary">
|
|
92
|
+
{release.healthSummary}
|
|
93
|
+
</p>
|
|
94
|
+
) : null}
|
|
95
|
+
{operation ? (
|
|
96
|
+
<div className="rounded-md border border-theme-border bg-theme-base/40 px-2.5 py-2">
|
|
97
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
98
|
+
<span className="text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
99
|
+
Last operation
|
|
100
|
+
</span>
|
|
101
|
+
<Badge tone="note" size="sm">
|
|
102
|
+
{operation.kind.replaceAll("_", " ")}
|
|
103
|
+
</Badge>
|
|
104
|
+
<Badge
|
|
105
|
+
severity={
|
|
106
|
+
operation.status === "completed"
|
|
107
|
+
? "success"
|
|
108
|
+
: operation.status === "failed"
|
|
109
|
+
? "error"
|
|
110
|
+
: "warning"
|
|
111
|
+
}
|
|
112
|
+
size="sm"
|
|
113
|
+
>
|
|
114
|
+
{operation.status.replaceAll("_", " ")}
|
|
115
|
+
</Badge>
|
|
116
|
+
</div>
|
|
117
|
+
{operation.message ? (
|
|
118
|
+
<p className="mt-1 text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
119
|
+
{operation.message}
|
|
120
|
+
</p>
|
|
121
|
+
) : null}
|
|
122
|
+
</div>
|
|
123
|
+
) : null}
|
|
124
|
+
{release.description ? (
|
|
125
|
+
<p className="text-xs text-theme-text-tertiary [overflow-wrap:anywhere]">
|
|
126
|
+
{release.description}
|
|
127
|
+
</p>
|
|
128
|
+
) : null}
|
|
129
|
+
{release.storageNamespace &&
|
|
130
|
+
release.storageNamespace !== release.namespace ? (
|
|
131
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
132
|
+
Release metadata stored in namespace {release.storageNamespace}
|
|
133
|
+
</p>
|
|
134
|
+
) : null}
|
|
135
|
+
{release.managedByFluxHelmRelease ? (
|
|
136
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
137
|
+
Managed by Flux HelmRelease {release.managedByFluxHelmRelease}
|
|
138
|
+
</p>
|
|
139
|
+
) : null}
|
|
140
|
+
{release.hooks && release.hooks.length > 0 ? (
|
|
141
|
+
<div>
|
|
142
|
+
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
143
|
+
Hooks
|
|
144
|
+
</div>
|
|
145
|
+
<div className="rounded-md border border-theme-border">
|
|
146
|
+
{release.hooks.map((hook, index) => (
|
|
147
|
+
<div
|
|
148
|
+
key={`${hook.kind}-${hook.namespace ?? ""}-${hook.name}-${index}`}
|
|
149
|
+
data-helm-hook
|
|
150
|
+
className={clsx(
|
|
151
|
+
"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 text-xs",
|
|
152
|
+
index > 0 && "border-t border-theme-border/60",
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
<Badge tone="structural" size="sm">
|
|
156
|
+
{hook.kind}
|
|
157
|
+
</Badge>
|
|
158
|
+
<span className="min-w-0 truncate font-mono text-theme-text-secondary">
|
|
159
|
+
{hook.name}
|
|
160
|
+
</span>
|
|
161
|
+
<span className="text-theme-text-tertiary">
|
|
162
|
+
{hook.events.join(", ")}
|
|
163
|
+
</span>
|
|
164
|
+
{hook.status ? (
|
|
165
|
+
<Badge severity={severityBadge(hook.status)} size="sm">
|
|
166
|
+
{hook.status}
|
|
167
|
+
</Badge>
|
|
168
|
+
) : null}
|
|
169
|
+
{hook.completedAt ? (
|
|
170
|
+
<span className="ml-auto shrink-0 text-theme-text-tertiary">
|
|
171
|
+
{formatRelativeAgeTime(hook.completedAt)}
|
|
172
|
+
</span>
|
|
173
|
+
) : null}
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
) : null}
|
|
179
|
+
{release.history && release.history.length > 0 ? (
|
|
180
|
+
<div>
|
|
181
|
+
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
182
|
+
History
|
|
183
|
+
</div>
|
|
184
|
+
<div className="rounded-md border border-theme-border">
|
|
185
|
+
{release.history.map((rev, index) => (
|
|
186
|
+
<div
|
|
187
|
+
key={rev.revision}
|
|
188
|
+
data-helm-revision={rev.revision}
|
|
189
|
+
className={clsx(
|
|
190
|
+
"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 text-xs",
|
|
191
|
+
index > 0 && "border-t border-theme-border/60",
|
|
192
|
+
rev.revision === release.revision && "bg-theme-hover/40",
|
|
193
|
+
)}
|
|
194
|
+
>
|
|
195
|
+
<span className="font-mono text-theme-text-primary">
|
|
196
|
+
rev {rev.revision}
|
|
197
|
+
</span>
|
|
198
|
+
<Badge severity={helmStatusSeverity(rev.status)} size="sm">
|
|
199
|
+
{rev.status}
|
|
200
|
+
</Badge>
|
|
201
|
+
<span className="font-mono text-theme-text-secondary">
|
|
202
|
+
{rev.chart}
|
|
203
|
+
</span>
|
|
204
|
+
{rev.description ? (
|
|
205
|
+
<span className="min-w-0 truncate text-theme-text-tertiary">
|
|
206
|
+
{rev.description}
|
|
207
|
+
</span>
|
|
208
|
+
) : null}
|
|
209
|
+
<span className="ml-auto shrink-0 text-theme-text-tertiary">
|
|
210
|
+
{formatRelativeAgeTime(rev.updated)}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
) : null}
|
|
217
|
+
{release.values && Object.keys(release.values).length > 0 ? (
|
|
218
|
+
<div>
|
|
219
|
+
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
220
|
+
Values set by the user
|
|
221
|
+
</div>
|
|
222
|
+
<div className="rounded-md border border-theme-border font-mono text-xs">
|
|
223
|
+
{Object.entries(release.values)
|
|
224
|
+
.slice(0, 40)
|
|
225
|
+
.map(([key, value], index) => (
|
|
226
|
+
<div
|
|
227
|
+
key={key}
|
|
228
|
+
data-helm-value={key}
|
|
229
|
+
className={clsx(
|
|
230
|
+
"flex min-w-0 gap-2 px-2.5 py-1",
|
|
231
|
+
index > 0 && "border-t border-theme-border/60",
|
|
232
|
+
)}
|
|
233
|
+
>
|
|
234
|
+
<span className="shrink-0 text-theme-text-secondary">
|
|
235
|
+
{key}
|
|
236
|
+
</span>
|
|
237
|
+
<span className="min-w-0 truncate text-theme-text-primary">
|
|
238
|
+
{helmValueText(value)}
|
|
239
|
+
</span>
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
) : null}
|
|
245
|
+
{release.resources.length > 0 ? (
|
|
246
|
+
<div className="max-h-52 overflow-y-auto rounded-md border border-theme-border">
|
|
247
|
+
{release.resources.map((owned, index) => (
|
|
248
|
+
<div
|
|
249
|
+
key={`${owned.kind}-${owned.namespace}-${owned.name}`}
|
|
250
|
+
className={clsx(
|
|
251
|
+
"flex min-w-0 items-center gap-2 px-2.5 py-1.5",
|
|
252
|
+
index > 0 && "border-t border-theme-border/60",
|
|
253
|
+
)}
|
|
254
|
+
>
|
|
255
|
+
<StatusDot
|
|
256
|
+
tone={mapHealthToTone(
|
|
257
|
+
owned.issue ? "unhealthy" : (owned.status ?? ""),
|
|
258
|
+
)}
|
|
259
|
+
className="shrink-0"
|
|
260
|
+
/>
|
|
261
|
+
<Badge tone="structural" size="sm">
|
|
262
|
+
{owned.kind}
|
|
263
|
+
</Badge>
|
|
264
|
+
<span className="min-w-0 flex-1">
|
|
265
|
+
<span className="block truncate font-mono text-xs">
|
|
266
|
+
<ResourceLink
|
|
267
|
+
name={owned.name}
|
|
268
|
+
kind={owned.kind}
|
|
269
|
+
namespace={owned.namespace}
|
|
270
|
+
group={
|
|
271
|
+
owned.apiVersion
|
|
272
|
+
? apiVersionToGroup(owned.apiVersion)
|
|
273
|
+
: undefined
|
|
274
|
+
}
|
|
275
|
+
label={`${owned.namespace ? `${owned.namespace}/` : ""}${owned.name}`}
|
|
276
|
+
onNavigate={
|
|
277
|
+
owned.apiVersion && onOpenResource
|
|
278
|
+
? (ref) => onOpenResource(ref)
|
|
279
|
+
: undefined
|
|
280
|
+
}
|
|
281
|
+
/>
|
|
282
|
+
</span>
|
|
283
|
+
{owned.issue ? (
|
|
284
|
+
<span className="block truncate text-xs text-warning-text">
|
|
285
|
+
{owned.issue}
|
|
286
|
+
</span>
|
|
287
|
+
) : owned.summary || owned.message ? (
|
|
288
|
+
<span className="block truncate text-xs text-theme-text-tertiary">
|
|
289
|
+
{owned.summary || owned.message}
|
|
290
|
+
</span>
|
|
291
|
+
) : null}
|
|
292
|
+
</span>
|
|
293
|
+
{owned.ready || owned.status ? (
|
|
294
|
+
<span className="ml-auto shrink-0 font-mono text-xs text-theme-text-tertiary">
|
|
295
|
+
{owned.ready || owned.status}
|
|
296
|
+
</span>
|
|
297
|
+
) : null}
|
|
298
|
+
</div>
|
|
299
|
+
))}
|
|
300
|
+
</div>
|
|
301
|
+
) : null}
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function PermissionsBody({
|
|
307
|
+
data,
|
|
308
|
+
}: {
|
|
309
|
+
data: EvidenceDataOf<"permissions">;
|
|
310
|
+
}) {
|
|
311
|
+
const { subject, accessCheck } = data;
|
|
312
|
+
const subjectBadges = (
|
|
313
|
+
<>
|
|
314
|
+
<Badge tone="structural" size="sm">
|
|
315
|
+
{subject.kind}
|
|
316
|
+
</Badge>
|
|
317
|
+
<span className="font-mono text-xs text-theme-text-secondary">
|
|
318
|
+
{subject.namespace ? `${subject.namespace}/` : ""}
|
|
319
|
+
{subject.name}
|
|
320
|
+
</span>
|
|
321
|
+
</>
|
|
322
|
+
);
|
|
323
|
+
if (accessCheck) {
|
|
324
|
+
const facts = [
|
|
325
|
+
["Verb", accessCheck.verb],
|
|
326
|
+
["Resource", accessCheck.resource],
|
|
327
|
+
["Subresource", accessCheck.subresource],
|
|
328
|
+
["API group", accessCheck.group || "core"],
|
|
329
|
+
["Namespace", accessCheck.namespace || "cluster-wide"],
|
|
330
|
+
["Name", accessCheck.resourceName],
|
|
331
|
+
] as const;
|
|
332
|
+
return (
|
|
333
|
+
<div className="space-y-2.5">
|
|
334
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
335
|
+
{subjectBadges}
|
|
336
|
+
<Badge
|
|
337
|
+
severity={accessCheck.allowed ? "success" : "warning"}
|
|
338
|
+
size="sm"
|
|
339
|
+
>
|
|
340
|
+
{accessCheck.allowed
|
|
341
|
+
? "allowed"
|
|
342
|
+
: accessCheck.denied
|
|
343
|
+
? "denied"
|
|
344
|
+
: "not allowed"}
|
|
345
|
+
</Badge>
|
|
346
|
+
</div>
|
|
347
|
+
<dl className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
|
|
348
|
+
{facts.map(([label, value]) => (
|
|
349
|
+
<ResourceFact key={label} label={label} value={value} />
|
|
350
|
+
))}
|
|
351
|
+
</dl>
|
|
352
|
+
{accessCheck.reason ? (
|
|
353
|
+
<p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
354
|
+
{accessCheck.reason}
|
|
355
|
+
</p>
|
|
356
|
+
) : null}
|
|
357
|
+
{accessCheck.evaluationError ? (
|
|
358
|
+
<p className="text-xs leading-relaxed text-semantic-error">
|
|
359
|
+
{accessCheck.evaluationError}
|
|
360
|
+
</p>
|
|
361
|
+
) : null}
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const bindings = data.bindings ?? [];
|
|
366
|
+
const usedByPods = data.usedByPods ?? [];
|
|
367
|
+
return (
|
|
368
|
+
<div className="space-y-2.5">
|
|
369
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
370
|
+
{subjectBadges}
|
|
371
|
+
<Badge tone="note" size="sm">
|
|
372
|
+
{data.flatRulesCount ?? 0}
|
|
373
|
+
{data.truncated ? "+" : ""} effective rules
|
|
374
|
+
</Badge>
|
|
375
|
+
{data.truncated ? (
|
|
376
|
+
<Badge severity="warning" size="sm">
|
|
377
|
+
rule list truncated
|
|
378
|
+
</Badge>
|
|
379
|
+
) : null}
|
|
380
|
+
</div>
|
|
381
|
+
{bindings.length > 0 ? (
|
|
382
|
+
<div className="max-h-52 overflow-y-auto rounded-md border border-theme-border">
|
|
383
|
+
{bindings.map((binding, index) => (
|
|
384
|
+
<div
|
|
385
|
+
key={`${binding.bindingKind}-${binding.bindingNamespace ?? ""}-${binding.bindingName}`}
|
|
386
|
+
className={clsx(
|
|
387
|
+
"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 text-xs",
|
|
388
|
+
index > 0 && "border-t border-theme-border/60",
|
|
389
|
+
)}
|
|
390
|
+
>
|
|
391
|
+
<Badge tone="structural" size="sm">
|
|
392
|
+
{binding.bindingKind}
|
|
393
|
+
</Badge>
|
|
394
|
+
<span className="min-w-0 truncate font-mono text-theme-text-secondary">
|
|
395
|
+
{binding.bindingNamespace ? `${binding.bindingNamespace}/` : ""}
|
|
396
|
+
{binding.bindingName}
|
|
397
|
+
</span>
|
|
398
|
+
<span className="text-theme-text-tertiary">→</span>
|
|
399
|
+
<Badge tone="structural" size="sm">
|
|
400
|
+
{binding.roleKind}
|
|
401
|
+
</Badge>
|
|
402
|
+
<span className="min-w-0 truncate font-mono text-theme-text-primary">
|
|
403
|
+
{binding.roleNamespace ? `${binding.roleNamespace}/` : ""}
|
|
404
|
+
{binding.roleName}
|
|
405
|
+
</span>
|
|
406
|
+
<span className="ml-auto shrink-0 font-mono text-theme-text-tertiary">
|
|
407
|
+
{binding.rulesCount} rule{binding.rulesCount === 1 ? "" : "s"}
|
|
408
|
+
</span>
|
|
409
|
+
{binding.inheritedFromGroup ? (
|
|
410
|
+
<Badge tone="note" size="sm">
|
|
411
|
+
via {binding.inheritedFromGroup}
|
|
412
|
+
</Badge>
|
|
413
|
+
) : null}
|
|
414
|
+
</div>
|
|
415
|
+
))}
|
|
416
|
+
</div>
|
|
417
|
+
) : (
|
|
418
|
+
<p className="text-xs italic text-theme-text-tertiary">
|
|
419
|
+
No RoleBinding or ClusterRoleBinding grants this subject anything.
|
|
420
|
+
</p>
|
|
421
|
+
)}
|
|
422
|
+
{data.rules && data.rules.length > 0 ? (
|
|
423
|
+
<div>
|
|
424
|
+
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
425
|
+
Effective rules
|
|
426
|
+
</div>
|
|
427
|
+
<div className="max-h-60 overflow-y-auto rounded-md border border-theme-border">
|
|
428
|
+
{data.rules.slice(0, 60).map((rule, index) => (
|
|
429
|
+
<div
|
|
430
|
+
key={index}
|
|
431
|
+
data-permission-rule
|
|
432
|
+
className={clsx(
|
|
433
|
+
"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 font-mono text-xs",
|
|
434
|
+
index > 0 && "border-t border-theme-border/60",
|
|
435
|
+
)}
|
|
436
|
+
>
|
|
437
|
+
<span className="text-theme-text-primary">
|
|
438
|
+
{rule.verbs.join(", ")}
|
|
439
|
+
</span>
|
|
440
|
+
<span className="text-theme-text-tertiary">on</span>
|
|
441
|
+
<span className="min-w-0 truncate text-theme-text-secondary">
|
|
442
|
+
{rule.nonResourceURLs?.length
|
|
443
|
+
? rule.nonResourceURLs.join(", ")
|
|
444
|
+
: rule.resources.join(", ")}
|
|
445
|
+
{rule.resourceNames?.length
|
|
446
|
+
? ` [${rule.resourceNames.join(", ")}]`
|
|
447
|
+
: ""}
|
|
448
|
+
</span>
|
|
449
|
+
{rule.apiGroups.length > 0 ? (
|
|
450
|
+
<span className="ml-auto shrink-0 text-theme-text-tertiary">
|
|
451
|
+
{rule.apiGroups.map((g) => g || "core").join(", ")}
|
|
452
|
+
</span>
|
|
453
|
+
) : null}
|
|
454
|
+
</div>
|
|
455
|
+
))}
|
|
456
|
+
{data.rules.length > 60 ? (
|
|
457
|
+
<div className="border-t border-theme-border/60 px-2.5 py-1.5 text-xs text-theme-text-tertiary">
|
|
458
|
+
{data.rules.length - 60} more rules in the result
|
|
459
|
+
</div>
|
|
460
|
+
) : null}
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
) : null}
|
|
464
|
+
{usedByPods.length > 0 ? (
|
|
465
|
+
<p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
466
|
+
<span className="text-theme-text-tertiary">Used by pods: </span>
|
|
467
|
+
<span className="font-mono">{usedByPods.join(", ")}</span>
|
|
468
|
+
{data.podsTotal && data.podsTotal > usedByPods.length
|
|
469
|
+
? ` and ${data.podsTotal - usedByPods.length} more`
|
|
470
|
+
: ""}
|
|
471
|
+
</p>
|
|
472
|
+
) : null}
|
|
473
|
+
</div>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { Badge } from "@skyhook-io/k8s-ui";
|
|
3
|
+
import { severityBadge, type EvidenceDataOf } from "../cardParts";
|
|
4
|
+
import type { InvestigationPostureFinding } from "../types";
|
|
5
|
+
|
|
6
|
+
export function PostureFindingRow({
|
|
7
|
+
finding,
|
|
8
|
+
divider = false,
|
|
9
|
+
}: {
|
|
10
|
+
finding: InvestigationPostureFinding;
|
|
11
|
+
divider?: boolean;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
data-posture-finding={finding.target ? "target" : undefined}
|
|
16
|
+
className={clsx(
|
|
17
|
+
"space-y-1 px-2.5 py-1.5 text-xs",
|
|
18
|
+
divider && "border-t border-theme-border/60",
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
|
22
|
+
<Badge severity={severityBadge(finding.severity)} size="sm">
|
|
23
|
+
{finding.severity}
|
|
24
|
+
</Badge>
|
|
25
|
+
<span className="font-medium text-theme-text-primary">
|
|
26
|
+
{finding.check}
|
|
27
|
+
</span>
|
|
28
|
+
{finding.category ? (
|
|
29
|
+
<span className="text-theme-text-tertiary">{finding.category}</span>
|
|
30
|
+
) : null}
|
|
31
|
+
<span className="ml-auto min-w-0 truncate font-mono text-theme-text-tertiary">
|
|
32
|
+
{finding.kind} {finding.namespace ? `${finding.namespace}/` : ""}
|
|
33
|
+
{finding.name}
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
<p className="text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
37
|
+
{finding.message}
|
|
38
|
+
</p>
|
|
39
|
+
{finding.remediation ? (
|
|
40
|
+
<p className="text-theme-text-tertiary [overflow-wrap:anywhere]">
|
|
41
|
+
{finding.remediation}
|
|
42
|
+
</p>
|
|
43
|
+
) : null}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function PostureBody({ data }: { data: EvidenceDataOf<"posture"> }) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="max-h-80 overflow-y-auto rounded-md border border-theme-border">
|
|
51
|
+
{data.findings.map((finding, index) => (
|
|
52
|
+
<PostureFindingRow
|
|
53
|
+
key={`${finding.kind}-${finding.namespace ?? ""}-${finding.name}-${finding.check}`}
|
|
54
|
+
finding={finding}
|
|
55
|
+
divider={index > 0}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { Badge } from "@skyhook-io/k8s-ui";
|
|
3
|
+
import type { EvidenceDataOf } from "../cardParts";
|
|
4
|
+
import type { InvestigationRankingRow } from "../types";
|
|
5
|
+
|
|
6
|
+
export function RankingRow({
|
|
7
|
+
row,
|
|
8
|
+
divider = false,
|
|
9
|
+
}: {
|
|
10
|
+
row: InvestigationRankingRow;
|
|
11
|
+
divider?: boolean;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
data-ranking-row={row.target ? "target" : undefined}
|
|
16
|
+
className={clsx(
|
|
17
|
+
"flex min-w-0 items-center gap-2 px-2.5 py-1.5 font-mono text-xs",
|
|
18
|
+
divider && "border-t border-theme-border/60",
|
|
19
|
+
row.target && "bg-theme-hover/40",
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
<Badge tone="structural" size="sm">
|
|
23
|
+
{row.kind}
|
|
24
|
+
</Badge>
|
|
25
|
+
<span className="min-w-0 flex-1 truncate text-theme-text-secondary">
|
|
26
|
+
{row.namespace ? `${row.namespace}/` : ""}
|
|
27
|
+
<span className={row.target ? "text-theme-text-primary" : undefined}>
|
|
28
|
+
{row.name}
|
|
29
|
+
</span>
|
|
30
|
+
</span>
|
|
31
|
+
<span className="shrink-0 tabular-nums text-theme-text-primary">
|
|
32
|
+
{row.cpu}
|
|
33
|
+
{row.cpuLimit ? (
|
|
34
|
+
<span className="text-theme-text-tertiary">/{row.cpuLimit}</span>
|
|
35
|
+
) : null}
|
|
36
|
+
</span>
|
|
37
|
+
<span className="shrink-0 tabular-nums text-theme-text-primary">
|
|
38
|
+
{row.memory}
|
|
39
|
+
{row.memoryLimit ? (
|
|
40
|
+
<span className="text-theme-text-tertiary">/{row.memoryLimit}</span>
|
|
41
|
+
) : null}
|
|
42
|
+
</span>
|
|
43
|
+
{row.restarts ? (
|
|
44
|
+
<Badge severity="warning" size="sm">
|
|
45
|
+
{row.restarts} restarts
|
|
46
|
+
</Badge>
|
|
47
|
+
) : null}
|
|
48
|
+
{row.target ? (
|
|
49
|
+
<Badge tone="note" size="sm">
|
|
50
|
+
this workload
|
|
51
|
+
</Badge>
|
|
52
|
+
) : null}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function RankingBody({ data }: { data: EvidenceDataOf<"ranking"> }) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-1.5">
|
|
60
|
+
<div className="flex gap-2 px-2.5 text-[11px] text-theme-text-tertiary">
|
|
61
|
+
<span className="min-w-0 flex-1">ranked by {data.sort}</span>
|
|
62
|
+
<span className="shrink-0">cpu</span>
|
|
63
|
+
<span className="shrink-0">memory</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="max-h-72 overflow-y-auto rounded-md border border-theme-border">
|
|
66
|
+
{data.rows.map((row, index) => (
|
|
67
|
+
<RankingRow
|
|
68
|
+
key={`${row.kind}-${row.namespace ?? ""}-${row.name}`}
|
|
69
|
+
row={row}
|
|
70
|
+
divider={index > 0}
|
|
71
|
+
/>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|