pentesting 0.48.3 → 0.49.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.
Files changed (2) hide show
  1. package/dist/main.js +98 -47
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -13,7 +13,7 @@ import chalk from "chalk";
13
13
 
14
14
  // src/platform/tui/app.tsx
15
15
  import { useState as useState5, useCallback as useCallback4, useEffect as useEffect4, useRef as useRef5 } from "react";
16
- import { Box as Box6, useInput as useInput2, useApp } from "ink";
16
+ import { Box as Box6, useInput as useInput2, useApp, useStdout } from "ink";
17
17
 
18
18
  // src/platform/tui/hooks/useAgent.ts
19
19
  import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef3 } from "react";
@@ -331,7 +331,7 @@ var ORPHAN_PROCESS_NAMES = [
331
331
 
332
332
  // src/shared/constants/agent.ts
333
333
  var APP_NAME = "Pentest AI";
334
- var APP_VERSION = "0.48.3";
334
+ var APP_VERSION = "0.49.0";
335
335
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
336
336
  var LLM_ROLES = {
337
337
  SYSTEM: "system",
@@ -2920,6 +2920,8 @@ var AttackGraph = class {
2920
2920
  // ─── TUI Visualization ──────────────────────────────────────
2921
2921
  /**
2922
2922
  * Generate ASCII visualization for TUI /graph command.
2923
+ * WHY no truncation: /graph is the user's audit view — all nodes must be visible.
2924
+ * Truncation is only for toPrompt() (LLM context budget).
2923
2925
  */
2924
2926
  toASCII() {
2925
2927
  if (this.nodes.size === 0) return "(Empty attack graph \u2014 no discoveries yet)";
@@ -2945,17 +2947,13 @@ var AttackGraph = class {
2945
2947
  [NODE_STATUS.SUCCEEDED]: "\u25CF",
2946
2948
  [NODE_STATUS.FAILED]: "\u2717"
2947
2949
  };
2948
- let nodeCount = 0;
2950
+ const typeSummary = Object.entries(groups).map(([t, ns]) => `${typeIcons[t] || "\xB7"} ${ns.length}`).join(" ");
2951
+ lines.push(`\u2502 ${typeSummary}`);
2949
2952
  for (const [type, nodes] of Object.entries(groups)) {
2950
- if (nodeCount >= GRAPH_LIMITS.ASCII_MAX_NODES) {
2951
- lines.push(`\u2502 ... and ${this.nodes.size - nodeCount} more nodes`);
2952
- break;
2953
- }
2954
2953
  const icon = typeIcons[type] || "\xB7";
2955
2954
  lines.push(`\u2502`);
2956
2955
  lines.push(`\u2502 ${icon} ${type.toUpperCase()} (${nodes.length})`);
2957
2956
  for (const node of nodes) {
2958
- if (nodeCount >= GRAPH_LIMITS.ASCII_MAX_NODES) break;
2959
2957
  const sIcon = statusIcons[node.status] || "?";
2960
2958
  const fail = node.status === NODE_STATUS.FAILED && node.failReason ? ` \u2014 ${node.failReason}` : "";
2961
2959
  let detail = "";
@@ -2982,7 +2980,6 @@ var AttackGraph = class {
2982
2980
  return `${eName}${eStatus}`;
2983
2981
  }).join(", ")}` : "";
2984
2982
  lines.push(`\u2502 ${sIcon} ${node.label}${detail}${fail}${edgeStr}`);
2985
- nodeCount++;
2986
2983
  }
2987
2984
  }
2988
2985
  const succeededNodes = Array.from(this.nodes.values()).filter((n) => n.status === NODE_STATUS.SUCCEEDED);
@@ -3816,19 +3813,27 @@ var AgentEventEmitter = class {
3816
3813
  }
3817
3814
  }
3818
3815
  /**
3819
- * Emit an event
3816
+ * Emit an event.
3817
+ * WHY try-catch: Listeners must NEVER crash the emitter.
3818
+ * If a TUI listener throws (e.g., setState after unmount), the agent loop must survive.
3820
3819
  */
3821
3820
  emit(event) {
3822
3821
  const listeners = this.listeners.get(event.type);
3823
3822
  if (listeners) {
3824
3823
  for (const listener of listeners) {
3825
- listener(event);
3824
+ try {
3825
+ listener(event);
3826
+ } catch {
3827
+ }
3826
3828
  }
3827
3829
  }
3828
3830
  const anyListeners = this.listeners.get("*");
3829
3831
  if (anyListeners) {
3830
3832
  for (const listener of anyListeners) {
3831
- listener(event);
3833
+ try {
3834
+ listener(event);
3835
+ } catch {
3836
+ }
3832
3837
  }
3833
3838
  }
3834
3839
  }
@@ -10120,10 +10125,6 @@ function parseAnalystMemo(response) {
10120
10125
  }
10121
10126
  function formatAnalystDigest(digest, filePath, originalChars) {
10122
10127
  return [
10123
- "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
10124
- "\u2551 ANALYST DIGEST (Independent LLM analysis) \u2551",
10125
- "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
10126
- "",
10127
10128
  digest,
10128
10129
  "",
10129
10130
  `\u{1F4C2} Full output saved: ${filePath} (${originalChars} chars)`,
@@ -12571,8 +12572,14 @@ var useAgentState = () => {
12571
12572
  }
12572
12573
  }, []);
12573
12574
  const clearAllTimers = useCallback(() => {
12574
- if (timerRef.current) clearInterval(timerRef.current);
12575
- if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
12575
+ if (timerRef.current) {
12576
+ clearInterval(timerRef.current);
12577
+ timerRef.current = null;
12578
+ }
12579
+ if (retryCountdownRef.current) {
12580
+ clearInterval(retryCountdownRef.current);
12581
+ retryCountdownRef.current = null;
12582
+ }
12576
12583
  }, []);
12577
12584
  return {
12578
12585
  // State
@@ -12838,7 +12845,10 @@ function handleRetry(e, addMessage, setRetryState, retryCountdownRef, retryCount
12838
12845
  remaining -= 1;
12839
12846
  if (remaining <= 0) {
12840
12847
  setRetryState({ status: "idle" });
12841
- if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
12848
+ if (retryCountdownRef.current) {
12849
+ clearInterval(retryCountdownRef.current);
12850
+ retryCountdownRef.current = null;
12851
+ }
12842
12852
  } else {
12843
12853
  setRetryState((prev) => prev.status === "retrying" ? { ...prev, countdown: remaining } : prev);
12844
12854
  }
@@ -12906,25 +12916,33 @@ var useAgent = (shouldAutoApprove, target) => {
12906
12916
  }
12907
12917
  }, [agent, target]);
12908
12918
  useAgentEvents(agent, eventsRef, state);
12919
+ const abortedRef = useRef3(false);
12909
12920
  const executeTask = useCallback2(async (task) => {
12921
+ abortedRef.current = false;
12910
12922
  setIsProcessing(true);
12911
12923
  manageTimer("start");
12912
12924
  setCurrentStatus("Thinking");
12913
12925
  resetCumulativeCounters();
12914
12926
  try {
12915
12927
  const response = await agent.execute(task);
12928
+ if (abortedRef.current) return;
12916
12929
  const meta = lastResponseMetaRef.current;
12917
12930
  const suffix = meta ? ` ${formatMeta(meta.durationMs || 0, (meta.tokens?.input || 0) + (meta.tokens?.output || 0))}` : "";
12918
12931
  addMessage("ai", response + suffix);
12919
12932
  } catch (e) {
12920
- addMessage("error", e instanceof Error ? e.message : String(e));
12933
+ if (!abortedRef.current) {
12934
+ addMessage("error", e instanceof Error ? e.message : String(e));
12935
+ }
12921
12936
  } finally {
12922
- manageTimer("stop");
12923
- setIsProcessing(false);
12924
- setCurrentStatus("");
12937
+ if (!abortedRef.current) {
12938
+ manageTimer("stop");
12939
+ setIsProcessing(false);
12940
+ setCurrentStatus("");
12941
+ }
12925
12942
  }
12926
12943
  }, [agent, addMessage, manageTimer, resetCumulativeCounters, setIsProcessing, lastResponseMetaRef, setCurrentStatus]);
12927
12944
  const abort = useCallback2(() => {
12945
+ abortedRef.current = true;
12928
12946
  agent.abort();
12929
12947
  setIsProcessing(false);
12930
12948
  manageTimer("stop");
@@ -13353,6 +13371,7 @@ var ChatInput = memo4(({
13353
13371
  paddingX: showPreview ? 1 : 0,
13354
13372
  marginBottom: 0,
13355
13373
  height: showPreview ? void 0 : 0,
13374
+ overflowX: "hidden",
13356
13375
  children: showPreview && suggestions.map((cmd, i) => {
13357
13376
  const isFirst = i === 0;
13358
13377
  const nameColor = isFirst ? THEME.white : THEME.gray;
@@ -13380,6 +13399,7 @@ var ChatInput = memo4(({
13380
13399
  borderStyle: "single",
13381
13400
  borderColor: inputRequest.status === "active" ? THEME.yellow : THEME.border.default,
13382
13401
  paddingX: 1,
13402
+ overflowX: "hidden",
13383
13403
  children: inputRequest.status === "active" ? /* @__PURE__ */ jsxs4(Box4, { children: [
13384
13404
  /* @__PURE__ */ jsx5(Text5, { color: THEME.yellow, children: "[auth]" }),
13385
13405
  /* @__PURE__ */ jsxs4(Text5, { color: THEME.gray, children: [
@@ -13469,6 +13489,8 @@ var footer_default = Footer;
13469
13489
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
13470
13490
  var App = ({ autoApprove = false, target }) => {
13471
13491
  const { exit } = useApp();
13492
+ const { stdout } = useStdout();
13493
+ const terminalWidth = stdout?.columns ?? 80;
13472
13494
  const [input, setInput] = useState5("");
13473
13495
  const [secretInput, setSecretInput] = useState5("");
13474
13496
  const [autoApproveMode, setAutoApproveMode] = useState5(autoApprove);
@@ -13498,12 +13520,15 @@ var App = ({ autoApprove = false, target }) => {
13498
13520
  inputRequestRef.current = inputRequest;
13499
13521
  const handleExit = useCallback4(() => {
13500
13522
  const ir = inputRequestRef.current;
13501
- if (ir.status === "active") ir.resolve(null);
13523
+ if (ir.status === "active") {
13524
+ ir.resolve(null);
13525
+ setInputRequest({ status: "inactive" });
13526
+ }
13502
13527
  cleanupAllProcesses().catch(() => {
13503
13528
  });
13504
13529
  exit();
13505
13530
  setTimeout(() => process.exit(0), DISPLAY_LIMITS.EXIT_DELAY);
13506
- }, [exit]);
13531
+ }, [exit, setInputRequest]);
13507
13532
  const handleCommand = useCallback4(async (cmd, args) => {
13508
13533
  switch (cmd) {
13509
13534
  case UI_COMMANDS.HELP:
@@ -13562,28 +13587,54 @@ var App = ({ autoApprove = false, target }) => {
13562
13587
  addMessage("system", "No findings.");
13563
13588
  break;
13564
13589
  }
13565
- const findingLines = [`\u2500\u2500\u2500 ${findings.length} Findings \u2500\u2500\u2500`, ""];
13566
- findings.forEach((f, i) => {
13567
- const verified = f.isVerified ? `[${ICONS.success}] Verified` : `[${ICONS.warning}] Unverified`;
13568
- const atk = f.attackPattern ? ` | ATT&CK: ${f.attackPattern}` : "";
13569
- findingLines.push(`[${i + 1}] [${f.severity.toUpperCase()}] ${f.title}`);
13570
- findingLines.push(` ${verified}${atk}`);
13571
- if (f.affected.length > 0) {
13572
- findingLines.push(` Affected: ${f.affected.join(", ")}`);
13573
- }
13574
- if (f.description) {
13575
- findingLines.push(` ${f.description}`);
13576
- }
13577
- if (f.evidence.length > 0) {
13578
- findingLines.push(` Evidence:`);
13579
- f.evidence.slice(0, DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW).forEach((e) => {
13580
- const preview = e.length > DISPLAY_LIMITS.EVIDENCE_PREVIEW_LENGTH ? e.slice(0, DISPLAY_LIMITS.EVIDENCE_PREVIEW_LENGTH) + "..." : e;
13581
- findingLines.push(` \u25B8 ${preview}`);
13582
- });
13583
- if (f.evidence.length > DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW) findingLines.push(` ... +${f.evidence.length - DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW} more`);
13584
- }
13590
+ const severityOrder = ["critical", "high", "medium", "low", "info"];
13591
+ const severityIcons = {
13592
+ critical: "\u{1F534}",
13593
+ high: "\u{1F7E0}",
13594
+ medium: "\u{1F7E1}",
13595
+ low: "\u{1F7E2}",
13596
+ info: "\u26AA"
13597
+ };
13598
+ const grouped = {};
13599
+ for (const f of findings) {
13600
+ const sev = f.severity.toLowerCase();
13601
+ if (!grouped[sev]) grouped[sev] = [];
13602
+ grouped[sev].push(f);
13603
+ }
13604
+ const findingLines = [];
13605
+ const sevCounts = severityOrder.filter((s) => grouped[s]?.length).map((s) => `${severityIcons[s]} ${s.toUpperCase()}: ${grouped[s].length}`).join(" ");
13606
+ findingLines.push(`\u2500\u2500\u2500 ${findings.length} Findings \u2500\u2500 ${sevCounts} \u2500\u2500\u2500`);
13607
+ findingLines.push("");
13608
+ for (const sev of severityOrder) {
13609
+ const group = grouped[sev];
13610
+ if (!group?.length) continue;
13611
+ const icon = severityIcons[sev] || "\u2022";
13612
+ findingLines.push(`${icon} \u2500\u2500 ${sev.toUpperCase()} (${group.length}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
13585
13613
  findingLines.push("");
13586
- });
13614
+ group.forEach((f, i) => {
13615
+ const verified = f.isVerified ? `\u2713 Verified` : `? Unverified`;
13616
+ const atk = f.attackPattern ? ` \u2502 ATT&CK: ${f.attackPattern}` : "";
13617
+ const cat = f.category ? ` \u2502 ${f.category}` : "";
13618
+ findingLines.push(` [${i + 1}] ${f.title}`);
13619
+ findingLines.push(` ${verified}${atk}${cat}`);
13620
+ if (f.affected.length > 0) {
13621
+ findingLines.push(` Affected: ${f.affected.join(", ")}`);
13622
+ }
13623
+ if (f.description) {
13624
+ findingLines.push(` ${f.description}`);
13625
+ }
13626
+ if (f.evidence.length > 0) {
13627
+ findingLines.push(` Evidence:`);
13628
+ f.evidence.forEach((e) => {
13629
+ findingLines.push(` \u25B8 ${e}`);
13630
+ });
13631
+ }
13632
+ if (f.remediation) {
13633
+ findingLines.push(` Fix: ${f.remediation}`);
13634
+ }
13635
+ findingLines.push("");
13636
+ });
13637
+ }
13587
13638
  addMessage("system", findingLines.join("\n"));
13588
13639
  break;
13589
13640
  case UI_COMMANDS.ASSETS:
@@ -13693,7 +13744,7 @@ ${procData.stdout || "(no output)"}
13693
13744
  process.off("SIGTERM", onSignal);
13694
13745
  };
13695
13746
  }, [handleCtrlC]);
13696
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 1, children: [
13747
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 1, width: terminalWidth, children: [
13697
13748
  /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginBottom: 1, flexGrow: 1, children: /* @__PURE__ */ jsx7(MessageList, { messages }) }),
13698
13749
  /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
13699
13750
  /* @__PURE__ */ jsx7(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pentesting",
3
- "version": "0.48.3",
3
+ "version": "0.49.0",
4
4
  "description": "Autonomous Penetration Testing AI Agent",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",