@burdenoff/microfe-vibecontrols 2026.528.1 → 2026.528.2
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/dist/components/security/SecurityScanList.js +308 -0
- package/dist/components/security/SecurityScanList.js.map +1 -0
- package/dist/components/security/SecurityTabPanel.js +22 -18
- package/dist/components/security/SecurityTabPanel.js.map +1 -1
- package/dist/components/security/agentClient.js +78 -0
- package/dist/components/security/agentClient.js.map +1 -0
- package/dist/components/security/hooks.js +33 -0
- package/dist/components/security/hooks.js.map +1 -0
- package/dist/components/security/index.js +3 -0
- package/dist/pages/VibeDetailsPage.js +570 -567
- package/dist/pages/VibeDetailsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { useSecurityRecentScansQuery as e } from "./hooks.js";
|
|
2
|
+
import { useMemo as t } from "react";
|
|
3
|
+
import { AlertCircle as n, CheckCircle2 as r, Clock as i, Loader2 as a, MinusCircle as o, ShieldAlert as s, XCircle as c } from "lucide-react";
|
|
4
|
+
import { Fragment as l, jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/security/SecurityScanList.tsx
|
|
6
|
+
var f = {
|
|
7
|
+
queued: "bg-bg-sunken text-text-secondary",
|
|
8
|
+
running: "bg-status-info-bg/20 text-status-info-text",
|
|
9
|
+
succeeded: "bg-status-success-bg/20 text-status-success-text",
|
|
10
|
+
failed: "bg-status-error-bg/20 text-status-error-text",
|
|
11
|
+
errored: "bg-status-error-bg/20 text-status-error-text",
|
|
12
|
+
cancelled: "bg-bg-sunken text-text-tertiary"
|
|
13
|
+
}, p = {
|
|
14
|
+
pass: "bg-status-success-bg/20 text-status-success-text",
|
|
15
|
+
warn: "bg-status-warning-bg/20 text-status-warning-text",
|
|
16
|
+
fail: "bg-status-error-bg/20 text-status-error-text",
|
|
17
|
+
skipped: "bg-bg-sunken text-text-tertiary"
|
|
18
|
+
};
|
|
19
|
+
function m(e) {
|
|
20
|
+
switch (e) {
|
|
21
|
+
case "queued": return /* @__PURE__ */ u(i, {
|
|
22
|
+
className: "size-3.5",
|
|
23
|
+
"aria-hidden": "true"
|
|
24
|
+
});
|
|
25
|
+
case "running": return /* @__PURE__ */ u(a, {
|
|
26
|
+
className: "size-3.5 animate-spin",
|
|
27
|
+
"aria-hidden": "true"
|
|
28
|
+
});
|
|
29
|
+
case "succeeded": return /* @__PURE__ */ u(r, {
|
|
30
|
+
className: "size-3.5",
|
|
31
|
+
"aria-hidden": "true"
|
|
32
|
+
});
|
|
33
|
+
case "failed":
|
|
34
|
+
case "errored": return /* @__PURE__ */ u(c, {
|
|
35
|
+
className: "size-3.5",
|
|
36
|
+
"aria-hidden": "true"
|
|
37
|
+
});
|
|
38
|
+
case "cancelled": return /* @__PURE__ */ u(o, {
|
|
39
|
+
className: "size-3.5",
|
|
40
|
+
"aria-hidden": "true"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function h(e) {
|
|
45
|
+
if (!e) return "—";
|
|
46
|
+
let t = Date.now() - e, n = Math.round(t / 1e3);
|
|
47
|
+
if (n < 60) return `${n}s ago`;
|
|
48
|
+
let r = Math.round(n / 60);
|
|
49
|
+
if (r < 60) return `${r}m ago`;
|
|
50
|
+
let i = Math.round(r / 60);
|
|
51
|
+
return i < 24 ? `${i}h ago` : `${Math.round(i / 24)}d ago`;
|
|
52
|
+
}
|
|
53
|
+
function g(e) {
|
|
54
|
+
if (e == null) return "—";
|
|
55
|
+
if (e < 1e3) return `${e}ms`;
|
|
56
|
+
let t = e / 1e3;
|
|
57
|
+
if (t < 60) return `${t.toFixed(1)}s`;
|
|
58
|
+
let n = t / 60;
|
|
59
|
+
return n < 60 ? `${n.toFixed(1)}m` : `${(n / 60).toFixed(1)}h`;
|
|
60
|
+
}
|
|
61
|
+
function _(e) {
|
|
62
|
+
return e.replace(/\./g, " ");
|
|
63
|
+
}
|
|
64
|
+
function v({ run: e }) {
|
|
65
|
+
let t = e.summary;
|
|
66
|
+
return t.critical + t.high + t.medium + t.low + t.info === 0 ? /* @__PURE__ */ u("span", {
|
|
67
|
+
className: "text-xs text-text-tertiary",
|
|
68
|
+
children: "clean"
|
|
69
|
+
}) : /* @__PURE__ */ d("div", {
|
|
70
|
+
className: "flex items-center gap-1 text-xs",
|
|
71
|
+
children: [
|
|
72
|
+
t.critical > 0 && /* @__PURE__ */ d("span", {
|
|
73
|
+
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-error-bg/20 text-status-error-text font-mono",
|
|
74
|
+
title: `${t.critical} critical`,
|
|
75
|
+
children: ["C ", t.critical]
|
|
76
|
+
}),
|
|
77
|
+
t.high > 0 && /* @__PURE__ */ d("span", {
|
|
78
|
+
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg/20 text-status-warning-text font-mono",
|
|
79
|
+
title: `${t.high} high`,
|
|
80
|
+
children: ["H ", t.high]
|
|
81
|
+
}),
|
|
82
|
+
t.medium > 0 && /* @__PURE__ */ d("span", {
|
|
83
|
+
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg/10 text-status-warning-text/80 font-mono",
|
|
84
|
+
title: `${t.medium} medium`,
|
|
85
|
+
children: ["M ", t.medium]
|
|
86
|
+
}),
|
|
87
|
+
t.low > 0 && /* @__PURE__ */ d("span", {
|
|
88
|
+
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-bg-sunken text-text-secondary font-mono",
|
|
89
|
+
title: `${t.low} low`,
|
|
90
|
+
children: ["L ", t.low]
|
|
91
|
+
}),
|
|
92
|
+
t.info > 0 && /* @__PURE__ */ d("span", {
|
|
93
|
+
className: "inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-bg-sunken text-text-tertiary font-mono",
|
|
94
|
+
title: `${t.info} info`,
|
|
95
|
+
children: ["I ", t.info]
|
|
96
|
+
})
|
|
97
|
+
]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function y({ rowKey: e }) {
|
|
101
|
+
return /* @__PURE__ */ d("tr", {
|
|
102
|
+
className: "border-t border-border-subtle",
|
|
103
|
+
children: [
|
|
104
|
+
/* @__PURE__ */ u("td", {
|
|
105
|
+
className: "px-3 py-2",
|
|
106
|
+
children: /* @__PURE__ */ u("div", { className: "h-3 w-20 rounded bg-bg-sunken animate-pulse" })
|
|
107
|
+
}),
|
|
108
|
+
/* @__PURE__ */ u("td", {
|
|
109
|
+
className: "px-3 py-2",
|
|
110
|
+
children: /* @__PURE__ */ u("div", { className: "h-3 w-24 rounded bg-bg-sunken animate-pulse" })
|
|
111
|
+
}),
|
|
112
|
+
/* @__PURE__ */ u("td", {
|
|
113
|
+
className: "px-3 py-2",
|
|
114
|
+
children: /* @__PURE__ */ u("div", { className: "h-3 w-16 rounded bg-bg-sunken animate-pulse" })
|
|
115
|
+
}),
|
|
116
|
+
/* @__PURE__ */ u("td", {
|
|
117
|
+
className: "px-3 py-2",
|
|
118
|
+
children: /* @__PURE__ */ u("div", { className: "h-3 w-16 rounded bg-bg-sunken animate-pulse" })
|
|
119
|
+
}),
|
|
120
|
+
/* @__PURE__ */ u("td", {
|
|
121
|
+
className: "px-3 py-2",
|
|
122
|
+
children: /* @__PURE__ */ u("div", { className: "h-3 w-20 rounded bg-bg-sunken animate-pulse" })
|
|
123
|
+
}),
|
|
124
|
+
/* @__PURE__ */ u("td", {
|
|
125
|
+
className: "px-3 py-2",
|
|
126
|
+
children: /* @__PURE__ */ u("div", { className: "h-3 w-14 rounded bg-bg-sunken animate-pulse" })
|
|
127
|
+
}),
|
|
128
|
+
/* @__PURE__ */ u("td", {
|
|
129
|
+
className: "px-3 py-2",
|
|
130
|
+
children: /* @__PURE__ */ u("div", { className: "h-3 w-28 rounded bg-bg-sunken animate-pulse" })
|
|
131
|
+
})
|
|
132
|
+
]
|
|
133
|
+
}, e);
|
|
134
|
+
}
|
|
135
|
+
function b({ vibeId: r, agentId: i, tunnelUrl: a, apiKey: o, profile: c, limit: b = 20 }) {
|
|
136
|
+
let { data: x, isLoading: S, error: C, isFetching: w, refetch: T } = e(t(() => ({
|
|
137
|
+
agentId: i,
|
|
138
|
+
tunnelUrl: a,
|
|
139
|
+
apiKey: o,
|
|
140
|
+
profile: c
|
|
141
|
+
}), [
|
|
142
|
+
i,
|
|
143
|
+
a,
|
|
144
|
+
o,
|
|
145
|
+
c
|
|
146
|
+
]), r, b);
|
|
147
|
+
return i && a && o ? /* @__PURE__ */ d("section", {
|
|
148
|
+
"data-testid": "security-scan-list",
|
|
149
|
+
className: "rounded-lg border border-border-default bg-bg-surface",
|
|
150
|
+
children: [
|
|
151
|
+
/* @__PURE__ */ d("header", {
|
|
152
|
+
className: "flex items-center justify-between gap-3 border-b border-border-default px-4 py-3",
|
|
153
|
+
children: [/* @__PURE__ */ d("div", {
|
|
154
|
+
className: "min-w-0",
|
|
155
|
+
children: [/* @__PURE__ */ u("h3", {
|
|
156
|
+
className: "text-sm font-semibold text-text-primary",
|
|
157
|
+
children: "Recent scan runs"
|
|
158
|
+
}), /* @__PURE__ */ d("p", {
|
|
159
|
+
className: "text-xs text-text-secondary mt-0.5",
|
|
160
|
+
children: [
|
|
161
|
+
"Read live from the agent. Showing up to ",
|
|
162
|
+
b,
|
|
163
|
+
" most recently active runs for this vibe."
|
|
164
|
+
]
|
|
165
|
+
})]
|
|
166
|
+
}), /* @__PURE__ */ u("button", {
|
|
167
|
+
type: "button",
|
|
168
|
+
onClick: () => void T(),
|
|
169
|
+
disabled: w,
|
|
170
|
+
className: "px-2.5 py-1 text-xs rounded-md border border-border-default bg-bg-sunken text-text-primary hover:bg-action-primary-bg-hover disabled:opacity-50",
|
|
171
|
+
children: w ? "Refreshing…" : "Refresh"
|
|
172
|
+
})]
|
|
173
|
+
}),
|
|
174
|
+
C && /* @__PURE__ */ d("div", {
|
|
175
|
+
className: "m-4 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg/10 p-3 text-sm",
|
|
176
|
+
children: [/* @__PURE__ */ u(n, { className: "size-4 text-status-error-text mt-0.5 shrink-0" }), /* @__PURE__ */ d("div", {
|
|
177
|
+
className: "text-text-primary",
|
|
178
|
+
children: [/* @__PURE__ */ u("p", {
|
|
179
|
+
className: "font-medium",
|
|
180
|
+
children: "Couldn't load scan runs"
|
|
181
|
+
}), /* @__PURE__ */ u("p", {
|
|
182
|
+
className: "mt-0.5 text-xs text-text-secondary",
|
|
183
|
+
children: C.message
|
|
184
|
+
})]
|
|
185
|
+
})]
|
|
186
|
+
}),
|
|
187
|
+
/* @__PURE__ */ u("div", {
|
|
188
|
+
className: "overflow-x-auto",
|
|
189
|
+
children: /* @__PURE__ */ d("table", {
|
|
190
|
+
className: "w-full text-sm",
|
|
191
|
+
children: [/* @__PURE__ */ u("thead", {
|
|
192
|
+
className: "bg-bg-sunken/40 text-text-secondary",
|
|
193
|
+
children: /* @__PURE__ */ d("tr", { children: [
|
|
194
|
+
/* @__PURE__ */ u("th", {
|
|
195
|
+
className: "px-3 py-2 text-left text-xs font-medium",
|
|
196
|
+
children: "Stage"
|
|
197
|
+
}),
|
|
198
|
+
/* @__PURE__ */ u("th", {
|
|
199
|
+
className: "px-3 py-2 text-left text-xs font-medium",
|
|
200
|
+
children: "Provider"
|
|
201
|
+
}),
|
|
202
|
+
/* @__PURE__ */ u("th", {
|
|
203
|
+
className: "px-3 py-2 text-left text-xs font-medium",
|
|
204
|
+
children: "Status"
|
|
205
|
+
}),
|
|
206
|
+
/* @__PURE__ */ u("th", {
|
|
207
|
+
className: "px-3 py-2 text-left text-xs font-medium",
|
|
208
|
+
children: "Conclusion"
|
|
209
|
+
}),
|
|
210
|
+
/* @__PURE__ */ u("th", {
|
|
211
|
+
className: "px-3 py-2 text-left text-xs font-medium",
|
|
212
|
+
children: "Started"
|
|
213
|
+
}),
|
|
214
|
+
/* @__PURE__ */ u("th", {
|
|
215
|
+
className: "px-3 py-2 text-left text-xs font-medium",
|
|
216
|
+
children: "Duration"
|
|
217
|
+
}),
|
|
218
|
+
/* @__PURE__ */ u("th", {
|
|
219
|
+
className: "px-3 py-2 text-left text-xs font-medium",
|
|
220
|
+
children: "Findings"
|
|
221
|
+
})
|
|
222
|
+
] })
|
|
223
|
+
}), /* @__PURE__ */ d("tbody", { children: [
|
|
224
|
+
S && x === void 0 && /* @__PURE__ */ d(l, { children: [
|
|
225
|
+
/* @__PURE__ */ u(y, { rowKey: 0 }),
|
|
226
|
+
/* @__PURE__ */ u(y, { rowKey: 1 }),
|
|
227
|
+
/* @__PURE__ */ u(y, { rowKey: 2 })
|
|
228
|
+
] }),
|
|
229
|
+
!S && (x?.length ?? 0) === 0 && !C && /* @__PURE__ */ u("tr", { children: /* @__PURE__ */ u("td", {
|
|
230
|
+
colSpan: 7,
|
|
231
|
+
className: "px-3 py-8",
|
|
232
|
+
children: /* @__PURE__ */ d("div", {
|
|
233
|
+
className: "flex flex-col items-center gap-2 text-center",
|
|
234
|
+
children: [
|
|
235
|
+
/* @__PURE__ */ u(s, { className: "size-6 text-text-tertiary" }),
|
|
236
|
+
/* @__PURE__ */ u("p", {
|
|
237
|
+
className: "text-sm text-text-primary",
|
|
238
|
+
children: "No scans yet"
|
|
239
|
+
}),
|
|
240
|
+
/* @__PURE__ */ u("p", {
|
|
241
|
+
className: "text-xs text-text-secondary max-w-md",
|
|
242
|
+
children: "Install a provider above and run a scan to see results here. Findings live in the agent's local SQLite cache."
|
|
243
|
+
})
|
|
244
|
+
]
|
|
245
|
+
})
|
|
246
|
+
}) }),
|
|
247
|
+
(x ?? []).map((e) => /* @__PURE__ */ d("tr", {
|
|
248
|
+
className: "border-t border-border-subtle align-middle",
|
|
249
|
+
children: [
|
|
250
|
+
/* @__PURE__ */ u("td", {
|
|
251
|
+
className: "px-3 py-2",
|
|
252
|
+
children: /* @__PURE__ */ u("span", {
|
|
253
|
+
className: "inline-flex items-center rounded-md bg-bg-sunken px-2 py-0.5 text-xs text-text-primary",
|
|
254
|
+
children: _(e.stage)
|
|
255
|
+
})
|
|
256
|
+
}),
|
|
257
|
+
/* @__PURE__ */ u("td", {
|
|
258
|
+
className: "px-3 py-2 text-xs text-text-primary",
|
|
259
|
+
children: e.providerName
|
|
260
|
+
}),
|
|
261
|
+
/* @__PURE__ */ u("td", {
|
|
262
|
+
className: "px-3 py-2",
|
|
263
|
+
children: /* @__PURE__ */ d("span", {
|
|
264
|
+
className: `inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium ${f[e.status]}`,
|
|
265
|
+
children: [m(e.status), e.status]
|
|
266
|
+
})
|
|
267
|
+
}),
|
|
268
|
+
/* @__PURE__ */ u("td", {
|
|
269
|
+
className: "px-3 py-2",
|
|
270
|
+
children: e.conclusion ? /* @__PURE__ */ u("span", {
|
|
271
|
+
className: `inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${p[e.conclusion]}`,
|
|
272
|
+
children: e.conclusion
|
|
273
|
+
}) : /* @__PURE__ */ u("span", {
|
|
274
|
+
className: "text-xs text-text-tertiary",
|
|
275
|
+
children: "—"
|
|
276
|
+
})
|
|
277
|
+
}),
|
|
278
|
+
/* @__PURE__ */ u("td", {
|
|
279
|
+
className: "px-3 py-2 text-xs text-text-secondary",
|
|
280
|
+
children: /* @__PURE__ */ u("span", {
|
|
281
|
+
title: e.startedAt ? new Date(e.startedAt).toLocaleString() : "",
|
|
282
|
+
children: h(e.startedAt)
|
|
283
|
+
})
|
|
284
|
+
}),
|
|
285
|
+
/* @__PURE__ */ u("td", {
|
|
286
|
+
className: "px-3 py-2 text-xs text-text-secondary font-mono",
|
|
287
|
+
children: g(e.durationMs)
|
|
288
|
+
}),
|
|
289
|
+
/* @__PURE__ */ u("td", {
|
|
290
|
+
className: "px-3 py-2",
|
|
291
|
+
children: /* @__PURE__ */ u(v, { run: e })
|
|
292
|
+
})
|
|
293
|
+
]
|
|
294
|
+
}, e.runId))
|
|
295
|
+
] })]
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
]
|
|
299
|
+
}) : /* @__PURE__ */ u("section", {
|
|
300
|
+
"data-testid": "security-scan-list",
|
|
301
|
+
className: "rounded-lg border border-border-default bg-bg-surface p-4 text-sm text-text-secondary",
|
|
302
|
+
children: "Connect to a reachable agent to load scan history. Scan runs are read directly from the agent over the tunnel — there's no backend cache."
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
export { b as SecurityScanList };
|
|
307
|
+
|
|
308
|
+
//# sourceMappingURL=SecurityScanList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecurityScanList.js","names":[],"sources":["../../../src/components/security/SecurityScanList.tsx"],"sourcesContent":["/**\n * Recent scan runs list for a vibe, read directly from the agent over\n * the tunnel. No backend GraphQL — see `./agentClient.ts` for the\n * REST surface and `./hooks.ts` for the React Query wrappers.\n *\n * The agent doesn't expose a list-runs endpoint; we derive distinct\n * run ids from the findings cache (see `listRecentScanRuns`). When a\n * vibe has zero findings the list will be empty even if scans have\n * been queued — that's correct: an in-flight queued/running scan\n * doesn't yet have any findings, and the empty state already nudges\n * the user toward kicking off a scan.\n */\nimport { useMemo, type ReactElement } from 'react';\nimport {\n AlertCircle,\n CheckCircle2,\n Clock,\n Loader2,\n MinusCircle,\n ShieldAlert,\n XCircle,\n} from 'lucide-react';\n\nimport { useSecurityRecentScansQuery, type UseSecurityAgentContext } from './hooks';\nimport type { AgentScanConclusion, AgentScanStatus, SecurityScanRun } from './agentClient';\n\ninterface Props extends UseSecurityAgentContext {\n vibeId: string;\n /** Max rows to surface. Defaults to 20. */\n limit?: number;\n}\n\nconst STATUS_BADGE: Record<AgentScanStatus, string> = {\n queued: 'bg-bg-sunken text-text-secondary',\n running: 'bg-status-info-bg/20 text-status-info-text',\n succeeded: 'bg-status-success-bg/20 text-status-success-text',\n failed: 'bg-status-error-bg/20 text-status-error-text',\n errored: 'bg-status-error-bg/20 text-status-error-text',\n cancelled: 'bg-bg-sunken text-text-tertiary',\n};\n\nconst CONCLUSION_BADGE: Record<AgentScanConclusion, string> = {\n pass: 'bg-status-success-bg/20 text-status-success-text',\n warn: 'bg-status-warning-bg/20 text-status-warning-text',\n fail: 'bg-status-error-bg/20 text-status-error-text',\n skipped: 'bg-bg-sunken text-text-tertiary',\n};\n\nfunction statusIcon(status: AgentScanStatus): ReactElement {\n switch (status) {\n case 'queued':\n return <Clock className=\"size-3.5\" aria-hidden=\"true\" />;\n case 'running':\n return <Loader2 className=\"size-3.5 animate-spin\" aria-hidden=\"true\" />;\n case 'succeeded':\n return <CheckCircle2 className=\"size-3.5\" aria-hidden=\"true\" />;\n case 'failed':\n case 'errored':\n return <XCircle className=\"size-3.5\" aria-hidden=\"true\" />;\n case 'cancelled':\n return <MinusCircle className=\"size-3.5\" aria-hidden=\"true\" />;\n }\n}\n\nfunction relativeTime(epochMs?: number): string {\n if (!epochMs) return '—';\n const diff = Date.now() - epochMs;\n const sec = Math.round(diff / 1000);\n if (sec < 60) return `${sec}s ago`;\n const min = Math.round(sec / 60);\n if (min < 60) return `${min}m ago`;\n const hr = Math.round(min / 60);\n if (hr < 24) return `${hr}h ago`;\n const day = Math.round(hr / 24);\n return `${day}d ago`;\n}\n\nfunction formatDuration(ms?: number): string {\n if (ms === undefined || ms === null) return '—';\n if (ms < 1000) return `${ms}ms`;\n const sec = ms / 1000;\n if (sec < 60) return `${sec.toFixed(1)}s`;\n const min = sec / 60;\n if (min < 60) return `${min.toFixed(1)}m`;\n const hr = min / 60;\n return `${hr.toFixed(1)}h`;\n}\n\n/**\n * Prettify the agent's dotted stage id (e.g. `pull_request.fast` →\n * `pull_request fast`). We don't gate on the canonical 14-stage taxonomy\n * here because providers can — in principle — emit arbitrary stages and\n * we want to surface them rather than hide them.\n */\nfunction formatStage(stage: string): string {\n return stage.replace(/\\./g, ' ');\n}\n\nfunction SummaryCell({ run }: { run: SecurityScanRun }): ReactElement {\n const s = run.summary;\n const total = s.critical + s.high + s.medium + s.low + s.info;\n if (total === 0) {\n return <span className=\"text-xs text-text-tertiary\">clean</span>;\n }\n return (\n <div className=\"flex items-center gap-1 text-xs\">\n {s.critical > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-error-bg/20 text-status-error-text font-mono\"\n title={`${s.critical} critical`}\n >\n C {s.critical}\n </span>\n )}\n {s.high > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg/20 text-status-warning-text font-mono\"\n title={`${s.high} high`}\n >\n H {s.high}\n </span>\n )}\n {s.medium > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-status-warning-bg/10 text-status-warning-text/80 font-mono\"\n title={`${s.medium} medium`}\n >\n M {s.medium}\n </span>\n )}\n {s.low > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-bg-sunken text-text-secondary font-mono\"\n title={`${s.low} low`}\n >\n L {s.low}\n </span>\n )}\n {s.info > 0 && (\n <span\n className=\"inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 bg-bg-sunken text-text-tertiary font-mono\"\n title={`${s.info} info`}\n >\n I {s.info}\n </span>\n )}\n </div>\n );\n}\n\nfunction SkeletonRow({ rowKey }: { rowKey: number }): ReactElement {\n return (\n <tr key={rowKey} className=\"border-t border-border-subtle\">\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-20 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-24 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-16 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-16 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-20 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-14 rounded bg-bg-sunken animate-pulse\" />\n </td>\n <td className=\"px-3 py-2\">\n <div className=\"h-3 w-28 rounded bg-bg-sunken animate-pulse\" />\n </td>\n </tr>\n );\n}\n\nexport function SecurityScanList({\n vibeId,\n agentId,\n tunnelUrl,\n apiKey,\n profile,\n limit = 20,\n}: Props): ReactElement {\n const ctx = useMemo(\n () => ({ agentId, tunnelUrl, apiKey, profile }),\n [agentId, tunnelUrl, apiKey, profile]\n );\n const { data, isLoading, error, isFetching, refetch } = useSecurityRecentScansQuery(\n ctx,\n vibeId,\n limit\n );\n\n const ready = !!agentId && !!tunnelUrl && !!apiKey;\n\n if (!ready) {\n return (\n <section\n data-testid=\"security-scan-list\"\n className=\"rounded-lg border border-border-default bg-bg-surface p-4 text-sm text-text-secondary\"\n >\n Connect to a reachable agent to load scan history. Scan runs are read directly from the\n agent over the tunnel — there's no backend cache.\n </section>\n );\n }\n\n return (\n <section\n data-testid=\"security-scan-list\"\n className=\"rounded-lg border border-border-default bg-bg-surface\"\n >\n <header className=\"flex items-center justify-between gap-3 border-b border-border-default px-4 py-3\">\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Recent scan runs</h3>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n Read live from the agent. Showing up to {limit} most recently active runs for this vibe.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => void refetch()}\n disabled={isFetching}\n className=\"px-2.5 py-1 text-xs rounded-md border border-border-default bg-bg-sunken text-text-primary hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {isFetching ? 'Refreshing…' : 'Refresh'}\n </button>\n </header>\n\n {error && (\n <div className=\"m-4 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg/10 p-3 text-sm\">\n <AlertCircle className=\"size-4 text-status-error-text mt-0.5 shrink-0\" />\n <div className=\"text-text-primary\">\n <p className=\"font-medium\">Couldn't load scan runs</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">{error.message}</p>\n </div>\n </div>\n )}\n\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-bg-sunken/40 text-text-secondary\">\n <tr>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Stage</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Provider</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Status</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Conclusion</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Started</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Duration</th>\n <th className=\"px-3 py-2 text-left text-xs font-medium\">Findings</th>\n </tr>\n </thead>\n <tbody>\n {isLoading && data === undefined && (\n <>\n <SkeletonRow rowKey={0} />\n <SkeletonRow rowKey={1} />\n <SkeletonRow rowKey={2} />\n </>\n )}\n {!isLoading && (data?.length ?? 0) === 0 && !error && (\n <tr>\n <td colSpan={7} className=\"px-3 py-8\">\n <div className=\"flex flex-col items-center gap-2 text-center\">\n <ShieldAlert className=\"size-6 text-text-tertiary\" />\n <p className=\"text-sm text-text-primary\">No scans yet</p>\n <p className=\"text-xs text-text-secondary max-w-md\">\n Install a provider above and run a scan to see results here. Findings live in\n the agent's local SQLite cache.\n </p>\n </div>\n </td>\n </tr>\n )}\n {(data ?? []).map((run) => (\n <tr key={run.runId} className=\"border-t border-border-subtle align-middle\">\n <td className=\"px-3 py-2\">\n <span className=\"inline-flex items-center rounded-md bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\">\n {formatStage(run.stage)}\n </span>\n </td>\n <td className=\"px-3 py-2 text-xs text-text-primary\">{run.providerName}</td>\n <td className=\"px-3 py-2\">\n <span\n className={`inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium ${STATUS_BADGE[run.status]}`}\n >\n {statusIcon(run.status)}\n {run.status}\n </span>\n </td>\n <td className=\"px-3 py-2\">\n {run.conclusion ? (\n <span\n className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${CONCLUSION_BADGE[run.conclusion]}`}\n >\n {run.conclusion}\n </span>\n ) : (\n <span className=\"text-xs text-text-tertiary\">—</span>\n )}\n </td>\n <td className=\"px-3 py-2 text-xs text-text-secondary\">\n <span title={run.startedAt ? new Date(run.startedAt).toLocaleString() : ''}>\n {relativeTime(run.startedAt)}\n </span>\n </td>\n <td className=\"px-3 py-2 text-xs text-text-secondary font-mono\">\n {formatDuration(run.durationMs)}\n </td>\n <td className=\"px-3 py-2\">\n <SummaryCell run={run} />\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </section>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,48 +1,52 @@
|
|
|
1
1
|
import { SECURITY_META_PACKAGE as e, SECURITY_PROVIDER_PACKAGES as t } from "../../constants/pluginCatalog.js";
|
|
2
2
|
import { PluginGate as n } from "../shared/PluginGate.js";
|
|
3
3
|
import { PluginHarnessPicker as r } from "../ai/PluginHarnessPicker.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { SecurityScanList as i } from "./SecurityScanList.js";
|
|
5
|
+
import { Shield as a } from "lucide-react";
|
|
6
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
6
7
|
//#region src/components/security/SecurityTabPanel.tsx
|
|
7
|
-
function
|
|
8
|
-
return /* @__PURE__ */
|
|
8
|
+
function c({ vibeId: c, agentId: l, profile: u = "default", tunnelUrl: d = null, apiKey: f = null }) {
|
|
9
|
+
return /* @__PURE__ */ s("div", {
|
|
9
10
|
"data-testid": "security-tab-panel",
|
|
10
11
|
className: "space-y-4",
|
|
11
|
-
children: [/* @__PURE__ */
|
|
12
|
+
children: [/* @__PURE__ */ s("header", {
|
|
12
13
|
className: "flex items-start gap-3",
|
|
13
|
-
children: [/* @__PURE__ */
|
|
14
|
+
children: [/* @__PURE__ */ o("div", {
|
|
14
15
|
className: "size-9 rounded-md bg-bg-sunken flex items-center justify-center shrink-0",
|
|
15
|
-
children: /* @__PURE__ */ a
|
|
16
|
-
}), /* @__PURE__ */
|
|
16
|
+
children: /* @__PURE__ */ o(a, { className: "size-4 text-text-secondary" })
|
|
17
|
+
}), /* @__PURE__ */ s("div", {
|
|
17
18
|
className: "min-w-0",
|
|
18
|
-
children: [/* @__PURE__ */
|
|
19
|
+
children: [/* @__PURE__ */ o("h2", {
|
|
19
20
|
className: "text-base font-semibold text-text-primary",
|
|
20
21
|
children: "Security"
|
|
21
|
-
}), /* @__PURE__ */
|
|
22
|
+
}), /* @__PURE__ */ o("p", {
|
|
22
23
|
className: "text-sm text-text-secondary mt-0.5",
|
|
23
24
|
children: "Install + run security scans across the 14-stage lifecycle. Plugins run on the agent; findings live in its local SQLite cache."
|
|
24
25
|
})]
|
|
25
26
|
})]
|
|
26
|
-
}), /* @__PURE__ */
|
|
27
|
-
agentId:
|
|
27
|
+
}), /* @__PURE__ */ s(n, {
|
|
28
|
+
agentId: l,
|
|
28
29
|
required: [e],
|
|
29
30
|
mode: "all",
|
|
30
31
|
eyebrow: "Security plugin required",
|
|
31
32
|
title: "Install the Security Orchestrator",
|
|
32
33
|
description: "@vibecontrols/vibe-plugin-security routes scans across per-stage providers. Required before installing individual stage providers.",
|
|
33
|
-
children: [/* @__PURE__ */
|
|
34
|
+
children: [/* @__PURE__ */ o(r, {
|
|
34
35
|
packages: t,
|
|
35
|
-
agentId:
|
|
36
|
+
agentId: l,
|
|
36
37
|
title: "Security Providers",
|
|
37
38
|
description: "One plugin per stage in the security lifecycle. Install the providers you want to use; uninstalled stages stay skipped."
|
|
38
|
-
}), /* @__PURE__ */
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
}), /* @__PURE__ */ o(i, {
|
|
40
|
+
vibeId: c,
|
|
41
|
+
agentId: l,
|
|
42
|
+
tunnelUrl: d,
|
|
43
|
+
apiKey: f,
|
|
44
|
+
profile: u
|
|
41
45
|
})]
|
|
42
46
|
})]
|
|
43
47
|
});
|
|
44
48
|
}
|
|
45
49
|
//#endregion
|
|
46
|
-
export {
|
|
50
|
+
export { c as SecurityTabPanel };
|
|
47
51
|
|
|
48
52
|
//# sourceMappingURL=SecurityTabPanel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecurityTabPanel.js","names":[],"sources":["../../../src/components/security/SecurityTabPanel.tsx"],"sourcesContent":["/**\n * Security tab on Vibe detail.\n *\n * Wave 1
|
|
1
|
+
{"version":3,"file":"SecurityTabPanel.js","names":[],"sources":["../../../src/components/security/SecurityTabPanel.tsx"],"sourcesContent":["/**\n * Security tab on Vibe detail.\n *\n * Wave 1: surface the security plugins so the user can find +\n * install them from the UI (same pattern as the AI page).\n *\n * Wave 2 (this commit): once the meta plugin is installed, mount\n * `SecurityScanList` which reads recent scan runs straight from the\n * agent over the tunnel (`/api/profiles/<profile>/security/*`). No\n * backend GraphQL involvement — all per-vibe scan history lives in\n * the agent's local SQLite cache.\n */\n\nimport { Shield } from 'lucide-react';\nimport { PluginGate } from '../shared/PluginGate';\nimport { PluginHarnessPicker } from '../ai/PluginHarnessPicker';\nimport { SECURITY_META_PACKAGE, SECURITY_PROVIDER_PACKAGES } from '@/constants/pluginCatalog';\nimport { SecurityScanList } from './SecurityScanList';\n\ninterface PanelProps {\n vibeId: string;\n agentId: string | null;\n /**\n * Canonical agent profile name. Threaded through from VibeDetailsPage's\n * `useActiveAgentProfile` so the tab targets the same profile the rest\n * of the page is talking to.\n */\n profile?: string;\n /** Agent tunnel URL. Null when the active agent is unreachable. */\n tunnelUrl?: string | null;\n /** Agent API key. Null when the agent hasn't published one yet. */\n apiKey?: string | null;\n}\n\nexport function SecurityTabPanel({\n vibeId,\n agentId,\n profile = 'default',\n tunnelUrl = null,\n apiKey = null,\n}: PanelProps): React.ReactElement {\n return (\n <div data-testid=\"security-tab-panel\" className=\"space-y-4\">\n <header className=\"flex items-start gap-3\">\n <div className=\"size-9 rounded-md bg-bg-sunken flex items-center justify-center shrink-0\">\n <Shield className=\"size-4 text-text-secondary\" />\n </div>\n <div className=\"min-w-0\">\n <h2 className=\"text-base font-semibold text-text-primary\">Security</h2>\n <p className=\"text-sm text-text-secondary mt-0.5\">\n Install + run security scans across the 14-stage lifecycle. Plugins run on the agent;\n findings live in its local SQLite cache.\n </p>\n </div>\n </header>\n\n <PluginGate\n agentId={agentId}\n required={[SECURITY_META_PACKAGE]}\n mode=\"all\"\n eyebrow=\"Security plugin required\"\n title=\"Install the Security Orchestrator\"\n description=\"@vibecontrols/vibe-plugin-security routes scans across per-stage providers. Required before installing individual stage providers.\"\n >\n <PluginHarnessPicker\n packages={SECURITY_PROVIDER_PACKAGES}\n agentId={agentId}\n title=\"Security Providers\"\n description=\"One plugin per stage in the security lifecycle. Install the providers you want to use; uninstalled stages stay skipped.\"\n />\n <SecurityScanList\n vibeId={vibeId}\n agentId={agentId}\n tunnelUrl={tunnelUrl}\n apiKey={apiKey}\n profile={profile}\n />\n </PluginGate>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAkCA,SAAgB,EAAiB,EAC/B,WACA,YACA,aAAU,WACV,eAAY,MACZ,YAAS,QACwB;AACjC,QACE,kBAAC,OAAD;EAAK,eAAY;EAAqB,WAAU;YAAhD,CACE,kBAAC,UAAD;GAAQ,WAAU;aAAlB,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAQ,WAAU,8BAA+B,CAAA;IAC7C,CAAA,EACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAA4C;KAAa,CAAA,EACvE,kBAAC,KAAD;KAAG,WAAU;eAAqC;KAG9C,CAAA,CACA;MACC;MAET,kBAAC,GAAD;GACW;GACT,UAAU,CAAC,EAAsB;GACjC,MAAK;GACL,SAAQ;GACR,OAAM;GACN,aAAY;aANd,CAQE,kBAAC,GAAD;IACE,UAAU;IACD;IACT,OAAM;IACN,aAAY;IACZ,CAAA,EACF,kBAAC,GAAD;IACU;IACC;IACE;IACH;IACC;IACT,CAAA,CACS;KACT"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/components/security/agentClient.ts
|
|
2
|
+
var e = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
constructor(e, t, n) {
|
|
6
|
+
super(n), this.name = "SecurityAgentError", this.code = e, this.status = t;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function t(e) {
|
|
10
|
+
return `${e.tunnelUrl.replace(/\/$/, "")}/api/profiles/${encodeURIComponent(e.profile)}/security`;
|
|
11
|
+
}
|
|
12
|
+
function n(e, t) {
|
|
13
|
+
return {
|
|
14
|
+
"x-agent-api-key": e.apiKey,
|
|
15
|
+
Accept: "application/json",
|
|
16
|
+
...t ?? {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function r(t, n) {
|
|
20
|
+
if (!t.ok) {
|
|
21
|
+
let r = "UPSTREAM", i = `security ${n} ${t.status}`;
|
|
22
|
+
try {
|
|
23
|
+
let e = await t.json();
|
|
24
|
+
e.code && (r = e.code), e.error && (i = e.error);
|
|
25
|
+
} catch {}
|
|
26
|
+
throw new e(r, t.status, i);
|
|
27
|
+
}
|
|
28
|
+
return await t.json();
|
|
29
|
+
}
|
|
30
|
+
async function i(e, i) {
|
|
31
|
+
return r(await fetch(`${t(e)}${i}`, { headers: n(e) }), i);
|
|
32
|
+
}
|
|
33
|
+
async function a(e, i, a) {
|
|
34
|
+
return r(await fetch(`${t(e)}${i}`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: n(e, { "Content-Type": "application/json" }),
|
|
37
|
+
body: a === void 0 ? void 0 : JSON.stringify(a)
|
|
38
|
+
}), i);
|
|
39
|
+
}
|
|
40
|
+
var o = {
|
|
41
|
+
listProviders(e) {
|
|
42
|
+
return i(e, "/providers");
|
|
43
|
+
},
|
|
44
|
+
listFindings(e, t = {}) {
|
|
45
|
+
let n = new URLSearchParams();
|
|
46
|
+
t.vibeId && n.set("vibeId", t.vibeId), t.severity && n.set("severity", t.severity), t.status && n.set("status", t.status), t.limit !== void 0 && n.set("limit", String(t.limit));
|
|
47
|
+
let r = n.toString();
|
|
48
|
+
return i(e, `/findings${r ? `?${r}` : ""}`);
|
|
49
|
+
},
|
|
50
|
+
async getScan(e, t) {
|
|
51
|
+
return (await i(e, `/scan/${encodeURIComponent(t)}`)).run;
|
|
52
|
+
},
|
|
53
|
+
startScan(e, t) {
|
|
54
|
+
return a(e, "/scan", t);
|
|
55
|
+
},
|
|
56
|
+
cancelScan(e, t) {
|
|
57
|
+
return a(e, `/scan/${encodeURIComponent(t)}/cancel`);
|
|
58
|
+
},
|
|
59
|
+
async listRecentScanRuns(t, n, r = 20) {
|
|
60
|
+
let i = await o.listFindings(t, {
|
|
61
|
+
vibeId: n,
|
|
62
|
+
limit: Math.min(1e3, Math.max(100, r * 25))
|
|
63
|
+
}), a = /* @__PURE__ */ new Set(), s = [];
|
|
64
|
+
for (let e of i.findings) if (!a.has(e.scanRunId) && (a.add(e.scanRunId), s.push(e.scanRunId), s.length >= r)) break;
|
|
65
|
+
return (await Promise.all(s.map(async (n) => {
|
|
66
|
+
try {
|
|
67
|
+
return await o.getScan(t, n);
|
|
68
|
+
} catch (t) {
|
|
69
|
+
if (t instanceof e && t.status === 404) return null;
|
|
70
|
+
throw t;
|
|
71
|
+
}
|
|
72
|
+
}))).filter((e) => e !== null);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { e as SecurityAgentError, o as securityAgent };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=agentClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentClient.js","names":[],"sources":["../../../src/components/security/agentClient.ts"],"sourcesContent":["/**\n * Agent-tunnel REST client for the security meta plugin.\n *\n * All calls go from the browser straight to the agent's\n * `/api/profiles/<profile>/security/*` routes via the tunnel URL.\n * No backend GraphQL hop — same direct-tunnel pattern as\n * `src/services/gitopsApi.ts`. Auth header: `x-agent-api-key`.\n *\n * The agent mounts `vibe-plugin-security` with `apiPrefix: \"/api/security\"`;\n * the agent's `dispatchProfileRequest` strips `/api/profiles/<profile>`\n * and prepends `/api`, so the canonical external path collapses to\n * `/api/profiles/<profile>/security/<endpoint>`.\n */\nimport type { SecuritySummary } from './types';\n\nexport interface SecurityAgentOpts {\n /** Agent tunnel URL (HTTPS), e.g. https://abc123.tunnels.example.com. */\n tunnelUrl: string;\n /** Agent API key (rotated + persisted per agent). */\n apiKey: string;\n /** Canonical profile name (e.g. \"default\"). */\n profile: string;\n}\n\nexport class SecurityAgentError extends Error {\n readonly code: string;\n readonly status: number;\n constructor(code: string, status: number, message: string) {\n super(message);\n this.name = 'SecurityAgentError';\n this.code = code;\n this.status = status;\n }\n}\n\n/** Agent-side severity enum (lowercase) — see vibe-plugin-security types.ts. */\nexport type AgentSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info';\n\n/** Agent-side finding status enum (lowercase). */\nexport type AgentFindingStatus = 'open' | 'accepted' | 'fixed' | 'false_positive' | 'suppressed';\n\n/** Agent-side scan status enum (lowercase). */\nexport type AgentScanStatus =\n | 'queued'\n | 'running'\n | 'succeeded'\n | 'failed'\n | 'errored'\n | 'cancelled';\n\n/** Agent-side scan conclusion enum (lowercase). */\nexport type AgentScanConclusion = 'pass' | 'warn' | 'fail' | 'skipped';\n\n/** Agent-side policy level enum (lowercase). */\nexport type AgentPolicyLevel = 'advisory' | 'warn' | 'block';\n\n/** Agent-side stage enum — dotted form (e.g. `pull_request.fast`). */\nexport type AgentStage =\n | 'repo.onboard'\n | 'developer.local'\n | 'pull_request.fast'\n | 'pull_request.deep'\n | 'main.merge'\n | 'build'\n | 'package.publish'\n | 'deploy.preview'\n | 'deploy.alpha'\n | 'promote.prod'\n | 'runtime.continuous'\n | 'scheduled.rescan'\n | 'incident.response'\n | 'archive.offboard';\n\n/** Per-stage provider directory returned by GET /providers. */\nexport interface SecurityProvidersByStage {\n stages: Record<string, Array<{ name: string; isDefault: boolean }>>;\n}\n\n/** One scan run as surfaced by GET /scan/:runId. Mirrors `ScanRunRecord`. */\nexport interface SecurityScanRun {\n runId: string;\n vibeId: string;\n workspaceId: string;\n repoUrl: string;\n commit: string;\n stage: AgentStage;\n providerName: string;\n status: AgentScanStatus;\n conclusion?: AgentScanConclusion;\n startedAt?: number;\n finishedAt?: number;\n durationMs?: number;\n summary: SecuritySummary;\n policyLevel: AgentPolicyLevel;\n errorReason?: string;\n pushed?: boolean;\n}\n\n/** One finding as surfaced by GET /findings. Mirrors `FindingRecord`. */\nexport interface SecurityFinding {\n fingerprint: string;\n vibeId: string;\n scanRunId: string;\n ruleId: string;\n title: string;\n description?: string;\n severity: AgentSeverity;\n category: string;\n status: AgentFindingStatus;\n cwe?: string[];\n cve?: string;\n file?: string;\n line?: number;\n column?: number;\n packageName?: string;\n packageVersion?: string;\n fixedVersion?: string;\n remediation?: string;\n rawProviderRef?: string;\n redactedSample?: string;\n firstSeenAt: number;\n lastSeenAt: number;\n}\n\nfunction base(o: SecurityAgentOpts): string {\n return `${o.tunnelUrl.replace(/\\/$/, '')}/api/profiles/${encodeURIComponent(o.profile)}/security`;\n}\n\nfunction headers(o: SecurityAgentOpts, extra?: Record<string, string>): HeadersInit {\n return {\n 'x-agent-api-key': o.apiKey,\n Accept: 'application/json',\n ...(extra ?? {}),\n };\n}\n\nasync function handle<T>(res: Response, path: string): Promise<T> {\n if (!res.ok) {\n let code = 'UPSTREAM';\n let message = `security ${path} ${res.status}`;\n try {\n const body = (await res.json()) as { code?: string; error?: string };\n if (body.code) code = body.code;\n if (body.error) message = body.error;\n } catch {\n // body parse failure — keep default message\n }\n throw new SecurityAgentError(code, res.status, message);\n }\n return (await res.json()) as T;\n}\n\nasync function get<T>(o: SecurityAgentOpts, path: string): Promise<T> {\n const res = await fetch(`${base(o)}${path}`, { headers: headers(o) });\n return handle<T>(res, path);\n}\n\nasync function post<T>(o: SecurityAgentOpts, path: string, body?: unknown): Promise<T> {\n const res = await fetch(`${base(o)}${path}`, {\n method: 'POST',\n headers: headers(o, { 'Content-Type': 'application/json' }),\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n return handle<T>(res, path);\n}\n\nexport interface FindingsFilter {\n vibeId?: string;\n severity?: AgentSeverity;\n status?: AgentFindingStatus;\n limit?: number;\n}\n\nexport interface FindingsPage {\n findings: SecurityFinding[];\n total: number;\n}\n\nexport interface StartScanInput {\n vibeId: string;\n workspaceId: string;\n repoUrl: string;\n repoLocalPath: string;\n commit: string;\n stage: AgentStage;\n providerName?: string;\n policyLevel?: AgentPolicyLevel;\n config?: Record<string, unknown>;\n profile?: { kind: string; languages: string[]; runtimes: string[] };\n}\n\nexport interface StartScanResult {\n runId: string;\n status: AgentScanStatus;\n}\n\nexport const securityAgent = {\n /** GET /providers — per-stage provider directory. */\n listProviders(o: SecurityAgentOpts): Promise<SecurityProvidersByStage> {\n return get(o, '/providers');\n },\n\n /** GET /findings — filtered finding list. */\n listFindings(o: SecurityAgentOpts, filter: FindingsFilter = {}): Promise<FindingsPage> {\n const q = new URLSearchParams();\n if (filter.vibeId) q.set('vibeId', filter.vibeId);\n if (filter.severity) q.set('severity', filter.severity);\n if (filter.status) q.set('status', filter.status);\n if (filter.limit !== undefined) q.set('limit', String(filter.limit));\n const qs = q.toString();\n return get(o, `/findings${qs ? `?${qs}` : ''}`);\n },\n\n /** GET /scan/:runId — single scan run by id. */\n async getScan(o: SecurityAgentOpts, runId: string): Promise<SecurityScanRun> {\n const res = await get<{ run: SecurityScanRun }>(o, `/scan/${encodeURIComponent(runId)}`);\n return res.run;\n },\n\n /** POST /scan — start a scan. */\n startScan(o: SecurityAgentOpts, input: StartScanInput): Promise<StartScanResult> {\n return post(o, '/scan', input);\n },\n\n /** POST /scan/:runId/cancel — cancel an in-flight scan. */\n cancelScan(o: SecurityAgentOpts, runId: string): Promise<{ cancelled: boolean }> {\n return post(o, `/scan/${encodeURIComponent(runId)}/cancel`);\n },\n\n /**\n * Aggregate recent scan runs for a vibe by fanning out from the\n * findings list (the agent doesn't expose a list-runs endpoint).\n *\n * Strategy: list findings filtered by vibeId, take the distinct\n * `scanRunId`s (preserving last-seen order from the agent's\n * `ORDER BY last_seen_at DESC`), then resolve each id via\n * GET /scan/:runId. Unresolvable runs are dropped silently.\n */\n async listRecentScanRuns(\n o: SecurityAgentOpts,\n vibeId: string,\n limit = 20\n ): Promise<SecurityScanRun[]> {\n // Pull enough findings to surface ~`limit` distinct runs even when a\n // single run produces many findings. The agent caps at 1000.\n const page = await securityAgent.listFindings(o, {\n vibeId,\n limit: Math.min(1000, Math.max(100, limit * 25)),\n });\n const seen = new Set<string>();\n const ids: string[] = [];\n for (const f of page.findings) {\n if (!seen.has(f.scanRunId)) {\n seen.add(f.scanRunId);\n ids.push(f.scanRunId);\n if (ids.length >= limit) break;\n }\n }\n const runs = await Promise.all(\n ids.map(async (id) => {\n try {\n return await securityAgent.getScan(o, id);\n } catch (err) {\n if (err instanceof SecurityAgentError && err.status === 404) return null;\n throw err;\n }\n })\n );\n return runs.filter((r): r is SecurityScanRun => r !== null);\n },\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SecurityAgentError as e, securityAgent as t } from "./agentClient.js";
|
|
2
|
+
import { useQuery as n } from "@tanstack/react-query";
|
|
3
|
+
//#region src/components/security/hooks.ts
|
|
4
|
+
function r(e) {
|
|
5
|
+
return !e.tunnelUrl || !e.apiKey || !e.agentId ? null : {
|
|
6
|
+
tunnelUrl: e.tunnelUrl,
|
|
7
|
+
apiKey: e.apiKey,
|
|
8
|
+
profile: e.profile
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function i(e, ...t) {
|
|
12
|
+
return [
|
|
13
|
+
"security",
|
|
14
|
+
e.agentId ?? "_",
|
|
15
|
+
e.profile,
|
|
16
|
+
...t
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
function a(a, o, s = 20) {
|
|
20
|
+
return n({
|
|
21
|
+
queryKey: i(a, "recent-scans", o, s),
|
|
22
|
+
enabled: !!r(a) && !!o,
|
|
23
|
+
queryFn: async () => {
|
|
24
|
+
let n = r(a);
|
|
25
|
+
if (!n) throw new e("INVALID", 0, "no agent opts");
|
|
26
|
+
return t.listRecentScanRuns(n, o, s);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { a as useSecurityRecentScansQuery };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","names":[],"sources":["../../../src/components/security/hooks.ts"],"sourcesContent":["/**\n * React Query hooks over the security agent REST API.\n *\n * Cache keys include the agent identity (tunnelUrl proxy via agentId +\n * profile) so two simultaneously-active vibes against different agents\n * stay isolated. Mirrors the layout of `src/hooks/useGitops.ts`.\n */\nimport {\n useMutation,\n useQuery,\n useQueryClient,\n type UseMutationResult,\n type UseQueryResult,\n} from '@tanstack/react-query';\n\nimport {\n securityAgent,\n SecurityAgentError,\n type FindingsFilter,\n type FindingsPage,\n type SecurityAgentOpts,\n type SecurityProvidersByStage,\n type SecurityScanRun,\n type StartScanInput,\n type StartScanResult,\n} from './agentClient';\n\nexport interface UseSecurityAgentContext {\n /** Required: agent tunnel URL. */\n tunnelUrl: string | null;\n /** Required: agent API key. */\n apiKey: string | null;\n /** Required: canonical profile name. */\n profile: string;\n /** Agent id — cache scope, not sent to agent. */\n agentId: string | null;\n}\n\nfunction ensureOpts(ctx: UseSecurityAgentContext): SecurityAgentOpts | null {\n if (!ctx.tunnelUrl || !ctx.apiKey || !ctx.agentId) return null;\n return { tunnelUrl: ctx.tunnelUrl, apiKey: ctx.apiKey, profile: ctx.profile };\n}\n\nfunction key(ctx: UseSecurityAgentContext, ...rest: unknown[]): unknown[] {\n return ['security', ctx.agentId ?? '_', ctx.profile, ...rest];\n}\n\nexport function useSecurityProvidersQuery(\n ctx: UseSecurityAgentContext\n): UseQueryResult<SecurityProvidersByStage, SecurityAgentError> {\n return useQuery({\n queryKey: key(ctx, 'providers'),\n enabled: !!ensureOpts(ctx),\n queryFn: async () => {\n const opts = ensureOpts(ctx);\n if (!opts) throw new SecurityAgentError('INVALID', 0, 'no agent opts');\n return securityAgent.listProviders(opts);\n },\n });\n}\n\nexport function useSecurityFindingsQuery(\n ctx: UseSecurityAgentContext,\n filter: FindingsFilter\n): UseQueryResult<FindingsPage, SecurityAgentError> {\n return useQuery({\n queryKey: key(ctx, 'findings', filter.vibeId, filter.severity, filter.status, filter.limit),\n enabled: !!ensureOpts(ctx),\n queryFn: async () => {\n const opts = ensureOpts(ctx);\n if (!opts) throw new SecurityAgentError('INVALID', 0, 'no agent opts');\n return securityAgent.listFindings(opts, filter);\n },\n });\n}\n\nexport function useSecurityRecentScansQuery(\n ctx: UseSecurityAgentContext,\n vibeId: string,\n limit = 20\n): UseQueryResult<SecurityScanRun[], SecurityAgentError> {\n return useQuery({\n queryKey: key(ctx, 'recent-scans', vibeId, limit),\n enabled: !!ensureOpts(ctx) && !!vibeId,\n queryFn: async () => {\n const opts = ensureOpts(ctx);\n if (!opts) throw new SecurityAgentError('INVALID', 0, 'no agent opts');\n return securityAgent.listRecentScanRuns(opts, vibeId, limit);\n },\n });\n}\n\nexport function useStartSecurityScanMutation(\n ctx: UseSecurityAgentContext\n): UseMutationResult<StartScanResult, SecurityAgentError, StartScanInput> {\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: StartScanInput) => {\n const opts = ensureOpts(ctx);\n if (!opts) throw new SecurityAgentError('INVALID', 0, 'no agent opts');\n return securityAgent.startScan(opts, input);\n },\n onSuccess: () => {\n void qc.invalidateQueries({ queryKey: key(ctx) });\n },\n });\n}\n"],"mappings":";;;AAsCA,SAAS,EAAW,GAAwD;AAE1E,QADI,CAAC,EAAI,aAAa,CAAC,EAAI,UAAU,CAAC,EAAI,UAAgB,OACnD;EAAE,WAAW,EAAI;EAAW,QAAQ,EAAI;EAAQ,SAAS,EAAI;EAAS;;AAG/E,SAAS,EAAI,GAA8B,GAAG,GAA4B;AACxE,QAAO;EAAC;EAAY,EAAI,WAAW;EAAK,EAAI;EAAS,GAAG;EAAK;;AAgC/D,SAAgB,EACd,GACA,GACA,IAAQ,IAC+C;AACvD,QAAO,EAAS;EACd,UAAU,EAAI,GAAK,gBAAgB,GAAQ,EAAM;EACjD,SAAS,CAAC,CAAC,EAAW,EAAI,IAAI,CAAC,CAAC;EAChC,SAAS,YAAY;GACnB,IAAM,IAAO,EAAW,EAAI;AAC5B,OAAI,CAAC,EAAM,OAAM,IAAI,EAAmB,WAAW,GAAG,gBAAgB;AACtE,UAAO,EAAc,mBAAmB,GAAM,GAAQ,EAAM;;EAE/D,CAAC"}
|