dsh-pentester 2.0.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -36,7 +36,6 @@ __export(plugin_exports, {
36
36
  getPentesterEndpointState: () => getPentesterEndpointState2,
37
37
  getRemoteMountError: () => getRemoteMountError,
38
38
  inject: () => inject,
39
- isBuildOk: () => isBuildOk,
40
39
  isRemoteMounted: () => isRemoteMounted,
41
40
  setPentesterEndpointState: () => setPentesterEndpointState
42
41
  });
@@ -14772,7 +14771,7 @@ function resetPentesterEndpointState() {
14772
14771
  pentesterEndpointState = "unknown";
14773
14772
  }
14774
14773
  }
14775
- function pentesterViewCommand(face, input2, timeoutMs = 1e4) {
14774
+ function pentesterViewCommand(face, input2, timeoutMs = 3e4) {
14776
14775
  if (face === void 0 || typeof face.command !== "function") {
14777
14776
  return Promise.resolve({ ok: false, error: "rpc_unavailable", code: "rpc_unavailable" });
14778
14777
  }
@@ -16118,170 +16117,587 @@ var SEVERITY_COLORS = {
16118
16117
  low: "#3b82f6",
16119
16118
  info: "#6b7280"
16120
16119
  };
16121
- function ExpandableSection(props) {
16122
- const [open, setOpen] = (0, import_react10.useState)(props.defaultOpen);
16123
- return import_react10.default.createElement(
16124
- "div",
16125
- { style: { marginBottom: "4px" } },
16126
- import_react10.default.createElement(
16127
- "div",
16128
- {
16129
- onClick: () => setOpen(!open),
16130
- style: {
16131
- display: "flex",
16132
- alignItems: "center",
16133
- gap: "6px",
16134
- padding: "6px 8px",
16135
- cursor: "pointer",
16136
- borderRadius: "4px",
16137
- fontSize: "13px",
16138
- fontWeight: 600,
16139
- color: "var(--dsw-fg)",
16140
- userSelect: "none"
16141
- }
16142
- },
16143
- import_react10.default.createElement("span", {
16144
- style: { fontSize: "12px", transition: "transform 0.15s", transform: open ? "rotate(90deg)" : "rotate(0deg)" }
16145
- }, "\u25B6"),
16146
- import_react10.default.createElement("span", null, props.title)
16147
- ),
16148
- open && import_react10.default.createElement("div", { style: { paddingLeft: "20px" } }, props.children)
16149
- );
16150
- }
16120
+ var FILTER_TABS = [
16121
+ { id: "all", label: "All" },
16122
+ ...SEVERITY_ORDER.map((s) => ({ id: s, label: SEVERITY_LABELS[s] }))
16123
+ ];
16151
16124
  function FindingsView(props) {
16152
- const { snapshot, command, sessionId } = props;
16153
- const [selectedFinding, setSelectedFinding] = (0, import_react10.useState)(null);
16125
+ const { snapshot, command, sessionId, onNavigate } = props;
16126
+ const findings = snapshot?.findings ?? [];
16127
+ const [selectedId, setSelectedId] = (0, import_react10.useState)(null);
16154
16128
  const [detailContent, setDetailContent] = (0, import_react10.useState)(null);
16155
16129
  const [detailLoading, setDetailLoading] = (0, import_react10.useState)(false);
16156
- const findings = snapshot?.findings ?? [];
16157
- const grouped = (0, import_react10.useMemo)(() => {
16158
- const groups = /* @__PURE__ */ new Map();
16159
- for (const severity of SEVERITY_ORDER) {
16160
- groups.set(severity, []);
16161
- }
16162
- for (const f of findings) {
16163
- const list = groups.get(f.severity);
16164
- if (list !== void 0) list.push(f);
16165
- }
16166
- return groups;
16130
+ const [detailError, setDetailError] = (0, import_react10.useState)(false);
16131
+ const [filter, setFilter] = (0, import_react10.useState)("all");
16132
+ const [search, setSearch] = (0, import_react10.useState)("");
16133
+ const [focusDone, setFocusDone] = (0, import_react10.useState)(false);
16134
+ const loadSeqRef = (0, import_react10.useRef)(0);
16135
+ const severityCounts = (0, import_react10.useMemo)(() => {
16136
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
16137
+ for (const f of findings) counts[f.severity]++;
16138
+ return counts;
16167
16139
  }, [findings]);
16168
- const openDetail = (0, import_react10.useCallback)(async (finding) => {
16169
- setSelectedFinding(finding);
16140
+ const filtered = (0, import_react10.useMemo)(() => {
16141
+ let list = [...findings];
16142
+ if (filter !== "all") list = list.filter((f) => f.severity === filter);
16143
+ if (search.trim() !== "") {
16144
+ const q = search.toLowerCase();
16145
+ list = list.filter(
16146
+ (f) => f.title.toLowerCase().includes(q) || (f.affectedAsset?.toLowerCase().includes(q) ?? false) || f.sourceStage.toLowerCase().includes(q) || f.sourceDelegationId.toLowerCase().includes(q)
16147
+ );
16148
+ }
16149
+ const orderMap = new Map(SEVERITY_ORDER.map((s, i) => [s, i]));
16150
+ list.sort((a, b) => {
16151
+ const oa = orderMap.get(a.severity) ?? 99;
16152
+ const ob = orderMap.get(b.severity) ?? 99;
16153
+ if (oa !== ob) return oa - ob;
16154
+ return 0;
16155
+ });
16156
+ return list;
16157
+ }, [findings, filter, search]);
16158
+ const selectedFinding = (0, import_react10.useMemo)(
16159
+ () => selectedId !== null ? findings.find((f) => f.id === selectedId) ?? null : null,
16160
+ [findings, selectedId]
16161
+ );
16162
+ (0, import_react10.useEffect)(() => {
16163
+ if (props.focusFinding !== void 0 && !focusDone) {
16164
+ const match = findings.find((f) => f.id === props.focusFinding);
16165
+ if (match !== void 0) {
16166
+ setSelectedId(match.id);
16167
+ setFilter("all");
16168
+ setSearch("");
16169
+ setFocusDone(true);
16170
+ }
16171
+ }
16172
+ }, [props.focusFinding, focusDone, findings]);
16173
+ (0, import_react10.useEffect)(() => {
16174
+ setFocusDone(false);
16175
+ }, [props.focusFinding]);
16176
+ const loadDetail = (0, import_react10.useCallback)(async (finding) => {
16177
+ setSelectedId(finding.id);
16170
16178
  setDetailContent(null);
16171
16179
  setDetailLoading(true);
16180
+ setDetailError(false);
16181
+ const seq = ++loadSeqRef.current;
16172
16182
  try {
16173
16183
  const result = await command({
16174
16184
  kind: "readFinding",
16175
16185
  detailFile: finding.detailFile,
16176
16186
  sessionId
16177
16187
  });
16188
+ if (seq !== loadSeqRef.current) return;
16178
16189
  if (result.ok) {
16179
16190
  const fileResult = result.value;
16180
16191
  setDetailContent(fileResult.content ?? "");
16192
+ setDetailError(false);
16181
16193
  } else {
16182
- setDetailContent("*Unable to load finding detail.*");
16194
+ setDetailContent(null);
16195
+ setDetailError(true);
16183
16196
  }
16184
16197
  } catch {
16185
- setDetailContent("*Error loading finding detail.*");
16198
+ if (seq !== loadSeqRef.current) return;
16199
+ setDetailContent(null);
16200
+ setDetailError(true);
16186
16201
  } finally {
16187
- setDetailLoading(false);
16202
+ if (seq === loadSeqRef.current) setDetailLoading(false);
16188
16203
  }
16189
16204
  }, [command, sessionId]);
16190
- const closeDetail = (0, import_react10.useCallback)(() => {
16191
- setSelectedFinding(null);
16192
- setDetailContent(null);
16193
- }, []);
16194
- if (findings.length === 0) {
16195
- return import_react10.default.createElement("div", {
16196
- style: { padding: "24px", color: "var(--dsw-fg-muted)", fontSize: "14px" }
16197
- }, "No findings yet");
16198
- }
16199
- const severitySections = SEVERITY_ORDER.map((severity) => {
16200
- const items = grouped.get(severity) ?? [];
16201
- if (items.length === 0) return null;
16202
- const findingRows = items.map(
16203
- (finding) => import_react10.default.createElement(
16205
+ return import_react10.default.createElement(
16206
+ "div",
16207
+ {
16208
+ style: {
16209
+ display: "flex",
16210
+ flexDirection: "column",
16211
+ height: "100%",
16212
+ minHeight: 0,
16213
+ overflow: "hidden"
16214
+ }
16215
+ },
16216
+ // ── Top: severity summary strip ──────────────────────────────────────
16217
+ import_react10.default.createElement(SeveritySummary, {
16218
+ total: findings.length,
16219
+ counts: severityCounts,
16220
+ activeFilter: filter,
16221
+ onFilter: setFilter
16222
+ }),
16223
+ // ── Bottom: split view ───────────────────────────────────────────────
16224
+ import_react10.default.createElement(
16225
+ "div",
16226
+ {
16227
+ style: {
16228
+ flex: 1,
16229
+ minHeight: 0,
16230
+ display: "flex",
16231
+ overflow: "hidden"
16232
+ }
16233
+ },
16234
+ // ── Left: Navigator ───────────────────────────────────────────────
16235
+ import_react10.default.createElement(
16204
16236
  "div",
16205
16237
  {
16206
- key: finding.id,
16207
16238
  style: {
16239
+ flex: "0 0 340px",
16240
+ minWidth: "280px",
16241
+ maxWidth: "380px",
16242
+ minHeight: 0,
16208
16243
  display: "flex",
16244
+ flexDirection: "column",
16245
+ overflow: "hidden",
16246
+ borderRight: "1px solid var(--dsw-alias-border-l2)"
16247
+ }
16248
+ },
16249
+ // Search + filter (fixed header)
16250
+ import_react10.default.createElement(
16251
+ "div",
16252
+ {
16253
+ style: {
16254
+ flex: "none",
16255
+ padding: "8px 12px 4px"
16256
+ }
16257
+ },
16258
+ import_react10.default.createElement("input", {
16259
+ type: "text",
16260
+ placeholder: "Search findings...",
16261
+ value: search,
16262
+ onChange: (e) => setSearch(e.target.value),
16263
+ style: {
16264
+ width: "100%",
16265
+ boxSizing: "border-box",
16266
+ padding: "6px 10px",
16267
+ border: "1px solid var(--dsw-alias-border-l2)",
16268
+ borderRadius: "6px",
16269
+ background: "var(--dsw-alias-bg-base)",
16270
+ color: "var(--dsw-alias-label-primary)",
16271
+ fontSize: "13px",
16272
+ outline: "none",
16273
+ marginBottom: "4px"
16274
+ }
16275
+ }),
16276
+ import_react10.default.createElement(PentesterTabBar, {
16277
+ items: FILTER_TABS,
16278
+ active: filter,
16279
+ onSelect: (id) => setFilter(id),
16280
+ variant: "compact"
16281
+ })
16282
+ ),
16283
+ // FindingList (scrollable)
16284
+ import_react10.default.createElement(
16285
+ "div",
16286
+ {
16287
+ style: {
16288
+ flex: 1,
16289
+ minHeight: 0,
16290
+ overflowY: "auto",
16291
+ overflowX: "hidden",
16292
+ overscrollBehaviorY: "contain",
16293
+ scrollbarGutter: "stable"
16294
+ }
16295
+ },
16296
+ filtered.length === 0 ? import_react10.default.createElement("div", {
16297
+ style: {
16298
+ padding: "24px 12px",
16299
+ color: "var(--dsw-alias-label-tertiary)",
16300
+ fontSize: "13px",
16301
+ textAlign: "center"
16302
+ }
16303
+ }, findings.length === 0 ? "No findings yet" : "No matching findings") : filtered.map(
16304
+ (finding) => import_react10.default.createElement(FindingRow, {
16305
+ key: finding.id,
16306
+ finding,
16307
+ isSelected: finding.id === selectedId,
16308
+ onClick: () => loadDetail(finding)
16309
+ })
16310
+ )
16311
+ )
16312
+ ),
16313
+ // ── Right: Detail ──────────────────────────────────────────────────
16314
+ import_react10.default.createElement(
16315
+ "div",
16316
+ {
16317
+ style: {
16318
+ flex: 1,
16319
+ minWidth: 0,
16320
+ minHeight: 0,
16321
+ display: "flex",
16322
+ flexDirection: "column",
16323
+ overflow: "hidden"
16324
+ }
16325
+ },
16326
+ selectedFinding === null ? import_react10.default.createElement(
16327
+ "div",
16328
+ {
16329
+ style: {
16330
+ flex: 1,
16331
+ display: "flex",
16332
+ alignItems: "center",
16333
+ justifyContent: "center",
16334
+ flexDirection: "column",
16335
+ gap: "8px",
16336
+ color: "var(--dsw-alias-label-tertiary)",
16337
+ fontSize: "13px"
16338
+ }
16339
+ },
16340
+ findings.length === 0 ? import_react10.default.createElement(
16341
+ import_react10.default.Fragment,
16342
+ null,
16343
+ import_react10.default.createElement("div", { style: { fontWeight: 500, color: "var(--dsw-alias-label-primary)" } }, "No findings yet"),
16344
+ import_react10.default.createElement("div", null, "Validated findings will appear here as the assessment progresses.")
16345
+ ) : "Select a finding to inspect"
16346
+ ) : import_react10.default.createElement(FindingDetail, {
16347
+ finding: selectedFinding,
16348
+ content: detailContent,
16349
+ loading: detailLoading,
16350
+ error: detailError,
16351
+ onNavigate
16352
+ })
16353
+ )
16354
+ )
16355
+ );
16356
+ }
16357
+ function SeveritySummary(props) {
16358
+ const { total, counts, activeFilter, onFilter } = props;
16359
+ const items = [
16360
+ { key: "all", label: "Total", count: total },
16361
+ ...SEVERITY_ORDER.map((s) => ({
16362
+ key: s,
16363
+ label: SEVERITY_LABELS[s],
16364
+ count: counts[s],
16365
+ color: SEVERITY_COLORS[s]
16366
+ }))
16367
+ ];
16368
+ return import_react10.default.createElement(
16369
+ "div",
16370
+ {
16371
+ style: {
16372
+ flex: "none",
16373
+ display: "flex",
16374
+ alignItems: "center",
16375
+ gap: "4px",
16376
+ padding: "8px 12px",
16377
+ borderBottom: "1px solid var(--dsw-alias-border-l2)",
16378
+ flexWrap: "wrap",
16379
+ fontSize: "13px",
16380
+ color: "var(--dsw-alias-label-secondary)"
16381
+ }
16382
+ },
16383
+ ...items.map((item) => {
16384
+ const isActive = activeFilter === item.key;
16385
+ return import_react10.default.createElement(
16386
+ "button",
16387
+ {
16388
+ key: item.key,
16389
+ type: "button",
16390
+ onClick: () => onFilter(item.key),
16391
+ style: {
16392
+ display: "inline-flex",
16209
16393
  alignItems: "center",
16210
- gap: "8px",
16211
- padding: "6px 12px",
16394
+ gap: "5px",
16395
+ padding: "3px 10px",
16396
+ borderRadius: "12px",
16397
+ border: "none",
16398
+ background: isActive ? "var(--dsw-alias-interactive-bg-hover)" : "transparent",
16399
+ color: isActive ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-secondary)",
16212
16400
  cursor: "pointer",
16213
- borderRadius: "4px",
16214
16401
  fontSize: "13px",
16215
- color: "var(--dsw-fg)",
16216
- borderLeft: `3px solid ${SEVERITY_COLORS[finding.severity]}`,
16217
- marginBottom: "2px"
16218
- },
16219
- onClick: () => openDetail(finding)
16402
+ fontWeight: isActive ? 500 : 400,
16403
+ whiteSpace: "nowrap"
16404
+ }
16220
16405
  },
16221
- import_react10.default.createElement("span", {
16406
+ item.color !== void 0 && import_react10.default.createElement("span", {
16222
16407
  style: {
16223
16408
  width: "8px",
16224
16409
  height: "8px",
16225
16410
  borderRadius: "50%",
16226
- background: SEVERITY_COLORS[finding.severity],
16411
+ background: item.color,
16227
16412
  flexShrink: 0
16228
16413
  }
16229
16414
  }),
16230
- import_react10.default.createElement("span", { style: { fontWeight: 500 } }, finding.title),
16231
- finding.affectedAsset !== void 0 && import_react10.default.createElement("span", {
16232
- style: { fontSize: "11px", color: "var(--dsw-fg-muted)", marginLeft: "auto" }
16233
- }, finding.affectedAsset)
16234
- )
16235
- );
16236
- return import_react10.default.createElement(ExpandableSection, {
16237
- key: severity,
16238
- title: `${SEVERITY_LABELS[severity]} \xB7 ${items.length}`,
16239
- defaultOpen: severity === "critical" || severity === "high",
16240
- children: findingRows
16241
- });
16242
- });
16243
- const modalContent = selectedFinding !== null ? import_react10.default.createElement(
16244
- import_dsh_client_ui_primitives3.Modal,
16415
+ import_react10.default.createElement("span", null, item.label),
16416
+ import_react10.default.createElement("span", {
16417
+ style: { fontWeight: 600, color: isActive ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-tertiary)" }
16418
+ }, String(item.count))
16419
+ );
16420
+ })
16421
+ );
16422
+ }
16423
+ function FindingRow(props) {
16424
+ const { finding, isSelected, onClick } = props;
16425
+ const color = SEVERITY_COLORS[finding.severity];
16426
+ return import_react10.default.createElement(
16427
+ "div",
16245
16428
  {
16246
- open: true,
16247
- onClose: closeDetail,
16248
- title: selectedFinding.title,
16249
- contentClassName: "pentester-finding-detail"
16429
+ role: "button",
16430
+ tabIndex: 0,
16431
+ onClick,
16432
+ onKeyDown: (e) => {
16433
+ if (e.key === "Enter" || e.key === " ") {
16434
+ e.preventDefault();
16435
+ onClick();
16436
+ }
16437
+ },
16438
+ style: {
16439
+ display: "flex",
16440
+ flexDirection: "column",
16441
+ gap: "2px",
16442
+ padding: "8px 12px",
16443
+ cursor: "pointer",
16444
+ borderLeft: `3px solid ${isSelected ? color : "transparent"}`,
16445
+ background: isSelected ? "var(--dsw-alias-state-business-primary)" : "transparent",
16446
+ color: isSelected ? "var(--dsw-alias-state-business-primary-fg, #fff)" : "var(--dsw-alias-label-primary)",
16447
+ fontSize: "13px",
16448
+ lineHeight: "18px",
16449
+ borderBottom: "1px solid var(--dsw-alias-border-l2)",
16450
+ transition: "background 0.1s"
16451
+ },
16452
+ onMouseEnter: (e) => {
16453
+ if (!isSelected) e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover)";
16454
+ },
16455
+ onMouseLeave: (e) => {
16456
+ if (!isSelected) e.currentTarget.style.background = "transparent";
16457
+ }
16250
16458
  },
16459
+ // Row 1: title + severity
16251
16460
  import_react10.default.createElement(
16252
16461
  "div",
16253
- { style: { fontSize: "13px", lineHeight: "1.6" } },
16254
- import_react10.default.createElement("div", {
16462
+ {
16255
16463
  style: {
16256
- display: "inline-block",
16257
- padding: "2px 10px",
16258
- borderRadius: "10px",
16259
- background: SEVERITY_COLORS[selectedFinding.severity],
16260
- color: "#fff",
16261
- fontSize: "12px",
16464
+ display: "flex",
16465
+ alignItems: "center",
16466
+ gap: "8px",
16467
+ minWidth: 0
16468
+ }
16469
+ },
16470
+ import_react10.default.createElement("span", {
16471
+ style: {
16472
+ width: "7px",
16473
+ height: "7px",
16474
+ borderRadius: "50%",
16475
+ background: color,
16476
+ flexShrink: 0
16477
+ }
16478
+ }),
16479
+ import_react10.default.createElement("span", {
16480
+ style: {
16481
+ overflow: "hidden",
16482
+ textOverflow: "ellipsis",
16483
+ whiteSpace: "nowrap",
16484
+ fontWeight: 500,
16485
+ flex: 1,
16486
+ minWidth: 0
16487
+ }
16488
+ }, finding.title),
16489
+ import_react10.default.createElement("span", {
16490
+ style: {
16491
+ fontSize: "11px",
16262
16492
  fontWeight: 600,
16263
- marginBottom: "12px"
16493
+ color: isSelected ? "rgba(255,255,255,0.75)" : color,
16494
+ flexShrink: 0
16264
16495
  }
16265
- }, SEVERITY_LABELS[selectedFinding.severity]),
16266
- selectedFinding.affectedAsset !== void 0 && import_react10.default.createElement("div", {
16267
- style: { color: "var(--dsw-fg-muted)", marginBottom: "4px" }
16268
- }, "Affected: ", import_react10.default.createElement("code", null, selectedFinding.affectedAsset)),
16269
- import_react10.default.createElement("div", {
16270
- style: { color: "var(--dsw-fg-muted)", marginBottom: "12px", fontSize: "12px" }
16271
- }, `Source: ${selectedFinding.sourceStage} \xB7 ${selectedFinding.sourceDelegationId}`),
16272
- detailLoading && import_react10.default.createElement("div", {
16273
- style: { color: "var(--dsw-fg-muted)", padding: "16px 0" }
16274
- }, "Loading..."),
16275
- detailContent !== null && import_react10.default.createElement(import_dsh_client_ui_primitives3.MarkdownText, { text: detailContent })
16276
- )
16277
- ) : null;
16496
+ }, SEVERITY_LABELS[finding.severity].toUpperCase())
16497
+ ),
16498
+ // Row 2: affected asset (if exists)
16499
+ finding.affectedAsset !== void 0 && import_react10.default.createElement("div", {
16500
+ style: {
16501
+ fontSize: "12px",
16502
+ color: isSelected ? "rgba(255,255,255,0.6)" : "var(--dsw-alias-label-tertiary)",
16503
+ overflow: "hidden",
16504
+ textOverflow: "ellipsis",
16505
+ whiteSpace: "nowrap",
16506
+ marginLeft: "15px"
16507
+ }
16508
+ }, finding.affectedAsset),
16509
+ // Row 3: source stage + delegation
16510
+ import_react10.default.createElement("div", {
16511
+ style: {
16512
+ fontSize: "11px",
16513
+ color: isSelected ? "rgba(255,255,255,0.5)" : "var(--dsw-alias-label-tertiary)",
16514
+ marginLeft: "15px",
16515
+ overflow: "hidden",
16516
+ textOverflow: "ellipsis",
16517
+ whiteSpace: "nowrap"
16518
+ }
16519
+ }, `${finding.sourceStage} \xB7 ${finding.sourceDelegationId}`)
16520
+ );
16521
+ }
16522
+ function FindingDetail(props) {
16523
+ const { finding, content, loading, error: error51, onNavigate } = props;
16524
+ const color = SEVERITY_COLORS[finding.severity];
16278
16525
  return import_react10.default.createElement(
16279
16526
  "div",
16280
- { style: { maxWidth: "640px" } },
16281
- ...severitySections,
16282
- modalContent
16527
+ {
16528
+ style: {
16529
+ flex: 1,
16530
+ minHeight: 0,
16531
+ display: "flex",
16532
+ flexDirection: "column",
16533
+ overflow: "hidden"
16534
+ }
16535
+ },
16536
+ // ── Metadata header (fixed) ─────────────────────────────────────────
16537
+ import_react10.default.createElement(
16538
+ "div",
16539
+ {
16540
+ style: {
16541
+ flex: "none",
16542
+ padding: "12px 16px",
16543
+ borderBottom: "1px solid var(--dsw-alias-border-l2)"
16544
+ }
16545
+ },
16546
+ // Title + severity
16547
+ import_react10.default.createElement(
16548
+ "div",
16549
+ {
16550
+ style: {
16551
+ display: "flex",
16552
+ alignItems: "center",
16553
+ gap: "10px",
16554
+ marginBottom: "8px"
16555
+ }
16556
+ },
16557
+ import_react10.default.createElement("span", {
16558
+ style: {
16559
+ fontSize: "15px",
16560
+ fontWeight: 600,
16561
+ color: "var(--dsw-alias-label-primary)",
16562
+ overflow: "hidden",
16563
+ textOverflow: "ellipsis",
16564
+ whiteSpace: "nowrap",
16565
+ flex: 1,
16566
+ minWidth: 0
16567
+ }
16568
+ }, finding.title),
16569
+ import_react10.default.createElement("span", {
16570
+ style: {
16571
+ display: "inline-block",
16572
+ padding: "2px 8px",
16573
+ borderRadius: "10px",
16574
+ background: color,
16575
+ color: "#fff",
16576
+ fontSize: "11px",
16577
+ fontWeight: 600,
16578
+ flexShrink: 0
16579
+ }
16580
+ }, SEVERITY_LABELS[finding.severity].toUpperCase())
16581
+ ),
16582
+ // Affected asset
16583
+ finding.affectedAsset !== void 0 && import_react10.default.createElement(
16584
+ "div",
16585
+ {
16586
+ style: {
16587
+ fontSize: "13px",
16588
+ color: "var(--dsw-alias-label-secondary)",
16589
+ marginBottom: "6px",
16590
+ display: "flex",
16591
+ gap: "6px"
16592
+ }
16593
+ },
16594
+ import_react10.default.createElement("span", { style: { color: "var(--dsw-alias-label-tertiary)", flexShrink: 0 } }, "Affected asset"),
16595
+ import_react10.default.createElement("code", {
16596
+ style: {
16597
+ fontSize: "12px",
16598
+ background: "var(--dsw-alias-bg-subtle)",
16599
+ padding: "1px 6px",
16600
+ borderRadius: "3px"
16601
+ }
16602
+ }, finding.affectedAsset)
16603
+ ),
16604
+ // Stage + Delegation metadata
16605
+ import_react10.default.createElement(
16606
+ "div",
16607
+ {
16608
+ style: {
16609
+ display: "flex",
16610
+ gap: "20px",
16611
+ fontSize: "12px",
16612
+ color: "var(--dsw-alias-label-tertiary)"
16613
+ }
16614
+ },
16615
+ import_react10.default.createElement(
16616
+ "div",
16617
+ null,
16618
+ import_react10.default.createElement("span", { style: { fontWeight: 500, color: "var(--dsw-alias-label-secondary)" } }, "Stage"),
16619
+ " ",
16620
+ import_react10.default.createElement("span", null, finding.sourceStage)
16621
+ ),
16622
+ import_react10.default.createElement(
16623
+ "div",
16624
+ null,
16625
+ import_react10.default.createElement("span", { style: { fontWeight: 500, color: "var(--dsw-alias-label-secondary)" } }, "Delegation"),
16626
+ " ",
16627
+ import_react10.default.createElement("span", null, finding.sourceDelegationId)
16628
+ )
16629
+ )
16630
+ ),
16631
+ // ── DetailBody (scrollable) ────────────────────────────────────────
16632
+ import_react10.default.createElement(
16633
+ "div",
16634
+ {
16635
+ style: {
16636
+ flex: 1,
16637
+ minHeight: 0,
16638
+ overflowY: "auto",
16639
+ overflowX: "auto",
16640
+ overscrollBehaviorY: "contain",
16641
+ scrollbarGutter: "stable",
16642
+ padding: "16px",
16643
+ fontSize: "13px",
16644
+ lineHeight: "1.6",
16645
+ color: "var(--dsw-alias-label-primary)"
16646
+ }
16647
+ },
16648
+ loading && import_react10.default.createElement("div", {
16649
+ style: { color: "var(--dsw-alias-label-tertiary)", padding: "16px 0" }
16650
+ }, "Loading finding..."),
16651
+ error51 && import_react10.default.createElement("div", {
16652
+ style: { color: "var(--dsw-status-error)", padding: "16px 0" }
16653
+ }, "Unable to load finding detail"),
16654
+ content !== null && import_react10.default.createElement(import_dsh_client_ui_primitives3.MarkdownText, { text: content })
16655
+ ),
16656
+ // ── Footer actions (fixed) ──────────────────────────────────────────
16657
+ import_react10.default.createElement(
16658
+ "div",
16659
+ {
16660
+ style: {
16661
+ flex: "none",
16662
+ padding: "8px 16px",
16663
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
16664
+ display: "flex",
16665
+ gap: "8px"
16666
+ }
16667
+ },
16668
+ import_react10.default.createElement(ActionButton, {
16669
+ label: "View Stage",
16670
+ onClick: () => onNavigate("stages", { stage: finding.sourceStage })
16671
+ }),
16672
+ import_react10.default.createElement(ActionButton, {
16673
+ label: "View in Output",
16674
+ onClick: () => onNavigate("output", { output: `findings/${finding.detailFile}` })
16675
+ })
16676
+ )
16283
16677
  );
16284
16678
  }
16679
+ function ActionButton(props) {
16680
+ return import_react10.default.createElement("button", {
16681
+ type: "button",
16682
+ onClick: props.onClick,
16683
+ style: {
16684
+ padding: "4px 12px",
16685
+ border: "1px solid var(--dsw-alias-border-l2)",
16686
+ borderRadius: "4px",
16687
+ background: "transparent",
16688
+ color: "var(--dsw-alias-label-secondary)",
16689
+ cursor: "pointer",
16690
+ fontSize: "12px",
16691
+ lineHeight: "20px"
16692
+ },
16693
+ onMouseEnter: (e) => {
16694
+ e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover)";
16695
+ },
16696
+ onMouseLeave: (e) => {
16697
+ e.currentTarget.style.background = "transparent";
16698
+ }
16699
+ }, props.label);
16700
+ }
16285
16701
 
16286
16702
  // src/ui/client/pentester/TraceView.tsx
16287
16703
  var import_react15 = __toESM(require("react"), 1);
@@ -17768,7 +18184,7 @@ function TargetRow(props) {
17768
18184
  );
17769
18185
  }
17770
18186
  function OutputView(props) {
17771
- const { snapshot, command, sessionId } = props;
18187
+ const { snapshot, command, sessionId, focusOutput } = props;
17772
18188
  const [filter, setFilter] = (0, import_react17.useState)("all");
17773
18189
  const [preview, setPreview] = (0, import_react17.useState)(null);
17774
18190
  const [previewContent, setPreviewContent] = (0, import_react17.useState)(null);
@@ -17850,6 +18266,16 @@ function OutputView(props) {
17850
18266
  setPreview(null);
17851
18267
  setPreviewContent(null);
17852
18268
  }, []);
18269
+ const focusAppliedRef = (0, import_react17.useRef)(void 0);
18270
+ (0, import_react17.useEffect)(() => {
18271
+ if (focusOutput === void 0 || focusOutput === focusAppliedRef.current) return;
18272
+ if (outputTree === null) return;
18273
+ focusAppliedRef.current = focusOutput;
18274
+ const entry = findEntryByPath(outputTree.children, focusOutput);
18275
+ if (entry !== void 0) {
18276
+ openPreview(entry, outputTree.targetId);
18277
+ }
18278
+ }, [focusOutput, outputTree, openPreview]);
17853
18279
  const initialLoading = outputTree === null && outputLoading;
17854
18280
  if (initialLoading) {
17855
18281
  return import_react17.default.createElement("div", {
@@ -17908,7 +18334,7 @@ function OutputView(props) {
17908
18334
  return import_react17.default.createElement(
17909
18335
  "div",
17910
18336
  {
17911
- style: { display: "flex", gap: "16px", minHeight: "400px", overflow: "visible" }
18337
+ style: { display: "flex", gap: "16px", flex: 1, height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden" }
17912
18338
  },
17913
18339
  // Left: file tree
17914
18340
  import_react17.default.createElement(
@@ -17916,16 +18342,24 @@ function OutputView(props) {
17916
18342
  {
17917
18343
  style: {
17918
18344
  flex: "0 0 320px",
17919
- overflow: "auto",
17920
- borderRight: "1px solid var(--dsw-alias-border-l2)",
17921
- paddingRight: "12px",
18345
+ minHeight: 0,
18346
+ overflow: "hidden",
17922
18347
  display: "flex",
17923
- flexDirection: "column"
18348
+ flexDirection: "column",
18349
+ borderRight: "1px solid var(--dsw-alias-border-l2)",
18350
+ paddingRight: "12px"
17924
18351
  }
17925
18352
  },
18353
+ // Filter tabs (fixed)
17926
18354
  import_react17.default.createElement(
17927
18355
  "div",
17928
- { style: { marginBottom: "12px" } },
18356
+ {
18357
+ style: {
18358
+ flex: "none",
18359
+ marginBottom: "12px",
18360
+ paddingTop: "2px"
18361
+ }
18362
+ },
17929
18363
  import_react17.default.createElement(PentesterTabBar, {
17930
18364
  items: OUTPUT_FILTER_TABS,
17931
18365
  active: filter,
@@ -17933,10 +18367,20 @@ function OutputView(props) {
17933
18367
  variant: "compact"
17934
18368
  })
17935
18369
  ),
17936
- // ── Tree: "Targets" section label + target rows at root depth ─────
18370
+ // ── TreeViewport (scrollable) ──────────────────────────────────
17937
18371
  import_react17.default.createElement(
17938
18372
  "div",
17939
- { style: { flex: 1 } },
18373
+ {
18374
+ style: {
18375
+ flex: 1,
18376
+ minHeight: 0,
18377
+ minWidth: 0,
18378
+ overflowY: "auto",
18379
+ overflowX: "hidden",
18380
+ overscrollBehaviorY: "contain",
18381
+ scrollbarGutter: "stable"
18382
+ }
18383
+ },
17940
18384
  import_react17.default.createElement("div", {
17941
18385
  style: {
17942
18386
  fontSize: "11px",
@@ -17970,14 +18414,13 @@ function OutputView(props) {
17970
18414
  style: {
17971
18415
  flex: 1,
17972
18416
  minWidth: 0,
18417
+ minHeight: 0,
17973
18418
  display: "flex",
17974
18419
  flexDirection: "column",
17975
18420
  overflow: "hidden",
17976
18421
  border: "1px solid var(--dsw-alias-border-l2)",
17977
18422
  borderRadius: "8px",
17978
- background: "var(--dsw-alias-bg-base)",
17979
- minHeight: "320px",
17980
- maxHeight: "clamp(320px, 65vh, 680px)"
18423
+ background: "var(--dsw-alias-bg-base)"
17981
18424
  }
17982
18425
  },
17983
18426
  preview === null ? import_react17.default.createElement("div", {
@@ -17992,6 +18435,7 @@ function OutputView(props) {
17992
18435
  }, "Select a file to preview") : import_react17.default.createElement(
17993
18436
  import_react17.default.Fragment,
17994
18437
  null,
18438
+ // PreviewHeader (fixed)
17995
18439
  import_react17.default.createElement(
17996
18440
  "div",
17997
18441
  {
@@ -18020,10 +18464,20 @@ function OutputView(props) {
18020
18464
  "aria-label": "Close preview"
18021
18465
  }, "\xD7")
18022
18466
  ),
18467
+ // PreviewBody (scrollable)
18023
18468
  import_react17.default.createElement(
18024
18469
  "div",
18025
18470
  {
18026
- style: { flex: 1, minHeight: 0, overflow: "auto", padding: "12px" }
18471
+ style: {
18472
+ flex: 1,
18473
+ minHeight: 0,
18474
+ minWidth: 0,
18475
+ overflowY: "auto",
18476
+ overflowX: "auto",
18477
+ overscrollBehaviorY: "contain",
18478
+ scrollbarGutter: "stable",
18479
+ padding: "12px"
18480
+ }
18027
18481
  },
18028
18482
  previewLoading && import_react17.default.createElement("div", {
18029
18483
  style: { color: "var(--dsw-alias-label-tertiary)", padding: "16px 0" }
@@ -18110,6 +18564,16 @@ function formatSize(bytes) {
18110
18564
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
18111
18565
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
18112
18566
  }
18567
+ function findEntryByPath(entries, targetPath) {
18568
+ for (const entry of entries) {
18569
+ if (entry.path === targetPath) return entry;
18570
+ if (entry.kind === "directory" && entry.children !== void 0) {
18571
+ const found = findEntryByPath(entry.children, targetPath);
18572
+ if (found !== void 0) return found;
18573
+ }
18574
+ }
18575
+ return void 0;
18576
+ }
18113
18577
 
18114
18578
  // src/ui/client/pentester-view.tsx
18115
18579
  var CORE_REFRESH_MS = 1e4;
@@ -18245,18 +18709,17 @@ function PentesterView(props) {
18245
18709
  if (isPerf) console.log(`${RPC_PREFIX} circuit-open endpoint=missing reason=${reason}`);
18246
18710
  return;
18247
18711
  }
18248
- if (!isBuildOk()) {
18249
- const mismatch = getBuildMismatch();
18250
- if (mismatch !== null && reason === "startup") {
18251
- setDataState({
18252
- kind: "error",
18253
- error: `Pentester Host/Client build mismatch
18712
+ const mismatch = getBuildMismatch();
18713
+ if (mismatch !== null) {
18714
+ pollingPausedRef.current = true;
18715
+ setDataState({
18716
+ kind: "error",
18717
+ error: `Pentester Host/Client build mismatch
18254
18718
  Host: ${mismatch.host}
18255
18719
  Client: ${mismatch.client}`,
18256
- code: "build_mismatch"
18257
- });
18258
- setRequestState("idle");
18259
- }
18720
+ code: "build_mismatch"
18721
+ });
18722
+ setRequestState("idle");
18260
18723
  return;
18261
18724
  }
18262
18725
  if (pendingRef.current !== null) {
@@ -18282,7 +18745,7 @@ Client: ${mismatch.client}`,
18282
18745
  }
18283
18746
  }
18284
18747
  try {
18285
- const result = await pentesterCommand({ kind: "snapshot", sessionId }, 1e4);
18748
+ const result = await pentesterCommand({ kind: "snapshot", sessionId }, 3e4);
18286
18749
  if (pendingRef.current?.id !== id) {
18287
18750
  if (isPerf) console.log(`${RPC_PREFIX} id=${id} stale (current=${pendingRef.current?.id})`);
18288
18751
  return;
@@ -18413,12 +18876,14 @@ Client: ${mismatch.client}`,
18413
18876
  {
18414
18877
  "data-testid": "pentester-view",
18415
18878
  "data-pentester-view": "",
18879
+ "data-conversation-composer-overlay": "",
18416
18880
  style: {
18417
18881
  display: "flex",
18418
18882
  flexDirection: "column",
18419
18883
  width: "100%",
18420
- minHeight: "100%",
18421
- overflow: "visible"
18884
+ height: "100%",
18885
+ minHeight: 0,
18886
+ overflow: "hidden"
18422
18887
  }
18423
18888
  },
18424
18889
  // ── Layer 1: TabsBar (sticky) ────────────────────────────────────────
@@ -18428,8 +18893,6 @@ Client: ${mismatch.client}`,
18428
18893
  "data-testid": "pentester-tabs-bar",
18429
18894
  style: {
18430
18895
  flex: "none",
18431
- position: "sticky",
18432
- top: 0,
18433
18896
  zIndex: 29,
18434
18897
  background: "var(--dsw-alias-bg-base)",
18435
18898
  borderBottom: "1px solid var(--dsw-alias-border-l2)",
@@ -18484,15 +18947,18 @@ Client: ${mismatch.client}`,
18484
18947
  }
18485
18948
  }, isRetrying ? "Retrying\u2026" : "Retry")
18486
18949
  ),
18487
- // ── Body: flex 1, no scroll ──────────────────────────────────────────
18950
+ // ── Body: bounded flex viewport ────────────────────────────────────
18488
18951
  import_react18.default.createElement(
18489
18952
  "div",
18490
18953
  {
18491
18954
  style: {
18492
18955
  flex: 1,
18493
18956
  minHeight: 0,
18494
- overflow: "visible",
18495
- padding: "16px"
18957
+ overflow: "hidden",
18958
+ display: "flex",
18959
+ flexDirection: "column",
18960
+ padding: "16px",
18961
+ boxSizing: "border-box"
18496
18962
  }
18497
18963
  },
18498
18964
  // Initial loading: shell is already visible, show skeleton
@@ -18599,23 +19065,40 @@ Client: ${mismatch.client}`,
18599
19065
  }
18600
19066
  }, "Refresh")
18601
19067
  ),
18602
- // Bound content
19068
+ // Bound content — wrapped in a bounded viewport
18603
19069
  dataState.kind === "bound" && import_react18.default.createElement(
18604
- import_react18.default.Fragment,
18605
- null,
18606
- tab === "overview" && import_react18.default.createElement(OverviewView, {
18607
- snapshot,
18608
- liveActivity,
18609
- onNavigate: navigate,
18610
- onOpenSession: openSession
18611
- }),
18612
- tab === "stages" && import_react18.default.createElement(StagesView, {
18613
- snapshot,
18614
- liveActivity,
18615
- onNavigate: navigate,
18616
- onOpenSession: openSession,
18617
- focusStage: navFocus.stage
18618
- }),
19070
+ "div",
19071
+ {
19072
+ style: {
19073
+ flex: 1,
19074
+ minHeight: 0,
19075
+ minWidth: 0,
19076
+ overflow: "hidden",
19077
+ display: "flex",
19078
+ flexDirection: "column"
19079
+ }
19080
+ },
19081
+ tab === "overview" && import_react18.default.createElement(
19082
+ ScrollablePage,
19083
+ null,
19084
+ import_react18.default.createElement(OverviewView, {
19085
+ snapshot,
19086
+ liveActivity,
19087
+ onNavigate: navigate,
19088
+ onOpenSession: openSession
19089
+ })
19090
+ ),
19091
+ tab === "stages" && import_react18.default.createElement(
19092
+ ScrollablePage,
19093
+ null,
19094
+ import_react18.default.createElement(StagesView, {
19095
+ snapshot,
19096
+ liveActivity,
19097
+ onNavigate: navigate,
19098
+ onOpenSession: openSession,
19099
+ focusStage: navFocus.stage
19100
+ })
19101
+ ),
18619
19102
  tab === "findings" && import_react18.default.createElement(FindingsView, {
18620
19103
  snapshot,
18621
19104
  command: pentesterCommand,
@@ -18623,13 +19106,17 @@ Client: ${mismatch.client}`,
18623
19106
  onNavigate: navigate,
18624
19107
  focusFinding: navFocus.finding
18625
19108
  }),
18626
- tab === "trace" && import_react18.default.createElement(TraceView, {
18627
- snapshot,
18628
- liveActivity,
18629
- onNavigate: navigate,
18630
- onOpenSession: openSession,
18631
- activeTargetId
18632
- }),
19109
+ tab === "trace" && import_react18.default.createElement(
19110
+ ScrollablePage,
19111
+ null,
19112
+ import_react18.default.createElement(TraceView, {
19113
+ snapshot,
19114
+ liveActivity,
19115
+ onNavigate: navigate,
19116
+ onOpenSession: openSession,
19117
+ activeTargetId
19118
+ })
19119
+ ),
18633
19120
  tab === "output" && import_react18.default.createElement(OutputView, {
18634
19121
  snapshot,
18635
19122
  command: pentesterCommand,
@@ -18640,6 +19127,17 @@ Client: ${mismatch.client}`,
18640
19127
  )
18641
19128
  );
18642
19129
  }
19130
+ function ScrollablePage(props) {
19131
+ return import_react18.default.createElement("div", {
19132
+ style: {
19133
+ flex: 1,
19134
+ minHeight: 0,
19135
+ overflowY: "auto",
19136
+ overflowX: "hidden",
19137
+ overscrollBehavior: "contain"
19138
+ }
19139
+ }, props.children);
19140
+ }
18643
19141
 
18644
19142
  // src/ui/client/pentester/locales.ts
18645
19143
  var NS = "dsh-pentester-view";
@@ -18721,7 +19219,7 @@ var en = {
18721
19219
  };
18722
19220
 
18723
19221
  // src/build-id.ts
18724
- var BUILD_ID = "2.0.0+c5466b6";
19222
+ var BUILD_ID = "2.3.0+2a31301";
18725
19223
 
18726
19224
  // src/ui/client/plugin.tsx
18727
19225
  var SESSION_SWITCH_PERF = "[SESSION_SWITCH_PERF]";
@@ -18735,11 +19233,7 @@ function getPentesterEndpointState2() {
18735
19233
  function setPentesterEndpointState(state) {
18736
19234
  pentesterEndpointState2 = state;
18737
19235
  }
18738
- var buildOk = false;
18739
19236
  var buildMismatch = null;
18740
- function isBuildOk() {
18741
- return buildOk;
18742
- }
18743
19237
  function getBuildMismatch() {
18744
19238
  return buildMismatch;
18745
19239
  }
@@ -18753,7 +19247,7 @@ function getRemoteMountError() {
18753
19247
  }
18754
19248
  var inject = ["slots", "remote", "sessions", "locale"];
18755
19249
  function apply(ctx) {
18756
- console.log(`[dsh-pentester] client build=${BUILD_ID}`);
19250
+ console.log(`[dsh-pentester/client] build=${BUILD_ID}`);
18757
19251
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "pentester: locale");
18758
19252
  ctx.effect(() => {
18759
19253
  let disposeMount;
@@ -18786,18 +19280,20 @@ function apply(ctx) {
18786
19280
  if (result.ok && result.value !== null && typeof result.value === "object" && "ok" in result.value) {
18787
19281
  const inner = result.value.value;
18788
19282
  if (inner?.ready === true && typeof inner.buildId === "string") {
19283
+ console.log(`[dsh-pentester/client] health host=${inner.buildId} client=${BUILD_ID}`);
18789
19284
  if (inner.buildId === BUILD_ID) {
18790
- buildOk = true;
19285
+ if (buildMismatch !== null) {
19286
+ console.log(`[dsh-pentester/client] build mismatch cleared (was host=${buildMismatch.host} client=${buildMismatch.client})`);
19287
+ }
18791
19288
  buildMismatch = null;
18792
- console.log(`[dsh-pentester] health check passed host=${inner.buildId} client=${BUILD_ID}`);
18793
19289
  } else {
18794
- buildOk = false;
18795
19290
  buildMismatch = { host: inner.buildId, client: BUILD_ID };
18796
- console.error(`[dsh-pentester] BUILD MISMATCH host=${inner.buildId} client=${BUILD_ID}`);
19291
+ console.error(`[dsh-pentester/client] build mismatch set host=${inner.buildId} client=${BUILD_ID}`);
18797
19292
  }
18798
19293
  }
18799
19294
  }
18800
- } catch {
19295
+ } catch (error51) {
19296
+ console.warn("[dsh-pentester/client] build health check unavailable", error51);
18801
19297
  }
18802
19298
  })();
18803
19299
  }).catch((error51) => {