pentesting 0.47.4 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -42
- package/dist/main.js +440 -180
- package/dist/prompts/base.md +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,15 +33,8 @@ Pentesting support tool
|
|
|
33
33
|
|
|
34
34
|
## Quick Start with Docker (Recommended)
|
|
35
35
|
|
|
36
|
-
```bash
|
|
37
|
-
# One-time use (data deleted after exit)
|
|
38
|
-
docker run -it --rm \
|
|
39
|
-
-e PENTEST_API_KEY="your_glm_api_key" \
|
|
40
|
-
-e PENTEST_BASE_URL="https://open.bigmodel.cn/api/paas/v4" \
|
|
41
|
-
-e PENTEST_MODEL="glm-5" \
|
|
42
|
-
agnusdei1207/pentesting
|
|
43
36
|
|
|
44
|
-
|
|
37
|
+
```bash
|
|
45
38
|
docker run -it --rm \
|
|
46
39
|
-e PENTEST_API_KEY="your_glm_api_key" \
|
|
47
40
|
-e PENTEST_BASE_URL="https://open.bigmodel.cn/api/paas/v4" \
|
|
@@ -50,8 +43,6 @@ docker run -it --rm \
|
|
|
50
43
|
agnusdei1207/pentesting
|
|
51
44
|
```
|
|
52
45
|
|
|
53
|
-
Web search is automatically configured to use GLM Web Search with your `PENTEST_API_KEY`.
|
|
54
|
-
|
|
55
46
|
### Using Brave Search
|
|
56
47
|
|
|
57
48
|
```bash
|
|
@@ -65,38 +56,6 @@ docker run -it --rm \
|
|
|
65
56
|
agnusdei1207/pentesting
|
|
66
57
|
```
|
|
67
58
|
|
|
68
|
-
Get Brave Search API key at: https://brave.com/search/api/
|
|
69
|
-
|
|
70
|
-
### Using Serper (Google Search)
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
docker run -it --rm \
|
|
74
|
-
-e PENTEST_API_KEY="your_glm_api_key" \
|
|
75
|
-
-e PENTEST_BASE_URL="https://open.bigmodel.cn/api/paas/v4" \
|
|
76
|
-
-e PENTEST_MODEL="glm-5" \
|
|
77
|
-
-e SEARCH_API_KEY="your_serper_api_key" \
|
|
78
|
-
-e SEARCH_API_URL="https://google.serper.dev/search" \
|
|
79
|
-
-v ./pentest-data:/root/.pentest \
|
|
80
|
-
agnusdei1207/pentesting
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Get Serper API key at: https://serper.dev/
|
|
84
|
-
|
|
85
|
-
## Environment Variables
|
|
86
|
-
|
|
87
|
-
| Variable | Required | Default | Description |
|
|
88
|
-
|----------|----------|---------|-------------|
|
|
89
|
-
| `PENTEST_API_KEY` | ✅ Yes | - | LLM API key (also used for web search if `SEARCH_API_KEY` not set) |
|
|
90
|
-
| `PENTEST_BASE_URL` | No | - | Custom API endpoint URL |
|
|
91
|
-
| `PENTEST_MODEL` | No | - | Model name (e.g., `glm-5`) |
|
|
92
|
-
| `SEARCH_API_KEY` | No | Uses `PENTEST_API_KEY` | Web search API key (optional, falls back to main key) |
|
|
93
|
-
| `SEARCH_API_URL` | No | GLM Web Search | Web search API URL |
|
|
94
|
-
|
|
95
|
-
### Web Search Defaults
|
|
96
|
-
|
|
97
|
-
- **Default**: GLM Web Search (`https://open.bigmodel.cn/api/paas/v4/tools/web-search-pro`)
|
|
98
|
-
- **API Key**: Falls back to `PENTEST_API_KEY` if `SEARCH_API_KEY` not set
|
|
99
|
-
|
|
100
59
|
## Issue
|
|
101
60
|
|
|
102
61
|
email: agnusdei1207@gmail.com
|
package/dist/main.js
CHANGED
|
@@ -331,7 +331,7 @@ var ORPHAN_PROCESS_NAMES = [
|
|
|
331
331
|
|
|
332
332
|
// src/shared/constants/agent.ts
|
|
333
333
|
var APP_NAME = "Pentest AI";
|
|
334
|
-
var APP_VERSION = "0.
|
|
334
|
+
var APP_VERSION = "0.48.0";
|
|
335
335
|
var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
|
|
336
336
|
var LLM_ROLES = {
|
|
337
337
|
SYSTEM: "system",
|
|
@@ -698,6 +698,12 @@ var ATTACK_TACTICS = {
|
|
|
698
698
|
C2: "command_and_control",
|
|
699
699
|
IMPACT: "impact"
|
|
700
700
|
};
|
|
701
|
+
var ATTACK_VALUE_RANK = {
|
|
702
|
+
HIGH: 3,
|
|
703
|
+
MED: 2,
|
|
704
|
+
LOW: 1,
|
|
705
|
+
NONE: 0
|
|
706
|
+
};
|
|
701
707
|
var APPROVAL_STATUSES = {
|
|
702
708
|
AUTO: "auto",
|
|
703
709
|
USER_CONFIRMED: "user_confirmed",
|
|
@@ -816,10 +822,6 @@ var SECONDS_PER_HOUR = 3600;
|
|
|
816
822
|
|
|
817
823
|
// src/shared/constants/paths.ts
|
|
818
824
|
import path from "path";
|
|
819
|
-
import { fileURLToPath } from "url";
|
|
820
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
821
|
-
var __dirname = path.dirname(__filename);
|
|
822
|
-
var PROJECT_ROOT = path.resolve(__dirname, "../../../");
|
|
823
825
|
var PENTESTING_ROOT = ".pentesting";
|
|
824
826
|
var WORK_DIR = `${PENTESTING_ROOT}/tmp`;
|
|
825
827
|
var MEMORY_DIR = `${PENTESTING_ROOT}/memory`;
|
|
@@ -829,6 +831,7 @@ var LOOT_DIR = `${PENTESTING_ROOT}/loot`;
|
|
|
829
831
|
var OUTPUTS_DIR = `${PENTESTING_ROOT}/outputs`;
|
|
830
832
|
var DEBUG_DIR = `${PENTESTING_ROOT}/debug`;
|
|
831
833
|
var JOURNAL_DIR = `${PENTESTING_ROOT}/journal`;
|
|
834
|
+
var TURNS_DIR = `${PENTESTING_ROOT}/memory/turns`;
|
|
832
835
|
var WORKSPACE = {
|
|
833
836
|
/** Root directory */
|
|
834
837
|
get ROOT() {
|
|
@@ -865,6 +868,10 @@ var WORKSPACE = {
|
|
|
865
868
|
/** Persistent per-turn journal (§13 memo system) */
|
|
866
869
|
get JOURNAL() {
|
|
867
870
|
return path.resolve(JOURNAL_DIR);
|
|
871
|
+
},
|
|
872
|
+
/** Turn record files */
|
|
873
|
+
get TURNS() {
|
|
874
|
+
return path.resolve(TURNS_DIR);
|
|
868
875
|
}
|
|
869
876
|
};
|
|
870
877
|
|
|
@@ -7151,8 +7158,8 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
|
|
|
7151
7158
|
}
|
|
7152
7159
|
},
|
|
7153
7160
|
execute: async (p) => {
|
|
7154
|
-
const { existsSync:
|
|
7155
|
-
const { join:
|
|
7161
|
+
const { existsSync: existsSync12, statSync: statSync3, readdirSync: readdirSync4 } = await import("fs");
|
|
7162
|
+
const { join: join14 } = await import("path");
|
|
7156
7163
|
const category = p.category || "";
|
|
7157
7164
|
const search = p.search || "";
|
|
7158
7165
|
const minSize = p.min_size || 0;
|
|
@@ -7198,7 +7205,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
|
|
|
7198
7205
|
results.push("");
|
|
7199
7206
|
};
|
|
7200
7207
|
const scanDir = (dirPath, maxDepth = 3, depth = 0) => {
|
|
7201
|
-
if (depth > maxDepth || !
|
|
7208
|
+
if (depth > maxDepth || !existsSync12(dirPath)) return;
|
|
7202
7209
|
let entries;
|
|
7203
7210
|
try {
|
|
7204
7211
|
entries = readdirSync4(dirPath, { withFileTypes: true });
|
|
@@ -7207,7 +7214,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
|
|
|
7207
7214
|
}
|
|
7208
7215
|
for (const entry of entries) {
|
|
7209
7216
|
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
|
|
7210
|
-
const fullPath =
|
|
7217
|
+
const fullPath = join14(dirPath, entry.name);
|
|
7211
7218
|
if (entry.isDirectory()) {
|
|
7212
7219
|
scanDir(fullPath, maxDepth, depth + 1);
|
|
7213
7220
|
continue;
|
|
@@ -7584,8 +7591,8 @@ Requires root/sudo privileges.`,
|
|
|
7584
7591
|
const iface = p.interface || "";
|
|
7585
7592
|
const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
|
|
7586
7593
|
const hostsFile = createTempFile(FILE_EXTENSIONS.HOSTS);
|
|
7587
|
-
const { writeFileSync:
|
|
7588
|
-
|
|
7594
|
+
const { writeFileSync: writeFileSync10 } = await import("fs");
|
|
7595
|
+
writeFileSync10(hostsFile, `${spoofIp} ${domain}
|
|
7589
7596
|
${spoofIp} *.${domain}
|
|
7590
7597
|
`);
|
|
7591
7598
|
const ifaceFlag = iface ? `-i ${iface}` : "";
|
|
@@ -8962,80 +8969,80 @@ var ServiceParser = class {
|
|
|
8962
8969
|
|
|
8963
8970
|
// src/domains/registry.ts
|
|
8964
8971
|
import { join as join7, dirname as dirname3 } from "path";
|
|
8965
|
-
import { fileURLToPath
|
|
8966
|
-
var
|
|
8972
|
+
import { fileURLToPath } from "url";
|
|
8973
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
8967
8974
|
var DOMAINS = {
|
|
8968
8975
|
[SERVICE_CATEGORIES.NETWORK]: {
|
|
8969
8976
|
id: SERVICE_CATEGORIES.NETWORK,
|
|
8970
8977
|
name: "Network Infrastructure",
|
|
8971
8978
|
description: "Vulnerability scanning, port mapping, and network service exploitation.",
|
|
8972
|
-
promptPath: join7(
|
|
8979
|
+
promptPath: join7(__dirname, "network/prompt.md")
|
|
8973
8980
|
},
|
|
8974
8981
|
[SERVICE_CATEGORIES.WEB]: {
|
|
8975
8982
|
id: SERVICE_CATEGORIES.WEB,
|
|
8976
8983
|
name: "Web Application",
|
|
8977
8984
|
description: "Web app security testing, injection attacks, and auth bypass.",
|
|
8978
|
-
promptPath: join7(
|
|
8985
|
+
promptPath: join7(__dirname, "web/prompt.md")
|
|
8979
8986
|
},
|
|
8980
8987
|
[SERVICE_CATEGORIES.DATABASE]: {
|
|
8981
8988
|
id: SERVICE_CATEGORIES.DATABASE,
|
|
8982
8989
|
name: "Database Security",
|
|
8983
8990
|
description: "SQL injection, database enumeration, and data extraction.",
|
|
8984
|
-
promptPath: join7(
|
|
8991
|
+
promptPath: join7(__dirname, "database/prompt.md")
|
|
8985
8992
|
},
|
|
8986
8993
|
[SERVICE_CATEGORIES.AD]: {
|
|
8987
8994
|
id: SERVICE_CATEGORIES.AD,
|
|
8988
8995
|
name: "Active Directory",
|
|
8989
8996
|
description: "Kerberos, LDAP, and Windows domain privilege escalation.",
|
|
8990
|
-
promptPath: join7(
|
|
8997
|
+
promptPath: join7(__dirname, "ad/prompt.md")
|
|
8991
8998
|
},
|
|
8992
8999
|
[SERVICE_CATEGORIES.EMAIL]: {
|
|
8993
9000
|
id: SERVICE_CATEGORIES.EMAIL,
|
|
8994
9001
|
name: "Email Services",
|
|
8995
9002
|
description: "SMTP, IMAP, POP3 security and user enumeration.",
|
|
8996
|
-
promptPath: join7(
|
|
9003
|
+
promptPath: join7(__dirname, "email/prompt.md")
|
|
8997
9004
|
},
|
|
8998
9005
|
[SERVICE_CATEGORIES.REMOTE_ACCESS]: {
|
|
8999
9006
|
id: SERVICE_CATEGORIES.REMOTE_ACCESS,
|
|
9000
9007
|
name: "Remote Access",
|
|
9001
9008
|
description: "SSH, RDP, VNC and other remote control protocols.",
|
|
9002
|
-
promptPath: join7(
|
|
9009
|
+
promptPath: join7(__dirname, "remote-access/prompt.md")
|
|
9003
9010
|
},
|
|
9004
9011
|
[SERVICE_CATEGORIES.FILE_SHARING]: {
|
|
9005
9012
|
id: SERVICE_CATEGORIES.FILE_SHARING,
|
|
9006
9013
|
name: "File Sharing",
|
|
9007
9014
|
description: "SMB, NFS, FTP and shared resource security.",
|
|
9008
|
-
promptPath: join7(
|
|
9015
|
+
promptPath: join7(__dirname, "file-sharing/prompt.md")
|
|
9009
9016
|
},
|
|
9010
9017
|
[SERVICE_CATEGORIES.CLOUD]: {
|
|
9011
9018
|
id: SERVICE_CATEGORIES.CLOUD,
|
|
9012
9019
|
name: "Cloud Infrastructure",
|
|
9013
9020
|
description: "AWS, Azure, and GCP security and misconfiguration.",
|
|
9014
|
-
promptPath: join7(
|
|
9021
|
+
promptPath: join7(__dirname, "cloud/prompt.md")
|
|
9015
9022
|
},
|
|
9016
9023
|
[SERVICE_CATEGORIES.CONTAINER]: {
|
|
9017
9024
|
id: SERVICE_CATEGORIES.CONTAINER,
|
|
9018
9025
|
name: "Container Systems",
|
|
9019
9026
|
description: "Docker and Kubernetes security testing.",
|
|
9020
|
-
promptPath: join7(
|
|
9027
|
+
promptPath: join7(__dirname, "container/prompt.md")
|
|
9021
9028
|
},
|
|
9022
9029
|
[SERVICE_CATEGORIES.API]: {
|
|
9023
9030
|
id: SERVICE_CATEGORIES.API,
|
|
9024
9031
|
name: "API Security",
|
|
9025
9032
|
description: "REST, GraphQL, and SOAP API security testing.",
|
|
9026
|
-
promptPath: join7(
|
|
9033
|
+
promptPath: join7(__dirname, "api/prompt.md")
|
|
9027
9034
|
},
|
|
9028
9035
|
[SERVICE_CATEGORIES.WIRELESS]: {
|
|
9029
9036
|
id: SERVICE_CATEGORIES.WIRELESS,
|
|
9030
9037
|
name: "Wireless Networks",
|
|
9031
9038
|
description: "WiFi and Bluetooth security testing.",
|
|
9032
|
-
promptPath: join7(
|
|
9039
|
+
promptPath: join7(__dirname, "wireless/prompt.md")
|
|
9033
9040
|
},
|
|
9034
9041
|
[SERVICE_CATEGORIES.ICS]: {
|
|
9035
9042
|
id: SERVICE_CATEGORIES.ICS,
|
|
9036
9043
|
name: "Industrial Systems",
|
|
9037
9044
|
description: "Critical infrastructure - Modbus, DNP3, ENIP.",
|
|
9038
|
-
promptPath: join7(
|
|
9045
|
+
promptPath: join7(__dirname, "ics/prompt.md")
|
|
9039
9046
|
}
|
|
9040
9047
|
};
|
|
9041
9048
|
|
|
@@ -9675,10 +9682,10 @@ function logLLM(message, data) {
|
|
|
9675
9682
|
}
|
|
9676
9683
|
|
|
9677
9684
|
// src/engine/orchestrator/orchestrator.ts
|
|
9678
|
-
import { fileURLToPath as
|
|
9685
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9679
9686
|
import { dirname as dirname4, join as join8 } from "path";
|
|
9680
|
-
var
|
|
9681
|
-
var
|
|
9687
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
9688
|
+
var __dirname2 = dirname4(__filename);
|
|
9682
9689
|
|
|
9683
9690
|
// src/engine/state-persistence.ts
|
|
9684
9691
|
import { writeFileSync as writeFileSync6, readFileSync as readFileSync4, existsSync as existsSync6, readdirSync, statSync, unlinkSync as unlinkSync4, rmSync } from "fs";
|
|
@@ -9957,7 +9964,30 @@ var NOISE_PATTERNS = [
|
|
|
9957
9964
|
];
|
|
9958
9965
|
function structuralPreprocess(output) {
|
|
9959
9966
|
let cleaned = stripAnsi(output);
|
|
9960
|
-
const
|
|
9967
|
+
const filteredLines = filterAndDedup(cleaned.split("\n"));
|
|
9968
|
+
if (filteredLines.length > MAX_PREPROCESSED_LINES) {
|
|
9969
|
+
const headSize = Math.floor(MAX_PREPROCESSED_LINES * 0.5);
|
|
9970
|
+
const tailSize = Math.floor(MAX_PREPROCESSED_LINES * 0.3);
|
|
9971
|
+
const head = filteredLines.slice(0, headSize);
|
|
9972
|
+
const tail = filteredLines.slice(-tailSize);
|
|
9973
|
+
const skipped = filteredLines.length - headSize - tailSize;
|
|
9974
|
+
cleaned = [
|
|
9975
|
+
...head,
|
|
9976
|
+
"",
|
|
9977
|
+
`... [${skipped} lines skipped for Analyst LLM context \u2014 full output saved to file] ...`,
|
|
9978
|
+
"",
|
|
9979
|
+
...tail
|
|
9980
|
+
].join("\n");
|
|
9981
|
+
} else {
|
|
9982
|
+
cleaned = filteredLines.join("\n");
|
|
9983
|
+
}
|
|
9984
|
+
if (cleaned.length > ANALYST_MAX_INPUT_CHARS) {
|
|
9985
|
+
cleaned = cleaned.slice(0, ANALYST_MAX_INPUT_CHARS) + `
|
|
9986
|
+
... [truncated at ${ANALYST_MAX_INPUT_CHARS} chars for Analyst LLM \u2014 full output saved to file]`;
|
|
9987
|
+
}
|
|
9988
|
+
return cleaned;
|
|
9989
|
+
}
|
|
9990
|
+
function filterAndDedup(lines) {
|
|
9961
9991
|
const result2 = [];
|
|
9962
9992
|
let lastLine = "";
|
|
9963
9993
|
let consecutiveDupes = 0;
|
|
@@ -9991,27 +10021,7 @@ function structuralPreprocess(output) {
|
|
|
9991
10021
|
result2.push(lastLine);
|
|
9992
10022
|
}
|
|
9993
10023
|
}
|
|
9994
|
-
|
|
9995
|
-
const headSize = Math.floor(MAX_PREPROCESSED_LINES * 0.5);
|
|
9996
|
-
const tailSize = Math.floor(MAX_PREPROCESSED_LINES * 0.3);
|
|
9997
|
-
const head = result2.slice(0, headSize);
|
|
9998
|
-
const tail = result2.slice(-tailSize);
|
|
9999
|
-
const skipped = result2.length - headSize - tailSize;
|
|
10000
|
-
cleaned = [
|
|
10001
|
-
...head,
|
|
10002
|
-
"",
|
|
10003
|
-
`... [${skipped} lines skipped for Analyst LLM context \u2014 full output saved to file] ...`,
|
|
10004
|
-
"",
|
|
10005
|
-
...tail
|
|
10006
|
-
].join("\n");
|
|
10007
|
-
} else {
|
|
10008
|
-
cleaned = result2.join("\n");
|
|
10009
|
-
}
|
|
10010
|
-
if (cleaned.length > ANALYST_MAX_INPUT_CHARS) {
|
|
10011
|
-
cleaned = cleaned.slice(0, ANALYST_MAX_INPUT_CHARS) + `
|
|
10012
|
-
... [truncated at ${ANALYST_MAX_INPUT_CHARS} chars for Analyst LLM \u2014 full output saved to file]`;
|
|
10013
|
-
}
|
|
10014
|
-
return cleaned;
|
|
10024
|
+
return result2;
|
|
10015
10025
|
}
|
|
10016
10026
|
var ANALYST_SYSTEM_PROMPT = `You are an independent pentesting output analyst. You receive raw tool output and must extract ONLY actionable intelligence for the main attack agent.
|
|
10017
10027
|
|
|
@@ -10260,38 +10270,7 @@ var CoreAgent = class _CoreAgent {
|
|
|
10260
10270
|
}
|
|
10261
10271
|
if (progress.consecutiveIdleIterations >= AGENT_LIMITS.MAX_CONSECUTIVE_IDLE) {
|
|
10262
10272
|
progress.consecutiveIdleIterations = 0;
|
|
10263
|
-
|
|
10264
|
-
const targets = this.state.getTargets().size;
|
|
10265
|
-
const findings = this.state.getFindings().length;
|
|
10266
|
-
const phaseDirection = {
|
|
10267
|
-
[PHASES.RECON]: `RECON: Scan targets. Enumerate services and versions.`,
|
|
10268
|
-
[PHASES.VULN_ANALYSIS]: `VULN ANALYSIS: ${targets} target(s) discovered. Search for CVEs and known exploits.`,
|
|
10269
|
-
[PHASES.EXPLOIT]: `EXPLOIT: ${findings} finding(s) available. Attack the highest-severity one.`,
|
|
10270
|
-
[PHASES.POST_EXPLOIT]: `POST-EXPLOIT: Escalate privileges. Search for escalation paths.`,
|
|
10271
|
-
[PHASES.PRIV_ESC]: `PRIVESC: Find and exploit privilege escalation vectors.`,
|
|
10272
|
-
[PHASES.LATERAL]: `LATERAL: Reuse discovered credentials on other hosts.`,
|
|
10273
|
-
[PHASES.WEB]: `WEB: Enumerate the attack surface. Test every input for injection.`
|
|
10274
|
-
};
|
|
10275
|
-
const direction = phaseDirection[phase] || phaseDirection[PHASES.RECON];
|
|
10276
|
-
messages.push({
|
|
10277
|
-
role: LLM_ROLES.USER,
|
|
10278
|
-
content: `\u26A1 DEADLOCK: ${AGENT_LIMITS.MAX_CONSECUTIVE_IDLE} turns with ZERO tool calls.
|
|
10279
|
-
Phase: ${phase} | Targets: ${targets} | Findings: ${findings} | Tools executed: ${progress.totalToolsExecuted} (${progress.toolSuccesses}\u2713 ${progress.toolErrors}\u2717)
|
|
10280
|
-
|
|
10281
|
-
${direction}
|
|
10282
|
-
|
|
10283
|
-
ESCALATION CHAIN \u2014 follow this order:
|
|
10284
|
-
1. web_search: Search for techniques, bypasses, default creds, CVEs, HackTricks
|
|
10285
|
-
2. BYPASS: Try alternative approaches \u2014 different protocols, ports, encodings, methods
|
|
10286
|
-
3. ZERO-DAY EXPLORATION: Probe for unknown vulns \u2014 fuzz parameters, test edge cases, analyze error responses for leaks
|
|
10287
|
-
4. BRUTE-FORCE: Wordlists, credential stuffing, common passwords, custom password lists from context
|
|
10288
|
-
5. ask_user: ONLY as last resort \u2014 ask the user for hints, wordlists, or guidance
|
|
10289
|
-
|
|
10290
|
-
RULES:
|
|
10291
|
-
- Every turn MUST have tool calls
|
|
10292
|
-
- NEVER silently give up \u2014 exhaust ALL 5 steps above first
|
|
10293
|
-
- ACT NOW \u2014 do not plan, do not explain, do not summarize. EXECUTE.`
|
|
10294
|
-
});
|
|
10273
|
+
messages.push({ role: LLM_ROLES.USER, content: this.buildDeadlockNudge(progress) });
|
|
10295
10274
|
}
|
|
10296
10275
|
} catch (error) {
|
|
10297
10276
|
if (this.isAbortError(error)) {
|
|
@@ -10476,6 +10455,48 @@ ${firstLine}`, phase }
|
|
|
10476
10455
|
return callbacks;
|
|
10477
10456
|
}
|
|
10478
10457
|
// ─────────────────────────────────────────────────────────────────
|
|
10458
|
+
// SUBSECTION: Deadlock Nudge Builder
|
|
10459
|
+
// ─────────────────────────────────────────────────────────────────
|
|
10460
|
+
/**
|
|
10461
|
+
* Build a deadlock nudge message for the agent.
|
|
10462
|
+
*
|
|
10463
|
+
* WHY separated: The nudge template is ~30 lines of prompt engineering.
|
|
10464
|
+
* Keeping it in run() obscures the iteration control logic.
|
|
10465
|
+
* Philosophy §12: Nudge is a safety net, not a driver —
|
|
10466
|
+
* it reminds the agent to ACT, but never prescribes HOW.
|
|
10467
|
+
*/
|
|
10468
|
+
buildDeadlockNudge(progress) {
|
|
10469
|
+
const phase = this.state.getPhase();
|
|
10470
|
+
const targets = this.state.getTargets().size;
|
|
10471
|
+
const findings = this.state.getFindings().length;
|
|
10472
|
+
const phaseDirection = {
|
|
10473
|
+
[PHASES.RECON]: `RECON: Scan targets. Enumerate services and versions.`,
|
|
10474
|
+
[PHASES.VULN_ANALYSIS]: `VULN ANALYSIS: ${targets} target(s) discovered. Search for CVEs and known exploits.`,
|
|
10475
|
+
[PHASES.EXPLOIT]: `EXPLOIT: ${findings} finding(s) available. Attack the highest-severity one.`,
|
|
10476
|
+
[PHASES.POST_EXPLOIT]: `POST-EXPLOIT: Escalate privileges. Search for escalation paths.`,
|
|
10477
|
+
[PHASES.PRIV_ESC]: `PRIVESC: Find and exploit privilege escalation vectors.`,
|
|
10478
|
+
[PHASES.LATERAL]: `LATERAL: Reuse discovered credentials on other hosts.`,
|
|
10479
|
+
[PHASES.WEB]: `WEB: Enumerate the attack surface. Test every input for injection.`
|
|
10480
|
+
};
|
|
10481
|
+
const direction = phaseDirection[phase] || phaseDirection[PHASES.RECON];
|
|
10482
|
+
return `\u26A1 DEADLOCK: ${AGENT_LIMITS.MAX_CONSECUTIVE_IDLE} turns with ZERO tool calls.
|
|
10483
|
+
Phase: ${phase} | Targets: ${targets} | Findings: ${findings} | Tools executed: ${progress.totalToolsExecuted} (${progress.toolSuccesses}\u2713 ${progress.toolErrors}\u2717)
|
|
10484
|
+
|
|
10485
|
+
${direction}
|
|
10486
|
+
|
|
10487
|
+
ESCALATION CHAIN \u2014 follow this order:
|
|
10488
|
+
1. web_search: Search for techniques, bypasses, default creds, CVEs, HackTricks
|
|
10489
|
+
2. BYPASS: Try alternative approaches \u2014 different protocols, ports, encodings, methods
|
|
10490
|
+
3. ZERO-DAY EXPLORATION: Probe for unknown vulns \u2014 fuzz parameters, test edge cases, analyze error responses for leaks
|
|
10491
|
+
4. BRUTE-FORCE: Wordlists, credential stuffing, common passwords, custom password lists from context
|
|
10492
|
+
5. ask_user: ONLY as last resort \u2014 ask the user for hints, wordlists, or guidance
|
|
10493
|
+
|
|
10494
|
+
RULES:
|
|
10495
|
+
- Every turn MUST have tool calls
|
|
10496
|
+
- NEVER silently give up \u2014 exhaust ALL 5 steps above first
|
|
10497
|
+
- ACT NOW \u2014 do not plan, do not explain, do not summarize. EXECUTE.`;
|
|
10498
|
+
}
|
|
10499
|
+
// ─────────────────────────────────────────────────────────────────
|
|
10479
10500
|
// SUBSECTION: Event Emitters
|
|
10480
10501
|
// ─────────────────════════════════════════════════════════════
|
|
10481
10502
|
emitThink(iteration, progress) {
|
|
@@ -10640,83 +10661,20 @@ ${firstLine}`, phase }
|
|
|
10640
10661
|
const toolStartTime = Date.now();
|
|
10641
10662
|
logLLM("CoreAgent executing tool", { id: call.id, name: call.name, input: call.input });
|
|
10642
10663
|
if (!this.toolRegistry) {
|
|
10643
|
-
return {
|
|
10644
|
-
toolCallId: call.id,
|
|
10645
|
-
output: "",
|
|
10646
|
-
error: "Tool registry not initialized. Call setToolRegistry() first."
|
|
10647
|
-
};
|
|
10664
|
+
return { toolCallId: call.id, output: "", error: "Tool registry not initialized. Call setToolRegistry() first." };
|
|
10648
10665
|
}
|
|
10649
10666
|
try {
|
|
10650
|
-
const result2 = await this.toolRegistry.execute({
|
|
10651
|
-
name: call.name,
|
|
10652
|
-
input: call.input
|
|
10653
|
-
});
|
|
10667
|
+
const result2 = await this.toolRegistry.execute({ name: call.name, input: call.input });
|
|
10654
10668
|
let outputText = result2.output ?? "";
|
|
10655
10669
|
this.scanForFlags(outputText);
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
}
|
|
10665
|
-
const rawOutputForTUI = outputText;
|
|
10666
|
-
let digestedOutputForLLM = outputText;
|
|
10667
|
-
let digestResult = null;
|
|
10668
|
-
try {
|
|
10669
|
-
const llmDigestFn = createLLMDigestFn(this.llm);
|
|
10670
|
-
digestResult = await digestToolOutput(
|
|
10671
|
-
outputText,
|
|
10672
|
-
call.name,
|
|
10673
|
-
JSON.stringify(call.input).slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY),
|
|
10674
|
-
llmDigestFn
|
|
10675
|
-
);
|
|
10676
|
-
digestedOutputForLLM = digestResult.digestedOutput;
|
|
10677
|
-
} catch {
|
|
10678
|
-
if (digestedOutputForLLM.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
|
|
10679
|
-
const truncated = digestedOutputForLLM.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
|
|
10680
|
-
const remaining = digestedOutputForLLM.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
|
|
10681
|
-
digestedOutputForLLM = `${truncated}
|
|
10682
|
-
|
|
10683
|
-
... [TRUNCATED ${remaining} characters for context hygiene] ...
|
|
10684
|
-
\u{1F4A1} TIP: If you need to see the full output, use a tool to read the file directly or run the command with | head, | tail, or | grep.`;
|
|
10685
|
-
}
|
|
10686
|
-
}
|
|
10687
|
-
this.emitToolResult(call.name, result2.success, rawOutputForTUI, result2.error, Date.now() - toolStartTime);
|
|
10688
|
-
const inputSummary = JSON.stringify(call.input);
|
|
10689
|
-
this.turnToolJournal.push({
|
|
10690
|
-
name: call.name,
|
|
10691
|
-
inputSummary,
|
|
10692
|
-
success: result2.success,
|
|
10693
|
-
analystSummary: digestResult?.memo ? digestResult.memo.keyFindings.join("; ") || "No key findings" : digestedOutputForLLM,
|
|
10694
|
-
outputFile: digestResult?.fullOutputPath ?? null
|
|
10695
|
-
});
|
|
10696
|
-
if (digestResult?.memo) {
|
|
10697
|
-
const m = digestResult.memo;
|
|
10698
|
-
this.turnMemo.keyFindings.push(...m.keyFindings);
|
|
10699
|
-
this.turnMemo.credentials.push(...m.credentials);
|
|
10700
|
-
this.turnMemo.attackVectors.push(...m.attackVectors);
|
|
10701
|
-
this.turnMemo.failures.push(...m.failures);
|
|
10702
|
-
this.turnMemo.suspicions.push(...m.suspicions);
|
|
10703
|
-
const VALUE_RANK = { HIGH: 3, MED: 2, LOW: 1, NONE: 0 };
|
|
10704
|
-
if ((VALUE_RANK[m.attackValue] ?? 0) > (VALUE_RANK[this.turnMemo.attackValue] ?? 0)) {
|
|
10705
|
-
this.turnMemo.attackValue = m.attackValue;
|
|
10706
|
-
}
|
|
10707
|
-
this.turnMemo.nextSteps.push(...m.nextSteps);
|
|
10708
|
-
if (m.reflection) this.turnReflections.push(m.reflection);
|
|
10709
|
-
}
|
|
10710
|
-
if (digestResult?.memo?.credentials.length) {
|
|
10711
|
-
for (const cred of digestResult.memo.credentials) {
|
|
10712
|
-
this.state.addLoot({
|
|
10713
|
-
type: "credential",
|
|
10714
|
-
host: "auto-extracted",
|
|
10715
|
-
detail: cred,
|
|
10716
|
-
obtainedAt: Date.now()
|
|
10717
|
-
});
|
|
10718
|
-
}
|
|
10719
|
-
}
|
|
10670
|
+
outputText = this.handleToolResult(result2, call, outputText, progress);
|
|
10671
|
+
const { digestedOutputForLLM, digestResult } = await this.digestAndEmit(
|
|
10672
|
+
call,
|
|
10673
|
+
outputText,
|
|
10674
|
+
result2,
|
|
10675
|
+
toolStartTime
|
|
10676
|
+
);
|
|
10677
|
+
this.recordJournalMemo(call, result2, digestedOutputForLLM, digestResult);
|
|
10720
10678
|
return { toolCallId: call.id, output: digestedOutputForLLM, error: result2.error };
|
|
10721
10679
|
} catch (error) {
|
|
10722
10680
|
const errorMsg = String(error);
|
|
@@ -10726,6 +10684,90 @@ ${firstLine}`, phase }
|
|
|
10726
10684
|
return { toolCallId: call.id, output: enrichedError, error: errorMsg };
|
|
10727
10685
|
}
|
|
10728
10686
|
}
|
|
10687
|
+
/**
|
|
10688
|
+
* Handle tool result: enrich errors or track success.
|
|
10689
|
+
* @returns Possibly enriched output text.
|
|
10690
|
+
*/
|
|
10691
|
+
handleToolResult(result2, call, outputText, progress) {
|
|
10692
|
+
if (result2.error) {
|
|
10693
|
+
if (progress) progress.toolErrors++;
|
|
10694
|
+
return this.enrichToolError({ toolName: call.name, input: call.input, error: result2.error, originalOutput: outputText, progress });
|
|
10695
|
+
}
|
|
10696
|
+
if (progress) {
|
|
10697
|
+
progress.toolSuccesses++;
|
|
10698
|
+
progress.blockedCommandPatterns.clear();
|
|
10699
|
+
}
|
|
10700
|
+
return outputText;
|
|
10701
|
+
}
|
|
10702
|
+
/**
|
|
10703
|
+
* Digest tool output via Analyst LLM (§13 ③) and emit TUI event.
|
|
10704
|
+
*
|
|
10705
|
+
* WHY separated: Digest + emit is a self-contained pipeline:
|
|
10706
|
+
* raw output → Analyst → digest + file → TUI event.
|
|
10707
|
+
* Isolating it makes the pipeline testable without running actual tools.
|
|
10708
|
+
*/
|
|
10709
|
+
async digestAndEmit(call, outputText, result2, toolStartTime) {
|
|
10710
|
+
const digestFallbackOutput = outputText;
|
|
10711
|
+
let digestedOutputForLLM = outputText;
|
|
10712
|
+
let digestResult = null;
|
|
10713
|
+
try {
|
|
10714
|
+
const llmDigestFn = createLLMDigestFn(this.llm);
|
|
10715
|
+
digestResult = await digestToolOutput(
|
|
10716
|
+
outputText,
|
|
10717
|
+
call.name,
|
|
10718
|
+
JSON.stringify(call.input).slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY),
|
|
10719
|
+
llmDigestFn
|
|
10720
|
+
);
|
|
10721
|
+
digestedOutputForLLM = digestResult.digestedOutput;
|
|
10722
|
+
} catch {
|
|
10723
|
+
if (digestedOutputForLLM.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
|
|
10724
|
+
const truncated = digestedOutputForLLM.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
|
|
10725
|
+
const remaining = digestedOutputForLLM.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
|
|
10726
|
+
digestedOutputForLLM = `${truncated}
|
|
10727
|
+
|
|
10728
|
+
... [TRUNCATED ${remaining} characters for context hygiene] ...
|
|
10729
|
+
\u{1F4A1} TIP: If you need to see the full output, use a tool to read the file directly or run the command with | head, | tail, or | grep.`;
|
|
10730
|
+
}
|
|
10731
|
+
}
|
|
10732
|
+
const outputFilePath = digestResult?.fullOutputPath ?? null;
|
|
10733
|
+
const tuiOutput = digestResult?.digestedOutput ? `${digestResult.digestedOutput}${outputFilePath ? `
|
|
10734
|
+
\u{1F4C4} Full output: ${outputFilePath}` : ""}` : digestFallbackOutput.slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY);
|
|
10735
|
+
this.emitToolResult(call.name, result2.success, tuiOutput, result2.error, Date.now() - toolStartTime);
|
|
10736
|
+
return { digestedOutputForLLM, digestResult };
|
|
10737
|
+
}
|
|
10738
|
+
/**
|
|
10739
|
+
* Record tool execution results to Journal and aggregate memos.
|
|
10740
|
+
*
|
|
10741
|
+
* WHY no truncation on inputSummary: Strategist needs full context —
|
|
10742
|
+
* "hydra -l admin -P rockyou.txt ssh://10.0.0.1" must survive intact.
|
|
10743
|
+
*/
|
|
10744
|
+
recordJournalMemo(call, result2, digestedOutputForLLM, digestResult) {
|
|
10745
|
+
this.turnToolJournal.push({
|
|
10746
|
+
name: call.name,
|
|
10747
|
+
inputSummary: JSON.stringify(call.input),
|
|
10748
|
+
success: result2.success,
|
|
10749
|
+
analystSummary: digestResult?.memo ? digestResult.memo.keyFindings.join("; ") || "No key findings" : digestedOutputForLLM,
|
|
10750
|
+
outputFile: digestResult?.fullOutputPath ?? null
|
|
10751
|
+
});
|
|
10752
|
+
if (digestResult?.memo) {
|
|
10753
|
+
const m = digestResult.memo;
|
|
10754
|
+
this.turnMemo.keyFindings.push(...m.keyFindings);
|
|
10755
|
+
this.turnMemo.credentials.push(...m.credentials);
|
|
10756
|
+
this.turnMemo.attackVectors.push(...m.attackVectors);
|
|
10757
|
+
this.turnMemo.failures.push(...m.failures);
|
|
10758
|
+
this.turnMemo.suspicions.push(...m.suspicions);
|
|
10759
|
+
if ((ATTACK_VALUE_RANK[m.attackValue] ?? 0) > (ATTACK_VALUE_RANK[this.turnMemo.attackValue] ?? 0)) {
|
|
10760
|
+
this.turnMemo.attackValue = m.attackValue;
|
|
10761
|
+
}
|
|
10762
|
+
this.turnMemo.nextSteps.push(...m.nextSteps);
|
|
10763
|
+
if (m.reflection) this.turnReflections.push(m.reflection);
|
|
10764
|
+
}
|
|
10765
|
+
if (digestResult?.memo?.credentials.length) {
|
|
10766
|
+
for (const cred of digestResult.memo.credentials) {
|
|
10767
|
+
this.state.addLoot({ type: LOOT_TYPES.CREDENTIAL, host: "auto-extracted", detail: cred, obtainedAt: Date.now() });
|
|
10768
|
+
}
|
|
10769
|
+
}
|
|
10770
|
+
}
|
|
10729
10771
|
/**
|
|
10730
10772
|
* Enrich tool error — delegates to extracted module (§3-1)
|
|
10731
10773
|
*/
|
|
@@ -10794,7 +10836,7 @@ ${firstLine}`, phase }
|
|
|
10794
10836
|
// src/agents/prompt-builder.ts
|
|
10795
10837
|
import { readFileSync as readFileSync6, existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
|
|
10796
10838
|
import { join as join11, dirname as dirname5 } from "path";
|
|
10797
|
-
import { fileURLToPath as
|
|
10839
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10798
10840
|
|
|
10799
10841
|
// src/shared/constants/prompts.ts
|
|
10800
10842
|
var PROMPT_PATHS = {
|
|
@@ -10853,6 +10895,44 @@ var PROMPT_CONFIG = {
|
|
|
10853
10895
|
var INITIAL_TASKS = {
|
|
10854
10896
|
RECON: "Initial reconnaissance and target discovery"
|
|
10855
10897
|
};
|
|
10898
|
+
var CONTEXT_EXTRACTOR_PROMPT = `You are extracting actionable intelligence from a penetration testing session.
|
|
10899
|
+
DO NOT simply summarize or shorten. EXTRACT critical facts:
|
|
10900
|
+
|
|
10901
|
+
1. DISCOVERED: Services, versions, paths, parameters (exact IPs, ports, versions)
|
|
10902
|
+
2. CONFIRMED: Vulnerabilities or access confirmed
|
|
10903
|
+
3. CREDENTIALS: Usernames, passwords, tokens, keys
|
|
10904
|
+
4. DEAD ENDS: What failed \u2014 include EXACT command, tool, arguments, wordlist/file used.
|
|
10905
|
+
Distinguish between:
|
|
10906
|
+
- "This approach itself is impossible" (e.g., SSH key-only \u2192 no password brute force works)
|
|
10907
|
+
- "This specific attempt failed" (e.g., sqlmap with default tamper \u2192 try different tamper)
|
|
10908
|
+
5. OPEN LEADS: Unexplored paths worth pursuing
|
|
10909
|
+
|
|
10910
|
+
Every line must include exact commands/tools/files used.
|
|
10911
|
+
The reader must be able to judge whether a retry with different parameters is worthwhile.`;
|
|
10912
|
+
var REFLECTION_PROMPT = `You are a tactical reviewer for a penetration testing agent.
|
|
10913
|
+
Review ALL actions from this turn \u2014 successes AND failures.
|
|
10914
|
+
|
|
10915
|
+
1. ASSESSMENT: What did this turn accomplish? Rate: HIGH / MED / LOW / NONE.
|
|
10916
|
+
2. SUCCESSES: What worked? Can this pattern be replicated elsewhere?
|
|
10917
|
+
3. FAILURES: What failed? Is this a repeated pattern? If so \u2192 STOP this approach.
|
|
10918
|
+
4. BLIND SPOTS: What was missed or overlooked?
|
|
10919
|
+
5. NEXT PRIORITY: Single most valuable next action.
|
|
10920
|
+
|
|
10921
|
+
3-5 lines. Every word must be actionable.`;
|
|
10922
|
+
var SUMMARY_REGENERATOR_PROMPT = `Update this penetration testing session summary with the new turn data.
|
|
10923
|
+
|
|
10924
|
+
Must include:
|
|
10925
|
+
- All discovered hosts, services, versions (exact IPs, ports, software versions)
|
|
10926
|
+
- All confirmed vulnerabilities
|
|
10927
|
+
- All obtained credentials
|
|
10928
|
+
- Failed attempts with EXACT commands/tools/arguments/files used.
|
|
10929
|
+
For each failure, state:
|
|
10930
|
+
- The root cause (auth method? WAF? patched? wrong params?)
|
|
10931
|
+
- Whether retrying with different parameters could work
|
|
10932
|
+
- Top unexplored leads
|
|
10933
|
+
|
|
10934
|
+
Remove outdated/superseded info. Keep concise but COMPLETE.
|
|
10935
|
+
The reader must be able to decide what to retry and what to never attempt again.`;
|
|
10856
10936
|
|
|
10857
10937
|
// src/shared/constants/scoring.ts
|
|
10858
10938
|
var ATTACK_SCORING = {
|
|
@@ -11018,7 +11098,6 @@ function getAttacksForService(service, port) {
|
|
|
11018
11098
|
import { writeFileSync as writeFileSync8, readFileSync as readFileSync5, existsSync as existsSync8, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync5 } from "fs";
|
|
11019
11099
|
import { join as join10 } from "path";
|
|
11020
11100
|
var MAX_JOURNAL_ENTRIES = 50;
|
|
11021
|
-
var SUMMARY_REGEN_INTERVAL = 10;
|
|
11022
11101
|
var MAX_OUTPUT_FILES = 30;
|
|
11023
11102
|
var TURN_PREFIX = "turn-";
|
|
11024
11103
|
var SUMMARY_FILE = "summary.md";
|
|
@@ -11075,9 +11154,6 @@ function getNextTurnNumber() {
|
|
|
11075
11154
|
return 1;
|
|
11076
11155
|
}
|
|
11077
11156
|
}
|
|
11078
|
-
function shouldRegenerateSummary(currentTurn) {
|
|
11079
|
-
return currentTurn > 0 && currentTurn % SUMMARY_REGEN_INTERVAL === 0;
|
|
11080
|
-
}
|
|
11081
11157
|
function regenerateJournalSummary() {
|
|
11082
11158
|
try {
|
|
11083
11159
|
const entries = getRecentEntries();
|
|
@@ -11096,6 +11172,10 @@ function regenerateJournalSummary() {
|
|
|
11096
11172
|
}
|
|
11097
11173
|
}
|
|
11098
11174
|
function buildSummaryFromEntries(entries) {
|
|
11175
|
+
const buckets = collectSummaryBuckets(entries);
|
|
11176
|
+
return formatSummaryMarkdown(buckets, entries);
|
|
11177
|
+
}
|
|
11178
|
+
function collectSummaryBuckets(entries) {
|
|
11099
11179
|
const attempts = [];
|
|
11100
11180
|
const findings = [];
|
|
11101
11181
|
const credentials = [];
|
|
@@ -11104,19 +11184,11 @@ function buildSummaryFromEntries(entries) {
|
|
|
11104
11184
|
const suspicions = [];
|
|
11105
11185
|
const nextSteps = [];
|
|
11106
11186
|
const reflections = [];
|
|
11107
|
-
const VALUE_ORDER = { HIGH: 0, MED: 1, LOW: 2, NONE: 3 };
|
|
11108
11187
|
const reversed = [...entries].reverse();
|
|
11109
11188
|
for (const entry of reversed) {
|
|
11110
11189
|
const value = entry.memo.attackValue || "LOW";
|
|
11111
11190
|
for (const tool of entry.tools) {
|
|
11112
|
-
attempts.push({
|
|
11113
|
-
turn: entry.turn,
|
|
11114
|
-
phase: entry.phase,
|
|
11115
|
-
ok: tool.success,
|
|
11116
|
-
name: tool.name,
|
|
11117
|
-
input: tool.inputSummary,
|
|
11118
|
-
value
|
|
11119
|
-
});
|
|
11191
|
+
attempts.push({ turn: entry.turn, phase: entry.phase, ok: tool.success, name: tool.name, input: tool.inputSummary, value });
|
|
11120
11192
|
}
|
|
11121
11193
|
for (const finding of entry.memo.keyFindings) {
|
|
11122
11194
|
const line = `- [T${entry.turn}|\u26A1${value}] ${finding}`;
|
|
@@ -11155,9 +11227,13 @@ function buildSummaryFromEntries(entries) {
|
|
|
11155
11227
|
}
|
|
11156
11228
|
}
|
|
11157
11229
|
attempts.sort((a, b) => {
|
|
11158
|
-
const vd = (
|
|
11230
|
+
const vd = (ATTACK_VALUE_RANK[b.value] ?? 0) - (ATTACK_VALUE_RANK[a.value] ?? 0);
|
|
11159
11231
|
return vd !== 0 ? vd : b.turn - a.turn;
|
|
11160
11232
|
});
|
|
11233
|
+
return { attempts, findings, credentials, successes, failures, suspicions, nextSteps, reflections };
|
|
11234
|
+
}
|
|
11235
|
+
function formatSummaryMarkdown(buckets, entries) {
|
|
11236
|
+
const { attempts, findings, credentials, successes, failures, suspicions, nextSteps, reflections } = buckets;
|
|
11161
11237
|
const attemptLines = attempts.map(
|
|
11162
11238
|
(a) => `- [T${a.turn}|${a.phase}|\u26A1${a.value}] ${a.ok ? "\u2705" : "\u274C"} ${a.name}: ${a.input}`
|
|
11163
11239
|
);
|
|
@@ -11234,8 +11310,8 @@ function rotateOutputFiles() {
|
|
|
11234
11310
|
}
|
|
11235
11311
|
|
|
11236
11312
|
// src/agents/prompt-builder.ts
|
|
11237
|
-
var
|
|
11238
|
-
var PROMPTS_DIR = join11(
|
|
11313
|
+
var __dirname3 = dirname5(fileURLToPath3(import.meta.url));
|
|
11314
|
+
var PROMPTS_DIR = join11(__dirname3, "prompts");
|
|
11239
11315
|
var TECHNIQUES_DIR = join11(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
|
|
11240
11316
|
var { AGENT_FILES } = PROMPT_PATHS;
|
|
11241
11317
|
var PHASE_PROMPT_MAP = {
|
|
@@ -11537,12 +11613,20 @@ ${lines.join("\n")}
|
|
|
11537
11613
|
}
|
|
11538
11614
|
// --- §13: Session Journal Summary ---
|
|
11539
11615
|
/**
|
|
11540
|
-
* Load journal summary
|
|
11541
|
-
*
|
|
11542
|
-
* what was discovered. Main LLM uses this for continuity across many turns.
|
|
11616
|
+
* Load journal summary — prefers Summary Regenerator (⑥) output,
|
|
11617
|
+
* falls back to deterministic journal summary.
|
|
11543
11618
|
*/
|
|
11544
11619
|
getJournalFragment() {
|
|
11545
11620
|
try {
|
|
11621
|
+
const summaryPath = join11(WORKSPACE.TURNS, "summary.md");
|
|
11622
|
+
if (existsSync9(summaryPath)) {
|
|
11623
|
+
const summary2 = readFileSync6(summaryPath, "utf-8");
|
|
11624
|
+
if (summary2.trim()) {
|
|
11625
|
+
return `<session-journal>
|
|
11626
|
+
${summary2}
|
|
11627
|
+
</session-journal>`;
|
|
11628
|
+
}
|
|
11629
|
+
}
|
|
11546
11630
|
const summary = readJournalSummary();
|
|
11547
11631
|
if (!summary) return "";
|
|
11548
11632
|
return `<session-journal>
|
|
@@ -11566,9 +11650,9 @@ ${summary}
|
|
|
11566
11650
|
// src/agents/strategist.ts
|
|
11567
11651
|
import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
|
|
11568
11652
|
import { join as join12, dirname as dirname6 } from "path";
|
|
11569
|
-
import { fileURLToPath as
|
|
11570
|
-
var
|
|
11571
|
-
var STRATEGIST_PROMPT_PATH = join12(
|
|
11653
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
11654
|
+
var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
|
|
11655
|
+
var STRATEGIST_PROMPT_PATH = join12(__dirname4, "prompts", "strategist-system.md");
|
|
11572
11656
|
var Strategist = class {
|
|
11573
11657
|
llm;
|
|
11574
11658
|
state;
|
|
@@ -11626,7 +11710,14 @@ var Strategist = class {
|
|
|
11626
11710
|
sections.push(failures);
|
|
11627
11711
|
}
|
|
11628
11712
|
try {
|
|
11629
|
-
|
|
11713
|
+
let journalSummary = "";
|
|
11714
|
+
const summaryPath = join12(WORKSPACE.TURNS, "summary.md");
|
|
11715
|
+
if (existsSync10(summaryPath)) {
|
|
11716
|
+
journalSummary = readFileSync7(summaryPath, "utf-8").trim();
|
|
11717
|
+
}
|
|
11718
|
+
if (!journalSummary) {
|
|
11719
|
+
journalSummary = readJournalSummary();
|
|
11720
|
+
}
|
|
11630
11721
|
if (journalSummary) {
|
|
11631
11722
|
sections.push("");
|
|
11632
11723
|
sections.push("## Session Journal (past turns summary)");
|
|
@@ -11743,7 +11834,94 @@ Detect stalls (repeated failures, no progress) and force completely different at
|
|
|
11743
11834
|
Chain every finding: "If X works \u2192 immediately do Y \u2192 which enables Z."
|
|
11744
11835
|
Maximum 50 lines. Zero preamble. Direct imperatives only. Never repeat failed approaches.`;
|
|
11745
11836
|
|
|
11837
|
+
// src/shared/utils/turn-record.ts
|
|
11838
|
+
function formatTurnRecord(input) {
|
|
11839
|
+
const { turn, timestamp, phase, tools, memo: memo6, reflection } = input;
|
|
11840
|
+
const time = timestamp.slice(0, 19).replace("T", " ");
|
|
11841
|
+
const sections = [];
|
|
11842
|
+
sections.push(`# Turn ${turn} | ${time} | Phase: ${phase}`);
|
|
11843
|
+
sections.push("");
|
|
11844
|
+
sections.push("## \uC2E4\uD589 \uB3C4\uAD6C");
|
|
11845
|
+
if (tools.length === 0) {
|
|
11846
|
+
sections.push("- (\uB3C4\uAD6C \uC2E4\uD589 \uC5C6\uC74C)");
|
|
11847
|
+
} else {
|
|
11848
|
+
for (const tool of tools) {
|
|
11849
|
+
const status = tool.success ? "\u2705" : "\u274C";
|
|
11850
|
+
const line = `- ${tool.name}(${tool.inputSummary}) \u2192 ${status} ${tool.analystSummary}`;
|
|
11851
|
+
sections.push(line);
|
|
11852
|
+
}
|
|
11853
|
+
}
|
|
11854
|
+
sections.push("");
|
|
11855
|
+
sections.push("## \uD575\uC2EC \uC778\uC0AC\uC774\uD2B8");
|
|
11856
|
+
if (memo6.keyFindings.length > 0) {
|
|
11857
|
+
for (const f of memo6.keyFindings) sections.push(`- DISCOVERED: ${f}`);
|
|
11858
|
+
}
|
|
11859
|
+
if (memo6.credentials.length > 0) {
|
|
11860
|
+
for (const c of memo6.credentials) sections.push(`- CREDENTIAL: ${c}`);
|
|
11861
|
+
}
|
|
11862
|
+
if (memo6.attackVectors.length > 0) {
|
|
11863
|
+
for (const v of memo6.attackVectors) sections.push(`- CONFIRMED: ${v}`);
|
|
11864
|
+
}
|
|
11865
|
+
if (memo6.failures.length > 0) {
|
|
11866
|
+
for (const f of memo6.failures) sections.push(`- DEAD END: ${f}`);
|
|
11867
|
+
}
|
|
11868
|
+
if (memo6.suspicions.length > 0) {
|
|
11869
|
+
for (const s of memo6.suspicions) sections.push(`- SUSPICIOUS: ${s}`);
|
|
11870
|
+
}
|
|
11871
|
+
if (memo6.nextSteps.length > 0) {
|
|
11872
|
+
for (const n of memo6.nextSteps) sections.push(`- NEXT: ${n}`);
|
|
11873
|
+
}
|
|
11874
|
+
if (memo6.keyFindings.length === 0 && memo6.failures.length === 0 && memo6.credentials.length === 0) {
|
|
11875
|
+
sections.push("- (\uD2B9\uC774\uC0AC\uD56D \uC5C6\uC74C)");
|
|
11876
|
+
}
|
|
11877
|
+
sections.push("");
|
|
11878
|
+
sections.push("## \uC790\uAE30\uBC18\uC131");
|
|
11879
|
+
sections.push(reflection || "- (\uBC18\uC131 \uC5C6\uC74C)");
|
|
11880
|
+
sections.push("");
|
|
11881
|
+
return sections.join("\n");
|
|
11882
|
+
}
|
|
11883
|
+
function formatForExtraction(messages) {
|
|
11884
|
+
const parts = ["\uB2E4\uC74C\uC740 \uD39C\uD14C\uC2A4\uD305 \uC138\uC158\uC758 \uB300\uD654 \uAE30\uB85D\uC785\uB2C8\uB2E4. \uD575\uC2EC \uC778\uC0AC\uC774\uD2B8\uB97C \uCD94\uCD9C\uD558\uC138\uC694:\n"];
|
|
11885
|
+
for (const msg of messages) {
|
|
11886
|
+
const role = msg.role === "assistant" ? "AGENT" : msg.role === "user" ? "RESULT" : msg.role.toUpperCase();
|
|
11887
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
11888
|
+
const truncated = content.length > 3e3 ? content.slice(0, 1500) + "\n...(truncated)...\n" + content.slice(-1500) : content;
|
|
11889
|
+
parts.push(`[${role}]
|
|
11890
|
+
${truncated}
|
|
11891
|
+
`);
|
|
11892
|
+
}
|
|
11893
|
+
return parts.join("\n");
|
|
11894
|
+
}
|
|
11895
|
+
function formatReflectionInput(input) {
|
|
11896
|
+
const { tools, memo: memo6, phase } = input;
|
|
11897
|
+
const parts = [
|
|
11898
|
+
`\uD604\uC7AC Phase: ${phase}`,
|
|
11899
|
+
"",
|
|
11900
|
+
"\uC774\uBC88 \uD134 \uC2E4\uD589 \uACB0\uACFC:"
|
|
11901
|
+
];
|
|
11902
|
+
for (const tool of tools) {
|
|
11903
|
+
const status = tool.success ? "\u2705 \uC131\uACF5" : "\u274C \uC2E4\uD328";
|
|
11904
|
+
parts.push(`- ${tool.name}(${tool.inputSummary}) \u2192 ${status}`);
|
|
11905
|
+
if (tool.analystSummary) {
|
|
11906
|
+
parts.push(` \uC694\uC57D: ${tool.analystSummary}`);
|
|
11907
|
+
}
|
|
11908
|
+
}
|
|
11909
|
+
if (tools.length === 0) {
|
|
11910
|
+
parts.push("- (\uB3C4\uAD6C \uC2E4\uD589 \uC5C6\uC74C)");
|
|
11911
|
+
}
|
|
11912
|
+
parts.push("");
|
|
11913
|
+
parts.push("Analyst \uCD94\uCD9C \uBA54\uBAA8:");
|
|
11914
|
+
if (memo6.keyFindings.length > 0) parts.push(` \uBC1C\uACAC: ${memo6.keyFindings.join(", ")}`);
|
|
11915
|
+
if (memo6.credentials.length > 0) parts.push(` \uD06C\uB808\uB374\uC15C: ${memo6.credentials.join(", ")}`);
|
|
11916
|
+
if (memo6.failures.length > 0) parts.push(` \uC2E4\uD328: ${memo6.failures.join(", ")}`);
|
|
11917
|
+
if (memo6.suspicions.length > 0) parts.push(` \uC758\uC2EC: ${memo6.suspicions.join(", ")}`);
|
|
11918
|
+
parts.push(` \uACF5\uACA9 \uAC00\uCE58: ${memo6.attackValue}`);
|
|
11919
|
+
return parts.join("\n");
|
|
11920
|
+
}
|
|
11921
|
+
|
|
11746
11922
|
// src/agents/main-agent.ts
|
|
11923
|
+
import { writeFileSync as writeFileSync9, existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
11924
|
+
import { join as join13 } from "path";
|
|
11747
11925
|
var MainAgent = class extends CoreAgent {
|
|
11748
11926
|
promptBuilder;
|
|
11749
11927
|
strategist;
|
|
@@ -11797,6 +11975,45 @@ var MainAgent = class extends CoreAgent {
|
|
|
11797
11975
|
this.turnReflections = [];
|
|
11798
11976
|
const dynamicPrompt = await this.getCurrentPrompt();
|
|
11799
11977
|
const result2 = await super.step(iteration, messages, dynamicPrompt, progress);
|
|
11978
|
+
try {
|
|
11979
|
+
if (messages.length > 2) {
|
|
11980
|
+
const extraction = await this.llm.generateResponse(
|
|
11981
|
+
[{ role: "user", content: formatForExtraction(messages) }],
|
|
11982
|
+
void 0,
|
|
11983
|
+
CONTEXT_EXTRACTOR_PROMPT
|
|
11984
|
+
);
|
|
11985
|
+
if (extraction.content?.trim()) {
|
|
11986
|
+
messages.length = 0;
|
|
11987
|
+
messages.push({
|
|
11988
|
+
role: "user",
|
|
11989
|
+
content: `<session-context>
|
|
11990
|
+
${extraction.content.trim()}
|
|
11991
|
+
</session-context>`
|
|
11992
|
+
});
|
|
11993
|
+
}
|
|
11994
|
+
}
|
|
11995
|
+
} catch {
|
|
11996
|
+
}
|
|
11997
|
+
try {
|
|
11998
|
+
if (this.turnToolJournal.length > 0) {
|
|
11999
|
+
const reflection = await this.llm.generateResponse(
|
|
12000
|
+
[{
|
|
12001
|
+
role: "user",
|
|
12002
|
+
content: formatReflectionInput({
|
|
12003
|
+
tools: this.turnToolJournal,
|
|
12004
|
+
memo: this.turnMemo,
|
|
12005
|
+
phase: this.state.getPhase()
|
|
12006
|
+
})
|
|
12007
|
+
}],
|
|
12008
|
+
void 0,
|
|
12009
|
+
REFLECTION_PROMPT
|
|
12010
|
+
);
|
|
12011
|
+
if (reflection.content?.trim()) {
|
|
12012
|
+
this.turnReflections.push(reflection.content.trim());
|
|
12013
|
+
}
|
|
12014
|
+
}
|
|
12015
|
+
} catch {
|
|
12016
|
+
}
|
|
11800
12017
|
if (this.turnToolJournal.length > 0) {
|
|
11801
12018
|
try {
|
|
11802
12019
|
const entry = {
|
|
@@ -11808,7 +12025,50 @@ var MainAgent = class extends CoreAgent {
|
|
|
11808
12025
|
reflection: this.turnReflections.length > 0 ? this.turnReflections.join(" | ") : this.turnMemo.nextSteps.join("; ")
|
|
11809
12026
|
};
|
|
11810
12027
|
writeJournalEntry(entry);
|
|
11811
|
-
|
|
12028
|
+
try {
|
|
12029
|
+
ensureDirExists(WORKSPACE.TURNS);
|
|
12030
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
12031
|
+
const turnFileName = `turn-${String(this.turnCounter).padStart(3, "0")}_${ts}.md`;
|
|
12032
|
+
const turnPath = join13(WORKSPACE.TURNS, turnFileName);
|
|
12033
|
+
const turnContent = formatTurnRecord({
|
|
12034
|
+
turn: this.turnCounter,
|
|
12035
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12036
|
+
phase: this.state.getPhase(),
|
|
12037
|
+
tools: this.turnToolJournal,
|
|
12038
|
+
memo: this.turnMemo,
|
|
12039
|
+
reflection: entry.reflection
|
|
12040
|
+
});
|
|
12041
|
+
writeFileSync9(turnPath, turnContent, "utf-8");
|
|
12042
|
+
} catch {
|
|
12043
|
+
}
|
|
12044
|
+
try {
|
|
12045
|
+
const summaryPath = join13(WORKSPACE.TURNS, "summary.md");
|
|
12046
|
+
const existingSummary = existsSync11(summaryPath) ? readFileSync8(summaryPath, "utf-8") : "";
|
|
12047
|
+
const turnData = formatTurnRecord({
|
|
12048
|
+
turn: this.turnCounter,
|
|
12049
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12050
|
+
phase: this.state.getPhase(),
|
|
12051
|
+
tools: this.turnToolJournal,
|
|
12052
|
+
memo: this.turnMemo,
|
|
12053
|
+
reflection: entry.reflection
|
|
12054
|
+
});
|
|
12055
|
+
const summaryResponse = await this.llm.generateResponse(
|
|
12056
|
+
[{
|
|
12057
|
+
role: "user",
|
|
12058
|
+
content: existingSummary ? `\uAE30\uC874 \uC694\uC57D:
|
|
12059
|
+
${existingSummary}
|
|
12060
|
+
|
|
12061
|
+
\uC774\uBC88 \uD134:
|
|
12062
|
+
${turnData}` : `\uCCAB \uD134 \uB370\uC774\uD130:
|
|
12063
|
+
${turnData}`
|
|
12064
|
+
}],
|
|
12065
|
+
void 0,
|
|
12066
|
+
SUMMARY_REGENERATOR_PROMPT
|
|
12067
|
+
);
|
|
12068
|
+
if (summaryResponse.content?.trim()) {
|
|
12069
|
+
writeFileSync9(summaryPath, summaryResponse.content.trim(), "utf-8");
|
|
12070
|
+
}
|
|
12071
|
+
} catch {
|
|
11812
12072
|
regenerateJournalSummary();
|
|
11813
12073
|
}
|
|
11814
12074
|
rotateJournalEntries();
|
package/dist/prompts/base.md
CHANGED
|
@@ -596,4 +596,24 @@ Ask yourself at every Reflect step:
|
|
|
596
596
|
8. **Search when stuck** — `web_search` and `browse_url` are the most powerful weapons
|
|
597
597
|
9. **Write code directly if needed** — write scripts with `write_file` → execute with `run_cmd`
|
|
598
598
|
|
|
599
|
+
## 📂 Session Memory — Past Turn Records
|
|
599
600
|
|
|
601
|
+
Your past actions and insights are saved as files. Use them freely:
|
|
602
|
+
|
|
603
|
+
```
|
|
604
|
+
.pentesting/memory/turns/
|
|
605
|
+
├── summary.md ← Full session summary (updated every turn)
|
|
606
|
+
├── turn-001_2026-02-21T08-30-15.md ← Turn 1 details
|
|
607
|
+
├── turn-002_2026-02-21T08-31-22.md ← Turn 2 details
|
|
608
|
+
└── ...
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Each turn file has 3 sections:**
|
|
612
|
+
- `## 실행 도구` — Exact commands/tools/arguments used
|
|
613
|
+
- `## 핵심 인사이트` — What was discovered, confirmed, or failed
|
|
614
|
+
- `## 자기반성` — Turn assessment and next priority
|
|
615
|
+
|
|
616
|
+
**How to use:**
|
|
617
|
+
- `summary.md` gives you the full picture — read it to understand where you stand
|
|
618
|
+
- Need details of a specific past turn? → `read_file(".pentesting/memory/turns/turn-005_...")`
|
|
619
|
+
- All past findings, credentials, dead ends are preserved — never lost
|