@skyhook-io/radar-app 1.13.1 → 1.13.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.
Files changed (47) hide show
  1. package/package.json +2 -2
  2. package/src/App.tsx +19 -1
  3. package/src/RadarApp.tsx +2 -2
  4. package/src/api/diagnose.test.ts +268 -0
  5. package/src/api/diagnose.ts +74 -9
  6. package/src/components/diagnose/AISettings.tsx +1 -1
  7. package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
  8. package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
  9. package/src/components/diagnose/DiagnoseContext.tsx +12 -17
  10. package/src/components/diagnose/DiagnoseSurface.test.tsx +211 -16
  11. package/src/components/diagnose/DiagnoseSurface.tsx +464 -133
  12. package/src/components/diagnose/Home.test.tsx +293 -0
  13. package/src/components/diagnose/Home.tsx +289 -119
  14. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
  15. package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
  16. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
  17. package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
  18. package/src/components/diagnose/InvestigationView.test.ts +17 -0
  19. package/src/components/diagnose/InvestigationView.tsx +1913 -395
  20. package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
  21. package/src/components/diagnose/agentCatalog.ts +1 -1
  22. package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
  23. package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
  24. package/src/components/diagnose/investigationEvidence.ts +3492 -0
  25. package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
  26. package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
  27. package/src/components/diagnose/investigationExplanation.test.ts +92 -0
  28. package/src/components/diagnose/investigationExplanation.ts +29 -0
  29. package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
  30. package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
  31. package/src/components/diagnose/investigationSourceFocus.ts +98 -0
  32. package/src/components/diagnose/investigationState.test.ts +695 -0
  33. package/src/components/diagnose/investigationState.ts +451 -0
  34. package/src/components/diagnose/parts.test.tsx +864 -3
  35. package/src/components/diagnose/parts.tsx +1337 -541
  36. package/src/components/diagnose/target.test.ts +39 -0
  37. package/src/components/diagnose/target.ts +36 -0
  38. package/src/components/diagnose/useDisclosureReveal.ts +117 -0
  39. package/src/components/home/MCPSetupDialog.tsx +2 -2
  40. package/src/components/home/mcpToolCatalog.test.ts +22 -0
  41. package/src/components/home/mcpToolCatalog.ts +3 -2
  42. package/src/components/issues/IssuesPane.tsx +5 -1
  43. package/src/components/settings/SettingsDialog.tsx +11 -13
  44. package/src/components/workload/WorkloadView.tsx +1 -1
  45. package/src/context/DiagnoseCustomization.tsx +11 -8
  46. package/src/index.css +63 -79
  47. package/src/index.ts +1 -1
