pentesting 0.56.7 → 0.70.1

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/main.js CHANGED
@@ -239,7 +239,13 @@ var SPECIAL_FILES = {
239
239
  /** Persistent knowledge store */
240
240
  PERSISTENT_KNOWLEDGE: "persistent-knowledge.json",
241
241
  /** Debug log file */
242
- DEBUG_LOG: "debug.log"
242
+ DEBUG_LOG: "debug.log",
243
+ /**
244
+ * Session snapshot for Insane-level long session resumption (3차).
245
+ * Stores current phase, achieved foothold, next steps, credentials.
246
+ * Written by PlaybookSynthesizer on flag capture / manual update_mission.
247
+ */
248
+ SESSION_SNAPSHOT: "session-snapshot.json"
243
249
  };
244
250
 
245
251
  // src/shared/constants/files/patterns.ts
@@ -341,6 +347,9 @@ function initDebugLogger() {
341
347
  function debugLog(category, message, data) {
342
348
  logger.log(category, message, data);
343
349
  }
350
+ function flowLog(actor, direction, store, detail) {
351
+ logger.log("flow", `${actor} ${direction} ${store}: ${detail}`);
352
+ }
344
353
 
345
354
  // src/shared/utils/config/env.ts
346
355
  var ENV_KEYS = {
@@ -621,7 +630,11 @@ var DISPLAY_LIMITS = {
621
630
  /** Max evidence items to show in finding preview */
622
631
  EVIDENCE_ITEMS_PREVIEW: 3,
623
632
  /** Max characters per evidence item in preview */
624
- EVIDENCE_PREVIEW_LENGTH: 120
633
+ EVIDENCE_PREVIEW_LENGTH: 120,
634
+ /** Max characters for API error response body */
635
+ API_ERROR_PREVIEW: 500,
636
+ /** Max characters per session/storage value in auth capture */
637
+ SESSION_VALUE_PREVIEW: 300
625
638
  };
626
639
 
627
640
  // src/shared/constants/limits/agent.ts
@@ -711,7 +724,7 @@ var INPUT_PROMPT_PATTERNS = [
711
724
 
712
725
  // src/shared/constants/agent.ts
713
726
  var APP_NAME = "Pentest AI";
714
- var APP_VERSION = "0.56.7";
727
+ var APP_VERSION = "0.70.1";
715
728
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
716
729
  var LLM_ROLES = {
717
730
  SYSTEM: "system",
@@ -932,6 +945,8 @@ var TOOL_NAMES = {
932
945
  ADD_TARGET: "add_target",
933
946
  ASK_USER: "ask_user",
934
947
  HELP: "help",
948
+ // Session Management (3차 Insane — long session resumption)
949
+ SAVE_SESSION_SNAPSHOT: "save_session_snapshot",
935
950
  // Resource Management
936
951
  BG_STATUS: "bg_status",
937
952
  BG_CLEANUP: "bg_cleanup",
@@ -1108,14 +1123,18 @@ var NODE_TYPE = {
1108
1123
  VULNERABILITY: "vulnerability",
1109
1124
  ACCESS: "access",
1110
1125
  LOOT: "loot",
1111
- OSINT: "osint"
1112
- };
1113
- var NODE_TYPES = {
1114
- ...NODE_TYPE,
1115
- EXPLOIT: "exploit",
1116
- TECHNIQUE: "technique",
1117
- TARGET: "target",
1118
- FINDING: "finding"
1126
+ OSINT: "osint",
1127
+ // ─── Active Directory Node Types (2차 — Hard/Insane support) ───
1128
+ /** AD domain object (e.g. DOMAIN.LOCAL) */
1129
+ DOMAIN: "domain",
1130
+ /** AD user or computer account */
1131
+ USER_ACCOUNT: "user-account",
1132
+ /** Group Policy Object — may grant code exec if writable */
1133
+ GPO: "gpo",
1134
+ /** ACL entry — WriteDACL/GenericAll/etc. on another object */
1135
+ ACL: "acl",
1136
+ /** ADCS certificate template — ESC1-13 attack surface */
1137
+ CERTIFICATE_TEMPLATE: "certificate-template"
1119
1138
  };
1120
1139
  var NODE_STATUS = {
1121
1140
  DISCOVERED: "discovered",
@@ -1326,13 +1345,21 @@ function formatFailedNode(label, reason) {
1326
1345
  function recommendChains(nodes, edges) {
1327
1346
  const chains = [];
1328
1347
  const visited = /* @__PURE__ */ new Set();
1329
- const goalTypes = [NODE_TYPE.ACCESS, NODE_TYPE.LOOT];
1348
+ const goalTypes = [
1349
+ NODE_TYPE.ACCESS,
1350
+ NODE_TYPE.LOOT,
1351
+ NODE_TYPE.DOMAIN,
1352
+ // AD Domain compromise
1353
+ NODE_TYPE.CERTIFICATE_TEMPLATE
1354
+ // ADCS attack path goal
1355
+ ];
1330
1356
  const entryNodes = Array.from(nodes.values()).filter(
1331
1357
  (n) => n.type === NODE_TYPE.HOST || n.type === NODE_TYPE.SERVICE || n.type === NODE_TYPE.CREDENTIAL || n.type === NODE_TYPE.OSINT
1332
1358
  );
1359
+ const ctx = { nodes, edges, goalTypes, results: chains };
1333
1360
  for (const entry of entryNodes) {
1334
1361
  visited.clear();
1335
- dfsChains(entry.id, nodes, edges, [], [], visited, chains, goalTypes);
1362
+ dfsChains(entry.id, [], [], visited, ctx);
1336
1363
  }
1337
1364
  const seen = /* @__PURE__ */ new Set();
1338
1365
  const unique = chains.filter((c) => {
@@ -1345,7 +1372,8 @@ function recommendChains(nodes, edges) {
1345
1372
  );
1346
1373
  return unique.slice(0, GRAPH_LIMITS.MAX_CHAINS);
1347
1374
  }
1348
- function dfsChains(nodeId, nodes, edges, path2, pathEdges, visited, results, goalTypes) {
1375
+ function dfsChains(nodeId, path2, pathEdges, visited, ctx) {
1376
+ const { nodes, edges, goalTypes, results } = ctx;
1349
1377
  if (visited.has(nodeId)) return;
1350
1378
  if (path2.length >= GRAPH_LIMITS.MAX_CHAIN_DEPTH) return;
1351
1379
  const node = nodes.get(nodeId);
@@ -1369,7 +1397,7 @@ function dfsChains(nodeId, nodes, edges, path2, pathEdges, visited, results, goa
1369
1397
  (e) => e.from === nodeId && e.status !== EDGE_STATUS.FAILED
1370
1398
  );
1371
1399
  for (const edge of outEdges) {
1372
- dfsChains(edge.to, nodes, edges, path2, [...pathEdges, edge], visited, results, goalTypes);
1400
+ dfsChains(edge.to, path2, [...pathEdges, edge], visited, ctx);
1373
1401
  }
1374
1402
  path2.pop();
1375
1403
  visited.delete(nodeId);
@@ -1387,8 +1415,48 @@ function estimateImpact(node) {
1387
1415
  if (sev === SEVERITY.HIGH) return SEVERITY.HIGH;
1388
1416
  if (sev === SEVERITY.MEDIUM) return SEVERITY.MEDIUM;
1389
1417
  }
1418
+ if (node.type === NODE_TYPE.DOMAIN) return SEVERITY.CRITICAL;
1419
+ if (node.type === NODE_TYPE.CERTIFICATE_TEMPLATE) return SEVERITY.HIGH;
1390
1420
  return SEVERITY.LOW;
1391
1421
  }
1422
+ function extractSuccessfulChain(nodes, edges) {
1423
+ const MIN_CHAIN_LENGTH = 2;
1424
+ const succeededNodes = Array.from(nodes.values()).filter((n) => n.status === NODE_STATUS.SUCCEEDED);
1425
+ if (succeededNodes.length < MIN_CHAIN_LENGTH) return null;
1426
+ const succeededEdges = edges.filter((e) => e.status === EDGE_STATUS.SUCCEEDED);
1427
+ const chainLabels = [];
1428
+ const toolsUsed = [];
1429
+ const visited = /* @__PURE__ */ new Set();
1430
+ const incomingTargets = new Set(succeededEdges.map((e) => e.to));
1431
+ const entryNodes = succeededNodes.filter((n) => !incomingTargets.has(n.id));
1432
+ const queue = entryNodes.length > 0 ? [entryNodes[0]] : [succeededNodes[0]];
1433
+ while (queue.length > 0) {
1434
+ const current = queue.shift();
1435
+ if (visited.has(current.id)) continue;
1436
+ visited.add(current.id);
1437
+ const abstractLabel = abstractifyLabel(current.label);
1438
+ chainLabels.push(abstractLabel);
1439
+ if (current.technique) toolsUsed.push(current.technique.split(" ")[0]);
1440
+ const nextEdges = succeededEdges.filter((e) => e.from === current.id);
1441
+ for (const edge of nextEdges) {
1442
+ const nextNode = nodes.get(edge.to);
1443
+ if (nextNode && nextNode.status === NODE_STATUS.SUCCEEDED) {
1444
+ if (edge.action) toolsUsed.push(edge.action.split(" ")[0]);
1445
+ queue.push(nextNode);
1446
+ }
1447
+ }
1448
+ }
1449
+ if (chainLabels.length < MIN_CHAIN_LENGTH) return null;
1450
+ const serviceNodes = succeededNodes.filter((n) => n.type === NODE_TYPE.SERVICE);
1451
+ const serviceProfile = serviceNodes.length > 0 ? serviceNodes.map((n) => abstractifyLabel(n.label)).join(" + ") : "unknown";
1452
+ const chainSummary = chainLabels.join(" \u2192 ");
1453
+ const uniqueTools = [...new Set(toolsUsed.filter(Boolean))];
1454
+ return { serviceProfile, chainSummary, toolsUsed: uniqueTools };
1455
+ }
1456
+ function abstractifyLabel(label) {
1457
+ let result = label.replace(/^(host|service|vulnerability|access|loot|osint|credential|cve_search):/i, "").replace(/^https?:\/\/|^ftp:\/\//gi, "").replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?\b/g, "").replace(/:\d{2,5}\b/g, "").replace(/\b[0-9a-f]{16,}\b/gi, "").replace(/(\d+\.\d+)\.\d+/g, "$1.x").trim().replace(/\s+/g, " ");
1458
+ return result || label;
1459
+ }
1392
1460
 
1393
1461
  // src/shared/utils/attack-graph/prompt-formatter.ts
1394
1462
  function formatGraphForPrompt(nodes, edges, failedPaths) {
@@ -1461,7 +1529,13 @@ var TYPE_ICONS = {
1461
1529
  [NODE_TYPE.CREDENTIAL]: "\u{1F511}",
1462
1530
  [NODE_TYPE.ACCESS]: "\u{1F513}",
1463
1531
  [NODE_TYPE.LOOT]: "\u{1F3F4}",
1464
- [NODE_TYPE.OSINT]: "\u{1F50D}"
1532
+ [NODE_TYPE.OSINT]: "\u{1F50D}",
1533
+ // AD node icons (2차 — Hard/Insane support)
1534
+ [NODE_TYPE.DOMAIN]: "\u{1F3F0}",
1535
+ [NODE_TYPE.USER_ACCOUNT]: "\u{1F464}",
1536
+ [NODE_TYPE.GPO]: "\u{1F4CB}",
1537
+ [NODE_TYPE.ACL]: "\u{1F510}",
1538
+ [NODE_TYPE.CERTIFICATE_TEMPLATE]: "\u{1F4DC}"
1465
1539
  };
1466
1540
  var STATUS_ICONS = {
1467
1541
  [NODE_STATUS.DISCOVERED]: "\u25CB",
@@ -1897,6 +1971,19 @@ var AttackGraph = class {
1897
1971
  addOSINT(category, detail, data = {}) {
1898
1972
  return registerOSINT(this.graphContainer, category, detail, data);
1899
1973
  }
1974
+ /**
1975
+ * Record an Active Directory object (domain, user-account, GPO, ACL, certificate-template).
1976
+ *
1977
+ * WHY: AD attacks chain through complex object relationships (ACL → user → cert → DA).
1978
+ * Tracking these as graph nodes lets the Strategist see multi-hop AD paths.
1979
+ *
1980
+ * type: 'domain' | 'user-account' | 'gpo' | 'acl' | 'certificate-template'
1981
+ * label: human-readable name (e.g. "CORP.LOCAL", "svc_sql", "ESC1-vulnerable")
1982
+ * data: freeform metadata (members, rights, target, confidence, etc.)
1983
+ */
1984
+ addDomainObject(type, label, data = {}) {
1985
+ return this.addNode(type, label, { adObject: true, ...data });
1986
+ }
1900
1987
  // ─── Chain Discovery (DFS) ──────────────────────────────────
1901
1988
  /**
1902
1989
  * Find all viable attack paths using DFS.
@@ -2011,6 +2098,9 @@ function formatFingerprint(fp) {
2011
2098
  }
2012
2099
 
2013
2100
  // src/shared/utils/agent-memory/working-memory.ts
2101
+ var PRUNE_IMPORTANCE_WEIGHT = 0.7;
2102
+ var PRUNE_RECENCY_WEIGHT = 0.3;
2103
+ var PRUNE_WINDOW_MS = 6e5;
2014
2104
  var WorkingMemory = class {
2015
2105
  entries = [];
2016
2106
  maxEntries = MEMORY_LIMITS.WORKING_MEMORY_MAX_ENTRIES;
@@ -2032,8 +2122,9 @@ var WorkingMemory = class {
2032
2122
  * and "hydra with darkweb2017.txt" are stored as distinct attempts.
2033
2123
  * The LLM can see what parameter combinations have been tried and pick new ones.
2034
2124
  */
2035
- recordFailure(tool, command, error) {
2125
+ recordFailure(tool, command, error, hypothesizedReason) {
2036
2126
  const fp = extractFingerprint(tool, command);
2127
+ fp.hypothesizedReason = hypothesizedReason;
2037
2128
  const fpLabel = formatFingerprint(fp);
2038
2129
  const entry = {
2039
2130
  id: `wm_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
@@ -2066,12 +2157,32 @@ var WorkingMemory = class {
2066
2157
  (e) => e.category === "failure" && e.fingerprint != null && fingerprintsMatch(e.fingerprint, fp)
2067
2158
  );
2068
2159
  }
2160
+ /**
2161
+ * 사후 보완: Analyst가 분류한 hypothesizedReason을 기존 실패 항목에 추가한다.
2162
+ *
2163
+ * WHY: pipeline.ts에서 recordFailure()가 먼저 호출되고, Analyst 분석은
2164
+ * 비동기로 나중에 완료된다. command 매칭으로 최근 실패 fingerprint를 찾아
2165
+ * hypothesizedReason을 보완한다. 기존 reason이 있으면 덮어쓰지 않는다.
2166
+ */
2167
+ updateLastFailureReason(command, hypothesizedReason) {
2168
+ const effectiveTool = command.split(/\s+/)[0] || "";
2169
+ const fp = extractFingerprint(effectiveTool, command);
2170
+ for (let i = this.entries.length - 1; i >= 0; i--) {
2171
+ const entry = this.entries[i];
2172
+ if (entry.category === "failure" && entry.fingerprint && fingerprintsMatch(entry.fingerprint, fp)) {
2173
+ if (!entry.fingerprint.hypothesizedReason) {
2174
+ entry.fingerprint.hypothesizedReason = hypothesizedReason;
2175
+ }
2176
+ break;
2177
+ }
2178
+ }
2179
+ }
2069
2180
  /** Internal prune helper (used by both add() and recordFailure()) */
2070
2181
  pruneIfNeeded() {
2071
2182
  if (this.entries.length > this.maxEntries) {
2072
2183
  this.entries.sort((a, b) => {
2073
- const aScore = a.importance * 0.7 + (1 - (Date.now() - a.timestamp) / 6e5) * 0.3;
2074
- const bScore = b.importance * 0.7 + (1 - (Date.now() - b.timestamp) / 6e5) * 0.3;
2184
+ const aScore = a.importance * PRUNE_IMPORTANCE_WEIGHT + (1 - (Date.now() - a.timestamp) / PRUNE_WINDOW_MS) * PRUNE_RECENCY_WEIGHT;
2185
+ const bScore = b.importance * PRUNE_IMPORTANCE_WEIGHT + (1 - (Date.now() - b.timestamp) / PRUNE_WINDOW_MS) * PRUNE_RECENCY_WEIGHT;
2075
2186
  return bScore - aScore;
2076
2187
  });
2077
2188
  this.entries = this.entries.slice(0, this.maxEntries);
@@ -2102,36 +2213,47 @@ var WorkingMemory = class {
2102
2213
  const successes = this.entries.filter((e) => e.category === "success");
2103
2214
  const insights = this.entries.filter((e) => e.category === "insight" || e.category === "discovery");
2104
2215
  const lines = ["<working-memory>"];
2105
- if (failures.length > 0) {
2106
- lines.push(`\u26A0\uFE0F FAILED ATTEMPTS (${failures.length} \u2014 DO NOT REPEAT EXACT SAME PARAMS):`);
2107
- for (const f of failures.slice(-DISPLAY_LIMITS.RECENT_FAILURES)) {
2108
- const fp = f.fingerprint;
2109
- if (fp) {
2110
- lines.push(` \u2717 ${formatFingerprint(fp)} \u2192 ${f.content.split("\u2192").pop()?.trim() || ""}`);
2111
- } else {
2112
- lines.push(` \u2717 ${f.content}`);
2113
- }
2216
+ lines.push(...this.buildFailuresSection(failures));
2217
+ lines.push(...this.buildSuccessesSection(successes));
2218
+ lines.push(...this.buildInsightsSection(insights));
2219
+ lines.push("</working-memory>");
2220
+ return lines.join("\n");
2221
+ }
2222
+ buildFailuresSection(failures) {
2223
+ if (failures.length === 0) return [];
2224
+ const lines = [];
2225
+ lines.push(`\u26A0\uFE0F FAILED ATTEMPTS (${failures.length} \u2014 DO NOT REPEAT EXACT SAME PARAMS):`);
2226
+ for (const f of failures.slice(-DISPLAY_LIMITS.RECENT_FAILURES)) {
2227
+ const fp = f.fingerprint;
2228
+ if (fp) {
2229
+ const reason = fp.hypothesizedReason ? ` [${fp.hypothesizedReason}]` : "";
2230
+ lines.push(` \u2717 ${formatFingerprint(fp)}${reason} \u2192 ${f.content.split("\u2192").pop()?.trim() || ""}`);
2231
+ } else {
2232
+ lines.push(` \u2717 ${f.content}`);
2114
2233
  }
2115
- lines.push(...this.buildAttackCoverageLines(failures));
2116
2234
  }
2235
+ lines.push(...this.buildAttackCoverageLines(failures));
2117
2236
  const consecutiveFails = this.getConsecutiveFailures();
2118
2237
  if (consecutiveFails >= MEMORY_LIMITS.CONSECUTIVE_FAIL_THRESHOLD) {
2119
2238
  lines.push(`\u{1F534} ${consecutiveFails} CONSECUTIVE FAILURES \u2014 consider changing approach or parameters`);
2120
2239
  }
2121
- if (successes.length > 0) {
2122
- lines.push(`\u2705 RECENT SUCCESSES (${successes.length}):`);
2123
- for (const s of successes.slice(-DISPLAY_LIMITS.RECENT_SUCCESSES)) {
2124
- lines.push(` \u2713 ${s.content}`);
2125
- }
2240
+ return lines;
2241
+ }
2242
+ buildSuccessesSection(successes) {
2243
+ if (successes.length === 0) return [];
2244
+ const lines = [`\u2705 RECENT SUCCESSES (${successes.length}):`];
2245
+ for (const s of successes.slice(-DISPLAY_LIMITS.RECENT_SUCCESSES)) {
2246
+ lines.push(` \u2713 ${s.content}`);
2126
2247
  }
2127
- if (insights.length > 0) {
2128
- lines.push(`\u{1F4A1} INSIGHTS:`);
2129
- for (const i of insights.slice(-DISPLAY_LIMITS.RECENT_INSIGHTS)) {
2130
- lines.push(` \u2192 ${i.content}`);
2131
- }
2248
+ return lines;
2249
+ }
2250
+ buildInsightsSection(insights) {
2251
+ if (insights.length === 0) return [];
2252
+ const lines = ["\u{1F4A1} INSIGHTS:"];
2253
+ for (const i of insights.slice(-DISPLAY_LIMITS.RECENT_INSIGHTS)) {
2254
+ lines.push(` \u2192 ${i.content}`);
2132
2255
  }
2133
- lines.push("</working-memory>");
2134
- return lines.join("\n");
2256
+ return lines;
2135
2257
  }
2136
2258
  /**
2137
2259
  * Build ATTACK COVERAGE lines grouped by tool+target.
@@ -2227,9 +2349,38 @@ var EpisodicMemory = class {
2227
2349
  };
2228
2350
 
2229
2351
  // src/shared/utils/agent-memory/persistent-memory.ts
2230
- import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
2352
+ import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2, unlinkSync } from "fs";
2231
2353
  import { join as join2 } from "path";
2354
+
2355
+ // src/shared/utils/agent-memory/similarity.ts
2356
+ var JACCARD_MATCH_THRESHOLD = 0.15;
2357
+ var MIN_TOKEN_LENGTH = 2;
2358
+ function tokenize(s) {
2359
+ return new Set(
2360
+ s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= MIN_TOKEN_LENGTH)
2361
+ );
2362
+ }
2363
+ function jaccardSimilarity(a, b) {
2364
+ const setA = tokenize(a);
2365
+ const setB = tokenize(b);
2366
+ if (setA.size === 0 || setB.size === 0) return 0;
2367
+ let intersection = 0;
2368
+ for (const token of setA) {
2369
+ if (setB.has(token)) intersection++;
2370
+ }
2371
+ const union = setA.size + setB.size - intersection;
2372
+ return union === 0 ? 0 : intersection / union;
2373
+ }
2374
+ function matchesServiceProfile(serviceProfile, services) {
2375
+ if (services.length === 0) return false;
2376
+ const query = services.join(" ");
2377
+ return jaccardSimilarity(serviceProfile, query) >= JACCARD_MATCH_THRESHOLD;
2378
+ }
2379
+
2380
+ // src/shared/utils/agent-memory/persistent-memory.ts
2232
2381
  var MEMORY_FILE = join2(WORKSPACE.MEMORY, SPECIAL_FILES.PERSISTENT_KNOWLEDGE);
2382
+ var SNAPSHOT_FILE = join2(WORKSPACE.MEMORY, SPECIAL_FILES.SESSION_SNAPSHOT);
2383
+ var MAX_PLAYBOOK_DISPLAY = 3;
2233
2384
  var PersistentMemory = class {
2234
2385
  knowledge;
2235
2386
  constructor() {
@@ -2287,7 +2438,37 @@ var PersistentMemory = class {
2287
2438
  * Clear all knowledge (for testing isolation).
2288
2439
  */
2289
2440
  clear() {
2290
- this.knowledge = { successfulTechniques: [], failurePatterns: [], techFacts: [] };
2441
+ this.knowledge = { successfulTechniques: [], failurePatterns: [], techFacts: [], exploitChains: [] };
2442
+ this.save();
2443
+ try {
2444
+ if (existsSync2(SNAPSHOT_FILE)) {
2445
+ unlinkSync(SNAPSHOT_FILE);
2446
+ }
2447
+ } catch {
2448
+ }
2449
+ }
2450
+ /**
2451
+ * Record a successful exploit chain for session-level reuse (CurriculumPT §).
2452
+ *
2453
+ * WHY: 단일 기법 저장(recordSuccess)과 달리, 체인 전체를 저장한다.
2454
+ * 다음 세션에서 동일 서비스 스택 조우 시 체인 첫 단계부터 시도할 수 있다.
2455
+ */
2456
+ recordExploitChain(serviceProfile, chainSummary, toolsUsed) {
2457
+ const existing = this.knowledge.exploitChains.find(
2458
+ (c) => c.serviceProfile === serviceProfile && c.chainSummary === chainSummary
2459
+ );
2460
+ if (existing) {
2461
+ existing.successCount++;
2462
+ existing.lastSucceededAt = Date.now();
2463
+ } else {
2464
+ this.knowledge.exploitChains.push({
2465
+ serviceProfile,
2466
+ chainSummary,
2467
+ toolsUsed,
2468
+ successCount: 1,
2469
+ lastSucceededAt: Date.now()
2470
+ });
2471
+ }
2291
2472
  this.save();
2292
2473
  }
2293
2474
  /**
@@ -2301,22 +2482,94 @@ var PersistentMemory = class {
2301
2482
  relevant.push(`${svc}: ${techniques.map((t) => `${t.technique} (${t.successCount}x)`).join(", ")}`);
2302
2483
  }
2303
2484
  }
2304
- if (relevant.length === 0) return "";
2305
- return [
2306
- "<persistent-memory>",
2307
- "PREVIOUSLY SUCCESSFUL TECHNIQUES:",
2308
- ...relevant.map((r) => ` \u{1F4DA} ${r}`),
2309
- "</persistent-memory>"
2310
- ].join("\n");
2485
+ const matchedChains = this.knowledge.exploitChains.filter(
2486
+ (chain) => matchesServiceProfile(chain.serviceProfile, services)
2487
+ ).sort((a, b) => b.successCount - a.successCount).slice(0, MAX_PLAYBOOK_DISPLAY);
2488
+ if (relevant.length === 0 && matchedChains.length === 0) return "";
2489
+ const sections = ["<persistent-memory>"];
2490
+ if (relevant.length > 0) {
2491
+ sections.push(
2492
+ "PREVIOUSLY SUCCESSFUL TECHNIQUES:",
2493
+ ...relevant.map((r) => ` \u{1F4DA} ${r}`)
2494
+ );
2495
+ }
2496
+ if (matchedChains.length > 0) {
2497
+ sections.push(
2498
+ "EXPLOIT CHAIN PLAYBOOKS (matched to current target):",
2499
+ ...matchedChains.map(
2500
+ (c) => ` \u{1F517} [${c.serviceProfile}] ${c.chainSummary} (${c.successCount}x) tools:[${c.toolsUsed.join(",")}]`
2501
+ )
2502
+ );
2503
+ }
2504
+ sections.push("</persistent-memory>");
2505
+ return sections.join("\n");
2506
+ }
2507
+ /**
2508
+ * Save a session snapshot for long-session resumption (3차 Insane protocol).
2509
+ *
2510
+ * WHY: Insane-level boxes require multi-hour sessions spanning restarts.
2511
+ * The snapshot stores WHERE we are (phase), WHAT we achieved (foothold),
2512
+ * WHAT'S NEXT (prioritized list), and WHAT credentials are usable.
2513
+ * On session start, Strategist reads this and resumes immediately.
2514
+ */
2515
+ saveSessionSnapshot(snapshot) {
2516
+ try {
2517
+ ensureDirExists(WORKSPACE.MEMORY);
2518
+ writeFileSync2(SNAPSHOT_FILE, JSON.stringify(snapshot, null, 2));
2519
+ } catch {
2520
+ }
2521
+ }
2522
+ /**
2523
+ * Load the most recent session snapshot (if any).
2524
+ */
2525
+ loadSessionSnapshot() {
2526
+ try {
2527
+ if (existsSync2(SNAPSHOT_FILE)) {
2528
+ return JSON.parse(readFileSync(SNAPSHOT_FILE, "utf-8"));
2529
+ }
2530
+ } catch {
2531
+ }
2532
+ return null;
2533
+ }
2534
+ /**
2535
+ * Format session snapshot for Strategist prompt injection.
2536
+ * Returns empty string if no snapshot exists.
2537
+ */
2538
+ snapshotToPrompt() {
2539
+ const snap = this.loadSessionSnapshot();
2540
+ if (!snap) return "";
2541
+ const lines = [
2542
+ "<session-snapshot>",
2543
+ `TARGET: ${snap.target} PHASE: ${snap.phase} SAVED: ${new Date(snap.savedAt).toISOString()}`,
2544
+ "",
2545
+ "ACHIEVED:",
2546
+ ...snap.achieved.map((a) => ` \u2705 ${a}`),
2547
+ "",
2548
+ "NEXT PRIORITIES (resume here):",
2549
+ ...snap.next.map((n, i) => ` ${i + 1}. ${n}`)
2550
+ ];
2551
+ if (snap.credentials.length > 0) {
2552
+ lines.push("", "USABLE CREDENTIALS:");
2553
+ snap.credentials.forEach((c) => lines.push(` \u{1F511} ${c}`));
2554
+ }
2555
+ if (snap.notes) {
2556
+ lines.push("", `NOTES: ${snap.notes}`);
2557
+ }
2558
+ lines.push("</session-snapshot>");
2559
+ return lines.join("\n");
2311
2560
  }
2312
2561
  load() {
2313
2562
  try {
2314
2563
  if (existsSync2(MEMORY_FILE)) {
2315
- return JSON.parse(readFileSync(MEMORY_FILE, "utf-8"));
2564
+ const data = JSON.parse(readFileSync(MEMORY_FILE, "utf-8"));
2565
+ return {
2566
+ ...data,
2567
+ exploitChains: data.exploitChains ?? []
2568
+ };
2316
2569
  }
2317
2570
  } catch {
2318
2571
  }
2319
- return { successfulTechniques: [], failurePatterns: [], techFacts: [] };
2572
+ return { successfulTechniques: [], failurePatterns: [], techFacts: [], exploitChains: [] };
2320
2573
  }
2321
2574
  save() {
2322
2575
  try {
@@ -2551,10 +2804,29 @@ var TargetState = class {
2551
2804
  };
2552
2805
 
2553
2806
  // src/engine/state/finding-state.ts
2807
+ var FALLBACK_INFERENCE_DEPTH = 2;
2554
2808
  var FindingState = class {
2555
2809
  findings = [];
2810
+ /**
2811
+ * Add a finding to the state.
2812
+ *
2813
+ * inferenceDepth 자동 계산 (SAUP §18):
2814
+ * - finding.inferenceDepth가 명시되면 그대로 사용
2815
+ * - finding.basedOnFindingIds가 있으면 참조 Finding의 최대 depth+1 자동 설정
2816
+ * - 둘 다 없으면 undefined 유지 (기존 동작)
2817
+ */
2556
2818
  add(finding) {
2557
- this.findings.push(finding);
2819
+ let resolved = finding;
2820
+ if (finding.inferenceDepth === void 0 && finding.basedOnFindingIds?.length) {
2821
+ const referencedDepths = finding.basedOnFindingIds.map((id) => this.findings.find((f) => f.id === id)?.inferenceDepth).filter((d) => d !== void 0);
2822
+ if (referencedDepths.length > 0) {
2823
+ const maxDepth = Math.max(...referencedDepths);
2824
+ resolved = { ...finding, inferenceDepth: maxDepth + 1 };
2825
+ } else {
2826
+ resolved = { ...finding, inferenceDepth: FALLBACK_INFERENCE_DEPTH };
2827
+ }
2828
+ }
2829
+ this.findings.push(resolved);
2558
2830
  }
2559
2831
  getAll() {
2560
2832
  return this.findings;
@@ -2986,6 +3258,12 @@ var SharedState = class {
2986
3258
  episodicMemory = new EpisodicMemory();
2987
3259
  persistentMemory = new PersistentMemory();
2988
3260
  dynamicTechniques = new DynamicTechniqueLibrary();
3261
+ /**
3262
+ * §18 Epistemic Awareness: Reflector LLM의 자기인식 결과를 다음 턴에 주입하기 위해 저장.
3263
+ * WHY: Reflector가 "검증 안 된 전제"를 판단해도 PromptBuilder가 읽지 않으면 무의미.
3264
+ * 이 필드로 turn N의 reflection → turn N+1 prompt 레이어(epistemic-check)에 주입된다.
3265
+ */
3266
+ lastReflection = null;
2989
3267
  constructor() {
2990
3268
  this.targetState = new TargetState(this.attackGraph);
2991
3269
  this.findingState = new FindingState();
@@ -3003,6 +3281,7 @@ var SharedState = class {
3003
3281
  this.workingMemory.clear();
3004
3282
  this.episodicMemory.clear();
3005
3283
  this.dynamicTechniques.clear();
3284
+ this.lastReflection = null;
3006
3285
  }
3007
3286
  // Delegation to MissionState
3008
3287
  setMissionSummary(summary) {
@@ -3037,7 +3316,7 @@ var SharedState = class {
3037
3316
  setScope(scope) {
3038
3317
  this.engagementState.setScope(scope, this.sessionState.getPhase());
3039
3318
  }
3040
- // Delegation to TODOState
3319
+ // Delegation to TodoState
3041
3320
  addTodo(content, priority = PRIORITIES.MEDIUM) {
3042
3321
  return this.todoState.add(content, priority);
3043
3322
  }
@@ -3124,23 +3403,11 @@ var SharedState = class {
3124
3403
  getTimeStatus() {
3125
3404
  return this.sessionState.getTimeStatus();
3126
3405
  }
3127
- /** @deprecated Use StateSerializer.toPrompt(state) */
3128
- toPrompt() {
3129
- const lines = [];
3130
- const eng = this.getEngagement();
3131
- if (eng) lines.push(`Engagement: ${eng.name} (${eng.client})`);
3132
- const t = this.getAllTargets();
3133
- if (t.length > 0) lines.push(`Targets: ${t.length}`);
3134
- const f = this.getFindings();
3135
- if (f.length > 0) lines.push(`Findings: ${f.length}`);
3136
- lines.push(`Phase: ${this.getPhase()}`);
3137
- return lines.join("\n");
3138
- }
3139
3406
  };
3140
3407
 
3141
3408
  // src/engine/process/process-lifecycle.ts
3142
3409
  import { spawn as spawn4 } from "child_process";
3143
- import { writeFileSync as writeFileSync4, unlinkSync } from "fs";
3410
+ import { writeFileSync as writeFileSync4, unlinkSync as unlinkSync2 } from "fs";
3144
3411
 
3145
3412
  // src/engine/process/process-tree.ts
3146
3413
  import { execSync } from "child_process";
@@ -4258,7 +4525,7 @@ function startBackgroundProcess(command, options = {}) {
4258
4525
  if (!torLeak.safe) {
4259
4526
  for (const f of [stdoutFile, stderrFile, stdinFile]) {
4260
4527
  try {
4261
- unlinkSync(f);
4528
+ unlinkSync2(f);
4262
4529
  } catch {
4263
4530
  }
4264
4531
  }
@@ -4461,7 +4728,7 @@ function getAllProcessEntries() {
4461
4728
 
4462
4729
  // src/engine/process/process-stopper.ts
4463
4730
  import { execSync as execSync2 } from "child_process";
4464
- import { unlinkSync as unlinkSync2 } from "fs";
4731
+ import { unlinkSync as unlinkSync3 } from "fs";
4465
4732
  var cleanupDone = false;
4466
4733
  async function stopBackgroundProcess(processId) {
4467
4734
  const proc = getProcess(processId);
@@ -4473,15 +4740,15 @@ async function stopBackgroundProcess(processId) {
4473
4740
  await killProcessTree(proc.pid, proc.childPids);
4474
4741
  }
4475
4742
  try {
4476
- unlinkSync2(proc.stdoutFile);
4743
+ unlinkSync3(proc.stdoutFile);
4477
4744
  } catch {
4478
4745
  }
4479
4746
  try {
4480
- unlinkSync2(proc.stderrFile);
4747
+ unlinkSync3(proc.stderrFile);
4481
4748
  } catch {
4482
4749
  }
4483
4750
  try {
4484
- unlinkSync2(proc.stdinFile);
4751
+ unlinkSync3(proc.stdinFile);
4485
4752
  } catch {
4486
4753
  }
4487
4754
  logEvent(processId, PROCESS_EVENTS.STOPPED, `Stopped ${proc.role} (PID:${proc.pid}, children:${proc.childPids.length})`);
@@ -4551,15 +4818,15 @@ async function cleanupAllProcesses() {
4551
4818
  }
4552
4819
  for (const [, proc] of backgroundProcesses) {
4553
4820
  try {
4554
- unlinkSync2(proc.stdoutFile);
4821
+ unlinkSync3(proc.stdoutFile);
4555
4822
  } catch {
4556
4823
  }
4557
4824
  try {
4558
- unlinkSync2(proc.stderrFile);
4825
+ unlinkSync3(proc.stderrFile);
4559
4826
  } catch {
4560
4827
  }
4561
4828
  try {
4562
- unlinkSync2(proc.stdinFile);
4829
+ unlinkSync3(proc.stdinFile);
4563
4830
  } catch {
4564
4831
  }
4565
4832
  }
@@ -4651,22 +4918,22 @@ Ports In Use: ${ports.join(", ")}`);
4651
4918
  }
4652
4919
 
4653
4920
  // src/engine/process/process-cleanup.ts
4654
- import { unlinkSync as unlinkSync3 } from "fs";
4921
+ import { unlinkSync as unlinkSync4 } from "fs";
4655
4922
  function syncCleanupAllProcesses(processMap) {
4656
4923
  for (const [, proc] of processMap) {
4657
4924
  if (!proc.hasExited) {
4658
4925
  killProcessTreeSync(proc.pid, proc.childPids);
4659
4926
  }
4660
4927
  try {
4661
- unlinkSync3(proc.stdoutFile);
4928
+ unlinkSync4(proc.stdoutFile);
4662
4929
  } catch {
4663
4930
  }
4664
4931
  try {
4665
- unlinkSync3(proc.stderrFile);
4932
+ unlinkSync4(proc.stderrFile);
4666
4933
  } catch {
4667
4934
  }
4668
4935
  try {
4669
- unlinkSync3(proc.stdinFile);
4936
+ unlinkSync4(proc.stdinFile);
4670
4937
  } catch {
4671
4938
  }
4672
4939
  }
@@ -4691,12 +4958,25 @@ function registerExitHandlers(processMap) {
4691
4958
  registerExitHandlers(getBackgroundProcessesMap());
4692
4959
 
4693
4960
  // src/engine/state/state-serializer.ts
4961
+ var UNVERIFIED_CHAIN_DEPTH_THRESHOLD = 2;
4694
4962
  var StateSerializer = class {
4695
4963
  /**
4696
4964
  * Convert full state to a compact prompt summary
4697
4965
  */
4698
4966
  static toPrompt(state) {
4699
4967
  const lines = [];
4968
+ this.formatContextAndMission(state, lines);
4969
+ this.formatTargets(state, lines);
4970
+ this.formatFindings(state, lines);
4971
+ this.formatLoot(state, lines);
4972
+ this.formatTodos(state, lines);
4973
+ this.formatEnvironment(state, lines);
4974
+ const snapshot = state.persistentMemory.snapshotToPrompt();
4975
+ if (snapshot) lines.push(snapshot);
4976
+ lines.push(`Phase: ${state.getPhase()}`);
4977
+ return lines.join("\n");
4978
+ }
4979
+ static formatContextAndMission(state, lines) {
4700
4980
  const engagement = state.getEngagement();
4701
4981
  const scope = state.getScope();
4702
4982
  if (engagement) {
@@ -4713,54 +4993,83 @@ var StateSerializer = class {
4713
4993
  if (checklist.length > 0) {
4714
4994
  lines.push(`STRATEGIC CHECKLIST:`);
4715
4995
  for (const item of checklist) {
4716
- const status = item.isCompleted ? `${STATUS_MARKERS.RUNNING}` : "[...]";
4996
+ const status = item.isCompleted ? "[x]" : "[ ]";
4717
4997
  lines.push(` ${status} ${item.text} [${item.id}]`);
4718
4998
  }
4719
4999
  }
5000
+ }
5001
+ static formatTargets(state, lines) {
4720
5002
  const targets = state.getAllTargets();
4721
- if (targets.length > 0) {
4722
- const byCategory = /* @__PURE__ */ new Map();
4723
- for (const t of targets) {
4724
- const cat = t.primaryCategory || SERVICE_CATEGORIES.NETWORK;
4725
- if (!byCategory.has(cat)) byCategory.set(cat, []);
4726
- byCategory.get(cat).push(t);
4727
- }
4728
- lines.push(`Targets (${targets.length}):`);
4729
- for (const [cat, catTargets] of byCategory) {
4730
- lines.push(` [${cat}] ${catTargets.length} hosts`);
4731
- for (const t of catTargets.slice(0, DISPLAY_LIMITS.TARGET_PREVIEW)) {
4732
- const ports = (t.ports || []).map((p) => `${p.port}/${p.service}`).join(", ");
4733
- lines.push(` ${t.ip}${t.hostname ? ` (${t.hostname})` : ""}: ${ports || "unknown"}`);
4734
- }
5003
+ if (targets.length === 0) return;
5004
+ const byCategory = /* @__PURE__ */ new Map();
5005
+ for (const t of targets) {
5006
+ const cat = t.primaryCategory || SERVICE_CATEGORIES.NETWORK;
5007
+ if (!byCategory.has(cat)) byCategory.set(cat, []);
5008
+ byCategory.get(cat).push(t);
5009
+ }
5010
+ lines.push(`Targets (${targets.length}):`);
5011
+ for (const [cat, catTargets] of byCategory) {
5012
+ lines.push(` [${cat}] ${catTargets.length} hosts`);
5013
+ for (const t of catTargets.slice(0, DISPLAY_LIMITS.TARGET_PREVIEW)) {
5014
+ const ports = (t.ports || []).map((p) => `${p.port}/${p.service}`).join(", ");
5015
+ lines.push(` ${t.ip}${t.hostname ? ` (${t.hostname})` : ""}: ${ports || "unknown"}`);
4735
5016
  }
4736
5017
  }
5018
+ }
5019
+ static formatFindings(state, lines) {
4737
5020
  const findings = state.getFindings();
4738
- if (findings.length > 0) {
4739
- const confirmed = findings.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED).length;
4740
- const probable = findings.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.PROBABLE && f.confidence < CONFIDENCE_THRESHOLDS.CONFIRMED).length;
4741
- const possible = findings.filter((f) => f.confidence < CONFIDENCE_THRESHOLDS.PROBABLE).length;
4742
- lines.push(`Findings: ${findings.length} total (confirmed:${confirmed} probable:${probable} possible:${possible})`);
4743
- const highPriority = findings.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED).sort((a, b) => b.confidence - a.confidence);
4744
- if (highPriority.length > 0) {
4745
- lines.push(` Confirmed Findings (\u2265${CONFIDENCE_THRESHOLDS.CONFIRMED}):`);
4746
- for (const f of highPriority.slice(0, DISPLAY_LIMITS.FINDING_PREVIEW)) {
4747
- const tactic = f.attackPattern ? ` [ATT&CK:${f.attackPattern}]` : "";
4748
- lines.push(` [conf:${f.confidence}|${f.severity.toUpperCase()}] ${f.title} (${f.category || "general"})${tactic}`);
4749
- }
5021
+ if (findings.length === 0) return;
5022
+ const confirmed = findings.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED).length;
5023
+ const probable = findings.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.PROBABLE && f.confidence < CONFIDENCE_THRESHOLDS.CONFIRMED).length;
5024
+ const possible = findings.filter((f) => f.confidence < CONFIDENCE_THRESHOLDS.PROBABLE).length;
5025
+ const unverifiedChain = findings.filter((f) => (f.inferenceDepth ?? 0) >= UNVERIFIED_CHAIN_DEPTH_THRESHOLD).length;
5026
+ lines.push(`Findings: ${findings.length} total (confirmed:${confirmed} probable:${probable} possible:${possible}${unverifiedChain > 0 ? ` \u26A0\uFE0Funverified-chain:${unverifiedChain}` : ""})`);
5027
+ const highPriority = findings.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED).sort((a, b) => b.confidence - a.confidence);
5028
+ if (highPriority.length > 0) {
5029
+ lines.push(` Confirmed Findings (\u2265${CONFIDENCE_THRESHOLDS.CONFIRMED}):`);
5030
+ for (const f of highPriority.slice(0, DISPLAY_LIMITS.FINDING_PREVIEW)) {
5031
+ const tactic = f.attackPattern ? ` [ATT&CK:${f.attackPattern}]` : "";
5032
+ lines.push(` [conf:${f.confidence}|${f.severity.toUpperCase()}] ${f.title} (${f.category || "general"})${tactic}`);
5033
+ }
5034
+ }
5035
+ if (unverifiedChain > 0) {
5036
+ const unverified = findings.filter((f) => (f.inferenceDepth ?? 0) >= UNVERIFIED_CHAIN_DEPTH_THRESHOLD).sort((a, b) => (b.inferenceDepth ?? 0) - (a.inferenceDepth ?? 0));
5037
+ lines.push(` \u26A0\uFE0F Unverified Inference Chain Findings (direct verification recommended):`);
5038
+ for (const f of unverified.slice(0, DISPLAY_LIMITS.FINDING_PREVIEW)) {
5039
+ lines.push(` [depth:${f.inferenceDepth}|conf:${f.confidence}] ${f.title}`);
4750
5040
  }
4751
5041
  }
5042
+ }
5043
+ static formatLoot(state, lines) {
4752
5044
  const loot = state.getLoot();
4753
- if (loot.length > 0) {
4754
- lines.push(`Loot: ${loot.length} items`);
5045
+ if (loot.length === 0) return;
5046
+ const ACTIONABLE_TYPES = /* @__PURE__ */ new Set(["credential", "token", "hash", "api_key", "ssh_key", "ticket", "session"]);
5047
+ const actionable = loot.filter((l) => ACTIONABLE_TYPES.has(l.type));
5048
+ const other = loot.filter((l) => !ACTIONABLE_TYPES.has(l.type));
5049
+ lines.push(`Loot (${loot.length} items):`);
5050
+ for (const l of actionable.slice(0, DISPLAY_LIMITS.COMPACT_LIST_ITEMS)) {
5051
+ lines.push(` \u26A1 USABLE [${l.type}] ${l.host}: ${l.detail}`);
5052
+ }
5053
+ if (actionable.length > DISPLAY_LIMITS.COMPACT_LIST_ITEMS) {
5054
+ lines.push(` ... +${actionable.length - DISPLAY_LIMITS.COMPACT_LIST_ITEMS} more usable items`);
5055
+ }
5056
+ if (other.length > 0) {
5057
+ lines.push(` \u{1F4C1} Other: ${other.length} items (files, certificates, etc.)`);
4755
5058
  }
5059
+ if (actionable.length > 0) {
5060
+ lines.push(` \u26A0\uFE0F Cross-service attack: Try ALL \u26A1 USABLE credentials/tokens on ALL discovered services.`);
5061
+ }
5062
+ }
5063
+ static formatTodos(state, lines) {
4756
5064
  const todo = state.getTodo();
4757
- if (todo.length > 0) {
4758
- lines.push(`TODO (${todo.length}):`);
4759
- for (const t of todo.slice(0, DISPLAY_LIMITS.COMPACT_LIST_ITEMS)) {
4760
- const status = t.status === TODO_STATUSES.DONE ? "[x]" : t.status === TODO_STATUSES.IN_PROGRESS ? "[->]" : "[ ]";
4761
- lines.push(` ${status} ${t.content} (${t.priority})`);
4762
- }
5065
+ if (todo.length === 0) return;
5066
+ lines.push(`TODO (${todo.length}):`);
5067
+ for (const t of todo.slice(0, DISPLAY_LIMITS.COMPACT_LIST_ITEMS)) {
5068
+ const status = t.status === TODO_STATUSES.DONE ? "[x]" : t.status === TODO_STATUSES.IN_PROGRESS ? "[->]" : "[ ]";
5069
+ lines.push(` ${status} ${t.content} (${t.priority})`);
4763
5070
  }
5071
+ }
5072
+ static formatEnvironment(state, lines) {
4764
5073
  const resourceInfo = getResourceSummary();
4765
5074
  if (resourceInfo) {
4766
5075
  lines.push(resourceInfo);
@@ -4785,13 +5094,11 @@ BLOCKED (leak real IP): ping, traceroute, dig, nslookup, nmap -sU`
4785
5094
  } else {
4786
5095
  lines.push(`Tor Proxy: OFF \u2014 direct connections.`);
4787
5096
  }
4788
- lines.push(`Phase: ${state.getPhase()}`);
4789
- return lines.join("\n");
4790
5097
  }
4791
5098
  };
4792
5099
 
4793
5100
  // src/engine/state/state-persistence/saver.ts
4794
- import { writeFileSync as writeFileSync5, readdirSync, statSync, unlinkSync as unlinkSync4 } from "fs";
5101
+ import { writeFileSync as writeFileSync5, readdirSync, statSync, unlinkSync as unlinkSync5 } from "fs";
4795
5102
  import { join as join4 } from "path";
4796
5103
  function saveState(state) {
4797
5104
  const sessionsDir = WORKSPACE.SESSIONS;
@@ -4829,7 +5136,7 @@ function pruneOldSessions(sessionsDir) {
4829
5136
  }).sort((a, b) => b.mtime - a.mtime);
4830
5137
  const toDelete = sessionFiles.slice(AGENT_LIMITS.MAX_SESSION_FILES);
4831
5138
  for (const file of toDelete) {
4832
- unlinkSync4(file.path);
5139
+ unlinkSync5(file.path);
4833
5140
  }
4834
5141
  } catch {
4835
5142
  }
@@ -5587,6 +5894,18 @@ Examples:
5587
5894
  };
5588
5895
  }
5589
5896
  state.setPhase(newPhase);
5897
+ const targets = state.getAllTargets();
5898
+ const primaryTarget = targets[0]?.ip ?? "unknown";
5899
+ const loot = state.getLoot();
5900
+ const usableCreds = loot.filter((l) => ["credential", "hash", "token"].includes(l.type)).map((l) => l.detail).slice(0, 10);
5901
+ state.persistentMemory.saveSessionSnapshot({
5902
+ target: primaryTarget,
5903
+ phase: newPhase,
5904
+ achieved: [`Phase transition: ${oldPhase} \u2192 ${newPhase}. Reason: ${reason}`],
5905
+ next: [`Continue from ${newPhase} phase on ${primaryTarget}`],
5906
+ credentials: usableCreds,
5907
+ savedAt: Date.now()
5908
+ });
5590
5909
  events.emit({
5591
5910
  type: EVENT_TYPES.PHASE_CHANGE,
5592
5911
  timestamp: Date.now(),
@@ -5599,7 +5918,64 @@ Examples:
5599
5918
  return {
5600
5919
  success: true,
5601
5920
  output: `Phase changed: ${oldPhase} \u2192 ${newPhase}
5602
- Reason: ${reason}`
5921
+ Reason: ${reason}
5922
+ [Session snapshot saved for resumption]`
5923
+ };
5924
+ }
5925
+ },
5926
+ {
5927
+ name: TOOL_NAMES.SAVE_SESSION_SNAPSHOT,
5928
+ description: `Save a session snapshot for long-session resumption (Insane-level boxes).
5929
+
5930
+ WHY: Insane boxes require multi-hour sessions spanning restarts.
5931
+ Call this when you reach a major milestone so the next session can resume immediately.
5932
+
5933
+ WHEN to call:
5934
+ - You gained a shell (record it with what you know so far)
5935
+ - Phase transition (root \u2192 post-exploit \u2192 lateral)
5936
+ - Flag captured (record what's next)
5937
+ - Before ending a session voluntarily
5938
+
5939
+ The snapshot persists to disk and is auto-injected into the Strategist on the next session start.`,
5940
+ parameters: {
5941
+ target: { type: "string", description: "Target IP or hostname" },
5942
+ achieved: {
5943
+ type: "array",
5944
+ items: { type: "string" },
5945
+ description: "What has been achieved (foothold, privesc steps, flags captured)"
5946
+ },
5947
+ next: {
5948
+ type: "array",
5949
+ items: { type: "string" },
5950
+ description: "Prioritized list of next steps for the next session"
5951
+ },
5952
+ credentials: {
5953
+ type: "array",
5954
+ items: { type: "string" },
5955
+ description: 'Usable credentials in "user:pass" or "user:hash" format'
5956
+ },
5957
+ notes: { type: "string", description: "Optional free-text notes for next session" }
5958
+ },
5959
+ required: ["target", "achieved", "next"],
5960
+ execute: async (p) => {
5961
+ const snapshot = {
5962
+ target: p.target,
5963
+ phase: state.getPhase(),
5964
+ achieved: p.achieved || [],
5965
+ next: p.next || [],
5966
+ credentials: p.credentials || [],
5967
+ notes: p.notes,
5968
+ savedAt: Date.now()
5969
+ };
5970
+ state.persistentMemory.saveSessionSnapshot(snapshot);
5971
+ return {
5972
+ success: true,
5973
+ output: `Session snapshot saved.
5974
+ Target: ${snapshot.target}
5975
+ Phase: ${snapshot.phase}
5976
+ Achieved: ${snapshot.achieved.length} items
5977
+ Next: ${snapshot.next.length} priorities
5978
+ Creds: ${snapshot.credentials.length}`
5603
5979
  };
5604
5980
  }
5605
5981
  }
@@ -5747,11 +6123,16 @@ Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certifi
5747
6123
  }
5748
6124
  }
5749
6125
  state.episodicMemory.record("access_gained", `Loot [${lootTypeStr}] from ${host}: ${detail.slice(0, DISPLAY_LIMITS.MEMORY_EVENT_PREVIEW)}`);
6126
+ const targets = state.getAllTargets();
6127
+ const knownSvcs = targets.flatMap((t) => t.ports.map((p2) => `${t.ip}:${p2.port}(${p2.service || "?"})`)).slice(0, 6).join(", ");
6128
+ const sprayHint = knownSvcs ? `NEXT ACTION: spray on ALL discovered services \u2014 ${knownSvcs}` : `NEXT ACTION: spray on SSH/FTP/SMB/RDP/web-login on all discovered hosts`;
5750
6129
  return {
5751
6130
  success: true,
5752
6131
  output: `Loot recorded: [${lootTypeStr}] from ${host}
5753
6132
  Detail: ${detail}
5754
- ` + (isCrackable ? `This is crackable. Consider: hash_crack({ hashes: "${detail.slice(0, DISPLAY_LIMITS.LOOT_DETAIL_PREVIEW)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
6133
+ ` + (isCrackable ? `Hash detected. Options:
6134
+ (1) hash_crack immediately
6135
+ (2) pass-the-hash if NTLM: use on SMB/WMI/RDP without cracking` : sprayHint)
5755
6136
  };
5756
6137
  }
5757
6138
  });
@@ -5825,78 +6206,6 @@ function buildNote(matched, fpMatched, confidence) {
5825
6206
  return parts.join(" | ");
5826
6207
  }
5827
6208
 
5828
- // src/shared/utils/knowledge/service-vuln.ts
5829
- var SERVICE_VULN_MAP = {
5830
- // Apache
5831
- "apache/2.4.49": {
5832
- cves: ["CVE-2021-41773"],
5833
- exploits: ['curl --path-as-is "http://TARGET/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"'],
5834
- priority: "critical",
5835
- checks: ["Path traversal to RCE"]
5836
- },
5837
- "apache/2.4.50": {
5838
- cves: ["CVE-2021-42013"],
5839
- exploits: ['curl --path-as-is "http://TARGET/cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/etc/passwd"'],
5840
- priority: "critical",
5841
- checks: ["Path traversal bypass"]
5842
- },
5843
- // SSH
5844
- "openssh/7.2": {
5845
- cves: ["CVE-2016-10009", "CVE-2016-10010"],
5846
- exploits: ["Check for Roaming auth bypass"],
5847
- priority: "medium",
5848
- checks: ["Version disclosure", "Weak algorithms"]
5849
- },
5850
- // vsftpd
5851
- "vsftpd/2.3.4": {
5852
- cves: ["CVE-2011-2523"],
5853
- exploits: ["Connect with username containing :) to trigger backdoor on port 6200"],
5854
- priority: "critical",
5855
- checks: ["Backdoor trigger: user:)"]
5856
- },
5857
- // SMB
5858
- "samba/3.0": {
5859
- cves: ["CVE-2004-0882"],
5860
- exploits: ["trans2open exploit"],
5861
- priority: "high",
5862
- checks: ["SMBv1 enabled"]
5863
- },
5864
- "smb": {
5865
- cves: ["MS17-010"],
5866
- exploits: ["EternalBlue exploit"],
5867
- priority: "critical",
5868
- checks: ["nmap --script smb-vuln-ms17-010"]
5869
- },
5870
- // MySQL
5871
- "mysql/5.0": {
5872
- cves: ["CVE-2012-2122"],
5873
- exploits: ["MariaDB/CMySQL authentication bypass"],
5874
- priority: "high",
5875
- checks: ["Try root without password", "mysql -u root"]
5876
- },
5877
- // Redis
5878
- "redis": {
5879
- cves: [],
5880
- exploits: ["Unauthenticated RCE via CONFIG SET dir + SLAVEOF"],
5881
- priority: "critical",
5882
- checks: ["redis-cli -h TARGET ping", "CONFIG GET dir"]
5883
- },
5884
- // Jenkins
5885
- "jenkins": {
5886
- cves: [],
5887
- exploits: ["Script Console RCE", "CVE-2019-1003000"],
5888
- priority: "high",
5889
- checks: ["/scriptText endpoint", "/manage/scriptConsole"]
5890
- },
5891
- // Elasticsearch
5892
- "elasticsearch/1": {
5893
- cves: ["CVE-2014-3120"],
5894
- exploits: ["Groovy sandbox bypass RCE"],
5895
- priority: "critical",
5896
- checks: ["Dynamic script execution"]
5897
- }
5898
- };
5899
-
5900
6209
  // src/shared/utils/knowledge/ctf/patterns.ts
5901
6210
  var FLAG_PATTERNS = {
5902
6211
  // Generic CTF flag formats
@@ -5998,119 +6307,6 @@ function detectFlags(output) {
5998
6307
  return Array.from(found);
5999
6308
  }
6000
6309
 
6001
- // src/shared/utils/knowledge/vulns.ts
6002
- var CHALLENGE_TYPE_SIGNALS = {
6003
- web: [
6004
- "http",
6005
- "https",
6006
- "nginx",
6007
- "apache",
6008
- "flask",
6009
- "express",
6010
- "php",
6011
- "node",
6012
- "django",
6013
- "rails",
6014
- "wordpress",
6015
- "login form",
6016
- "graphql",
6017
- "websocket",
6018
- "oauth",
6019
- "jwt",
6020
- "cookie",
6021
- "grpc"
6022
- ],
6023
- pwn: [
6024
- "buffer overflow",
6025
- "SUID",
6026
- "stack",
6027
- "heap",
6028
- "format string",
6029
- "ELF",
6030
- "binary",
6031
- "pwntools",
6032
- "libc",
6033
- "canary",
6034
- "NX",
6035
- "PIE",
6036
- "seccomp",
6037
- "shellcode",
6038
- "tcache",
6039
- "fastbin",
6040
- "house of"
6041
- ],
6042
- crypto: [
6043
- "RSA",
6044
- "AES",
6045
- "cipher",
6046
- "decrypt",
6047
- "encrypt",
6048
- "key",
6049
- "modulus",
6050
- "prime",
6051
- "hash",
6052
- "base64",
6053
- "XOR",
6054
- "padding oracle",
6055
- "elliptic curve",
6056
- "lattice",
6057
- "LLL",
6058
- "ECDSA",
6059
- "nonce"
6060
- ],
6061
- forensics: [
6062
- "pcap",
6063
- "memory dump",
6064
- "disk image",
6065
- "steganography",
6066
- "exif",
6067
- "binwalk",
6068
- "volatility",
6069
- "autopsy",
6070
- "file carving",
6071
- "wireshark",
6072
- "firmware",
6073
- "ext4",
6074
- "ntfs",
6075
- "registry"
6076
- ],
6077
- reversing: [
6078
- "reverse",
6079
- "disassemble",
6080
- "decompile",
6081
- "ghidra",
6082
- "ida",
6083
- "radare2",
6084
- "upx",
6085
- "packed",
6086
- "obfuscated",
6087
- "android",
6088
- "apk",
6089
- "angr",
6090
- "z3",
6091
- "frida",
6092
- "crackme",
6093
- "keygen",
6094
- "dnspy"
6095
- ],
6096
- misc: [
6097
- "OSINT",
6098
- "trivia",
6099
- "scripting",
6100
- "pyjail",
6101
- "sandbox escape",
6102
- "qr code",
6103
- "morse",
6104
- "braille",
6105
- "jail",
6106
- "rbash",
6107
- "restricted",
6108
- "seccomp",
6109
- "filter bypass",
6110
- "escape"
6111
- ]
6112
- };
6113
-
6114
6310
  // src/shared/utils/knowledge/owasp/history/2017.ts
6115
6311
  var OWASP_2017 = {
6116
6312
  A01: { name: "Injection", desc: "SQL, NoSQL, OS, and LDAP injection.", tools: ["sqlmap", "commix"] },
@@ -6410,94 +6606,30 @@ var SMB_PORTS = [
6410
6606
  SERVICE_PORTS.SMB_NETBIOS
6411
6607
  ];
6412
6608
 
6413
- // src/shared/constants/scoring.ts
6414
- var ATTACK_SCORING = {
6415
- /** Base score for all attack prioritization */
6416
- BASE_SCORE: 50,
6417
- /** Maximum possible score */
6418
- MAX_SCORE: 100,
6419
- /** Bonus for critical infrastructure services (SSH, RDP, DB, etc.) */
6420
- CRITICAL_SERVICE_BONUS: 20,
6421
- /** Bonus when service version is known (enables CVE lookup) */
6422
- VERSION_KNOWN_BONUS: 15,
6423
- /** Bonus when known critical CVE exists for the service version */
6424
- CRITICAL_CVE_BONUS: 30,
6425
- /** Bonus for sensitive services running without authentication */
6426
- NO_AUTH_BONUS: 25,
6427
- /** Bonus for plaintext HTTP on service ports (no HTTPS) */
6428
- PLAINTEXT_HTTP_BONUS: 10
6429
- };
6430
-
6431
- // src/shared/utils/knowledge/planning/scoring.ts
6432
- function calculateAttackPriority(findings) {
6433
- let score = ATTACK_SCORING.BASE_SCORE;
6434
- if (CRITICAL_SERVICE_PORTS.includes(findings.port)) {
6435
- score += ATTACK_SCORING.CRITICAL_SERVICE_BONUS;
6436
- }
6437
- if (findings.version) {
6438
- score += ATTACK_SCORING.VERSION_KNOWN_BONUS;
6439
- const key = `${findings.service.toLowerCase()}/${findings.version.split(".")[0]}`;
6440
- if (SERVICE_VULN_MAP[key]?.priority === "critical") {
6441
- score += ATTACK_SCORING.CRITICAL_CVE_BONUS;
6442
- }
6443
- }
6444
- if (!findings.hasAuth && NO_AUTH_CRITICAL_PORTS.includes(findings.port)) {
6445
- score += ATTACK_SCORING.NO_AUTH_BONUS;
6446
- }
6447
- if (PLAINTEXT_HTTP_PORTS.includes(findings.port) && !findings.isHttps) {
6448
- score += ATTACK_SCORING.PLAINTEXT_HTTP_BONUS;
6449
- }
6450
- return Math.min(score, ATTACK_SCORING.MAX_SCORE);
6451
- }
6452
-
6453
6609
  // src/shared/utils/knowledge/planning/recommendations.ts
6454
- function getAttacksForService(service, port) {
6455
- const attacks = [];
6610
+ function getServiceContext(service, port) {
6456
6611
  const svc = service.toLowerCase();
6612
+ const facts = [];
6457
6613
  if (WEB_SERVICE_PORTS.includes(port)) {
6458
- attacks.push(
6459
- "OWASP-A01: Directory brute force",
6460
- "OWASP-A03: SQLi testing",
6461
- "OWASP-A03: XSS testing",
6462
- "OWASP-A05: Header analysis",
6463
- "OWASP-A10: SSRF testing",
6464
- "OWASP-A06: Version fingerprinting"
6465
- );
6614
+ if (port === 443 || port === 8443) facts.push("TLS/HTTPS");
6615
+ if (port === 8080 || port === 8e3 || port === 8888) facts.push("Non-standard HTTP port \u2014 often dev/admin panels");
6616
+ return { category: "web", facts };
6466
6617
  }
6467
6618
  if (port === SERVICE_PORTS.SSH || svc.includes("ssh")) {
6468
- attacks.push(
6469
- "SSH version scan",
6470
- "Brute force common credentials",
6471
- "SSH key enumeration",
6472
- "Check for weak algorithms"
6473
- );
6619
+ if (port !== SERVICE_PORTS.SSH) facts.push(`Non-standard SSH port (${port})`);
6620
+ return { category: "ssh", facts };
6474
6621
  }
6475
- if (SMB_PORTS.includes(port) || svc.includes("smb")) {
6476
- attacks.push(
6477
- "MS17-010 EternalBlue check",
6478
- "SMB enumeration",
6479
- "Null session test",
6480
- "Share enumeration"
6481
- );
6622
+ if (SMB_PORTS.includes(port) || svc.includes("smb") || svc.includes("netbios")) {
6623
+ return { category: "smb", facts };
6482
6624
  }
6483
- if (DATABASE_PORTS.includes(port) || svc.includes("sql") || svc.includes("mongo") || svc.includes("redis")) {
6484
- attacks.push(
6485
- "Default credential test",
6486
- "Unauthenticated access check",
6487
- "SQLi through web app",
6488
- "UDF injection",
6489
- "NoSQL injection"
6490
- );
6625
+ if (DATABASE_PORTS.includes(port) || svc.includes("sql") || svc.includes("mongo") || svc.includes("redis") || svc.includes("postgres")) {
6626
+ facts.push("Database service \u2014 check for unauthenticated access");
6627
+ return { category: "database", facts };
6491
6628
  }
6492
6629
  if (port === SERVICE_PORTS.FTP || svc.includes("ftp")) {
6493
- attacks.push(
6494
- "Anonymous login test",
6495
- "Brute force credentials",
6496
- "VSFTPD backdoor check",
6497
- "Directory traversal"
6498
- );
6630
+ return { category: "ftp", facts };
6499
6631
  }
6500
- return attacks;
6632
+ return { category: "other", facts };
6501
6633
  }
6502
6634
 
6503
6635
  // src/shared/constants/prompts/paths.ts
@@ -6536,7 +6668,11 @@ var TECHNIQUE_FILES = {
6536
6668
  REVERSING: "reversing",
6537
6669
  FORENSICS: "forensics",
6538
6670
  PWN: "pwn",
6539
- SHELLS: "shells"
6671
+ SHELLS: "shells",
6672
+ /** Multi-hop pivoting and tunneling (2차 — Hard/Insane support) */
6673
+ PIVOTING: "pivoting",
6674
+ /** Enterprise internal network: AD Forest, Cloud pivoting, SCCM, persistence (4차 — DEF CON/Enterprise) */
6675
+ ENTERPRISE_PENTEST: "enterprise-pentest"
6540
6676
  };
6541
6677
  var PROMPT_CONFIG = {
6542
6678
  ENCODING: "utf-8"
@@ -6598,7 +6734,7 @@ var TYPE_TECHNIQUE_MAP = {
6598
6734
  forensics: [TECHNIQUE_FILES.FORENSICS, TECHNIQUE_FILES.REVERSING, TECHNIQUE_FILES.CRYPTO],
6599
6735
  reversing: [TECHNIQUE_FILES.REVERSING, TECHNIQUE_FILES.PWN],
6600
6736
  misc: [TECHNIQUE_FILES.SANDBOX_ESCAPE, TECHNIQUE_FILES.CRYPTO, TECHNIQUE_FILES.FORENSICS],
6601
- network: [TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.LATERAL, TECHNIQUE_FILES.AD_ATTACK],
6737
+ network: [TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.LATERAL, TECHNIQUE_FILES.AD_ATTACK, TECHNIQUE_FILES.PIVOTING],
6602
6738
  unknown: [TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.INJECTION, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.FILE_ATTACKS]
6603
6739
  };
6604
6740
  var TYPE_PHASE_PROMPT_MAP = {
@@ -6611,19 +6747,122 @@ var TYPE_PHASE_PROMPT_MAP = {
6611
6747
  network: "recon.md",
6612
6748
  unknown: "recon.md"
6613
6749
  };
6614
- var TYPE_STRATEGY_MAP = {
6615
- web: "Web challenge detected. Priority: directory fuzzing \u2192 injection testing \u2192 authentication bypass \u2192 file upload \u2192 SSRF/SSTI. Check source code, cookies, JWT tokens.",
6616
- pwn: "Binary exploitation challenge. Priority: checksec \u2192 binary analysis \u2192 find vulnerability class \u2192 develop exploit. Tools: gdb, pwntools, radare2.",
6617
- crypto: "Cryptography challenge. Priority: identify algorithm \u2192 find weakness \u2192 write solver script. Check for weak keys, reused nonces, padding oracle, ECB mode.",
6618
- forensics: "Forensics challenge. Priority: file identification \u2192 extract data \u2192 analyze artifacts. Tools: binwalk, volatility, wireshark, exiftool, foremost.",
6619
- reversing: "Reverse engineering challenge. Priority: identify binary type \u2192 static analysis \u2192 dynamic analysis \u2192 extract flag/keygen. Tools: ghidra, radare2, ltrace, strace.",
6620
- misc: "Miscellaneous challenge. Priority: understand the puzzle \u2192 check for sandbox escape, scripting, OSINT, encoding. Think creatively.",
6621
- network: "Network/infrastructure challenge. Priority: service enumeration \u2192 version CVE search \u2192 exploit \u2192 privilege escalation \u2192 lateral movement.",
6622
- unknown: "Challenge type unclear. Run broad reconnaissance first, then adapt based on findings."
6623
- };
6624
6750
 
6625
6751
  // src/shared/utils/knowledge/challenge/engine.ts
6626
6752
  var SECONDARY_TYPE_RATIO = 0.5;
6753
+ var WEB_PORT_PATTERN = /\b(80|443|8080|8443|3000|5000|8000)\b/;
6754
+ var INFRA_PORT_PATTERN = /\b(21|22|23|25|53|445|139|3389|5985)\b/;
6755
+ var CHALLENGE_TYPE_SIGNALS = {
6756
+ web: [
6757
+ "http",
6758
+ "https",
6759
+ "nginx",
6760
+ "apache",
6761
+ "flask",
6762
+ "express",
6763
+ "php",
6764
+ "node",
6765
+ "django",
6766
+ "rails",
6767
+ "wordpress",
6768
+ "login form",
6769
+ "graphql",
6770
+ "websocket",
6771
+ "oauth",
6772
+ "jwt",
6773
+ "cookie",
6774
+ "grpc"
6775
+ ],
6776
+ pwn: [
6777
+ "buffer overflow",
6778
+ "SUID",
6779
+ "stack",
6780
+ "heap",
6781
+ "format string",
6782
+ "ELF",
6783
+ "binary",
6784
+ "pwntools",
6785
+ "libc",
6786
+ "canary",
6787
+ "NX",
6788
+ "PIE",
6789
+ "seccomp",
6790
+ "shellcode",
6791
+ "tcache",
6792
+ "fastbin",
6793
+ "house of"
6794
+ ],
6795
+ crypto: [
6796
+ "RSA",
6797
+ "AES",
6798
+ "cipher",
6799
+ "decrypt",
6800
+ "encrypt",
6801
+ "key",
6802
+ "modulus",
6803
+ "prime",
6804
+ "hash",
6805
+ "base64",
6806
+ "XOR",
6807
+ "padding oracle",
6808
+ "elliptic curve",
6809
+ "lattice",
6810
+ "LLL",
6811
+ "ECDSA",
6812
+ "nonce"
6813
+ ],
6814
+ forensics: [
6815
+ "pcap",
6816
+ "memory dump",
6817
+ "disk image",
6818
+ "steganography",
6819
+ "exif",
6820
+ "binwalk",
6821
+ "volatility",
6822
+ "autopsy",
6823
+ "file carving",
6824
+ "wireshark",
6825
+ "firmware",
6826
+ "ext4",
6827
+ "ntfs",
6828
+ "registry"
6829
+ ],
6830
+ reversing: [
6831
+ "reverse",
6832
+ "disassemble",
6833
+ "decompile",
6834
+ "ghidra",
6835
+ "ida",
6836
+ "radare2",
6837
+ "upx",
6838
+ "packed",
6839
+ "obfuscated",
6840
+ "android",
6841
+ "apk",
6842
+ "angr",
6843
+ "z3",
6844
+ "frida",
6845
+ "crackme",
6846
+ "keygen",
6847
+ "dnspy"
6848
+ ],
6849
+ misc: [
6850
+ "OSINT",
6851
+ "trivia",
6852
+ "scripting",
6853
+ "pyjail",
6854
+ "sandbox escape",
6855
+ "qr code",
6856
+ "morse",
6857
+ "braille",
6858
+ "jail",
6859
+ "rbash",
6860
+ "restricted",
6861
+ "seccomp",
6862
+ "filter bypass",
6863
+ "escape"
6864
+ ]
6865
+ };
6627
6866
  function analyzeChallenge(reconData) {
6628
6867
  if (!reconData || reconData.trim().length === 0) {
6629
6868
  return {
@@ -6632,8 +6871,7 @@ function analyzeChallenge(reconData) {
6632
6871
  confidence: 0,
6633
6872
  matchedSignals: [],
6634
6873
  recommendedTechniques: TYPE_TECHNIQUE_MAP.unknown,
6635
- recommendedPhasePrompt: TYPE_PHASE_PROMPT_MAP.unknown,
6636
- strategySuggestion: TYPE_STRATEGY_MAP.unknown
6874
+ recommendedPhasePrompt: TYPE_PHASE_PROMPT_MAP.unknown
6637
6875
  };
6638
6876
  }
6639
6877
  const lowerData = reconData.toLowerCase();
@@ -6657,11 +6895,11 @@ function analyzeChallenge(reconData) {
6657
6895
  }
6658
6896
  }
6659
6897
  }
6660
- if (/\b(80|443|8080|8443|3000|5000|8000)\b/.test(reconData)) {
6898
+ if (WEB_PORT_PATTERN.test(reconData)) {
6661
6899
  scores.web.score += 2;
6662
6900
  scores.web.signals.push("web-port");
6663
6901
  }
6664
- if (/\b(21|22|23|25|53|445|139|3389|5985)\b/.test(reconData)) {
6902
+ if (INFRA_PORT_PATTERN.test(reconData)) {
6665
6903
  scores.network.score += 2;
6666
6904
  scores.network.signals.push("infra-port");
6667
6905
  }
@@ -6689,8 +6927,7 @@ function analyzeChallenge(reconData) {
6689
6927
  confidence,
6690
6928
  matchedSignals: primaryData.signals,
6691
6929
  recommendedTechniques: TYPE_TECHNIQUE_MAP[primaryType] || TYPE_TECHNIQUE_MAP.unknown,
6692
- recommendedPhasePrompt: TYPE_PHASE_PROMPT_MAP[primaryType] || TYPE_PHASE_PROMPT_MAP.unknown,
6693
- strategySuggestion: TYPE_STRATEGY_MAP[primaryType] || TYPE_STRATEGY_MAP.unknown
6930
+ recommendedPhasePrompt: TYPE_PHASE_PROMPT_MAP[primaryType] || TYPE_PHASE_PROMPT_MAP.unknown
6694
6931
  };
6695
6932
  }
6696
6933
 
@@ -6704,9 +6941,9 @@ function formatChallengeAnalysis(analysis) {
6704
6941
  if (analysis.secondaryTypes.length > 0) {
6705
6942
  lines.push(`Secondary: ${analysis.secondaryTypes.join(", ")}`);
6706
6943
  }
6707
- lines.push(`Signals: ${analysis.matchedSignals.join(", ")}`);
6708
- lines.push(`Strategy: ${analysis.strategySuggestion}`);
6709
- lines.push(`Focus techniques: ${analysis.recommendedTechniques.join(", ")}`);
6944
+ lines.push(`Matched signals: ${analysis.matchedSignals.join(", ")}`);
6945
+ lines.push(`Reference docs loaded: ${analysis.recommendedTechniques.join(", ")}`);
6946
+ lines.push(`Use get_owasp_knowledge("${analysis.primaryType}") or web_search for current attack techniques.`);
6710
6947
  lines.push(`</challenge-analysis>`);
6711
6948
  return lines.join("\n");
6712
6949
  }
@@ -6775,7 +7012,11 @@ confidence score (0-100) \u2014 technical verification level:
6775
7012
  0 = Pure speculation, no actual test performed
6776
7013
 
6777
7014
  Omit confidence to let the system auto-calculate from evidence.
6778
- Findings with confidence >= 80 appear as CONFIRMED in reports.`,
7015
+ Findings with confidence >= 80 appear as CONFIRMED in reports.
7016
+
7017
+ basedOnFindingIds: If this finding is INFERRED from existing findings (not directly observed),
7018
+ provide the IDs of those findings. The system will automatically set inferenceDepth = max(ref.depth)+1.
7019
+ Example: you found SQLi evidence from a previous LFI finding \u2192 basedOnFindingIds: ["<lfi-finding-id>"]`,
6779
7020
  parameters: {
6780
7021
  title: { type: "string", description: 'Concise title (e.g., "Path Traversal via /download endpoint")' },
6781
7022
  severity: { type: "string", description: "Business impact severity: critical, high, medium, low, info" },
@@ -6783,7 +7024,8 @@ Findings with confidence >= 80 appear as CONFIRMED in reports.`,
6783
7024
  description: { type: "string", description: "What the vulnerability is, how you exploited it step-by-step, what access it gives, and the impact." },
6784
7025
  evidence: { type: "array", items: { type: "string" }, description: "Actual command outputs proving the finding. Copy real output here." },
6785
7026
  attackPattern: { type: "string", description: "MITRE ATT&CK tactic: initial_access, execution, persistence, privilege_escalation, defense_evasion, credential_access, discovery, lateral_movement, collection, exfiltration, command_and_control, impact" },
6786
- confidence: { type: "number", description: "Optional override (0-100). Omit to auto-calculate from evidence." }
7027
+ confidence: { type: "number", description: "Optional override (0-100). Omit to auto-calculate from evidence." },
7028
+ basedOnFindingIds: { type: "array", items: { type: "string" }, description: "IDs of existing findings this finding is inferred from. Triggers automatic inferenceDepth calculation. Omit for directly observed findings." }
6787
7029
  },
6788
7030
  required: ["title", "severity", "description", "evidence"],
6789
7031
  execute: async (p) => {
@@ -6793,6 +7035,7 @@ Findings with confidence >= 80 appear as CONFIRMED in reports.`,
6793
7035
  const affected = parseStringArray(p.affected);
6794
7036
  const description = parseString(p.description);
6795
7037
  const attackPattern = parseString(p.attackPattern);
7038
+ const basedOnFindingIds = parseStringArray(p.basedOnFindingIds).filter(Boolean);
6796
7039
  const validation = validateFinding(evidence);
6797
7040
  const rawOverride = p.confidence;
6798
7041
  const confidence = typeof rawOverride === "number" && rawOverride >= 0 && rawOverride <= 100 ? Math.round(rawOverride) : validation.confidence;
@@ -6806,7 +7049,8 @@ Findings with confidence >= 80 appear as CONFIRMED in reports.`,
6806
7049
  evidence,
6807
7050
  remediation: "",
6808
7051
  foundAt: Date.now(),
6809
- ...attackPattern && isValidAttackTactic(attackPattern) ? { attackPattern } : {}
7052
+ ...attackPattern && isValidAttackTactic(attackPattern) ? { attackPattern } : {},
7053
+ ...basedOnFindingIds.length > 0 ? { basedOnFindingIds } : {}
6810
7054
  });
6811
7055
  const hasExploit = confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED;
6812
7056
  const target = affected[0] || DEFAULTS.UNKNOWN_SERVICE;
@@ -6967,6 +7211,7 @@ async function searchCVE(service, version) {
6967
7211
 
6968
7212
  // src/engine/web-search-providers/common.ts
6969
7213
  var SEARCH_TIMEOUT_MS = 15e3;
7214
+ var API_ERROR_PREVIEW = DISPLAY_LIMITS.API_ERROR_PREVIEW;
6970
7215
 
6971
7216
  // src/engine/web-search-providers/glm.ts
6972
7217
  function generateRequestId() {
@@ -7005,7 +7250,7 @@ async function searchWithGLM(query, apiKey, apiUrl) {
7005
7250
  } catch {
7006
7251
  }
7007
7252
  debugLog("search", "GLM response ERROR", { status: response.status, error: errorText });
7008
- return { success: false, output: "", error: `GLM Search API error ${response.status}: ${errorText.slice(0, 500)}` };
7253
+ return { success: false, output: "", error: `GLM Search API error ${response.status}: ${errorText.slice(0, API_ERROR_PREVIEW)}` };
7009
7254
  }
7010
7255
  let data;
7011
7256
  try {
@@ -7056,7 +7301,7 @@ async function searchWithBrave(query, apiKey, apiUrl) {
7056
7301
  } catch {
7057
7302
  }
7058
7303
  debugLog("search", "Brave response ERROR", { status: response.status, error: errorText });
7059
- return { success: false, output: "", error: `Brave API error ${response.status}: ${errorText.slice(0, 500)}` };
7304
+ return { success: false, output: "", error: `Brave API error ${response.status}: ${errorText.slice(0, API_ERROR_PREVIEW)}` };
7060
7305
  }
7061
7306
  let data;
7062
7307
  try {
@@ -7106,7 +7351,7 @@ async function searchWithSerper(query, apiKey, apiUrl) {
7106
7351
  } catch {
7107
7352
  }
7108
7353
  debugLog("search", "Serper response ERROR", { status: response.status, error: errorText });
7109
- return { success: false, output: "", error: `Serper API error ${response.status}: ${errorText.slice(0, 500)}` };
7354
+ return { success: false, output: "", error: `Serper API error ${response.status}: ${errorText.slice(0, API_ERROR_PREVIEW)}` };
7110
7355
  }
7111
7356
  let data;
7112
7357
  try {
@@ -7153,7 +7398,7 @@ async function searchWithGenericApi(query, apiKey, apiUrl) {
7153
7398
  errorText = await response.text();
7154
7399
  } catch {
7155
7400
  }
7156
- return { success: false, output: "", error: `Search API error ${response.status}: ${errorText.slice(0, 500)}` };
7401
+ return { success: false, output: "", error: `Search API error ${response.status}: ${errorText.slice(0, API_ERROR_PREVIEW)}` };
7157
7402
  }
7158
7403
  let data;
7159
7404
  try {
@@ -7180,7 +7425,11 @@ var BROWSER_LIMITS = {
7180
7425
  ADVISORY_WAIT_TIME: 2e3
7181
7426
  };
7182
7427
  var BROWSER_PATHS = {
7183
- TEMP_DIR_NAME: "pentest-browser"
7428
+ TEMP_DIR_NAME: "pentest-browser",
7429
+ /** §4 Session: Playwright storageState 저장 파일명 */
7430
+ SESSION_FILE: "browser-session.json",
7431
+ /** §4 Session: 추출된 Bearer/Cookie 헤더 저장 파일명 (curl/python 등 범용 도구용) */
7432
+ AUTH_HEADERS_FILE: "auth-headers.json"
7184
7433
  };
7185
7434
 
7186
7435
  // src/shared/constants/browser/playwright.ts
@@ -7292,7 +7541,7 @@ function safeJsString(str) {
7292
7541
 
7293
7542
  // src/engine/tools/web-browser/scripts/runner.ts
7294
7543
  import { spawn as spawn6 } from "child_process";
7295
- import { writeFileSync as writeFileSync6, unlinkSync as unlinkSync5 } from "fs";
7544
+ import { writeFileSync as writeFileSync6, unlinkSync as unlinkSync6 } from "fs";
7296
7545
  import { join as join7 } from "path";
7297
7546
  import { tmpdir as tmpdir2 } from "os";
7298
7547
  function runPlaywrightScript(script, timeout, scriptPrefix) {
@@ -7325,7 +7574,7 @@ function runPlaywrightScript(script, timeout, scriptPrefix) {
7325
7574
  child.stderr.on("data", (data) => stderr += data);
7326
7575
  child.on("close", (code) => {
7327
7576
  try {
7328
- unlinkSync5(scriptPath);
7577
+ unlinkSync6(scriptPath);
7329
7578
  } catch {
7330
7579
  }
7331
7580
  if (code !== 0) {
@@ -7352,7 +7601,7 @@ function runPlaywrightScript(script, timeout, scriptPrefix) {
7352
7601
  });
7353
7602
  child.on("error", (err) => {
7354
7603
  try {
7355
- unlinkSync5(scriptPath);
7604
+ unlinkSync6(scriptPath);
7356
7605
  } catch {
7357
7606
  }
7358
7607
  resolve({
@@ -7364,16 +7613,97 @@ function runPlaywrightScript(script, timeout, scriptPrefix) {
7364
7613
  });
7365
7614
  }
7366
7615
 
7616
+ // src/engine/tools/web-browser/scripts/snippets/auth-capture.ts
7617
+ function getNetworkAuthInterceptSnippet(varName = "_capturedHeaders") {
7618
+ return `
7619
+ // \xA7LLM-AUTONOMY: Network intercept \u2014 capture real auth headers from HTTP traffic.
7620
+ // No hardcoded key names. Whatever the app sends, we record it.
7621
+ const ${varName} = {};
7622
+ await page.route('**', async (route) => {
7623
+ const hdrs = route.request().headers();
7624
+ if (hdrs['authorization']) ${varName}['Authorization'] = hdrs['authorization'];
7625
+ if (hdrs['x-csrf-token']) ${varName}['x-csrf-token'] = hdrs['x-csrf-token'];
7626
+ if (hdrs['x-api-key']) ${varName}['x-api-key'] = hdrs['x-api-key'];
7627
+ if (hdrs['x-auth-token']) ${varName}['x-auth-token'] = hdrs['x-auth-token'];
7628
+ await route.continue();
7629
+ });
7630
+ `;
7631
+ }
7632
+ function getStorageAndAuthDumpSnippet(capturedVarName = "_capturedHeaders", safeSessionPath) {
7633
+ return `
7634
+ // \u2460 Network intercept: capture actual Authorization/Cookie headers sent during this session
7635
+ // WHY: More reliable than guessing storage key names.
7636
+ // Whatever the app calls its token, we capture it from real HTTP traffic.
7637
+ const _intercepted = ${capturedVarName} || {};
7638
+
7639
+ // \u2461 Full storage dump \u2192 LLM decides which value is the auth token
7640
+ const _allStorage = await page.evaluate(() => {
7641
+ const out = { localStorage: {}, sessionStorage: {} };
7642
+ for (let i = 0; i < localStorage.length; i++) {
7643
+ const k = localStorage.key(i);
7644
+ if (k) out.localStorage[k] = localStorage.getItem(k);
7645
+ }
7646
+ for (let i = 0; i < sessionStorage.length; i++) {
7647
+ const k = sessionStorage.key(i);
7648
+ if (k) out.sessionStorage[k] = sessionStorage.getItem(k);
7649
+ }
7650
+ return out;
7651
+ });
7652
+
7653
+ // \u2462 Cookies \u2014 full list including HttpOnly (server-side)
7654
+ const _cookies = await context.cookies();
7655
+ const _cookieHeader = _cookies.map(c => c.name + '=' + c.value).join('; ');
7656
+
7657
+ // \u2463 Build auth-headers.json:
7658
+ // Priority: intercepted network header > storage token-like values > cookies
7659
+ const _authHeaders = {};
7660
+
7661
+ // Network-intercepted Authorization wins (most reliable)
7662
+ if (_intercepted.Authorization) {
7663
+ _authHeaders['Authorization'] = _intercepted.Authorization;
7664
+ } else {
7665
+ // Fallback: find any storage value that looks like a JWT/Bearer token
7666
+ // Let LLM decide later by inspecting raw 'storage' field
7667
+ const _allValues = [
7668
+ ...Object.entries(_allStorage.localStorage),
7669
+ ...Object.entries(_allStorage.sessionStorage)
7670
+ ];
7671
+ const _tokenEntry = _allValues.find(([, v]) =>
7672
+ typeof v === 'string' && (v.startsWith('eyJ') || v.startsWith('Bearer '))
7673
+ );
7674
+ if (_tokenEntry) {
7675
+ const _raw = _tokenEntry[1];
7676
+ _authHeaders['Authorization'] = _raw.startsWith('Bearer ') ? _raw : 'Bearer ' + _raw;
7677
+ }
7678
+ }
7679
+
7680
+ if (_cookieHeader) _authHeaders['Cookie'] = _cookieHeader;
7681
+ if (_intercepted['x-csrf-token']) _authHeaders['X-CSRF-Token'] = _intercepted['x-csrf-token'];
7682
+ if (_intercepted['x-api-key']) _authHeaders['X-API-Key'] = _intercepted['x-api-key'];
7683
+
7684
+ // Raw storage dump for LLM to inspect (truncated per value)
7685
+ _authHeaders['_storage'] = JSON.stringify({
7686
+ ls: Object.fromEntries(Object.entries(_allStorage.localStorage).map(([k, v]) => [k, String(v).slice(0, 300)])),
7687
+ ss: Object.fromEntries(Object.entries(_allStorage.sessionStorage).map(([k, v]) => [k, String(v).slice(0, 300)]))
7688
+ });
7689
+
7690
+ const _authHeadersPath = ${safeSessionPath}.replace('${BROWSER_PATHS.SESSION_FILE}', '${BROWSER_PATHS.AUTH_HEADERS_FILE}');
7691
+ fs.writeFileSync(_authHeadersPath, JSON.stringify(_authHeaders, null, 2), { mode: 0o600 });
7692
+ `;
7693
+ }
7694
+
7367
7695
  // src/engine/tools/web-browser/scripts/builder.ts
7368
- function buildBrowseScript(url, options, screenshotPath) {
7696
+ function buildBrowseScript(url, options, screenshotPath, sessionPath) {
7369
7697
  const safeUrl = safeJsString(url);
7370
7698
  const safeUserAgent = safeJsString(options.userAgent || BROWSER_CONFIG.DEFAULT_USER_AGENT);
7371
7699
  const safeScreenshotPath = screenshotPath ? safeJsString(screenshotPath) : "null";
7372
7700
  const safeExtraHeaders = JSON.stringify(options.extraHeaders || {});
7373
7701
  const playwrightPath = getPlaywrightPath();
7374
7702
  const safePlaywrightPath = safeJsString(playwrightPath);
7703
+ const safeSessionPath = sessionPath ? safeJsString(sessionPath) : null;
7375
7704
  return `
7376
7705
  const { chromium } = require(${safePlaywrightPath});
7706
+ const fs = require('fs');
7377
7707
 
7378
7708
  (async () => {
7379
7709
  const browser = await chromium.launch({
@@ -7381,21 +7711,30 @@ const { chromium } = require(${safePlaywrightPath});
7381
7711
  args: ['${PLAYWRIGHT_ARG.NO_SANDBOX}', '${PLAYWRIGHT_ARG.DISABLE_SETUID_SANDBOX}', ${JSON.stringify(getTorBrowserArgs()).slice(1, -1)}].filter(Boolean)
7382
7712
  });
7383
7713
 
7384
- const context = await browser.newContext({
7714
+ // \xA74 Browser Session: load existing session if requested
7715
+ const contextOptions = {
7385
7716
  userAgent: ${safeUserAgent},
7386
7717
  viewport: { width: ${options.viewport.width}, height: ${options.viewport.height} },
7387
7718
  extraHTTPHeaders: ${safeExtraHeaders}
7388
- });
7719
+ };
7720
+ ${safeSessionPath && options.useSession ? `
7721
+ if (fs.existsSync(${safeSessionPath})) {
7722
+ contextOptions.storageState = ${safeSessionPath};
7723
+ }` : ""}
7724
+
7725
+ const context = await browser.newContext(contextOptions);
7389
7726
 
7390
7727
  const page = await context.newPage();
7391
7728
 
7729
+ ${getNetworkAuthInterceptSnippet("_capturedHeaders")}
7730
+
7392
7731
  try {
7393
7732
  await page.goto(${safeUrl}, { waitUntil: 'networkidle', timeout: ${options.timeout} });
7394
7733
 
7395
7734
  // Wait for dynamic content
7396
7735
  await page.waitForTimeout(${options.waitAfterLoad});
7397
7736
 
7398
- const result = {};
7737
+ const result = { _interceptedAuth: _capturedHeaders };
7399
7738
 
7400
7739
  // Extract title
7401
7740
  result.title = await page.title();
@@ -7442,6 +7781,118 @@ const { chromium } = require(${safePlaywrightPath});
7442
7781
  // Take screenshot
7443
7782
  ${screenshotPath ? `await page.screenshot({ path: ${safeScreenshotPath}, fullPage: false });` : ""}
7444
7783
 
7784
+ // \xA74 Browser Session: save state + capture auth headers (LLM-independent, no hardcoded keys)
7785
+ ${safeSessionPath && options.saveSession ? `
7786
+ await context.storageState({ path: ${safeSessionPath} });
7787
+ result.sessionSaved = ${safeSessionPath};
7788
+
7789
+ ${getStorageAndAuthDumpSnippet("result._interceptedAuth", safeSessionPath)}
7790
+ result.authHeadersSaved = _authHeadersPath;
7791
+ result.authHeadersNote = 'auth-headers.json contains intercepted network headers + full storage dump. LLM should inspect _storage field for unlabeled tokens.';
7792
+ ` : ""}
7793
+
7794
+ console.log(JSON.stringify(result));
7795
+ } catch (error) {
7796
+ console.log(JSON.stringify({ error: error.message }));
7797
+ } finally {
7798
+ await browser.close();
7799
+ }
7800
+ })();
7801
+ `;
7802
+ }
7803
+ function buildFormScript(params) {
7804
+ const { safeUrl, safeFormData, safePlaywrightPath, timeout, useSession, saveSession, safeSessionPath } = params;
7805
+ return `
7806
+ const { chromium } = require(${safePlaywrightPath});
7807
+ const fs = require('fs');
7808
+
7809
+ (async () => {
7810
+ const browser = await chromium.launch({
7811
+ headless: true,
7812
+ args: ['${PLAYWRIGHT_ARG.NO_SANDBOX}', '${PLAYWRIGHT_ARG.DISABLE_SETUID_SANDBOX}', ${JSON.stringify(getTorBrowserArgs()).slice(1, -1)}].filter(Boolean)
7813
+ });
7814
+
7815
+ // \xA74 Session: load existing session if requested
7816
+ const contextOptions = {};
7817
+ ${safeSessionPath && useSession ? `
7818
+ if (fs.existsSync(${safeSessionPath})) {
7819
+ contextOptions.storageState = ${safeSessionPath};
7820
+ }` : ""}
7821
+ const context = await browser.newContext(contextOptions);
7822
+ const page = await context.newPage();
7823
+
7824
+ ${getNetworkAuthInterceptSnippet("_capturedHeaders")}
7825
+
7826
+ try {
7827
+ await page.goto(${safeUrl}, { waitUntil: 'networkidle', timeout: ${timeout} });
7828
+
7829
+ // \xA7LLM-AUTONOMY: Semantic form field matching via Accessibility Tree + multiple strategies.
7830
+ // No hardcoded CSS selector rules. Tries in priority order:
7831
+ // 1. name/id attribute (exact)
7832
+ // 2. aria-label / placeholder semantic match (case-insensitive)
7833
+ // 3. input[type] semantic inference (username\u2192text, password\u2192password)
7834
+ const formData = ${safeFormData};
7835
+ for (const [fieldKey, fieldValue] of Object.entries(formData)) {
7836
+ const lk = fieldKey.toLowerCase();
7837
+
7838
+ // Strategy 1: exact name/id/aria-label attribute
7839
+ let el = await page.$(\`[name="\${fieldKey}"], #\${fieldKey}\`);
7840
+
7841
+ // Strategy 2: semantic aria-label or placeholder match
7842
+ if (!el) {
7843
+ el = await page.evaluateHandle((key) => {
7844
+ const inputs = [...document.querySelectorAll('input, textarea, select')];
7845
+ return inputs.find(i =>
7846
+ (i.getAttribute('aria-label') || '').toLowerCase().includes(key) ||
7847
+ (i.getAttribute('placeholder') || '').toLowerCase().includes(key) ||
7848
+ (i.getAttribute('name') || '').toLowerCase().includes(key) ||
7849
+ (i.getAttribute('id') || '').toLowerCase().includes(key)
7850
+ ) || null;
7851
+ }, lk).then(h => h.asElement());
7852
+ }
7853
+
7854
+ // Strategy 3: type-based inference (username/email/user \u2192 type=text/email, password \u2192 type=password)
7855
+ if (!el) {
7856
+ let inferredType = null;
7857
+ if (['username', 'user', 'email', 'login'].includes(lk)) inferredType = ['text', 'email'];
7858
+ if (['password', 'pass', 'pwd'].includes(lk)) inferredType = ['password'];
7859
+ if (inferredType) {
7860
+ el = await page.$(\`input[type="\${inferredType[0]}"\${inferredType[1] ? \`, input[type="\${inferredType[1]}"\` : ''}]\`);
7861
+ }
7862
+ }
7863
+
7864
+ if (el) {
7865
+ await el.fill(String(fieldValue));
7866
+ }
7867
+ }
7868
+
7869
+ // \xA7LLM-AUTONOMY: Submit via semantic search \u2014 finds any element that looks like a submit action.
7870
+ const submitEl = await page.$([
7871
+ 'button[type="submit"]',
7872
+ 'input[type="submit"]',
7873
+ '[role="button"][type="submit"]',
7874
+ 'button:has-text("Login"), button:has-text("Sign in"), button:has-text("Submit")',
7875
+ 'button:has-text("\uB85C\uADF8\uC778"), button:has-text("\uB85C\uADF8\uC778\uD558\uAE30")',
7876
+ ].join(', ')).catch(() => null);
7877
+
7878
+ if (submitEl) {
7879
+ await submitEl.click();
7880
+ await page.waitForLoadState('networkidle').catch(() => {});
7881
+ }
7882
+
7883
+ // \xA74 Session: save state + capture real auth headers (no hardcoded keys)
7884
+ ${safeSessionPath && saveSession ? `
7885
+ await context.storageState({ path: ${safeSessionPath} });
7886
+
7887
+ ${getStorageAndAuthDumpSnippet("_capturedHeaders", safeSessionPath)}
7888
+ ` : ""}
7889
+
7890
+ const result = {
7891
+ url: page.url(),
7892
+ title: await page.title(),
7893
+ ${safeSessionPath && saveSession ? `authHeadersSaved: ${safeSessionPath}.replace('${BROWSER_PATHS.SESSION_FILE}', '${BROWSER_PATHS.AUTH_HEADERS_FILE}'),` : ""}
7894
+ };
7895
+
7445
7896
  console.log(JSON.stringify(result));
7446
7897
  } catch (error) {
7447
7898
  console.log(JSON.stringify({ error: error.message }));
@@ -7493,12 +7944,27 @@ function formatBrowserOutput(data, options) {
7493
7944
  lines.push("");
7494
7945
  });
7495
7946
  }
7947
+ if (data.authHeadersSaved) {
7948
+ lines.push("## Session Saved");
7949
+ lines.push(`- Playwright state : ${data.sessionSaved || BROWSER_PATHS.SESSION_FILE}`);
7950
+ lines.push(`- Auth headers file: ${data.authHeadersSaved}`);
7951
+ lines.push("");
7952
+ lines.push(`**Next step** \u2014 use ${BROWSER_PATHS.AUTH_HEADERS_FILE} in run_cmd:`);
7953
+ lines.push("```");
7954
+ lines.push(`AUTH=${data.authHeadersSaved}`);
7955
+ lines.push(`TOKEN=$(jq -r .Authorization $AUTH)`);
7956
+ lines.push(`COOKIE=$(jq -r '.["Cookie"]' $AUTH)`);
7957
+ lines.push(`curl -s -H "Authorization: $TOKEN" -H "Cookie: $COOKIE" http://TARGET/api/endpoint`);
7958
+ lines.push("```");
7959
+ lines.push("");
7960
+ }
7496
7961
  return lines.join("\n");
7497
7962
  }
7498
7963
 
7499
7964
  // src/engine/tools/web-browser/browse.ts
7500
7965
  import { join as join8 } from "path";
7501
7966
  import { tmpdir as tmpdir3 } from "os";
7967
+ import { mkdirSync as mkdirSync2 } from "fs";
7502
7968
  async function browseUrl(url, options = {}) {
7503
7969
  try {
7504
7970
  const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
@@ -7514,7 +7980,12 @@ async function browseUrl(url, options = {}) {
7514
7980
  }
7515
7981
  }
7516
7982
  const screenshotPath = browserOptions.screenshot ? join8(tmpdir3(), BROWSER_PATHS.TEMP_DIR_NAME, `screenshot-${Date.now()}.png`) : void 0;
7517
- const script = buildBrowseScript(url, browserOptions, screenshotPath);
7983
+ let sessionPath;
7984
+ if (browserOptions.saveSession || browserOptions.useSession) {
7985
+ mkdirSync2(WORKSPACE.TMP, { recursive: true });
7986
+ sessionPath = join8(WORKSPACE.TMP, BROWSER_PATHS.SESSION_FILE);
7987
+ }
7988
+ const script = buildBrowseScript(url, browserOptions, screenshotPath, sessionPath);
7518
7989
  const result = await runPlaywrightScript(script, browserOptions.timeout, "browse");
7519
7990
  if (!result.success) {
7520
7991
  return {
@@ -7547,6 +8018,8 @@ async function browseUrl(url, options = {}) {
7547
8018
  }
7548
8019
 
7549
8020
  // src/engine/tools/web-browser/form.ts
8021
+ import { mkdirSync as mkdirSync3 } from "fs";
8022
+ import { join as join9 } from "path";
7550
8023
  async function fillAndSubmitForm(url, formData, options = {}) {
7551
8024
  try {
7552
8025
  const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
@@ -7554,49 +8027,21 @@ async function fillAndSubmitForm(url, formData, options = {}) {
7554
8027
  const safeFormData = JSON.stringify(formData);
7555
8028
  const playwrightPath = getPlaywrightPath();
7556
8029
  const safePlaywrightPath = safeJsString(playwrightPath);
7557
- const script = `
7558
- const { chromium } = require(${safePlaywrightPath});
7559
-
7560
- (async () => {
7561
- const browser = await chromium.launch({
7562
- headless: true,
7563
- args: ['--no-sandbox', '--disable-setuid-sandbox', ${JSON.stringify(getTorBrowserArgs()).slice(1, -1)}].filter(Boolean)
8030
+ let sessionPath;
8031
+ if (browserOptions.useSession || browserOptions.saveSession) {
8032
+ mkdirSync3(WORKSPACE.TMP, { recursive: true });
8033
+ sessionPath = join9(WORKSPACE.TMP, BROWSER_PATHS.SESSION_FILE);
8034
+ }
8035
+ const safeSessionPath = sessionPath ? safeJsString(sessionPath) : null;
8036
+ const script = buildFormScript({
8037
+ safeUrl,
8038
+ safeFormData,
8039
+ safePlaywrightPath,
8040
+ timeout: browserOptions.timeout,
8041
+ useSession: !!browserOptions.useSession,
8042
+ saveSession: !!browserOptions.saveSession,
8043
+ safeSessionPath
7564
8044
  });
7565
- const page = await browser.newPage();
7566
-
7567
- try {
7568
- await page.goto(${safeUrl}, { waitUntil: 'networkidle', timeout: ${browserOptions.timeout} });
7569
-
7570
- // Fill form fields
7571
- const formData = ${safeFormData};
7572
- for (const [name, value] of Object.entries(formData)) {
7573
- const input = await page.$(\`[name="\${name}"], #\${name}, .\${name}\`);
7574
- if (input) {
7575
- await input.fill(value);
7576
- }
7577
- }
7578
-
7579
- // Submit form
7580
- const submitBtn = await page.$('button[type="submit"], input[type="submit"], [type="submit"]');
7581
- if (submitBtn) {
7582
- await submitBtn.click();
7583
- await page.waitForLoadState('networkidle');
7584
- }
7585
-
7586
- const result = {
7587
- url: page.url(),
7588
- title: await page.title(),
7589
- content: await page.content()
7590
- };
7591
-
7592
- console.log(JSON.stringify(result));
7593
- } catch (error) {
7594
- console.log(JSON.stringify({ error: error.message }));
7595
- } finally {
7596
- await browser.close();
7597
- }
7598
- })();
7599
- `;
7600
8045
  const result = await runPlaywrightScript(script, browserOptions.timeout, "form");
7601
8046
  if (!result.success) {
7602
8047
  return {
@@ -7607,10 +8052,33 @@ const { chromium } = require(${safePlaywrightPath});
7607
8052
  }
7608
8053
  if (result.parsedData) {
7609
8054
  const data = result.parsedData;
8055
+ let output = `Form submitted. Current URL: ${data.url}
8056
+ Title: ${data.title}`;
8057
+ if (data.authHeadersSaved) {
8058
+ output += `
8059
+
8060
+ ## Session Saved`;
8061
+ output += `
8062
+ - Auth headers: ${data.authHeadersSaved}`;
8063
+ output += `
8064
+
8065
+ Usage in run_cmd:`;
8066
+ output += `
8067
+ \`\`\``;
8068
+ output += `
8069
+ AUTH=${data.authHeadersSaved}`;
8070
+ output += `
8071
+ TOKEN=$(jq -r .Authorization $AUTH)`;
8072
+ output += `
8073
+ COOKIE=$(jq -r '.["Cookie"]' $AUTH)`;
8074
+ output += `
8075
+ curl -s -H "Authorization: $TOKEN" -H "Cookie: $COOKIE" http://TARGET/api/endpoint`;
8076
+ output += `
8077
+ \`\`\``;
8078
+ }
7610
8079
  return {
7611
8080
  success: true,
7612
- output: `Form submitted. Current URL: ${data.url}
7613
- Title: ${data.title}`,
8081
+ output,
7614
8082
  extractedData: result.parsedData
7615
8083
  };
7616
8084
  }
@@ -7707,7 +8175,7 @@ async function searchWithZai(query) {
7707
8175
  errorText = await response.text();
7708
8176
  } catch {
7709
8177
  }
7710
- lastError = `z.ai web search API error ${response.status}: ${errorText.slice(0, 500)}`;
8178
+ lastError = `z.ai web search API error ${response.status}: ${errorText.slice(0, DISPLAY_LIMITS.API_ERROR_PREVIEW)}`;
7711
8179
  debugLog("search", "z.ai web search ERROR", { attempt, status: response.status, error: errorText });
7712
8180
  if (!isTransientHttpError(response.status)) {
7713
8181
  break;
@@ -7730,6 +8198,8 @@ var SEARCH_API_KEY_REQUIRED = `SEARCH_API_KEY is required for web search. Set it
7730
8198
  # Or Serper (Google)
7731
8199
  export SEARCH_API_KEY=your_serper_key
7732
8200
  export SEARCH_API_URL=https://google.serper.dev/search`;
8201
+ var MAX_SEARCH_ATTEMPTS = 2;
8202
+ var SEARCH_RETRY_WAIT_MS = 2e3;
7733
8203
  async function webSearch(query) {
7734
8204
  debugLog("search", "webSearch START", { query });
7735
8205
  if (isZaiProvider()) {
@@ -7751,7 +8221,7 @@ async function webSearch(query) {
7751
8221
  };
7752
8222
  }
7753
8223
  let lastError = "";
7754
- for (let attempt = 0; attempt < 2; attempt++) {
8224
+ for (let attempt = 0; attempt < MAX_SEARCH_ATTEMPTS; attempt++) {
7755
8225
  try {
7756
8226
  if (apiUrl.includes(SEARCH_URL_PATTERN.GLM) || apiUrl.includes(SEARCH_URL_PATTERN.ZHIPU)) {
7757
8227
  debugLog("search", `Using GLM search (attempt ${attempt + 1})`);
@@ -7771,13 +8241,13 @@ async function webSearch(query) {
7771
8241
  debugLog("search", `webSearch attempt ${attempt + 1} ERROR`, { error: lastError });
7772
8242
  const isTransient = /timeout|ECONNRESET|ENOTFOUND|429|5\d{2}|rate.?limit|temporarily/i.test(lastError);
7773
8243
  if (attempt === 0 && isTransient) {
7774
- await new Promise((r) => setTimeout(r, 2e3));
8244
+ await new Promise((r) => setTimeout(r, SEARCH_RETRY_WAIT_MS));
7775
8245
  continue;
7776
8246
  }
7777
8247
  }
7778
8248
  }
7779
8249
  const fallbackGuidance = [
7780
- `Search failed after 2 attempts: ${lastError}`,
8250
+ `Search failed after ${MAX_SEARCH_ATTEMPTS} attempts: ${lastError}`,
7781
8251
  "",
7782
8252
  "FALLBACK \u2014 Use browse_url on these sources directly:",
7783
8253
  ` browse_url("https://book.hacktricks.wiki/en/network-services-pentesting/") \u2014 for service-specific attacks`,
@@ -7911,7 +8381,7 @@ function createDomain(def) {
7911
8381
  }
7912
8382
 
7913
8383
  // src/engine/tools/pentest-intel-tools/web-tools.ts
7914
- var webSearchTool = {
8384
+ var createWebSearchTool = (dynamicTechniques) => ({
7915
8385
  name: TOOL_NAMES.WEB_SEARCH,
7916
8386
  description: `Search the web for information. Use this to:
7917
8387
  - Find CVE details and exploit code
@@ -7934,31 +8404,56 @@ Returns search results with links and summaries.`,
7934
8404
  execute: async (p) => {
7935
8405
  const query = p.query;
7936
8406
  const useBrowser = p.use_browser;
8407
+ let result;
7937
8408
  if (useBrowser) {
7938
- const result = await webSearchWithBrowser(query, "google");
7939
- return { success: result.success, output: result.output, error: result.error };
8409
+ const r = await webSearchWithBrowser(query, "google");
8410
+ result = { success: r.success, output: r.output, error: r.error };
8411
+ } else {
8412
+ result = await webSearch(query);
7940
8413
  }
7941
- return webSearch(query);
8414
+ if (result.success && result.output) {
8415
+ dynamicTechniques.learnFromSearchResult(query, result.output);
8416
+ }
8417
+ return result;
7942
8418
  }
7943
- };
8419
+ });
7944
8420
  var browseUrlTool = {
7945
8421
  name: TOOL_NAMES.BROWSE_URL,
7946
8422
  description: `Navigate to a URL using a headless browser (Playwright).
7947
8423
  Use this for:
7948
8424
  - Browsing JavaScript-heavy websites
7949
8425
  - Extracting links, forms, and content
7950
- - Viewing security advisories
7951
- - Accessing documentation pages
7952
- - Analyzing web application structure
7953
- - Discovering web attack surface (forms, inputs, API endpoints)
7954
-
7955
- Can extract forms and inputs for security testing.`,
8426
+ - Viewing security advisories, accessing documentation pages
8427
+ - Analyzing web application structure and attack surface
8428
+ - Multi-step OAuth/JWT flows requiring session continuity
8429
+
8430
+ Session persistence (save_session: true saves TWO files):
8431
+ 1. .pentesting/workspace/browser-session.json \u2192 Playwright storageState (for use_session)
8432
+ 2. .pentesting/workspace/auth-headers.json \u2192 Extracted Bearer token + Cookie header
8433
+
8434
+ auth-headers.json format:
8435
+ { "Authorization": "Bearer eyJ...", "Cookie": "session=abc; csrf=xyz" }
8436
+
8437
+ Usage in run_cmd after save_session:
8438
+ AUTH_FILE=.pentesting/workspace/auth-headers.json
8439
+ TOKEN=$(jq -r .Authorization $AUTH_FILE)
8440
+ COOKIE=$(jq -r '.["Cookie"]' $AUTH_FILE)
8441
+ curl -H "Authorization: $TOKEN" -H "Cookie: $COOKIE" http://target/api/admin
8442
+ sqlmap -u "http://target/api" --headers="Authorization: $TOKEN"
8443
+ python3 -c "import json,requests; h=json.load(open('$AUTH_FILE')); r=requests.get('...', headers=h); print(r.text)"
8444
+
8445
+ Example full OAuth attack flow:
8446
+ 1. browse_url(login_url, {save_session: true}) # login \u2192 saves both files
8447
+ 2. browse_url(protected, {use_session: true}) # browser tool with session
8448
+ 3. run_cmd "curl -H \\"Authorization: $(jq -r .Authorization .pentesting/workspace/auth-headers.json)\\" http://target/api/admin" # curl with Bearer`,
7956
8449
  parameters: {
7957
8450
  url: { type: "string", description: "URL to browse" },
7958
8451
  extract_forms: { type: "boolean", description: "Extract form information (inputs, actions, methods)" },
7959
8452
  extract_links: { type: "boolean", description: "Extract all links from the page" },
7960
8453
  screenshot: { type: "boolean", description: "Take a screenshot of the page" },
7961
- extra_headers: { type: "object", description: "Custom HTTP headers", additionalProperties: { type: "string" } }
8454
+ extra_headers: { type: "object", description: "Custom HTTP headers", additionalProperties: { type: "string" } },
8455
+ save_session: { type: "boolean", description: "Save browser session (cookies, localStorage) to disk after navigation" },
8456
+ use_session: { type: "boolean", description: "Load previously saved browser session before navigation" }
7962
8457
  },
7963
8458
  required: ["url"],
7964
8459
  execute: async (p) => {
@@ -7967,7 +8462,9 @@ Can extract forms and inputs for security testing.`,
7967
8462
  extractForms: p.extract_forms,
7968
8463
  extractLinks: p.extract_links,
7969
8464
  screenshot: p.screenshot,
7970
- extraHeaders: p.extra_headers
8465
+ extraHeaders: p.extra_headers,
8466
+ saveSession: p.save_session,
8467
+ useSession: p.use_session
7971
8468
  });
7972
8469
  return { success: result.success, output: result.output, error: result.error };
7973
8470
  }
@@ -7978,15 +8475,21 @@ var fillFormTool = {
7978
8475
  - Testing form-based authentication
7979
8476
  - Submitting search forms
7980
8477
  - Testing for form injection vulnerabilities
7981
- - Automated form interaction`,
8478
+ - Automated form interaction
8479
+ - OAuth/authentication flows (use save_session/use_session to persist session)`,
7982
8480
  parameters: {
7983
8481
  url: { type: "string", description: "URL containing the form" },
7984
- fields: { type: "object", description: "Form field names and values to fill", additionalProperties: { type: "string" } }
8482
+ fields: { type: "object", description: "Form field names and values to fill", additionalProperties: { type: "string" } },
8483
+ save_session: { type: "boolean", description: "Save browser session after form submission (useful after login)" },
8484
+ use_session: { type: "boolean", description: "Load previously saved session before navigating (use after browse_url login)" }
7985
8485
  },
7986
8486
  required: ["url", "fields"],
7987
8487
  execute: async (p) => {
7988
8488
  const url = ensureUrlProtocol(p.url);
7989
- const result = await fillAndSubmitForm(url, p.fields);
8489
+ const result = await fillAndSubmitForm(url, p.fields, {
8490
+ saveSession: p.save_session,
8491
+ useSession: p.use_session
8492
+ });
7990
8493
  return { success: result.success, output: result.output, error: result.error };
7991
8494
  }
7992
8495
  };
@@ -8121,10 +8624,10 @@ For CVEs not in this list, use web_search to find them.`
8121
8624
  };
8122
8625
 
8123
8626
  // src/engine/tools/pentest-intel-tools/index.ts
8124
- var createIntelTools = () => [
8627
+ var createIntelTools = (dynamicTechniques) => [
8125
8628
  nmapTool,
8126
8629
  searchCveTool,
8127
- webSearchTool,
8630
+ createWebSearchTool(dynamicTechniques),
8128
8631
  browseUrlTool,
8129
8632
  fillFormTool,
8130
8633
  getOwaspTool,
@@ -8476,28 +8979,28 @@ function getContextRecommendations(context, variantCount) {
8476
8979
  recs.push(`Generated ${variantCount} variants for context: ${context}`);
8477
8980
  switch (context) {
8478
8981
  case PAYLOAD_CONTEXT.URL_PARAM:
8479
- recs.push("If all URL encoding fails: try HTTP Parameter Pollution (send same param twice)");
8480
- recs.push("Try switching GET -> POST or changing Content-Type");
8481
- recs.push("Check if WebSocket endpoint exists (often unfiltered)");
8982
+ recs.push("bypass-hint: HTTP Parameter Pollution (same param twice) often unfiltered");
8983
+ recs.push("bypass-hint: GET\u2192POST or Content-Type change may bypass client-side filters");
8984
+ recs.push("bypass-hint: WebSocket endpoints are often unfiltered");
8482
8985
  break;
8483
8986
  case PAYLOAD_CONTEXT.SQL_STRING:
8484
8987
  case PAYLOAD_CONTEXT.SQL_NUMERIC:
8485
- recs.push("If inline comments fail: try MySQL version comments /*!50000SELECT*/");
8486
- recs.push("Try time-based blind if error/union payloads are blocked");
8487
- recs.push("Use sqlmap with --tamper scripts for automated bypass");
8988
+ recs.push("bypass-hint: MySQL version comments /*!50000SELECT*/ bypass pattern-match filters");
8989
+ recs.push("bypass-hint: Time-based blind works when error/union paths are blocked");
8990
+ recs.push("bypass-hint: sqlmap --tamper scripts for automated WAF bypass");
8488
8991
  break;
8489
8992
  case PAYLOAD_CONTEXT.HTML_BODY:
8490
- recs.push("If common XSS tags blocked: try SVG, MathML, mutation XSS");
8491
- recs.push("Check CSP header -- it determines what JS execution is possible");
8492
- recs.push("Try DOM-based XSS via document.location, innerHTML, postMessage");
8993
+ recs.push("bypass-hint: SVG/MathML/mutation XSS when common tags are blocked");
8994
+ recs.push("bypass-hint: CSP header determines what JS execution paths exist");
8995
+ recs.push("bypass-hint: DOM-based XSS via document.location, innerHTML, postMessage");
8493
8996
  break;
8494
8997
  case PAYLOAD_CONTEXT.SHELL_CMD:
8495
- recs.push("Use ${IFS} for space, quotes for keyword bypass, base64 for full obfuscation");
8496
- recs.push("Try: echo PAYLOAD_BASE64 | base64 -d | sh");
8497
- recs.push("Variable concatenation: a=c;b=at;$a$b /etc/passwd");
8998
+ recs.push("bypass-hint: ${IFS} for space, quotes for keyword bypass");
8999
+ recs.push("bypass-hint: base64 encoding for full payload obfuscation");
9000
+ recs.push("bypass-hint: variable concatenation: a=c;b=at;$a$b /etc/passwd");
8498
9001
  break;
8499
9002
  }
8500
- recs.push("If all variants fail: web_search for latest bypass techniques for this specific filter type");
9003
+ recs.push('If all variants fail: web_search("bypass <filter-type> <context> site:hacktricks.xyz") for current techniques.');
8501
9004
  return recs;
8502
9005
  }
8503
9006
 
@@ -8655,7 +9158,7 @@ ${variantList}
8655
9158
 
8656
9159
  // src/engine/tools/pentest-attack-tools/wordlists-tool.ts
8657
9160
  import { existsSync as existsSync10, statSync as statSync2, readdirSync as readdirSync2 } from "fs";
8658
- import { join as join9 } from "path";
9161
+ import { join as join10 } from "path";
8659
9162
  var CATEGORY_KEYWORDS = {
8660
9163
  passwords: ["password", "rockyou"],
8661
9164
  usernames: ["user"],
@@ -8742,7 +9245,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
8742
9245
  }
8743
9246
  for (const entry of entries) {
8744
9247
  if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
8745
- const fullPath = join9(dirPath, entry.name);
9248
+ const fullPath = join10(dirPath, entry.name);
8746
9249
  if (entry.isDirectory()) {
8747
9250
  scanDir(fullPath, maxDepth, depth + 1);
8748
9251
  continue;
@@ -8795,7 +9298,7 @@ var createAttackTools = () => [
8795
9298
  var createPentestTools = (state, events) => [
8796
9299
  ...createStateTools(state, events),
8797
9300
  ...createTargetTools(state),
8798
- ...createIntelTools(),
9301
+ ...createIntelTools(state.dynamicTechniques),
8799
9302
  ...createAttackTools()
8800
9303
  ];
8801
9304
 
@@ -9660,7 +10163,7 @@ Returns recommendations on process status, port conflicts, long-running tasks, e
9660
10163
  ];
9661
10164
 
9662
10165
  // src/domains/registry.ts
9663
- import { join as join10, dirname as dirname3 } from "path";
10166
+ import { join as join11, dirname as dirname3 } from "path";
9664
10167
  import { fileURLToPath } from "url";
9665
10168
 
9666
10169
  // src/domains/network/tools.ts
@@ -10072,73 +10575,73 @@ var DOMAINS = {
10072
10575
  id: SERVICE_CATEGORIES.NETWORK,
10073
10576
  name: "Network Infrastructure",
10074
10577
  description: "Vulnerability scanning, port mapping, and network service exploitation.",
10075
- promptPath: join10(__dirname, "network/prompt.md")
10578
+ promptPath: join11(__dirname, "network/prompt.md")
10076
10579
  },
10077
10580
  [SERVICE_CATEGORIES.WEB]: {
10078
10581
  id: SERVICE_CATEGORIES.WEB,
10079
10582
  name: "Web Application",
10080
10583
  description: "Web app security testing, injection attacks, and auth bypass.",
10081
- promptPath: join10(__dirname, "web/prompt.md")
10584
+ promptPath: join11(__dirname, "web/prompt.md")
10082
10585
  },
10083
10586
  [SERVICE_CATEGORIES.DATABASE]: {
10084
10587
  id: SERVICE_CATEGORIES.DATABASE,
10085
10588
  name: "Database Security",
10086
10589
  description: "SQL injection, database enumeration, and data extraction.",
10087
- promptPath: join10(__dirname, "database/prompt.md")
10590
+ promptPath: join11(__dirname, "database/prompt.md")
10088
10591
  },
10089
10592
  [SERVICE_CATEGORIES.AD]: {
10090
10593
  id: SERVICE_CATEGORIES.AD,
10091
10594
  name: "Active Directory",
10092
10595
  description: "Kerberos, LDAP, and Windows domain privilege escalation.",
10093
- promptPath: join10(__dirname, "ad/prompt.md")
10596
+ promptPath: join11(__dirname, "ad/prompt.md")
10094
10597
  },
10095
10598
  [SERVICE_CATEGORIES.EMAIL]: {
10096
10599
  id: SERVICE_CATEGORIES.EMAIL,
10097
10600
  name: "Email Services",
10098
10601
  description: "SMTP, IMAP, POP3 security and user enumeration.",
10099
- promptPath: join10(__dirname, "email/prompt.md")
10602
+ promptPath: join11(__dirname, "email/prompt.md")
10100
10603
  },
10101
10604
  [SERVICE_CATEGORIES.REMOTE_ACCESS]: {
10102
10605
  id: SERVICE_CATEGORIES.REMOTE_ACCESS,
10103
10606
  name: "Remote Access",
10104
10607
  description: "SSH, RDP, VNC and other remote control protocols.",
10105
- promptPath: join10(__dirname, "remote-access/prompt.md")
10608
+ promptPath: join11(__dirname, "remote-access/prompt.md")
10106
10609
  },
10107
10610
  [SERVICE_CATEGORIES.FILE_SHARING]: {
10108
10611
  id: SERVICE_CATEGORIES.FILE_SHARING,
10109
10612
  name: "File Sharing",
10110
10613
  description: "SMB, NFS, FTP and shared resource security.",
10111
- promptPath: join10(__dirname, "file-sharing/prompt.md")
10614
+ promptPath: join11(__dirname, "file-sharing/prompt.md")
10112
10615
  },
10113
10616
  [SERVICE_CATEGORIES.CLOUD]: {
10114
10617
  id: SERVICE_CATEGORIES.CLOUD,
10115
10618
  name: "Cloud Infrastructure",
10116
10619
  description: "AWS, Azure, and GCP security and misconfiguration.",
10117
- promptPath: join10(__dirname, "cloud/prompt.md")
10620
+ promptPath: join11(__dirname, "cloud/prompt.md")
10118
10621
  },
10119
10622
  [SERVICE_CATEGORIES.CONTAINER]: {
10120
10623
  id: SERVICE_CATEGORIES.CONTAINER,
10121
10624
  name: "Container Systems",
10122
10625
  description: "Docker and Kubernetes security testing.",
10123
- promptPath: join10(__dirname, "container/prompt.md")
10626
+ promptPath: join11(__dirname, "container/prompt.md")
10124
10627
  },
10125
10628
  [SERVICE_CATEGORIES.API]: {
10126
10629
  id: SERVICE_CATEGORIES.API,
10127
10630
  name: "API Security",
10128
10631
  description: "REST, GraphQL, and SOAP API security testing.",
10129
- promptPath: join10(__dirname, "api/prompt.md")
10632
+ promptPath: join11(__dirname, "api/prompt.md")
10130
10633
  },
10131
10634
  [SERVICE_CATEGORIES.WIRELESS]: {
10132
10635
  id: SERVICE_CATEGORIES.WIRELESS,
10133
10636
  name: "Wireless Networks",
10134
10637
  description: "WiFi and Bluetooth security testing.",
10135
- promptPath: join10(__dirname, "wireless/prompt.md")
10638
+ promptPath: join11(__dirname, "wireless/prompt.md")
10136
10639
  },
10137
10640
  [SERVICE_CATEGORIES.ICS]: {
10138
10641
  id: SERVICE_CATEGORIES.ICS,
10139
10642
  name: "Industrial Systems",
10140
10643
  description: "Critical infrastructure - Modbus, DNP3, ENIP.",
10141
- promptPath: join10(__dirname, "ics/prompt.md")
10644
+ promptPath: join11(__dirname, "ics/prompt.md")
10142
10645
  }
10143
10646
  };
10144
10647
  function loadAllDomainTools() {
@@ -10400,8 +10903,8 @@ function formatBinaryAnalysis(info) {
10400
10903
  `Protections: Canary=${info.canary ?? "?"} NX=${info.nx ?? "?"} PIE=${info.pie ?? "?"} RELRO=${info.relro ?? "?"}`,
10401
10904
  `Stripped: ${info.stripped ?? "?"} | Static: ${info.staticallyLinked ?? "?"}`,
10402
10905
  "",
10403
- "EXPLOIT SUGGESTIONS:",
10404
- ...suggestions.map((s) => ` \u2192 ${s}`),
10906
+ "\xA73 BINARY FEASIBILITY FACTS (what is technically possible given protections, NOT a prescribed attack plan):",
10907
+ ...suggestions.map((s) => ` fact: ${s}`),
10405
10908
  "</binary-analysis>"
10406
10909
  ];
10407
10910
  return lines.join("\n");
@@ -10981,11 +11484,11 @@ function safeParseJson(jsonString) {
10981
11484
  function stripThinkTags(text, existingReasoning) {
10982
11485
  let cleanText = text;
10983
11486
  let extractedReasoning = existingReasoning;
10984
- cleanText = cleanText.replace(/lte;think&gt;([\s\S]*?)&lt;\/think&gt;/gi, (_match, inner) => {
11487
+ cleanText = cleanText.replace(/<think>([\s\S]*?)<\/think>/gi, (_match, inner) => {
10985
11488
  extractedReasoning += inner;
10986
11489
  return "";
10987
11490
  });
10988
- cleanText = cleanText.replace(/&lt;\/?think&gt;/gi, "");
11491
+ cleanText = cleanText.replace(/<\/?think>/gi, "");
10989
11492
  return { cleanText, extractedReasoning };
10990
11493
  }
10991
11494
 
@@ -11220,49 +11723,8 @@ var LLMClient = class {
11220
11723
  context.getTotalChars = () => totalChars;
11221
11724
  context.currentBlockRef = currentBlockRef;
11222
11725
  context.toolCallsMap = toolCallsMap;
11223
- const reader = response.body?.getReader();
11224
- if (!reader) throw new Error("Response body is not readable");
11225
- const decoder = new TextDecoder();
11226
- let buffer = "";
11227
- try {
11228
- while (true) {
11229
- if (callbacks?.abortSignal?.aborted) {
11230
- wasAborted = true;
11231
- break;
11232
- }
11233
- const { done, value } = await reader.read();
11234
- if (done) break;
11235
- buffer += decoder.decode(value, { stream: true });
11236
- const lines = buffer.split("\n");
11237
- buffer = lines.pop() || "";
11238
- for (const line of lines) {
11239
- if (line.startsWith("data: ")) {
11240
- const data = line.slice(6);
11241
- if (data.trim() === "[DONE]") continue;
11242
- try {
11243
- const event = JSON.parse(data);
11244
- processStreamEvent(event, requestId, context);
11245
- } catch {
11246
- }
11247
- }
11248
- }
11249
- }
11250
- } finally {
11251
- reader.releaseLock();
11252
- }
11253
- const toolCalls = [];
11254
- for (const [id, toolCall] of toolCallsMap) {
11255
- if (toolCall._pendingJson) {
11256
- const parseResult = safeParseJson(toolCall._pendingJson);
11257
- if (parseResult.success) {
11258
- toolCall.input = parseResult.data;
11259
- } else {
11260
- toolCall.input = { _parse_error: parseResult.error, _raw_json: toolCall._pendingJson.slice(0, DISPLAY_LIMITS.RAW_JSON_ERROR_PREVIEW) };
11261
- }
11262
- delete toolCall._pendingJson;
11263
- }
11264
- toolCalls.push({ id: toolCall.id, name: toolCall.name, input: toolCall.input });
11265
- }
11726
+ wasAborted = await readSSEStream(response, requestId, context, callbacks?.abortSignal);
11727
+ const toolCalls = resolveToolCalls(toolCallsMap);
11266
11728
  const stripped = stripThinkTags(fullContent, fullReasoning);
11267
11729
  return {
11268
11730
  content: stripped.cleanText,
@@ -11274,6 +11736,55 @@ var LLMClient = class {
11274
11736
  };
11275
11737
  }
11276
11738
  };
11739
+ async function readSSEStream(response, requestId, context, abortSignal) {
11740
+ const reader = response.body?.getReader();
11741
+ if (!reader) throw new Error("Response body is not readable");
11742
+ const decoder = new TextDecoder();
11743
+ let buffer = "";
11744
+ let wasAborted = false;
11745
+ try {
11746
+ while (true) {
11747
+ if (abortSignal?.aborted) {
11748
+ wasAborted = true;
11749
+ break;
11750
+ }
11751
+ const { done, value } = await reader.read();
11752
+ if (done) break;
11753
+ buffer += decoder.decode(value, { stream: true });
11754
+ const lines = buffer.split("\n");
11755
+ buffer = lines.pop() || "";
11756
+ for (const line of lines) {
11757
+ if (!line.startsWith("data: ")) continue;
11758
+ const data = line.slice(6);
11759
+ if (data.trim() === "[DONE]") continue;
11760
+ try {
11761
+ const event = JSON.parse(data);
11762
+ processStreamEvent(event, requestId, context);
11763
+ } catch {
11764
+ }
11765
+ }
11766
+ }
11767
+ } finally {
11768
+ reader.releaseLock();
11769
+ }
11770
+ return wasAborted;
11771
+ }
11772
+ function resolveToolCalls(toolCallsMap) {
11773
+ const toolCalls = [];
11774
+ for (const [, toolCall] of toolCallsMap) {
11775
+ if (toolCall._pendingJson) {
11776
+ const parseResult = safeParseJson(toolCall._pendingJson);
11777
+ if (parseResult.success) {
11778
+ toolCall.input = parseResult.data;
11779
+ } else {
11780
+ toolCall.input = { _parse_error: parseResult.error, _raw_json: toolCall._pendingJson.slice(0, DISPLAY_LIMITS.RAW_JSON_ERROR_PREVIEW) };
11781
+ }
11782
+ delete toolCall._pendingJson;
11783
+ }
11784
+ toolCalls.push({ id: toolCall.id, name: toolCall.name, input: toolCall.input });
11785
+ }
11786
+ return toolCalls;
11787
+ }
11277
11788
 
11278
11789
  // src/engine/llm-client/factory.ts
11279
11790
  var llmInstance = null;
@@ -11313,7 +11824,7 @@ function handleLoopError(error, messages, progress, iteration, consecutiveLLMErr
11313
11824
  );
11314
11825
  }
11315
11826
  const unexpectedMsg = error instanceof Error ? error.message : String(error);
11316
- events.emit({
11827
+ events?.emit({
11317
11828
  type: EVENT_TYPES.ERROR,
11318
11829
  timestamp: Date.now(),
11319
11830
  data: { message: `Unexpected error: ${unexpectedMsg}`, phase: getPhase(), isRecoverable: true }
@@ -11324,7 +11835,7 @@ Continue your task.` });
11324
11835
  }
11325
11836
  function handleLLMError(error, messages, progress, iteration, consecutiveLLMErrors, maxConsecutiveLLMErrors, events, getPhase) {
11326
11837
  const errorInfo = error.errorInfo;
11327
- events.emit({
11838
+ events?.emit({
11328
11839
  type: EVENT_TYPES.ERROR,
11329
11840
  timestamp: Date.now(),
11330
11841
  data: {
@@ -11382,19 +11893,13 @@ function buildDeadlockNudge(phase, targetCount, findingCount) {
11382
11893
  [PHASES.WEB]: `WEB: Enumerate attack surface. Test every input.`
11383
11894
  };
11384
11895
  const direction = phaseDirection[phase] || phaseDirection[PHASES.RECON];
11385
- return `\u26A1 DEADLOCK: ${AGENT_LIMITS.MAX_CONSECUTIVE_IDLE} turns with ZERO tool calls.
11896
+ return `\u26A1 DEADLOCK DETECTED: ${AGENT_LIMITS.MAX_CONSECUTIVE_IDLE} consecutive turns with zero tool calls.
11386
11897
  Phase: ${phase} | Targets: ${targetCount} | Findings: ${findingCount}
11387
11898
 
11388
11899
  ${direction}
11389
11900
 
11390
- PICK ANY \u2014 do whatever fits best (no order, all are valid):
11391
- \u2022 Brute-force with wordlists (hydra/hashcat/ffuf + rockyou/seclists)
11392
- \u2022 web_search for techniques
11393
- \u2022 Try a completely different approach
11394
- \u2022 Probe for unknown vulns
11395
- \u2022 ask_user for hints
11396
-
11397
- ACT NOW \u2014 EXECUTE.`;
11901
+ Determine the highest-impact action available to you right now and execute it immediately.
11902
+ Do not explain your reasoning \u2014 call a tool.`;
11398
11903
  }
11399
11904
 
11400
11905
  // src/agents/core-agent/event-emitters.ts
@@ -11631,9 +12136,12 @@ function enrichToolErrorContext(ctx) {
11631
12136
  } else if (isPermissionError(errorLower)) {
11632
12137
  lines.push(`Fix: Insufficient permissions for this operation.`);
11633
12138
  lines.push(`Actions: (1) Try with sudo if appropriate, (2) Use a different approach, or (3) Check if the target path/resource is correct.`);
11634
- } else if (isConnectionError(errorLower)) {
11635
- lines.push(`Fix: Cannot reach the target service.`);
11636
- lines.push(`Actions: (1) Verify the target host/port is correct, (2) Check if the service is running, (3) Try a different port or protocol.`);
12139
+ } else if (isTimeoutError(errorLower)) {
12140
+ lines.push(`Fix: Connection timed out \u2014 service may be slow, filtered by firewall, or rate-limiting.`);
12141
+ lines.push(`Actions: (1) Increase timeout flag (--timeout, -w, --connect-timeout), (2) Try -Pn if nmap (ICMP blocked), (3) Try a slower scan rate, (4) Verify target is alive via other method.`);
12142
+ } else if (isConnectionRefusedError(errorLower)) {
12143
+ lines.push(`Fix: Connection refused \u2014 port is actively closed or service not running.`);
12144
+ lines.push(`Actions: (1) Verify the exact port number, (2) Check if service requires different protocol (TCP vs UDP), (3) Try alternative ports, (4) The service may have crashed \u2014 check via a different scan.`);
11637
12145
  } else if (isInvalidInputError(errorLower)) {
11638
12146
  lines.push(`Fix: The input format is incorrect.`);
11639
12147
  lines.push(`Actions: (1) Check the input format and fix it, (2) Use web_search to find the correct syntax, or (3) Try a simpler input.`);
@@ -11654,8 +12162,11 @@ function isNotFoundError(e) {
11654
12162
  function isPermissionError(e) {
11655
12163
  return e.includes("permission denied") || e.includes("access denied") || e.includes("eacces");
11656
12164
  }
11657
- function isConnectionError(e) {
11658
- return e.includes("connection refused") || e.includes("econnrefused") || e.includes("connection reset") || e.includes("timeout");
12165
+ function isTimeoutError(e) {
12166
+ return e.includes("timeout") || e.includes("timed out") || e.includes("etimedout");
12167
+ }
12168
+ function isConnectionRefusedError(e) {
12169
+ return e.includes("connection refused") || e.includes("econnrefused") || e.includes("connection reset");
11659
12170
  }
11660
12171
  function isInvalidInputError(e) {
11661
12172
  return e.includes("invalid") || e.includes("malformed") || e.includes("syntax error");
@@ -11761,40 +12272,32 @@ function stripAnsi(text) {
11761
12272
  function normalizeLine(line) {
11762
12273
  return line.replace(/\d+/g, "N").replace(/0x[a-f0-9]+/gi, "H").replace(/\s+/g, " ").trim().toLowerCase();
11763
12274
  }
12275
+ function flushDuplicates(result, lastLine, count) {
12276
+ if (count <= 0) return;
12277
+ if (count <= MAX_DUPLICATE_DISPLAY) {
12278
+ for (let i = 0; i < count; i++) result.push(lastLine);
12279
+ } else {
12280
+ result.push(` ... (${count} similar lines collapsed)`);
12281
+ }
12282
+ }
11764
12283
  function filterAndDedup(lines) {
11765
12284
  const result = [];
11766
12285
  let lastLine = "";
11767
12286
  let consecutiveDupes = 0;
11768
12287
  for (const line of lines) {
11769
12288
  const trimmed = line.trim();
11770
- if (NOISE_PATTERNS.some((p) => p.test(trimmed))) {
11771
- continue;
11772
- }
12289
+ if (NOISE_PATTERNS.some((p) => p.test(trimmed))) continue;
11773
12290
  const normalized = normalizeLine(trimmed);
11774
12291
  if (normalized === normalizeLine(lastLine)) {
11775
12292
  consecutiveDupes++;
11776
12293
  continue;
11777
12294
  }
11778
- if (consecutiveDupes > 0) {
11779
- if (consecutiveDupes <= MAX_DUPLICATE_DISPLAY) {
11780
- for (let i = 0; i < consecutiveDupes; i++) {
11781
- result.push(lastLine);
11782
- }
11783
- } else {
11784
- result.push(` ... (${consecutiveDupes} similar lines collapsed)`);
11785
- }
11786
- consecutiveDupes = 0;
11787
- }
12295
+ flushDuplicates(result, lastLine, consecutiveDupes);
12296
+ consecutiveDupes = 0;
11788
12297
  result.push(line);
11789
12298
  lastLine = trimmed;
11790
12299
  }
11791
- if (consecutiveDupes > MAX_DUPLICATE_DISPLAY) {
11792
- result.push(` ... (${consecutiveDupes} similar lines collapsed)`);
11793
- } else {
11794
- for (let i = 0; i < consecutiveDupes; i++) {
11795
- result.push(lastLine);
11796
- }
11797
- }
12300
+ flushDuplicates(result, lastLine, consecutiveDupes);
11798
12301
  return result;
11799
12302
  }
11800
12303
  function structuralPreprocess(output) {
@@ -11924,6 +12427,14 @@ function parseAnalystMemo(response) {
11924
12427
  ].filter(Boolean).join(" | ")
11925
12428
  };
11926
12429
  }
12430
+ function extractHypothesizedReason(failureLine) {
12431
+ const match = failureLine.match(/\[(FILTERED|WRONG_VECTOR|AUTH_REQUIRED|TOOL_ERROR|TIMEOUT|PATCHED)\]/);
12432
+ if (!match) return void 0;
12433
+ const type = match[1];
12434
+ const parts = failureLine.split("\u2192");
12435
+ const description = parts[1]?.trim().split("\u2192")[0]?.trim();
12436
+ return description ? `${type} \u2014 ${description}` : type;
12437
+ }
11927
12438
 
11928
12439
  // src/shared/utils/context-digest/formatters.ts
11929
12440
  function formatAnalystDigest(digest, filePath, originalChars) {
@@ -12152,6 +12663,13 @@ function recordJournalMemo(call, result, digestedOutputForLLM, digestResult, tur
12152
12663
  }
12153
12664
  turnState.memo.nextSteps.push(...m.nextSteps);
12154
12665
  if (m.reflection) turnState.reflections.push(m.reflection);
12666
+ if (!result.success && m.failures.length > 0) {
12667
+ const command = String(call.input.command || call.input.url || call.input.query || JSON.stringify(call.input));
12668
+ const hypothesizedReason = extractHypothesizedReason(m.failures[0]);
12669
+ if (hypothesizedReason) {
12670
+ state.workingMemory.updateLastFailureReason(command, hypothesizedReason);
12671
+ }
12672
+ }
12155
12673
  }
12156
12674
  if (digestResult?.memo?.credentials.length) {
12157
12675
  for (const cred of digestResult.memo.credentials) {
@@ -12183,7 +12701,9 @@ function recordJournalMemo(call, result, digestedOutputForLLM, digestResult, tur
12183
12701
  description,
12184
12702
  evidence,
12185
12703
  remediation: "",
12186
- foundAt: Date.now()
12704
+ foundAt: Date.now(),
12705
+ // SAUP §18: Analyst 해석 결과이므로 inferenceDepth=1 (직접 도구 관찰이 아님)
12706
+ inferenceDepth: 1
12187
12707
  });
12188
12708
  state.attackGraph.addVulnerability(title, "auto-detected", digestResult.memo.attackValue === "HIGH" ? "high" : "medium", confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED);
12189
12709
  existingSignatures.add(signature);
@@ -12551,32 +13071,36 @@ var CORE_KNOWLEDGE_FILES = [
12551
13071
  // Active Directory / infrastructure attack methodology
12552
13072
  ];
12553
13073
  var PHASE_TECHNIQUE_MAP = {
12554
- [PHASES.RECON]: [TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.CRYPTO],
12555
- [PHASES.VULN_ANALYSIS]: [TECHNIQUE_FILES.INJECTION, TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.FILE_ATTACKS, TECHNIQUE_FILES.CRYPTO, TECHNIQUE_FILES.REVERSING],
12556
- [PHASES.EXPLOIT]: [TECHNIQUE_FILES.INJECTION, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.FILE_ATTACKS, TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.PWN, TECHNIQUE_FILES.CONTAINER_ESCAPE, TECHNIQUE_FILES.SANDBOX_ESCAPE, TECHNIQUE_FILES.REVERSING],
12557
- [PHASES.POST_EXPLOIT]: [TECHNIQUE_FILES.PRIVESC, TECHNIQUE_FILES.LATERAL, TECHNIQUE_FILES.AUTH_ACCESS, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.CONTAINER_ESCAPE, TECHNIQUE_FILES.SANDBOX_ESCAPE, TECHNIQUE_FILES.FORENSICS],
13074
+ // RECON: 서비스 파악 + 기본 크레덴셜/익명 접근 초기 탐색
13075
+ [PHASES.RECON]: [TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.CRYPTO, TECHNIQUE_FILES.AUTH_ACCESS],
13076
+ // VULN_ANALYSIS: 인증 우회가 가장 흔한 취약점 클래스 auth-access 필수
13077
+ [PHASES.VULN_ANALYSIS]: [TECHNIQUE_FILES.INJECTION, TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.AUTH_ACCESS, TECHNIQUE_FILES.FILE_ATTACKS, TECHNIQUE_FILES.CRYPTO, TECHNIQUE_FILES.REVERSING],
13078
+ // EXPLOIT: JWT/OAuth/세션 우회 + 바이너리 + 인젝션 전체
13079
+ [PHASES.EXPLOIT]: [TECHNIQUE_FILES.INJECTION, TECHNIQUE_FILES.AUTH_ACCESS, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.FILE_ATTACKS, TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.PWN, TECHNIQUE_FILES.CONTAINER_ESCAPE, TECHNIQUE_FILES.SANDBOX_ESCAPE, TECHNIQUE_FILES.REVERSING],
13080
+ [PHASES.POST_EXPLOIT]: [TECHNIQUE_FILES.PRIVESC, TECHNIQUE_FILES.LATERAL, TECHNIQUE_FILES.AUTH_ACCESS, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.CONTAINER_ESCAPE, TECHNIQUE_FILES.SANDBOX_ESCAPE, TECHNIQUE_FILES.FORENSICS, TECHNIQUE_FILES.PIVOTING, TECHNIQUE_FILES.ENTERPRISE_PENTEST],
12558
13081
  [PHASES.PRIV_ESC]: [TECHNIQUE_FILES.PRIVESC, TECHNIQUE_FILES.AUTH_ACCESS, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.PWN, TECHNIQUE_FILES.CONTAINER_ESCAPE, TECHNIQUE_FILES.SANDBOX_ESCAPE],
12559
- [PHASES.LATERAL]: [TECHNIQUE_FILES.LATERAL, TECHNIQUE_FILES.AD_ATTACK, TECHNIQUE_FILES.AUTH_ACCESS, TECHNIQUE_FILES.CONTAINER_ESCAPE, TECHNIQUE_FILES.NETWORK_SVC],
12560
- [PHASES.PERSISTENCE]: [TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.PRIVESC, TECHNIQUE_FILES.LATERAL],
13082
+ [PHASES.LATERAL]: [TECHNIQUE_FILES.LATERAL, TECHNIQUE_FILES.AD_ATTACK, TECHNIQUE_FILES.AUTH_ACCESS, TECHNIQUE_FILES.CONTAINER_ESCAPE, TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.PIVOTING, TECHNIQUE_FILES.ENTERPRISE_PENTEST],
13083
+ [PHASES.PERSISTENCE]: [TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.PRIVESC, TECHNIQUE_FILES.LATERAL, TECHNIQUE_FILES.ENTERPRISE_PENTEST],
12561
13084
  [PHASES.EXFIL]: [TECHNIQUE_FILES.LATERAL, TECHNIQUE_FILES.NETWORK_SVC, TECHNIQUE_FILES.FORENSICS],
12562
- [PHASES.WEB]: [TECHNIQUE_FILES.INJECTION, TECHNIQUE_FILES.FILE_ATTACKS, TECHNIQUE_FILES.AUTH_ACCESS, TECHNIQUE_FILES.CRYPTO, TECHNIQUE_FILES.SHELLS],
13085
+ // WEB: 인젝션 + 인증 + 파일 + 쉘 + 네트워크 서비스 (내부 Redis/MySQL 연결 가능)
13086
+ [PHASES.WEB]: [TECHNIQUE_FILES.INJECTION, TECHNIQUE_FILES.FILE_ATTACKS, TECHNIQUE_FILES.AUTH_ACCESS, TECHNIQUE_FILES.CRYPTO, TECHNIQUE_FILES.SHELLS, TECHNIQUE_FILES.NETWORK_SVC],
12563
13087
  [PHASES.REPORT]: []
12564
13088
  // Report phase: no attack techniques needed
12565
13089
  };
12566
13090
 
12567
13091
  // src/agents/prompt-builder/prompt-loader.ts
12568
13092
  import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
12569
- import { join as join11, dirname as dirname4 } from "path";
13093
+ import { join as join12, dirname as dirname4 } from "path";
12570
13094
  import { fileURLToPath as fileURLToPath2 } from "url";
12571
13095
  var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
12572
- var PROMPTS_DIR = join11(__dirname2, "../prompts");
12573
- var TECHNIQUES_DIR = join11(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
13096
+ var PROMPTS_DIR = join12(__dirname2, "../prompts");
13097
+ var TECHNIQUES_DIR = join12(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
12574
13098
  function loadPromptFile(filename) {
12575
- const path2 = join11(PROMPTS_DIR, filename);
13099
+ const path2 = join12(PROMPTS_DIR, filename);
12576
13100
  return existsSync11(path2) ? readFileSync7(path2, PROMPT_CONFIG.ENCODING) : "";
12577
13101
  }
12578
13102
  function loadTechniqueFile(techniqueName) {
12579
- const filePath = join11(TECHNIQUES_DIR, `${techniqueName}.md`);
13103
+ const filePath = join12(TECHNIQUES_DIR, `${techniqueName}.md`);
12580
13104
  try {
12581
13105
  if (!existsSync11(filePath)) return "";
12582
13106
  return readFileSync7(filePath, PROMPT_CONFIG.ENCODING);
@@ -12680,7 +13204,19 @@ function buildPersistentMemoryFragment(state) {
12680
13204
  if (port.service) services.push(port.service);
12681
13205
  }
12682
13206
  }
12683
- return state.persistentMemory.toPrompt(services);
13207
+ const result = state.persistentMemory.toPrompt(services);
13208
+ flowLog("\u2461Main", "\u2190", "PersistentMemory", result ? `${result.length}B` : "EMPTY");
13209
+ return result;
13210
+ }
13211
+ function buildEpistemicCheckFragment(state) {
13212
+ if (!state.lastReflection) {
13213
+ flowLog("\u2461Main", "\u2190", "lastReflection (epistemic-check)", "EMPTY \u2014 \xA718 layer skipped");
13214
+ return "";
13215
+ }
13216
+ flowLog("\u2461Main", "\u2190", "lastReflection (epistemic-check)", `${state.lastReflection.length}B \u2705`);
13217
+ return `<epistemic-check>
13218
+ ${state.lastReflection}
13219
+ </epistemic-check>`;
12684
13220
  }
12685
13221
 
12686
13222
  // src/agents/prompt-builder/fragments/intelligence.ts
@@ -12698,31 +13234,26 @@ function buildAttackIntelligenceFragment(state) {
12698
13234
  const lines = [];
12699
13235
  for (const target of targets) {
12700
13236
  for (const port of target.ports) {
12701
- const attacks = getAttacksForService(port.service || "", port.port);
12702
- if (attacks.length === 0) continue;
12703
- const priority = calculateAttackPriority({
12704
- service: port.service || "",
12705
- version: port.version,
12706
- port: port.port
12707
- });
12708
- lines.push(`[${target.ip}:${port.port}] ${port.service || "unknown"} (priority: ${priority}/100)`);
12709
- attacks.forEach((a) => lines.push(` - ${a}`));
13237
+ const ctx = getServiceContext(port.service || "", port.port);
13238
+ const factStr = ctx.facts.length > 0 ? ` [${ctx.facts.join("; ")}]` : "";
13239
+ const versionStr = port.version ? ` v${port.version}` : "";
13240
+ lines.push(`${target.ip}:${port.port} ${port.service || "unknown"}${versionStr} category=${ctx.category}${factStr}`);
12710
13241
  }
12711
13242
  }
12712
13243
  if (lines.length === 0) return "";
12713
- return `<attack-intelligence>
12714
- Bootstrap attack suggestions (try these first, adapt if they fail):
13244
+ return `<service-inventory>
13245
+ Discovered services \u2014 use get_owasp_knowledge, web_search, or get_web_attack_surface for attack ideas:
12715
13246
  ${lines.join("\n")}
12716
- </attack-intelligence>`;
13247
+ </service-inventory>`;
12717
13248
  }
12718
13249
 
12719
13250
  // src/shared/utils/journal/reader.ts
12720
13251
  import { readFileSync as readFileSync8, existsSync as existsSync13 } from "fs";
12721
- import { join as join13 } from "path";
13252
+ import { join as join14 } from "path";
12722
13253
 
12723
13254
  // src/shared/utils/journal/rotation.ts
12724
13255
  import { existsSync as existsSync12, readdirSync as readdirSync3, statSync as statSync3, rmSync as rmSync2 } from "fs";
12725
- import { join as join12 } from "path";
13256
+ import { join as join13 } from "path";
12726
13257
  function parseTurnNumbers(turnsDir) {
12727
13258
  if (!existsSync12(turnsDir)) return [];
12728
13259
  return readdirSync3(turnsDir).filter((e) => e.startsWith(TURN_FOLDER_PREFIX) && /^\d+$/.test(e.slice(TURN_FOLDER_PREFIX.length))).map((e) => Number(e.slice(TURN_FOLDER_PREFIX.length)));
@@ -12731,12 +13262,12 @@ function rotateTurnRecords() {
12731
13262
  try {
12732
13263
  const turnsDir = WORKSPACE.TURNS;
12733
13264
  if (!existsSync12(turnsDir)) return;
12734
- const turnDirs = parseTurnNumbers(turnsDir).map((n) => `${TURN_FOLDER_PREFIX}${n}`).filter((e) => statSync3(join12(turnsDir, e)).isDirectory()).sort((a, b) => Number(a.slice(TURN_FOLDER_PREFIX.length)) - Number(b.slice(TURN_FOLDER_PREFIX.length)));
13265
+ const turnDirs = parseTurnNumbers(turnsDir).map((n) => `${TURN_FOLDER_PREFIX}${n}`).filter((e) => statSync3(join13(turnsDir, e)).isDirectory()).sort((a, b) => Number(a.slice(TURN_FOLDER_PREFIX.length)) - Number(b.slice(TURN_FOLDER_PREFIX.length)));
12735
13266
  if (turnDirs.length > MEMORY_LIMITS.MAX_TURN_ENTRIES) {
12736
13267
  const dirsToDel = turnDirs.slice(0, turnDirs.length - MEMORY_LIMITS.MAX_TURN_ENTRIES);
12737
13268
  for (const dir of dirsToDel) {
12738
13269
  try {
12739
- rmSync2(join12(turnsDir, dir), { recursive: true, force: true });
13270
+ rmSync2(join13(turnsDir, dir), { recursive: true, force: true });
12740
13271
  } catch {
12741
13272
  }
12742
13273
  }
@@ -12755,7 +13286,7 @@ function readJournalSummary() {
12755
13286
  const turnsDir = WORKSPACE.TURNS;
12756
13287
  const turnDirs = parseTurnNumbers(turnsDir).sort((a, b) => b - a);
12757
13288
  for (const turn of turnDirs) {
12758
- const summaryPath = join13(WORKSPACE.turnPath(turn), TURN_FILES.SUMMARY);
13289
+ const summaryPath = join14(WORKSPACE.turnPath(turn), TURN_FILES.SUMMARY);
12759
13290
  if (existsSync13(summaryPath)) {
12760
13291
  return readFileSync8(summaryPath, "utf-8");
12761
13292
  }
@@ -12772,7 +13303,7 @@ function getRecentEntries(count = MEMORY_LIMITS.MAX_TURN_ENTRIES) {
12772
13303
  const entries = [];
12773
13304
  for (const turn of turnDirs) {
12774
13305
  try {
12775
- const filePath = join13(WORKSPACE.turnPath(turn), TURN_FILES.STRUCTURED);
13306
+ const filePath = join14(WORKSPACE.turnPath(turn), TURN_FILES.STRUCTURED);
12776
13307
  if (existsSync13(filePath)) {
12777
13308
  const raw = readFileSync8(filePath, "utf-8");
12778
13309
  entries.push(JSON.parse(raw));
@@ -12798,7 +13329,7 @@ function getNextTurnNumber() {
12798
13329
 
12799
13330
  // src/shared/utils/journal/summary.ts
12800
13331
  import { writeFileSync as writeFileSync9 } from "fs";
12801
- import { join as join14 } from "path";
13332
+ import { join as join15 } from "path";
12802
13333
 
12803
13334
  // src/shared/utils/journal/summary-collector.ts
12804
13335
  function collectSummaryBuckets(entries) {
@@ -12903,7 +13434,7 @@ function regenerateJournalSummary() {
12903
13434
  const turnDir = WORKSPACE.turnPath(latestTurn);
12904
13435
  ensureDirExists(turnDir);
12905
13436
  const summary = buildSummaryFromEntries(entries);
12906
- const summaryPath = join14(turnDir, TURN_FILES.SUMMARY);
13437
+ const summaryPath = join15(turnDir, TURN_FILES.SUMMARY);
12907
13438
  writeFileSync9(summaryPath, summary, "utf-8");
12908
13439
  debugLog("general", "Journal summary regenerated", {
12909
13440
  entries: entries.length,
@@ -13042,7 +13573,7 @@ var PromptBuilder = class {
13042
13573
  * 4. Phase-relevant techniques — only attack techniques for current phase (~4-8K tok)
13043
13574
  * 5. Scope constraints
13044
13575
  * 6. Current state (targets, findings, loot, active processes)
13045
- * 7. TODO list
13576
+ * 7. Todo list
13046
13577
  * 8. Time awareness + adaptive strategy (#8)
13047
13578
  * 9. Challenge analysis (#2: Auto-Prompter)
13048
13579
  * 10. Attack graph (#6: recommended attack chains)
@@ -13070,6 +13601,8 @@ var PromptBuilder = class {
13070
13601
  buildAttackGraphFragment(this.state),
13071
13602
  buildAttackIntelligenceFragment(this.state),
13072
13603
  buildWorkingMemoryFragment(this.state),
13604
+ // §18: Reflector 자기인식 결과 주입 (turn N-1 → turn N)
13605
+ buildEpistemicCheckFragment(this.state),
13073
13606
  buildEpisodicMemoryFragment(this.state),
13074
13607
  buildDynamicTechniquesFragment(this.state),
13075
13608
  buildPersistentMemoryFragment(this.state),
@@ -13216,12 +13749,21 @@ var REFLECTOR_SYSTEM_PROMPT = `You are a tactical reviewer for a penetration tes
13216
13749
  Review ALL actions from this turn \u2014 successes AND failures.
13217
13750
 
13218
13751
  1. ASSESSMENT: What did this turn accomplish? Rate: HIGH / MED / LOW / NONE.
13219
- 2. SUCCESSES: What worked? Can this pattern be replicated elsewhere?
13752
+ 2. SUCCESSES: What worked? Can this pattern be replicated on other services/endpoints?
13220
13753
  3. FAILURES: What failed? Is this a repeated pattern? If so \u2192 STOP this approach.
13221
- 4. BLIND SPOTS: What was missed or overlooked?
13222
- 5. NEXT PRIORITY: Single most valuable next action.
13754
+ 4. BLIND SPOTS \u2014 answer each explicitly:
13755
+ a) What services/ports/endpoints have been DISCOVERED but NOT YET attacked?
13756
+ b) What credentials or tokens have been found but NOT yet sprayed on other services?
13757
+ c) Is there a SIMPLER explanation for the current behavior? (misconfiguration vs. complex vuln)
13758
+ d) Are we drilling too deep on one surface while ignoring others?
13759
+ e) Could a short custom script (Python/bash) accomplish what tool attempts have failed to do?
13760
+ f) Is there any finding from a PREVIOUS turn that was noted but never followed up on?
13761
+ g) What would an experienced human tester try RIGHT NOW that hasn't been attempted?
13762
+ 5. NEXT PRIORITY: Single most valuable next action with concrete reasoning.
13763
+ 6. EPISTEMIC CHECK (\xA718): Are any current findings marked \u26A0\uFE0F unverified-chain or depth\u22652?
13764
+ If yes \u2192 recommend direct verification (whoami, curl, targeted payload) BEFORE chaining further attacks on those findings.
13223
13765
 
13224
- 3-5 lines. Every word must be actionable.`;
13766
+ `;
13225
13767
  var Reflector = class extends AuxiliaryLLMBase {
13226
13768
  constructor(llm) {
13227
13769
  super(llm, {
@@ -13242,7 +13784,7 @@ var Reflector = class extends AuxiliaryLLMBase {
13242
13784
  success: true
13243
13785
  };
13244
13786
  }
13245
- createFailureOutput(reason) {
13787
+ createFailureOutput(_reason) {
13246
13788
  return {
13247
13789
  reflection: "",
13248
13790
  success: false
@@ -13277,13 +13819,13 @@ var SummaryRegenerator = class extends AuxiliaryLLMBase {
13277
13819
  }
13278
13820
  formatInput(input) {
13279
13821
  if (input.existingSummary) {
13280
- return `\uAE30\uC874 \uC694\uC57D:
13822
+ return `Previous summary:
13281
13823
  ${input.existingSummary}
13282
13824
 
13283
- \uC774\uBC88 \uD134:
13825
+ Current turn:
13284
13826
  ${input.turnData}`;
13285
13827
  } else {
13286
- return `\uCCAB \uD134 \uB370\uC774\uD130:
13828
+ return `First turn data:
13287
13829
  ${input.turnData}`;
13288
13830
  }
13289
13831
  }
@@ -13304,6 +13846,56 @@ function createSummaryRegenerator(llm) {
13304
13846
  return new SummaryRegenerator(llm);
13305
13847
  }
13306
13848
 
13849
+ // src/engine/auxiliary-llm/playbook-synthesizer.ts
13850
+ var PLAYBOOK_SYNTHESIZER_SYSTEM_PROMPT = `You are a penetration testing knowledge distiller.
13851
+ Given the steps of a successful attack chain, write ONE concise sentence (\u2264120 characters) capturing the REUSABLE PATTERN.
13852
+
13853
+ Rules:
13854
+ - Abstract away specific IPs, ports, file paths \u2014 keep service names and techniques
13855
+ - Use \u2192 to separate attack steps (e.g. "LFI \u2192 log poisoning \u2192 RCE via PHP session file")
13856
+ - Focus on WHAT worked, not WHO or WHEN
13857
+ - If the chain is trivial (e.g. single nmap scan), respond with: SKIP
13858
+ - No preamble, no explanation \u2014 just the one-line pattern or SKIP`;
13859
+ var PlaybookSynthesizer = class extends AuxiliaryLLMBase {
13860
+ constructor(llm) {
13861
+ super(llm, {
13862
+ systemPrompt: PLAYBOOK_SYNTHESIZER_SYSTEM_PROMPT,
13863
+ logErrors: true
13864
+ });
13865
+ }
13866
+ formatInput(input) {
13867
+ const nodeLines = input.succeededNodes.map((n) => ` [${n.type}] ${n.label}${n.technique ? ` (technique: ${n.technique})` : ""}`).join("\n");
13868
+ const edgeActions = input.succeededEdges.map((e) => e.action).filter(Boolean).slice(0, 8);
13869
+ const lines = [
13870
+ `SERVICE PROFILE: ${input.serviceProfile || "unknown"}`,
13871
+ "",
13872
+ "SUCCEEDED ATTACK NODES:",
13873
+ nodeLines || " (none)"
13874
+ ];
13875
+ if (edgeActions.length > 0) {
13876
+ lines.push("", "ATTACK ACTIONS (edges):", ...edgeActions.map((a) => ` \u2192 ${a}`));
13877
+ }
13878
+ if (input.existingChainSummary) {
13879
+ lines.push("", `EXISTING SUMMARY (regex-based, for reference): ${input.existingChainSummary}`);
13880
+ }
13881
+ return lines.join("\n");
13882
+ }
13883
+ parseOutput(response) {
13884
+ const trimmed = response.trim();
13885
+ if (trimmed.toUpperCase() === "SKIP" || trimmed.length === 0) {
13886
+ return { playbookSummary: "", success: false };
13887
+ }
13888
+ const summary = trimmed.length > 120 ? trimmed.slice(0, 117) + "..." : trimmed;
13889
+ return { playbookSummary: summary, success: true };
13890
+ }
13891
+ createFailureOutput(_reason) {
13892
+ return { playbookSummary: "", success: false };
13893
+ }
13894
+ };
13895
+ function createPlaybookSynthesizer(llm) {
13896
+ return new PlaybookSynthesizer(llm);
13897
+ }
13898
+
13307
13899
  // src/agents/strategist/constants.ts
13308
13900
  var CACHE_TTL_MS = 3 * 60 * 1e3;
13309
13901
  var STALL_TURNS_THRESHOLD = 2;
@@ -13370,6 +13962,9 @@ function buildInput(state) {
13370
13962
  sections.push("");
13371
13963
  sections.push("## Failed Attempts (DO NOT REPEAT THESE)");
13372
13964
  sections.push(failures);
13965
+ flowLog("\u2460Strategist", "\u2190", "WorkingMemory", `${failures.length}B`);
13966
+ } else {
13967
+ flowLog("\u2460Strategist", "\u2190", "WorkingMemory", "EMPTY");
13373
13968
  }
13374
13969
  try {
13375
13970
  const journalSummary = readJournalSummary();
@@ -13377,6 +13972,9 @@ function buildInput(state) {
13377
13972
  sections.push("");
13378
13973
  sections.push("## Session Journal (past turns summary)");
13379
13974
  sections.push(journalSummary);
13975
+ flowLog("\u2460Strategist", "\u2190", "summary.md", `${journalSummary.length}B \u2705`);
13976
+ } else {
13977
+ flowLog("\u2460Strategist", "\u2190", "summary.md", "EMPTY \u26A0\uFE0F");
13380
13978
  }
13381
13979
  } catch {
13382
13980
  }
@@ -13385,6 +13983,7 @@ function buildInput(state) {
13385
13983
  sections.push("");
13386
13984
  sections.push("## Attack Graph");
13387
13985
  sections.push(graph);
13986
+ flowLog("\u2460Strategist", "\u2190", "AttackGraph", `${graph.length}B`);
13388
13987
  }
13389
13988
  const timeline = state.episodicMemory.toPrompt();
13390
13989
  if (timeline) {
@@ -13404,8 +14003,9 @@ function buildInput(state) {
13404
14003
  const analysis = state.getChallengeAnalysis();
13405
14004
  if (analysis && analysis.primaryType !== "unknown") {
13406
14005
  sections.push("");
13407
- sections.push(`## Challenge Type: ${analysis.primaryType.toUpperCase()} (${(analysis.confidence * 100).toFixed(0)}%)`);
13408
- sections.push(analysis.strategySuggestion);
14006
+ sections.push(`## Challenge Type`);
14007
+ sections.push(`${analysis.primaryType.toUpperCase()} (confidence: ${(analysis.confidence * 100).toFixed(0)}%)`);
14008
+ sections.push(`Use get_owasp_knowledge("${analysis.primaryType}") to determine your attack approach.`);
13409
14009
  }
13410
14010
  return sections.join("\n");
13411
14011
  }
@@ -13428,10 +14028,10 @@ function formatForPrompt(directive, isStale = false) {
13428
14028
 
13429
14029
  // src/agents/strategist/prompt-loader.ts
13430
14030
  import { readFileSync as readFileSync9, existsSync as existsSync14 } from "fs";
13431
- import { join as join15, dirname as dirname5 } from "path";
14031
+ import { join as join16, dirname as dirname5 } from "path";
13432
14032
  import { fileURLToPath as fileURLToPath3 } from "url";
13433
14033
  var __dirname3 = dirname5(fileURLToPath3(import.meta.url));
13434
- var STRATEGIST_PROMPT_PATH = join15(__dirname3, "../prompts", "strategist-system.md");
14034
+ var STRATEGIST_PROMPT_PATH = join16(__dirname3, "../prompts", "strategist-system.md");
13435
14035
  function loadSystemPrompt() {
13436
14036
  try {
13437
14037
  if (existsSync14(STRATEGIST_PROMPT_PATH)) {
@@ -13832,7 +14432,7 @@ async function processReflection(toolJournal, memo14, phase, reflector) {
13832
14432
 
13833
14433
  // src/agents/main-agent/turn-recorder.ts
13834
14434
  import { writeFileSync as writeFileSync10, existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
13835
- import { join as join16 } from "path";
14435
+ import { join as join17 } from "path";
13836
14436
  async function recordTurn(context) {
13837
14437
  const { turnCounter, phase, toolJournal, memo: memo14, reflections, summaryRegenerator } = context;
13838
14438
  if (toolJournal.length === 0) {
@@ -13847,14 +14447,16 @@ async function recordTurn(context) {
13847
14447
  memo: memo14,
13848
14448
  reflection: reflections.length > 0 ? reflections.join(" | ") : memo14.nextSteps.join("; ")
13849
14449
  };
13850
- await createTurnArchive(turnCounter, entry, toolJournal, memo14, phase, reflections);
13851
- await regenerateSummary(turnCounter, summaryRegenerator, entry, toolJournal, memo14, phase, reflections);
14450
+ const entryCtx = { entry, journalTools: toolJournal, memo: memo14, phase, reflections };
14451
+ await createTurnArchive(turnCounter, entryCtx);
14452
+ await regenerateSummary(turnCounter, summaryRegenerator, entryCtx);
13852
14453
  rotateTurnRecords();
13853
14454
  } catch {
13854
14455
  }
13855
14456
  return turnCounter + 1;
13856
14457
  }
13857
- async function createTurnArchive(turnCounter, entry, journalTools, memo14, phase, reflections) {
14458
+ async function createTurnArchive(turnCounter, ctx) {
14459
+ const { entry, journalTools, memo: memo14, phase } = ctx;
13858
14460
  try {
13859
14461
  const turnDir = WORKSPACE.turnPath(turnCounter);
13860
14462
  const toolsDir = WORKSPACE.turnToolsPath(turnCounter);
@@ -13868,8 +14470,8 @@ async function createTurnArchive(turnCounter, entry, journalTools, memo14, phase
13868
14470
  memo: memo14,
13869
14471
  reflection: entry.reflection
13870
14472
  });
13871
- writeFileSync10(join16(turnDir, TURN_FILES.RECORD), turnContent, "utf-8");
13872
- writeFileSync10(join16(turnDir, TURN_FILES.STRUCTURED), JSON.stringify(entry, null, 2), "utf-8");
14473
+ writeFileSync10(join17(turnDir, TURN_FILES.RECORD), turnContent, "utf-8");
14474
+ writeFileSync10(join17(turnDir, TURN_FILES.STRUCTURED), JSON.stringify(entry, null, 2), "utf-8");
13873
14475
  writeAnalystFile(turnDir, memo14);
13874
14476
  } catch {
13875
14477
  }
@@ -13883,17 +14485,24 @@ function writeAnalystFile(turnDir, memo14) {
13883
14485
  if (memo14.suspicions.length > 0) memoLines.push("## Suspicious", ...memo14.suspicions.map((s) => `- ${s}`), "");
13884
14486
  if (memo14.nextSteps.length > 0) memoLines.push("## Next Steps", ...memo14.nextSteps.map((n) => `- ${n}`), "");
13885
14487
  if (memoLines.length > 0) {
13886
- writeFileSync10(join16(turnDir, TURN_FILES.ANALYST), memoLines.join("\n"), "utf-8");
14488
+ writeFileSync10(join17(turnDir, TURN_FILES.ANALYST), memoLines.join("\n"), "utf-8");
14489
+ flowLog(
14490
+ "\u2462Analyst",
14491
+ "\u2192",
14492
+ `archive/turn-${turnDir.split("/").pop()}/analyst.md`,
14493
+ `findings:${memo14.keyFindings.length} creds:${memo14.credentials.length} vectors:${memo14.attackVectors.length} failures:${memo14.failures.length}`
14494
+ );
13887
14495
  }
13888
14496
  }
13889
- async function regenerateSummary(turnCounter, summaryRegenerator, entry, journalTools, memo14, phase, reflections) {
14497
+ async function regenerateSummary(turnCounter, summaryRegenerator, ctx) {
14498
+ const { entry, journalTools, memo: memo14, phase } = ctx;
13890
14499
  try {
13891
14500
  const turnDir = WORKSPACE.turnPath(turnCounter);
13892
- const summaryPath = join16(turnDir, TURN_FILES.SUMMARY);
14501
+ const summaryPath = join17(turnDir, TURN_FILES.SUMMARY);
13893
14502
  const prevTurn = turnCounter - 1;
13894
14503
  let existingSummary = null;
13895
14504
  if (prevTurn >= 1) {
13896
- const prevSummaryPath = join16(WORKSPACE.turnPath(prevTurn), TURN_FILES.SUMMARY);
14505
+ const prevSummaryPath = join17(WORKSPACE.turnPath(prevTurn), TURN_FILES.SUMMARY);
13897
14506
  if (existsSync15(prevSummaryPath)) {
13898
14507
  existingSummary = readFileSync10(prevSummaryPath, "utf-8");
13899
14508
  }
@@ -13913,6 +14522,9 @@ async function regenerateSummary(turnCounter, summaryRegenerator, entry, journal
13913
14522
  if (summaryResult.success && summaryResult.summary) {
13914
14523
  ensureDirExists(turnDir);
13915
14524
  writeFileSync10(summaryPath, summaryResult.summary, "utf-8");
14525
+ flowLog("\u2465SummaryRegen", "\u2192", "summary.md", `${summaryResult.summary.length}B \u2192 turn:${turnCounter}`);
14526
+ } else {
14527
+ flowLog("\u2465SummaryRegen", "\u2192", "summary.md", "SKIPPED (empty result)");
13916
14528
  }
13917
14529
  } catch {
13918
14530
  regenerateJournalSummary();
@@ -13942,6 +14554,38 @@ var session = {
13942
14554
  load: (state) => loadState(state)
13943
14555
  };
13944
14556
 
14557
+ // src/agents/main-agent/playbook-handler.ts
14558
+ async function synthesizeAndSavePlaybook(state, synthesizer) {
14559
+ try {
14560
+ const nodes = state.attackGraph.getAllNodes();
14561
+ const edges = state.attackGraph.getAllEdges();
14562
+ const succeededNodes = nodes.filter((n) => n.status === NODE_STATUS.SUCCEEDED).map((n) => ({ label: n.label, type: n.type, technique: n.technique }));
14563
+ const succeededEdges = edges.filter((e) => e.status === EDGE_STATUS.SUCCEEDED).map((e) => ({ action: e.action }));
14564
+ if (succeededNodes.length < 2) return;
14565
+ const serviceProfile = nodes.filter((n) => n.type === NODE_TYPE.SERVICE && n.status === NODE_STATUS.SUCCEEDED).map((n) => n.label.replace(/^service:/i, "").replace(/\b\d{1,3}(\.\d{1,3}){3}(:\d+)?\b/g, "").trim()).filter(Boolean).join(" + ") || "unknown";
14566
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
14567
+ const existingChain = extractSuccessfulChain(nodeMap, edges);
14568
+ const existingChainSummary = existingChain?.chainSummary ?? "";
14569
+ const result = await synthesizer.execute({
14570
+ succeededNodes,
14571
+ succeededEdges,
14572
+ serviceProfile,
14573
+ existingChainSummary
14574
+ });
14575
+ if (result.success && result.playbookSummary) {
14576
+ const toolsUsed = [...new Set(
14577
+ succeededNodes.map((n) => n.technique?.split(" ")[0]).filter(Boolean)
14578
+ )];
14579
+ state.persistentMemory.recordExploitChain(
14580
+ serviceProfile,
14581
+ result.playbookSummary,
14582
+ toolsUsed
14583
+ );
14584
+ }
14585
+ } catch {
14586
+ }
14587
+ }
14588
+
13945
14589
  // src/agents/main-agent/core.ts
13946
14590
  var MainAgent = class extends CoreAgent {
13947
14591
  promptBuilder;
@@ -13954,6 +14598,7 @@ var MainAgent = class extends CoreAgent {
13954
14598
  contextExtractor;
13955
14599
  reflector;
13956
14600
  summaryRegenerator;
14601
+ playbookSynthesizer;
13957
14602
  constructor(state, events, toolRegistry, approvalGate, scopeGuard) {
13958
14603
  super(AGENT_ROLES.ORCHESTRATOR, state, events, toolRegistry);
13959
14604
  this.approvalGate = approvalGate;
@@ -13964,6 +14609,7 @@ var MainAgent = class extends CoreAgent {
13964
14609
  this.contextExtractor = createContextExtractor(this.llm);
13965
14610
  this.reflector = createReflector(this.llm);
13966
14611
  this.summaryRegenerator = createSummaryRegenerator(this.llm);
14612
+ this.playbookSynthesizer = createPlaybookSynthesizer(this.llm);
13967
14613
  }
13968
14614
  async execute(userInput) {
13969
14615
  this.userInput = userInput;
@@ -13992,6 +14638,7 @@ var MainAgent = class extends CoreAgent {
13992
14638
  messages.push({ role: "user", content: userMessage });
13993
14639
  }
13994
14640
  }
14641
+ const flagsBefore = this.state.getFlags().length;
13995
14642
  const dynamicPrompt = await this.getCurrentPrompt();
13996
14643
  const result = await super.step(iteration, messages, dynamicPrompt, progress);
13997
14644
  const turnToolJournal = this.getTurnToolJournal();
@@ -13999,7 +14646,19 @@ var MainAgent = class extends CoreAgent {
13999
14646
  const turnReflections = [];
14000
14647
  await processContextExtraction(messages, this.contextExtractor);
14001
14648
  const reflection = await processReflection(turnToolJournal, turnMemo, this.state.getPhase(), this.reflector);
14002
- if (reflection) turnReflections.push(reflection);
14649
+ if (reflection) {
14650
+ turnReflections.push(reflection);
14651
+ this.state.lastReflection = reflection;
14652
+ flowLog("\u2464Reflector", "\u2192", "lastReflection (RAM)", `${reflection.length}B \u2192 next-turn \u2461Main`);
14653
+ } else if (turnToolJournal.length > 0) {
14654
+ this.state.lastReflection = null;
14655
+ flowLog("\u2464Reflector", "\u2192", "lastReflection (RAM)", "CLEARED (no reflection)");
14656
+ }
14657
+ const flagsAfter = this.state.getFlags().length;
14658
+ if (flagsAfter > flagsBefore) {
14659
+ flowLog("\u2466PlaybookSynth", "\u2192", "PersistentMemory", `flag captured (${flagsAfter - flagsBefore} new)`);
14660
+ await synthesizeAndSavePlaybook(this.state, this.playbookSynthesizer);
14661
+ }
14003
14662
  this.turnCounter = await recordTurn({
14004
14663
  turnCounter: this.turnCounter,
14005
14664
  phase: this.state.getPhase(),
@@ -14979,48 +15638,30 @@ var formatServiceLabel = (target, port) => {
14979
15638
  var formatFindingsWithFlags = (findings, flags) => {
14980
15639
  const findingsOutput = formatFindings(findings);
14981
15640
  const flagsOutput = formatFlags(flags);
14982
- if (flagsOutput) {
14983
- return `${findingsOutput}
15641
+ if (flagsOutput) return `${findingsOutput}
14984
15642
  ${flagsOutput}`;
14985
- }
14986
15643
  return findingsOutput;
14987
15644
  };
14988
- var formatGraphWithSummary = (state, findings, flags) => {
15645
+ function formatHostSection(targets, hostNodes, hostCount, w) {
14989
15646
  const lines = [];
14990
- const w = getBoxWidth();
14991
- const innerDash = w - 2;
14992
- const targets = state.getAllTargets();
14993
- const graphStats = state.attackGraph.getStats();
14994
- const allNodes = state.attackGraph.getAllNodes();
14995
- const hostNodes = allNodes.filter((n) => n.type === NODE_TYPE.HOST);
14996
- const serviceNodes = allNodes.filter((n) => n.type === NODE_TYPE.SERVICE);
14997
- const vulnNodes = allNodes.filter((n) => n.type === NODE_TYPE.VULNERABILITY);
14998
- const hostCount = hostNodes.length || targets.length;
14999
- const serviceCount = serviceNodes.length || targets.reduce((sum, t) => sum + t.ports.length, 0);
15000
- const vulnCount = findings.length;
15001
- const topTitle = "\u2500\u2500\u2500 Attack Graph ";
15002
- lines.push(`\u250C${topTitle}${"\u2500".repeat(Math.max(0, innerDash - topTitle.length))}\u2510`);
15003
- lines.push(boxLine(`\u{1F5A5} ${hostCount} \u26A0 ${vulnCount} \u2699 ${serviceCount}`, w));
15004
- lines.push(boxLine("", w));
15005
15647
  lines.push(boxLine(`\u{1F5A5} HOST (${hostCount})`, w));
15006
15648
  if (targets.length > 0) {
15007
15649
  for (const t of targets.slice(0, 5)) {
15008
15650
  lines.push(boxLine(` \u25CB ${formatTargetLine(t)}`, w));
15009
15651
  }
15010
- if (targets.length > 5) {
15011
- lines.push(boxLine(` ... and ${targets.length - 5} more hosts`, w));
15012
- }
15652
+ if (targets.length > 5) lines.push(boxLine(` ... and ${targets.length - 5} more hosts`, w));
15013
15653
  } else if (hostNodes.length > 0) {
15014
15654
  for (const node of hostNodes.slice(0, 5)) {
15015
15655
  lines.push(boxLine(` \u25CB ${node.label}`, w));
15016
15656
  }
15017
- if (hostNodes.length > 5) {
15018
- lines.push(boxLine(` ... and ${hostNodes.length - 5} more hosts`, w));
15019
- }
15657
+ if (hostNodes.length > 5) lines.push(boxLine(` ... and ${hostNodes.length - 5} more hosts`, w));
15020
15658
  } else {
15021
15659
  lines.push(boxLine(" (no hosts discovered)", w));
15022
15660
  }
15023
- lines.push(boxLine("", w));
15661
+ return lines;
15662
+ }
15663
+ function formatVulnSection(findings, vulnNodes, vulnCount, w) {
15664
+ const lines = [];
15024
15665
  lines.push(boxLine(`\u26A0 VULNERABILITY (${vulnCount})`, w));
15025
15666
  if (findings.length > 0) {
15026
15667
  const sortedFindings = [...findings].sort((a, b) => b.confidence - a.confidence).slice(0, 5);
@@ -15030,52 +15671,88 @@ var formatGraphWithSummary = (state, findings, flags) => {
15030
15671
  lines.push(boxLine(` \u25CB ${icon} ${f.title.slice(0, 60)}${f.title.length > 60 ? "..." : ""}`, w));
15031
15672
  lines.push(boxLine(` ${confLabel(f.confidence).toUpperCase()} \u2502 ${f.severity.toUpperCase()}${cat}`, w));
15032
15673
  }
15033
- if (findings.length > 5) {
15034
- lines.push(boxLine(` ... and ${findings.length - 5} more findings`, w));
15674
+ if (findings.length > 5) lines.push(boxLine(` ... and ${findings.length - 5} more findings`, w));
15675
+ const cveFindings = findings.filter((f) => f.title.includes("CVE"));
15676
+ if (cveFindings.length > 0) {
15677
+ const cveList = cveFindings.slice(0, 3).map((f) => f.title.match(/CVE-\d{4}-\d+/)?.[0] ?? f.title).join(", ");
15678
+ const suffix = cveFindings.length > 3 ? ` (+${cveFindings.length - 3} more)` : "";
15679
+ lines.push(boxLine(` \u25CB CVE found: ${cveList}${suffix}`, w));
15035
15680
  }
15036
15681
  } else if (vulnNodes.length > 0) {
15037
15682
  for (const node of vulnNodes.slice(0, 5)) {
15038
15683
  lines.push(boxLine(` \u25CB ${node.label}`, w));
15039
15684
  }
15040
- if (vulnNodes.length > 5) {
15041
- lines.push(boxLine(` ... and ${vulnNodes.length - 5} more`, w));
15042
- }
15685
+ if (vulnNodes.length > 5) lines.push(boxLine(` ... and ${vulnNodes.length - 5} more`, w));
15043
15686
  } else {
15044
15687
  lines.push(boxLine(" (no vulnerabilities found)", w));
15045
15688
  }
15046
- const cveFindings = findings.filter((f) => f.title.includes("CVE"));
15047
- if (cveFindings.length > 0) {
15048
- const cveList = cveFindings.slice(0, 3).map((f) => f.title.match(/CVE-\d{4}-\d+/)?.[0] ?? f.title).join(", ");
15049
- const suffix = cveFindings.length > 3 ? ` (+${cveFindings.length - 3} more)` : "";
15050
- lines.push(boxLine(` \u25CB CVE found: ${cveList}${suffix}`, w));
15051
- }
15052
- lines.push(boxLine("", w));
15689
+ return lines;
15690
+ }
15691
+ function formatServiceSection(targets, serviceNodes, serviceCount, w) {
15692
+ const lines = [];
15693
+ const MAX_SHOW = 5;
15053
15694
  lines.push(boxLine(`\u2699 SERVICE (${serviceCount})`, w));
15054
15695
  if (targets.length > 0) {
15055
- let shownServices = 0;
15056
- const maxShow = 5;
15057
- for (const t of targets) {
15696
+ let shown = 0;
15697
+ outer: for (const t of targets) {
15058
15698
  for (const p of t.ports) {
15059
- if (shownServices >= maxShow) break;
15699
+ if (shown >= MAX_SHOW) break outer;
15060
15700
  lines.push(boxLine(` \u25CB ${formatServiceLabel(t, p)}`, w));
15061
- shownServices++;
15701
+ shown++;
15062
15702
  }
15063
- if (shownServices >= maxShow) break;
15064
15703
  }
15065
15704
  const totalServices = targets.reduce((sum, t) => sum + t.ports.length, 0);
15066
- if (totalServices > maxShow) {
15067
- lines.push(boxLine(` ... and ${totalServices - maxShow} more services`, w));
15068
- }
15705
+ if (totalServices > MAX_SHOW) lines.push(boxLine(` ... and ${totalServices - MAX_SHOW} more services`, w));
15069
15706
  } else if (serviceNodes.length > 0) {
15070
- for (const node of serviceNodes.slice(0, 5)) {
15707
+ for (const node of serviceNodes.slice(0, MAX_SHOW)) {
15071
15708
  lines.push(boxLine(` \u25CB ${node.label}`, w));
15072
15709
  }
15073
- if (serviceNodes.length > 5) {
15074
- lines.push(boxLine(` ... and ${serviceNodes.length - 5} more`, w));
15075
- }
15710
+ if (serviceNodes.length > MAX_SHOW) lines.push(boxLine(` ... and ${serviceNodes.length - MAX_SHOW} more`, w));
15076
15711
  } else {
15077
15712
  lines.push(boxLine(" (no services discovered)", w));
15078
15713
  }
15714
+ return lines;
15715
+ }
15716
+ function formatADSection(adNodes, w) {
15717
+ if (adNodes.length === 0) return [];
15718
+ const lines = [];
15719
+ lines.push(boxLine(`\u{1F3F0} ACTIVE DIRECTORY (${adNodes.length})`, w));
15720
+ for (const node of adNodes.slice(0, 5)) {
15721
+ const typeLabel = node.type === "domain" ? "DOMAIN" : node.type === "user-account" ? "USER" : node.type === "gpo" ? "GPO" : node.type === "acl" ? "ACL" : node.type === "certificate-template" ? "CERT" : node.type.toUpperCase();
15722
+ lines.push(boxLine(` \u25CB [${typeLabel}] ${node.label}`, w));
15723
+ }
15724
+ if (adNodes.length > 5) lines.push(boxLine(` ... and ${adNodes.length - 5} more AD objects`, w));
15725
+ return lines;
15726
+ }
15727
+ var formatGraphWithSummary = (state, findings, flags) => {
15728
+ const w = getBoxWidth();
15729
+ const innerDash = w - 2;
15730
+ const targets = state.getAllTargets();
15731
+ const graphStats = state.attackGraph.getStats();
15732
+ const allNodes = state.attackGraph.getAllNodes();
15733
+ const AD_NODE_TYPES = [NODE_TYPE.DOMAIN, NODE_TYPE.USER_ACCOUNT, NODE_TYPE.GPO, NODE_TYPE.ACL, NODE_TYPE.CERTIFICATE_TEMPLATE];
15734
+ const hostNodes = allNodes.filter((n) => n.type === NODE_TYPE.HOST);
15735
+ const serviceNodes = allNodes.filter((n) => n.type === NODE_TYPE.SERVICE);
15736
+ const vulnNodes = allNodes.filter((n) => n.type === NODE_TYPE.VULNERABILITY);
15737
+ const adNodes = allNodes.filter((n) => AD_NODE_TYPES.includes(n.type));
15738
+ const hostCount = hostNodes.length || targets.length;
15739
+ const serviceCount = serviceNodes.length || targets.reduce((sum, t) => sum + t.ports.length, 0);
15740
+ const vulnCount = findings.length;
15741
+ const lines = [];
15742
+ const topTitle = "\u2500\u2500\u2500 Attack Graph ";
15743
+ lines.push(`\u250C${topTitle}${"\u2500".repeat(Math.max(0, innerDash - topTitle.length))}\u2510`);
15744
+ lines.push(boxLine(`\u{1F5A5} ${hostCount} \u26A0 ${vulnCount} \u2699 ${serviceCount}`, w));
15745
+ lines.push(boxLine("", w));
15746
+ lines.push(...formatHostSection(targets, hostNodes, hostCount, w));
15747
+ lines.push(boxLine("", w));
15748
+ lines.push(...formatVulnSection(findings, vulnNodes, vulnCount, w));
15749
+ lines.push(boxLine("", w));
15750
+ lines.push(...formatServiceSection(targets, serviceNodes, serviceCount, w));
15751
+ const adSection = formatADSection(adNodes, w);
15752
+ if (adSection.length > 0) {
15753
+ lines.push(boxLine("", w));
15754
+ lines.push(...adSection);
15755
+ }
15079
15756
  if (flags.length > 0) {
15080
15757
  lines.push(boxLine("", w));
15081
15758
  lines.push(boxLine(`\u{1F3F4} Captured: ${flags.length}`, w));