@@ -0,0 +1,257 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { InvestigationResourceEvidence } from "./InvestigationResourceEvidence";
5
+ import {
6
+ investigationResourceEvidenceHasDetails,
7
+ investigationResourceEvidenceSummary,
8
+ isSensitiveInvestigationConfigKey,
9
+ } from "./investigationResourceEvidenceModel";
10
+ import type { InvestigationKubernetesResource } from "./investigationEvidence";
11
+
12
+ function render(resource: InvestigationKubernetesResource): string {
13
+ return renderToStaticMarkup(
14
+ <InvestigationResourceEvidence resource={resource} />,
15
+ );
16
+ }
17
+
18
+ describe("InvestigationResourceEvidence", () => {
19
+ it("keeps every ConfigMap key inspectable in a bounded table and hides sensitive-looking values", () => {
20
+ const html = render({
21
+ apiVersion: "v1",
22
+ kind: "ConfigMap",
23
+ metadata: { namespace: "dev", name: "app-vars" },
24
+ data: {
25
+ Z_MISC: "last",
26
+ WORKERS: "4",
27
+ LOG_LEVEL: "info",
28
+ FEATURE_FLAG: "on",
29
+ CACHE_SIZE: "128",
30
+ DB_NAME: "atlas",
31
+ MONGO_USER: "opendev-nonprod",
32
+ API_ENDPOINT: "https://api.example.test",
33
+ REDIS_HOST: "redis.dev.svc",
34
+ DATABASE_URL: "mongodb://visible-user:hidden-pass@mongo.dev/db",
35
+ REDIS_URL: "redis://:another-hidden-pass@redis.dev/0",
36
+ DATABASE_PASSWORD: "do-not-render-this-password",
37
+ },
38
+ });
39
+
40
+ expect(html).toContain("ConfigMap values");
41
+ expect(html).toContain("12 keys");
42
+ expect(html).toContain("https://api.example.test");
43
+ expect(html).toContain("redis.dev.svc");
44
+ expect(html).toContain("opendev-nonprod");
45
+ expect(html).toContain("Value hidden · potentially sensitive");
46
+ expect(html).not.toContain("do-not-render-this-password");
47
+ expect(html).not.toContain("hidden-pass");
48
+ expect(html).not.toContain("another-hidden-pass");
49
+ expect(html).toContain("max-h-64 overflow-y-auto");
50
+ expect(html).toContain("Z_MISC");
51
+ expect(html.match(/<tr>/g)).toHaveLength(12);
52
+ expect(html).not.toContain("more keys available in Activity");
53
+ expect(html.indexOf("API_ENDPOINT")).toBeLessThan(
54
+ html.indexOf("MONGO_USER"),
55
+ );
56
+ expect(html.indexOf("MONGO_USER")).toBeLessThan(html.indexOf("CACHE_SIZE"));
57
+ });
58
+
59
+ it("renders the producer's key-only Secret shape and never exposes unexpected values", () => {
60
+ const html = render({
61
+ apiVersion: "v1",
62
+ kind: "Secret",
63
+ metadata: { namespace: "dev", name: "database" },
64
+ type: "Opaque",
65
+ keys: ["MONGO_PASSWORD", "username", "connection", "API_KEY"],
66
+ data: {
67
+ MONGO_PASSWORD: "c3VwZXItc2VjcmV0",
68
+ username: "b3BlbmRldg==",
69
+ },
70
+ stringData: { connection: "mongodb://user:password@example.test" },
71
+ });
72
+
73
+ expect(html).toContain("MONGO_PASSWORD");
74
+ expect(html).toContain("username");
75
+ expect(html).toContain("connection");
76
+ expect(html).not.toContain("Opaque");
77
+ expect(html).not.toContain("Secret values are never shown here.");
78
+ expect(html).not.toContain("c3VwZXItc2VjcmV0");
79
+ expect(html).not.toContain("b3BlbmRldg==");
80
+ expect(html).not.toContain("mongodb://");
81
+ });
82
+
83
+ it("shows SealedSecret state and key names without rendering ciphertext", () => {
84
+ const html = render({
85
+ apiVersion: "bitnami.com/v1alpha1",
86
+ kind: "SealedSecret",
87
+ metadata: {
88
+ namespace: "dev",
89
+ name: "project-infra",
90
+ generation: 8,
91
+ creationTimestamp: "2026-09-01T10:15:00Z",
92
+ annotations: {
93
+ "sealedsecrets.bitnami.com/namespace-wide": "true",
94
+ },
95
+ },
96
+ spec: {
97
+ encryptedData: {
98
+ MONGO_PASSWORD: "AgA-ciphertext-that-must-not-render",
99
+ API_TOKEN: "AgA-other-ciphertext-that-must-not-render",
100
+ },
101
+ },
102
+ status: {
103
+ observedGeneration: 8,
104
+ conditions: [
105
+ {
106
+ type: "Synced",
107
+ status: "True",
108
+ reason: "SealedSecretSynced",
109
+ message: "SealedSecret reconciled successfully",
110
+ },
111
+ ],
112
+ },
113
+ });
114
+
115
+ expect(html).toContain("Synced");
116
+ expect(html).toContain("Namespace-wide");
117
+ expect(html).not.toContain("Observed generation");
118
+ expect(html).toContain("2026-09-01 10:15 UTC");
119
+ expect(html).toContain("MONGO_PASSWORD");
120
+ expect(html).toContain("API_TOKEN");
121
+ expect(html).toContain("Synced=True");
122
+ expect(html).toContain("SealedSecretSynced");
123
+ expect(html.indexOf("SealedSecret reconciled successfully")).toBeLessThan(
124
+ html.indexOf("Namespace-wide"),
125
+ );
126
+ expect(html).not.toContain("AgA-ciphertext");
127
+ expect(html).not.toContain("AgA-other");
128
+ expect(html).not.toContain("Encrypted values stay hidden");
129
+ });
130
+
131
+ it("returns no specialized UI or summary for generic resources", () => {
132
+ const deployment = {
133
+ apiVersion: "apps/v1",
134
+ kind: "Deployment",
135
+ metadata: { namespace: "dev", name: "api" },
136
+ };
137
+
138
+ expect(render(deployment)).toBe("");
139
+ expect(investigationResourceEvidenceSummary(deployment)).toBeUndefined();
140
+ expect(investigationResourceEvidenceHasDetails(deployment)).toBe(false);
141
+ });
142
+
143
+ it("does not add an empty second disclosure layer for resources without specialized details", () => {
144
+ const emptyConfigMap = {
145
+ apiVersion: "v1",
146
+ kind: "ConfigMap",
147
+ metadata: { namespace: "dev", name: "empty-config" },
148
+ data: {},
149
+ };
150
+ const emptySecret = {
151
+ apiVersion: "v1",
152
+ kind: "Secret",
153
+ metadata: { namespace: "dev", name: "empty-secret" },
154
+ type: "Opaque",
155
+ keys: [],
156
+ };
157
+
158
+ for (const resource of [emptyConfigMap, emptySecret]) {
159
+ expect(investigationResourceEvidenceSummary(resource)).toBeTruthy();
160
+ expect(investigationResourceEvidenceHasDetails(resource)).toBe(false);
161
+ expect(render(resource)).toBe("");
162
+ }
163
+ expect(investigationResourceEvidenceSummary(emptySecret)).toBe(
164
+ "No key names in this result",
165
+ );
166
+ });
167
+ });
168
+
169
+ describe("investigation resource summaries", () => {
170
+ it.each([0, 1, 3, 4])(
171
+ "adds Secret details only beyond the inline preview (%i keys)",
172
+ (count) => {
173
+ const resource = {
174
+ apiVersion: "v1",
175
+ kind: "Secret",
176
+ metadata: { name: "keys", namespace: "dev" },
177
+ keys: Array.from({ length: count }, (_, index) => `KEY_${index}`),
178
+ };
179
+ expect(investigationResourceEvidenceHasDetails(resource)).toBe(count > 3);
180
+ const summary = investigationResourceEvidenceSummary(resource)!;
181
+ if (count > 0) expect(summary).toContain("KEY_0");
182
+ if (count === 4) expect(summary).toContain("1 more");
183
+ expect(summary).not.toContain("values hidden");
184
+ },
185
+ );
186
+
187
+ it("summarizes ConfigMap key names without exposing values", () => {
188
+ const summary = investigationResourceEvidenceSummary({
189
+ apiVersion: "v1",
190
+ kind: "ConfigMap",
191
+ metadata: { namespace: "dev", name: "vars" },
192
+ data: {
193
+ ZZZ: "last",
194
+ MONGO_USER: "opendev-nonprod",
195
+ MONGO_ADDRESS: "nonprod-boxer.example.mongodb.net",
196
+ API_TOKEN: "must-stay-hidden",
197
+ },
198
+ });
199
+
200
+ expect(summary).toBe("Keys: MONGO_ADDRESS, MONGO_USER, API_TOKEN, ZZZ");
201
+ expect(summary).not.toContain("must-stay-hidden");
202
+ });
203
+
204
+ it("includes key names even when their values must stay hidden", () => {
205
+ const summary = investigationResourceEvidenceSummary({
206
+ apiVersion: "v1",
207
+ kind: "ConfigMap",
208
+ metadata: { namespace: "dev", name: "vars" },
209
+ data: {
210
+ DATABASE_URL: "mongodb://user:hidden@mongo.dev/db",
211
+ PRIVATE_ENDPOINT: "https://private.example.test",
212
+ MONGO_ADDRESS: "mongo.dev.svc",
213
+ MONGO_USER: "opendev",
214
+ },
215
+ });
216
+
217
+ expect(summary).toBe(
218
+ "Keys: DATABASE_URL, MONGO_ADDRESS, PRIVATE_ENDPOINT, MONGO_USER",
219
+ );
220
+ expect(summary).not.toContain("hidden@mongo");
221
+ expect(summary).not.toContain("private.example.test");
222
+ });
223
+
224
+ it("uses conservative sensitive-key matching without treating ordinary words as keys", () => {
225
+ expect(isSensitiveInvestigationConfigKey("DATABASE_PASSWORD")).toBe(true);
226
+ expect(isSensitiveInvestigationConfigKey("clientSecret")).toBe(true);
227
+ expect(isSensitiveInvestigationConfigKey("private-key")).toBe(true);
228
+ expect(isSensitiveInvestigationConfigKey("monkey_species")).toBe(false);
229
+ });
230
+
231
+ it("hides bearer and provider tokens even when a ConfigMap key looks harmless", () => {
232
+ for (const value of [
233
+ "Bearer abcdefghijklmnopqrstuvwxyz012345",
234
+ "sk-proj-abcdefghijklmnopqrstuvwxyz012345",
235
+ "github_pat_11AA22BB33CC44DD55EE66FF77GG",
236
+ "dsn options password=correct-horse-battery-staple",
237
+ "$2y$10$abcdefghijklmnopqrstuv",
238
+ ]) {
239
+ const html = render({
240
+ apiVersion: "v1",
241
+ kind: "ConfigMap",
242
+ metadata: { namespace: "dev", name: "vars" },
243
+ data: { AUTHORIZATION: value },
244
+ });
245
+ expect(html).toContain("Value hidden · potentially sensitive");
246
+ expect(html).not.toContain(value);
247
+ expect(
248
+ investigationResourceEvidenceSummary({
249
+ apiVersion: "v1",
250
+ kind: "ConfigMap",
251
+ metadata: { namespace: "dev", name: "vars" },
252
+ data: { AUTHORIZATION: value },
253
+ }),
254
+ ).toBe("Keys: AUTHORIZATION");
255
+ }
256
+ });
257
+ });
@@ -0,0 +1,214 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ buildInvestigationResourceEvidenceModel,
4
+ type InvestigationResourceEvidenceInput,
5
+ type InvestigationResourceEvidenceModel,
6
+ } from "./investigationResourceEvidenceModel";
7
+
8
+ export function InvestigationResourceEvidence({
9
+ resource,
10
+ }: {
11
+ resource: InvestigationResourceEvidenceInput;
12
+ }) {
13
+ const model = buildInvestigationResourceEvidenceModel(resource);
14
+ if (!model?.hasDetails) return null;
15
+ switch (model.kind) {
16
+ case "configmap":
17
+ return <ConfigMapEvidence model={model} />;
18
+ case "secret":
19
+ return <SecretEvidence model={model} />;
20
+ case "sealedsecret":
21
+ return <SealedSecretEvidence model={model} />;
22
+ }
23
+ }
24
+
25
+ function ConfigMapEvidence({
26
+ model,
27
+ }: {
28
+ model: Extract<InvestigationResourceEvidenceModel, { kind: "configmap" }>;
29
+ }) {
30
+ const entries = model.entries;
31
+
32
+ return (
33
+ <div className="overflow-hidden rounded-md border border-theme-border bg-theme-base/40">
34
+ <div className="flex items-center justify-between gap-3 border-b border-theme-border px-2.5 py-1.5">
35
+ <span className="text-xs font-medium text-theme-text-tertiary">
36
+ ConfigMap values
37
+ </span>
38
+ <span className="text-xs tabular-nums text-theme-text-tertiary">
39
+ {entries.length} {entries.length === 1 ? "key" : "keys"}
40
+ </span>
41
+ </div>
42
+ {entries.length > 0 ? (
43
+ <div className="max-h-64 overflow-y-auto">
44
+ <table className="w-full table-fixed text-left text-xs">
45
+ <caption className="sr-only">
46
+ Selected ConfigMap keys and values
47
+ </caption>
48
+ <colgroup>
49
+ <col className="w-[38%]" />
50
+ <col />
51
+ </colgroup>
52
+ <tbody className="divide-y divide-theme-border/70">
53
+ {entries.map((entry) => {
54
+ return (
55
+ <tr key={`${entry.binary ? "binary" : "data"}-${entry.key}`}>
56
+ <th
57
+ scope="row"
58
+ className="px-2.5 py-1.5 align-top font-mono text-xs font-medium text-theme-text-secondary"
59
+ >
60
+ <span className="block truncate">{entry.key}</span>
61
+ </th>
62
+ <td className="px-2.5 py-1.5 align-top font-mono text-xs leading-relaxed text-theme-text-primary">
63
+ {entry.sensitive ? (
64
+ <span className="font-sans text-theme-text-tertiary">
65
+ Value hidden · potentially sensitive
66
+ </span>
67
+ ) : entry.binary ? (
68
+ <span className="font-sans text-theme-text-tertiary">
69
+ Binary value not shown
70
+ </span>
71
+ ) : (
72
+ <span className="block max-h-20 overflow-y-auto whitespace-pre-wrap break-all pr-1">
73
+ {entry.value}
74
+ </span>
75
+ )}
76
+ </td>
77
+ </tr>
78
+ );
79
+ })}
80
+ </tbody>
81
+ </table>
82
+ </div>
83
+ ) : (
84
+ <p className="px-2.5 py-2 text-xs text-theme-text-tertiary">
85
+ No data keys were found.
86
+ </p>
87
+ )}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ function SecretEvidence({
93
+ model,
94
+ }: {
95
+ model: Extract<InvestigationResourceEvidenceModel, { kind: "secret" }>;
96
+ }) {
97
+ return (
98
+ <div>
99
+ <div className="flex flex-wrap items-center justify-between gap-2">
100
+ <span className="text-xs font-medium text-theme-text-tertiary">
101
+ Secret keys
102
+ </span>
103
+ </div>
104
+ {model.keys.length > 0 ? (
105
+ <div className="mt-1.5 flex flex-wrap gap-1">
106
+ {model.keys.map((key) => (
107
+ <span
108
+ key={key}
109
+ className="rounded border border-theme-border bg-theme-elevated/60 px-1.5 py-0.5 font-mono text-xs text-theme-text-secondary"
110
+ >
111
+ {key}
112
+ </span>
113
+ ))}
114
+ </div>
115
+ ) : (
116
+ <p className="mt-1.5 text-xs text-theme-text-tertiary">
117
+ No Secret key names were found.
118
+ </p>
119
+ )}
120
+ </div>
121
+ );
122
+ }
123
+
124
+ function SealedSecretEvidence({
125
+ model,
126
+ }: {
127
+ model: Extract<InvestigationResourceEvidenceModel, { kind: "sealedsecret" }>;
128
+ }) {
129
+ return (
130
+ <div className="overflow-hidden rounded-md border border-theme-border bg-theme-base/40">
131
+ <div className="flex flex-wrap items-center justify-between gap-2 border-b border-theme-border px-2.5 py-1.5">
132
+ <span className="text-xs font-medium text-theme-text-tertiary">
133
+ SealedSecret state
134
+ </span>
135
+ <span
136
+ className={
137
+ model.syncLabel === "Synced"
138
+ ? "text-xs font-medium text-emerald-500"
139
+ : model.syncLabel === "Not synced"
140
+ ? "text-xs font-medium text-red-400"
141
+ : "text-xs font-medium text-theme-text-tertiary"
142
+ }
143
+ >
144
+ {model.syncLabel}
145
+ </span>
146
+ </div>
147
+ {model.conditions.length > 0 ? (
148
+ <div className="px-2.5 py-2">
149
+ <div className="mb-1 text-xs font-medium text-theme-text-tertiary">
150
+ Conditions
151
+ </div>
152
+ <div className="space-y-1.5">
153
+ {model.conditions.map((condition) => (
154
+ <div
155
+ key={`${condition.type}-${condition.status}-${condition.reason ?? ""}`}
156
+ className="text-sm leading-relaxed text-theme-text-primary"
157
+ >
158
+ <span className="font-medium text-theme-text-primary">
159
+ {condition.type}={condition.status}
160
+ </span>
161
+ {condition.reason ? ` · ${condition.reason}` : ""}
162
+ {condition.message ? ` — ${condition.message}` : ""}
163
+ </div>
164
+ ))}
165
+ </div>
166
+ </div>
167
+ ) : null}
168
+ <dl className="flex flex-wrap gap-x-5 gap-y-1 border-b border-theme-border px-2.5 py-1.5 text-xs">
169
+ <ResourceFact label="Scope" value={model.scope} />
170
+ <ResourceFact
171
+ label="Created"
172
+ value={
173
+ model.created ? (
174
+ <time dateTime={model.created.dateTime}>
175
+ {model.created.label}
176
+ </time>
177
+ ) : undefined
178
+ }
179
+ />
180
+ </dl>
181
+ <div className="px-2.5 py-2">
182
+ <div className="text-xs font-medium text-theme-text-tertiary">
183
+ Encrypted keys · {model.encryptedKeys.length}
184
+ </div>
185
+ {model.encryptedKeys.length > 0 ? (
186
+ <div className="mt-1 flex flex-wrap gap-1">
187
+ {model.encryptedKeys.map((key) => (
188
+ <span
189
+ key={key}
190
+ className="rounded border border-theme-border bg-theme-elevated/60 px-1.5 py-0.5 font-mono text-xs text-theme-text-secondary"
191
+ >
192
+ {key}
193
+ </span>
194
+ ))}
195
+ </div>
196
+ ) : (
197
+ <p className="mt-1 text-xs text-theme-text-tertiary">
198
+ No encrypted key names were found.
199
+ </p>
200
+ )}
201
+ </div>
202
+ </div>
203
+ );
204
+ }
205
+
206
+ function ResourceFact({ label, value }: { label: string; value: ReactNode }) {
207
+ if (value === undefined || value === null || value === "") return null;
208
+ return (
209
+ <div className="flex min-w-0 gap-1.5">
210
+ <dt className="text-theme-text-tertiary">{label}</dt>
211
+ <dd className="font-medium text-theme-text-secondary">{value}</dd>
212
+ </div>
213
+ );
214
+ }
@@ -0,0 +1,17 @@
1
+ import { createElement } from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { expect, it } from "vitest";
4
+ import { InvestigationStartErrorAlert } from "./InvestigationView";
5
+
6
+ it("renders new-run failures as a pane-independent workspace alert", () => {
7
+ const html = renderToStaticMarkup(
8
+ createElement(InvestigationStartErrorAlert, {
9
+ error: "The run could not be created.",
10
+ onDismiss: () => {},
11
+ }),
12
+ );
13
+ expect(html).toContain('role="alert"');
14
+ expect(html).toContain("Couldn&#x27;t start a new investigation");
15
+ expect(html).toContain("The run could not be created.");
16
+ expect(html).not.toContain('class="hidden');
17
+ });