@pensar/apex 1.8.0 → 1.8.2-canary.fb75c486

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +11 -0
  2. package/build/agent-6dj1qm50.js +221 -0
  3. package/build/agent-6xr8vpgm.js +28 -0
  4. package/build/agent-x1htbpe3.js +22 -0
  5. package/build/apps-t0gmwc7z.js +446 -0
  6. package/build/{auth-dxjgy41e.js → auth-p4r1m7xq.js} +50 -13
  7. package/build/authentication-je2b0c3w.js +22 -0
  8. package/build/blackboxAgent-a4jnt0y5.js +22 -0
  9. package/build/{blackboxPentest-8ps4yvbk.js → blackboxPentest-b5741n3h.js} +19 -17
  10. package/build/{cli-y61d9433.js → cli-0tnv1vkp.js} +138 -38
  11. package/build/{cli-jg7r7y5n.js → cli-4xb21y6g.js} +30 -2
  12. package/build/{cli-k0tckznm.js → cli-6p7d2k55.js} +39701 -31695
  13. package/build/cli-87zakjb2.js +17 -0
  14. package/build/{authentication-e30mfzbe.js → cli-8frjr68r.js} +11 -18
  15. package/build/cli-8xknm7d9.js +204 -0
  16. package/build/cli-9egg9azd.js +22 -0
  17. package/build/cli-9fsre5pt.js +0 -0
  18. package/build/cli-abbka8n3.js +501 -0
  19. package/build/{cli-3y0dgy56.js → cli-c8131c4q.js} +2 -2
  20. package/build/cli-e08r86zk.js +24 -0
  21. package/build/{cli-0ghkg3w6.js → cli-e6rgwtpb.js} +19950 -18556
  22. package/build/cli-g5h24ny8.js +197 -0
  23. package/build/{cli-nr1cjfr9.js → cli-gtcd5c3f.js} +26 -7
  24. package/build/cli-k0730f59.js +52 -0
  25. package/build/{cli-tp1tqn3k.js → cli-mswm4k81.js} +1 -1
  26. package/build/{cli-m788e4f3.js → cli-q8dfq25x.js} +584 -33
  27. package/build/cli-rhry8mat.js +7213 -0
  28. package/build/{cli-g8t710ew.js → cli-ryy39d77.js} +253 -250
  29. package/build/cli-s1nckt4k.js +20 -0
  30. package/build/{cli-k4hrygff.js → cli-v9ds4jb8.js} +9 -5
  31. package/build/{cli-dqt80sw3.js → cli-w5990vr6.js} +199 -68
  32. package/build/{cli-3w2syxpv.js → cli-wfmdch3r.js} +102695 -104816
  33. package/build/cli.js +351 -280
  34. package/build/config-3bvtf3j8.js +188 -0
  35. package/build/{doctor-8tva8j99.js → doctor-2bkpddws.js} +1 -1
  36. package/build/{fixes-q5bhgxhc.js → fixes-60k3ts71.js} +23 -4
  37. package/build/{index-pfee23kv.js → index-0gp3x2r8.js} +19306 -18954
  38. package/build/index-861hkebg.js +12 -0
  39. package/build/{index-y5xpp21a.js → index-acc00eq4.js} +77 -108
  40. package/build/index-acdgrqa0.js +36 -0
  41. package/build/{index-e898mdyh.js → index-cfberehw.js} +4 -2
  42. package/build/{index-wfeb2gcc.js → index-hxn4rk8f.js} +9 -11
  43. package/build/{index-dw1xbhfn.js → index-vc29b21w.js} +161 -26
  44. package/build/index-vwt27stc.js +184 -0
  45. package/build/{issues-qbmdneej.js → issues-1bynat5q.js} +33 -9
  46. package/build/{logs-xm5vbymy.js → logs-e78vx2dy.js} +23 -4
  47. package/build/{main-3d7dfdvs.js → main-3zneyg7p.js} +93 -17
  48. package/build/{offesecAgent-re6kt2ff.js → offesecAgent-w9m0svwk.js} +14 -11
  49. package/build/parse-15kqmy2v.js +207 -0
  50. package/build/pentest-gpvqpvmd.js +31 -0
  51. package/build/{pentests-e3rj5845.js → pentests-nq7wa8yb.js} +36 -17
  52. package/build/{targetedPentest-fs0v570s.js → targetedPentest-fjxqn089.js} +15 -12
  53. package/build/threatModel-9yqx7d7x.js +29 -0
  54. package/build/{uninstall-qb2xbh2t.js → uninstall-9zbf4cwc.js} +6 -4
  55. package/build/{utils-jf52rmrb.js → utils-dh1t2r1e.js} +13 -10
  56. package/package.json +86 -88
  57. package/build/agent-4d8j2jsw.js +0 -278
  58. package/build/agent-z2s6h7n2.js +0 -19
  59. package/build/blackboxAgent-j9pczwym.js +0 -19
  60. package/build/cli-03z6pswp.js +0 -1423
  61. package/build/cli-0fy9j5dw.js +0 -61
  62. package/build/cli-asyas1xb.js +0 -110
  63. package/build/cli-dj1dgw2n.js +0 -190
  64. package/build/cli-q7r2sth7.js +0 -103
  65. package/build/cli-vkwch0bc.js +0 -1207
  66. package/build/cli-wr7g9qcr.js +0 -645
  67. package/build/index-bz6f8jry.js +0 -32
  68. package/build/pentest-mfm4hake.js +0 -29
  69. package/build/projects-qk22qcbt.js +0 -35
  70. package/build/threatModel-xfvc6cch.js +0 -67
@@ -1,1207 +0,0 @@
1
- import {
2
- REPORT_FILENAME_JSON,
3
- REPORT_FILENAME_MD,
4
- buildManifestEntries,
5
- buildPentestReport,
6
- finalizeManifest,
7
- formatDurationHmsFromMs,
8
- getCompletedAgentIds,
9
- loadSubagentMessages,
10
- readAgentManifest,
11
- renderJson,
12
- renderMarkdown,
13
- saveSubagentData,
14
- updateManifestEntryStatus,
15
- writeAgentManifest,
16
- writeExecutionMetrics
17
- } from "./cli-wr7g9qcr.js";
18
- import {
19
- TargetedPentestAgent,
20
- buildPentestSystemPrompt
21
- } from "./cli-y61d9433.js";
22
- import {
23
- EndpointSchema
24
- } from "./cli-0fy9j5dw.js";
25
- import {
26
- BlackboxAttackSurfaceAgent
27
- } from "./cli-k4hrygff.js";
28
- import {
29
- createThreatModelPrompt
30
- } from "./cli-fw5r7pfj.js";
31
- import {
32
- CodeAgent
33
- } from "./cli-jg7r7y5n.js";
34
- import {
35
- FindingsRegistry,
36
- OffensiveSecurityAgent,
37
- PLAN_MODE_TOOL_NAMES
38
- } from "./cli-3w2syxpv.js";
39
- import {
40
- hasToolCall
41
- } from "./cli-k0tckznm.js";
42
- import {
43
- exports_external1 as exports_external,
44
- init_zod
45
- } from "./cli-0ghkg3w6.js";
46
-
47
- // src/core/workflows/pentest.ts
48
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
49
- import { join as join3 } from "path";
50
-
51
- // src/core/utils/concurrency.ts
52
- async function runWithBoundedConcurrency(items, concurrency, fn, abortSignal) {
53
- const results = new Array(items.length).fill(null);
54
- let nextIdx = 0;
55
- let completed = 0;
56
- await new Promise((resolve) => {
57
- if (items.length === 0) {
58
- resolve();
59
- return;
60
- }
61
- let active = 0;
62
- function canLaunchMore() {
63
- return active < concurrency && nextIdx < items.length && !abortSignal?.aborted;
64
- }
65
- function next() {
66
- if (completed >= nextIdx && !canLaunchMore()) {
67
- resolve();
68
- return;
69
- }
70
- while (canLaunchMore()) {
71
- const idx = nextIdx++;
72
- active++;
73
- fn(items[idx], idx).then((r) => {
74
- results[idx] = r;
75
- }).catch(() => {
76
- results[idx] = null;
77
- }).finally(() => {
78
- active--;
79
- completed++;
80
- next();
81
- });
82
- }
83
- }
84
- next();
85
- });
86
- return results;
87
- }
88
-
89
- // src/core/session/loader.ts
90
- import { join } from "path";
91
- import { existsSync, readFileSync } from "fs";
92
- function loadAttackSurfaceResults(rootPath) {
93
- const resultsPath = join(rootPath, "attack-surface-results.json");
94
- if (!existsSync(resultsPath)) {
95
- return null;
96
- }
97
- try {
98
- return JSON.parse(readFileSync(resultsPath, "utf-8"));
99
- } catch (e) {
100
- console.error("Failed to load attack surface results:", e);
101
- return null;
102
- }
103
- }
104
-
105
- // src/core/workflows/whiteboxAttackSurface.ts
106
- init_zod();
107
- import {
108
- writeFileSync,
109
- mkdirSync,
110
- readdirSync,
111
- readFileSync as readFileSync2,
112
- existsSync as existsSync2,
113
- statSync
114
- } from "fs";
115
- import { join as join2 } from "path";
116
- var DEFAULT_CONCURRENCY = 5;
117
- function sanitizeName(name) {
118
- return name.toLowerCase().replace(/[^a-z0-9-_.]/g, "_");
119
- }
120
- var WHITEBOX_CODE_AGENT_SYSTEM_PROMPT = `You are an expert source-code analyst with direct filesystem access. You will be given a specific objective — focus exclusively on completing it.
121
-
122
- Your focus is on **deployed applications and services** — APIs, web apps, microservices — that listen on a port and serve traffic, as well as **owned cloud resources** (S3 buckets, cloud storage, CDN origins, etc.) that are part of the attack surface. Ignore libraries, shared packages, SDKs, CLI tools, build scripts, and test suites unless they are part of a deployable service.
123
-
124
- # Tool Usage Guide
125
-
126
- ## read_file
127
- Read the contents of any file. You can read the whole file or a specific line range.
128
- - When a file is large, read it in chunks using startLine / endLine to stay focused.
129
- - Follow imports and references — when you see an interesting function call, read its source.
130
-
131
- ## list_files
132
- List files and directories. Use this to orient yourself in the codebase.
133
- - Start by listing the project root or relevant subdirectory to understand the structure.
134
- - Use recursive=true sparingly on targeted subdirectories to avoid flooding context.
135
-
136
- ## grep
137
- Search file contents by pattern. This is your most powerful navigation tool.
138
- - Use it to find route definitions, middleware, controllers, endpoint registrations, etc.
139
- - Use -i for case-insensitive searches.
140
- - Use --include="*.ext" to narrow to relevant file types.
141
- - Use -C 3 or -C 5 to get context around matches.
142
- - Use -rn (default for directories) for recursive search with line numbers.
143
- - Use -l to get just file paths when you need a broad overview of where something appears.
144
-
145
- ## execute_command
146
- Run shell commands when needed.
147
- - Use for build tools, git operations, package managers, linters, etc.
148
-
149
- ## document_app
150
- Use this to document each application/service you identify. Persists a JSON record to the session's apps directory.
151
-
152
- ## document_endpoint
153
- **This is your primary output tool for endpoints.** Use it to document every endpoint you discover. Each call persists a JSON record to the session's endpoints directory, organized by app.
154
-
155
- **HARD RULE — call this tool DIRECTLY, one route at a time.** The moment you have enough information about a route to document it, your very next tool call must be \`document_endpoint\` for that route. Do not defer. Do not batch. Do not collect routes into a list to "process later."
156
-
157
- **You MUST NOT, under any circumstances:**
158
- - Build a manifest, JSON file, list, or array of routes to document later (e.g. \`cat > /tmp/pages.json << EOF [...] EOF\`).
159
- - Write a shell, Python, or any other script whose purpose is to generate \`document_endpoint\` tool calls.
160
- - Use a single message to "summarize all the routes I'll document" before documenting them.
161
- - Stop documentation early because you "have enough" or it's "getting repetitive." If you discovered N routes, you must produce N \`document_endpoint\` calls.
162
-
163
- These patterns silently truncate at output-token limits and routes get dropped. The only correct workflow is: discover a route → call \`document_endpoint\` for it → discover the next route → call \`document_endpoint\` for it → ... until every route is documented. Repetition is expected and required.
164
-
165
- **CRITICAL — endpoint documentation rules:**
166
- - **One entry per unique route path.** Do NOT create separate entries for different HTTP methods on the same path. If \`/api/users\` supports GET, POST, and DELETE, that is ONE entry with \`method: ["GET", "POST", "DELETE"]\`.
167
- - **Use \`method: "PAGE"\`** for web pages and views (non-API routes).
168
- - **Always set \`appName\`** to the application name provided in your objective.
169
- - **Always set \`routePath\`** to the HTTP route this endpoint serves (e.g., \`/api/users/:id\`, \`/dashboard\`). This is the URL path a client requests — NOT a source-file path.
170
- - **Always set \`file\`** to the source-code file where the route is defined (e.g., \`src/routes/users.ts\`). This is NOT the HTTP route.
171
- - **Set \`line\`** to the line number when determinable.
172
- - **Set \`handler\`** to the handler function or component name.
173
- - **Set \`authRequired\`** to true/false based on middleware, guards, or decorators.
174
-
175
-
176
- ## response
177
- When your objective includes structured output, call \`response\` with your final results once you are done. This ends your run.
178
-
179
- # Working Approach
180
- 1. **Orient first** — list files and read key entry points to understand the structure.
181
- 2. **Ignore submodules** — check for a \`.gitmodules\` file or run \`git submodule status\`. Any directories that are git submodules are external dependencies and must be **completely excluded** from your analysis.
182
- 3. **Search, then read** — use grep to locate what you need, then read the relevant files.
183
- 4. **Document each item the instant you discover it** — every \`document_app\` / \`document_endpoint\` call must be made directly, one item per call, immediately after you identify it. Never collect items into a manifest, JSON file, or batch script. If you find yourself thinking "let me list all of these and then document them," stop — that pattern silently drops items when output tokens run out.
184
- 5. **Follow the trail** — trace through imports, function calls, and references to build full understanding.
185
- 6. **Be thorough** — don't stop at the first match. Cover everything relevant to the objective. Repetitive \`document_endpoint\` calls are expected; do not summarize, deduplicate, or shortcut them.
186
- `;
187
- var AppInfoSchema = exports_external.object({
188
- name: exports_external.string().describe("Application or service name"),
189
- framework: exports_external.string().describe("Framework or cloud service (e.g. Express, Next.js, Django, FastAPI, Rails, AWS S3, CloudFront)"),
190
- description: exports_external.string().describe("Brief description of what this app does"),
191
- location: exports_external.string().describe("Path to the app root relative to the repository root, or resource identifier for cloud resources"),
192
- type: exports_external.enum([
193
- "web_application",
194
- "api",
195
- "full_stack",
196
- "domain",
197
- "subdomain",
198
- "database",
199
- "cloud_resource",
200
- "storage"
201
- ]).default("web_application").describe("Application type — web_application for frontend apps, api for backend services, " + "full_stack for frameworks like Next.js/Remix that serve both, " + "database for databases, cloud_resource for owned cloud infra, storage for S3/GCS/blob storage")
202
- });
203
- var AppsDiscoveryResultSchema = exports_external.object({
204
- repoType: exports_external.string().describe("e.g. monorepo, single-app, multi-package"),
205
- packageManager: exports_external.string().describe("e.g. npm, yarn, pnpm, pip, cargo, go modules"),
206
- apps: exports_external.array(AppInfoSchema).describe("All applications/services discovered in the repository")
207
- });
208
- var DiscoverySummarySchema = exports_external.object({
209
- endpointsDocumented: exports_external.number().describe("Number of endpoints documented via document_endpoint"),
210
- summary: exports_external.string().describe("Brief summary of what was found")
211
- });
212
- async function runWhiteboxAttackSurfaceWorkflow(input) {
213
- const {
214
- codebasePath,
215
- model,
216
- session,
217
- authConfig,
218
- abortSignal,
219
- eventBus,
220
- attackSurfaceRegistry,
221
- onStepFinish,
222
- onCacheMetrics,
223
- domains,
224
- projectThreatModel,
225
- environments
226
- } = input;
227
- const appsAgent = new CodeAgent({
228
- codebasePath,
229
- objective: buildAppsDiscoveryObjective(codebasePath, domains, environments),
230
- system: WHITEBOX_CODE_AGENT_SYSTEM_PROMPT,
231
- model,
232
- session,
233
- authConfig,
234
- abortSignal,
235
- attackSurfaceRegistry,
236
- eventBus,
237
- subagentId: "whitebox-apps-discovery",
238
- onStepFinish: (event) => onStepFinish?.(event),
239
- onCacheMetrics,
240
- responseSchema: AppsDiscoveryResultSchema,
241
- projectThreatModel
242
- });
243
- console.log(`[whitebox-workflow] Phase 1: discovering apps in ${codebasePath}${domains?.length ? ` (${domains.length} known domains)` : ""}`);
244
- eventBus?.emit("subagent-spawn", {
245
- subagentId: "whitebox-apps-discovery",
246
- name: "Whitebox Apps Discovery",
247
- input: { codebasePath }
248
- });
249
- const appsResult = await appsAgent.consume();
250
- eventBus?.emit("subagent-complete", {
251
- subagentId: "whitebox-apps-discovery",
252
- status: "completed"
253
- });
254
- console.log(`[whitebox-workflow] Phase 1 complete: ${appsResult?.apps.length ?? 0} apps discovered` + (appsResult ? ` (repoType=${appsResult.repoType}, packageManager=${appsResult.packageManager})` : " (no result returned)"));
255
- if (appsResult?.apps.length) {
256
- for (const app of appsResult.apps) {
257
- console.log(`[whitebox-workflow] app: "${app.name}" type=${app.type} framework="${app.framework}" location="${app.location}"`);
258
- }
259
- }
260
- if (!appsResult || appsResult.apps.length === 0) {
261
- return {
262
- repoType: appsResult?.repoType ?? "unknown",
263
- packageManager: appsResult?.packageManager ?? "unknown",
264
- apps: [],
265
- summary: {
266
- totalApps: 0,
267
- totalPages: 0,
268
- totalApiEndpoints: 0,
269
- totalPentestObjectives: 0
270
- }
271
- };
272
- }
273
- const assetsPath = join2(session.rootPath, "assets");
274
- mkdirSync(assetsPath, { recursive: true });
275
- for (const app of appsResult.apps) {
276
- const appDir = join2(assetsPath, sanitizeName(app.name));
277
- mkdirSync(appDir, { recursive: true });
278
- const metadata = {
279
- name: app.name,
280
- type: app.type,
281
- framework: app.framework,
282
- description: app.description,
283
- location: app.location
284
- };
285
- writeFileSync(join2(appDir, "app.json"), JSON.stringify(metadata, null, 2), "utf-8");
286
- }
287
- const NON_SERVICE_TYPES = ["cloud_resource", "storage", "database"];
288
- const serviceApps = appsResult.apps.filter((app) => !NON_SERVICE_TYPES.includes(app.type));
289
- const cloudApps = appsResult.apps.filter((app) => NON_SERVICE_TYPES.includes(app.type));
290
- console.log(`[whitebox-workflow] Phase 2: ${serviceApps.length} service apps (pages+api each), ${cloudApps.length} cloud resources → ${serviceApps.length * 2 + cloudApps.length} total tasks`);
291
- const totalApps = appsResult.apps.length;
292
- let completedAppCount = 0;
293
- eventBus?.emit("app-analysis-progress", {
294
- totalApps,
295
- completedApps: 0
296
- });
297
- const tasks = [
298
- ...serviceApps.flatMap((app) => [
299
- { appInfo: app, type: "pages" },
300
- { appInfo: app, type: "apiEndpoints" }
301
- ]),
302
- ...cloudApps.map((app) => ({
303
- appInfo: app,
304
- type: "cloudResourceEndpoints"
305
- }))
306
- ];
307
- const appTaskDoneCount = new Map;
308
- for (const app of serviceApps) {
309
- appTaskDoneCount.set(app.name, { done: 0, total: 2 });
310
- }
311
- for (const app of cloudApps) {
312
- appTaskDoneCount.set(app.name, { done: 0, total: 1 });
313
- }
314
- await runWithBoundedConcurrency(tasks, DEFAULT_CONCURRENCY, async (task, _index) => {
315
- const subagentId = `${task.type}-${task.appInfo.name}`;
316
- console.log(`[whitebox-workflow] Phase 2: spawning agent "${subagentId}" (app="${task.appInfo.name}", type=${task.type}, appType=${task.appInfo.type})`);
317
- eventBus?.emit("subagent-spawn", {
318
- subagentId,
319
- name: task.appInfo.name,
320
- input: { app: task.appInfo.name, type: task.type }
321
- });
322
- const objective = task.type === "pages" ? buildPagesDiscoveryObjective(codebasePath, task.appInfo) : task.type === "cloudResourceEndpoints" ? buildCloudResourceEndpointsObjective(codebasePath, task.appInfo, environments) : buildApiEndpointsDiscoveryObjective(codebasePath, task.appInfo);
323
- const agent = new CodeAgent({
324
- codebasePath,
325
- objective,
326
- system: WHITEBOX_CODE_AGENT_SYSTEM_PROMPT,
327
- model,
328
- session,
329
- authConfig,
330
- abortSignal,
331
- attackSurfaceRegistry,
332
- eventBus,
333
- subagentId,
334
- onStepFinish: (event) => onStepFinish?.(event),
335
- onCacheMetrics,
336
- responseSchema: DiscoverySummarySchema,
337
- excludeTools: ["document_app"],
338
- projectThreatModel
339
- });
340
- try {
341
- await agent.consume();
342
- console.log(`[whitebox-workflow] Phase 2: agent "${subagentId}" completed`);
343
- eventBus?.emit("subagent-complete", {
344
- subagentId,
345
- status: "completed"
346
- });
347
- } catch (error) {
348
- console.error(`[whitebox-workflow] Phase 2: agent "${subagentId}" FAILED:`, error instanceof Error ? error.message : String(error));
349
- eventBus?.emit("subagent-complete", {
350
- subagentId,
351
- status: "failed"
352
- });
353
- }
354
- const counter = appTaskDoneCount.get(task.appInfo.name);
355
- if (counter) {
356
- counter.done++;
357
- if (counter.done >= counter.total) {
358
- completedAppCount++;
359
- eventBus?.emit("app-analysis-progress", {
360
- totalApps,
361
- completedApps: completedAppCount,
362
- appName: task.appInfo.name
363
- });
364
- }
365
- }
366
- });
367
- console.log(`[whitebox-workflow] Phase 3: reading assets from ${assetsPath}`);
368
- const {
369
- apps: parsedApps,
370
- repoType,
371
- packageManager
372
- } = readAppsFromAssetsDirectory(assetsPath, appsResult);
373
- for (const app of parsedApps) {
374
- console.log(`[whitebox-workflow] Phase 3: "${app.name}" → ${app.pages.length} pages, ${app.apiEndpoints.length} API endpoints`);
375
- }
376
- const apps = parsedApps;
377
- const totalPages = apps.reduce((sum, a) => sum + a.pages.length, 0);
378
- const totalApiEndpoints = apps.reduce((sum, a) => sum + a.apiEndpoints.length, 0);
379
- const totalPentestObjectives = apps.reduce((sum, a) => sum + [...a.pages, ...a.apiEndpoints].reduce((s, ep) => s + ep.pentestObjectives.length, 0), 0);
380
- return {
381
- repoType,
382
- packageManager,
383
- apps,
384
- summary: {
385
- totalApps: apps.length,
386
- totalPages,
387
- totalApiEndpoints,
388
- totalPentestObjectives
389
- }
390
- };
391
- }
392
- function readAppsFromAssetsDirectory(assetsPath, appsDiscovery) {
393
- const repoType = appsDiscovery?.repoType ?? "unknown";
394
- const packageManager = appsDiscovery?.packageManager ?? "unknown";
395
- if (!existsSync2(assetsPath)) {
396
- console.log(`[readAssets] Assets directory does not exist: ${assetsPath}`);
397
- return { apps: [], repoType, packageManager };
398
- }
399
- const entries = readdirSync(assetsPath);
400
- console.log(`[readAssets] Found ${entries.length} entries in ${assetsPath}: [${entries.join(", ")}]`);
401
- const apps = [];
402
- for (const entry of entries) {
403
- const entryPath = join2(assetsPath, entry);
404
- if (!statSync(entryPath).isDirectory()) {
405
- console.log(`[readAssets] Skipping non-directory: ${entry}`);
406
- continue;
407
- }
408
- const appJsonPath = join2(entryPath, "app.json");
409
- let metadata;
410
- if (existsSync2(appJsonPath)) {
411
- try {
412
- metadata = JSON.parse(readFileSync2(appJsonPath, "utf-8"));
413
- } catch {
414
- console.warn(`[readAssets] Skipping app folder with unreadable app.json: ${entry}`);
415
- continue;
416
- }
417
- } else {
418
- console.log(`[readAssets] Skipping folder without app.json: ${entry}`);
419
- continue;
420
- }
421
- const pages = [];
422
- const apiEndpoints = [];
423
- const assetFiles = readdirSync(entryPath).filter((f) => f.endsWith(".json") && f !== "app.json");
424
- console.log(`[readAssets] App "${metadata.name}" (${entry}): ${assetFiles.length} asset files`);
425
- let parseFailed = 0;
426
- for (const file of assetFiles) {
427
- try {
428
- const raw = readFileSync2(join2(entryPath, file), "utf-8");
429
- const data = JSON.parse(raw);
430
- const endpoint = assetRecordToEndpoint(data);
431
- if (!endpoint) {
432
- console.log(`[readAssets] ${file}: failed schema validation (assetRecordToEndpoint returned null)`);
433
- parseFailed++;
434
- continue;
435
- }
436
- if (isPageEndpoint(data)) {
437
- pages.push(endpoint);
438
- } else {
439
- apiEndpoints.push(endpoint);
440
- }
441
- } catch {
442
- console.warn(`[readAssets] Skipping unreadable asset file: ${entry}/${file}`);
443
- parseFailed++;
444
- }
445
- }
446
- console.log(`[readAssets] App "${metadata.name}": ${pages.length} pages, ${apiEndpoints.length} API endpoints, ${parseFailed} failed`);
447
- apps.push({
448
- name: metadata.name,
449
- type: metadata.type ?? "web_application",
450
- framework: metadata.framework,
451
- description: metadata.description,
452
- location: metadata.location,
453
- pages,
454
- apiEndpoints
455
- });
456
- }
457
- return { apps, repoType, packageManager };
458
- }
459
- function assetRecordToEndpoint(record) {
460
- const rawMethod = record.method;
461
- const method = Array.isArray(rawMethod) ? rawMethod.join(", ") : rawMethod ?? "UNKNOWN";
462
- const path = record.routePath;
463
- const file = record.file ?? "";
464
- const parsed = EndpointSchema.safeParse({
465
- method,
466
- path,
467
- handler: record.handler,
468
- file,
469
- line: record.line,
470
- authRequired: record.authRequired,
471
- description: record.description,
472
- pentestObjectives: record.pentestObjectives ?? [],
473
- riskScore: record.riskScore,
474
- threatModel: record.threatModel
475
- });
476
- return parsed.success ? parsed.data : null;
477
- }
478
- function isPageEndpoint(record) {
479
- const method = record.method;
480
- if (typeof method === "string") {
481
- return method.toUpperCase() === "PAGE";
482
- }
483
- if (Array.isArray(method)) {
484
- return method.length === 1 && method[0].toUpperCase() === "PAGE";
485
- }
486
- return false;
487
- }
488
- function buildAppsDiscoveryObjective(codebasePath, domains, environments) {
489
- const domainSection = domains?.length ? `
490
- ## Known Domains
491
- The following domains are associated with this project. When you document an application, set the \`domain\` field on \`document_app\` if you can determine which domain the app is served from:
492
- ${domains.map((d) => `- ${d}`).join(`
493
- `)}
494
- ` : "";
495
- const environmentsSection = environments?.length ? `
496
- ## Target Environments
497
- This project is deployed to the following environments:
498
- ${environments.map((e) => `- **${e}**`).join(`
499
- `)}
500
-
501
- **Per-environment app creation:** When infrastructure-as-code or configuration defines resources that are dynamically named per environment (e.g. environment-prefixed S3 buckets, stage-scoped databases, per-environment API endpoints), you MUST create a **separate app entry for each environment**. Use the environment name as a prefix in the app name (e.g. \`${environments[0]}-user-uploads-bucket\`, \`${environments.length > 1 ? environments[1] : "staging"}-api-gateway\`).
502
-
503
- **How to identify environment-scoped resources:**
504
- - IaC that interpolates a stage/environment variable into resource names (e.g. \`\${stage}-my-bucket\`, \`\${env}-api\`, \`$app.$stage.example.com\`)
505
- - Separate config blocks, Terraform workspaces, SST stages, or CDK stacks per environment
506
- - Environment variables or config files that change resource identifiers per stage
507
-
508
- **For each environment** (${environments.join(", ")}), create an app entry with:
509
- - **name**: \`<environment>-<resource-name>\` (e.g. \`${environments[0]}-data-bucket\`)
510
- - **domain**: Substitute the environment name into the IaC naming pattern to derive the environment-specific URL (e.g. IaC has \`\${stage}-data\` → \`https://${environments[0]}-data.s3.amazonaws.com\`). Omit if no naming pattern exists in the code.
511
-
512
- **Shared resources:** If a resource is clearly shared across all environments (e.g. a single CDN distribution, a shared auth service), document it once without an environment prefix.
513
- ` : "";
514
- return `# Identify All Applications in the Repository
515
-
516
- ## Codebase
517
- - **Path:** ${codebasePath}
518
- ${domainSection}${environmentsSection}
519
- ## Task
520
- Analyze the repository structure and identify every **deployed application or service** (APIs, web apps, microservices) defined within it. Also discover **cloud resources and external services** referenced in the code that are owned by the target (e.g. S3 buckets, cloud storage, CDN origins, message queues).
521
-
522
- **IMPORTANT: Only include deployable apps, services, and owned cloud resources.** Exclude:
523
- - Libraries, SDKs, and shared packages that are consumed by other code but not deployed on their own
524
- - Git submodules (external dependencies)
525
- - Build tools, scripts, CLI utilities, and dev tooling
526
- - Test suites, fixtures, and test helpers
527
- - Documentation packages
528
- - Third-party SaaS services not owned by the target (e.g. Stripe, auth providers)
529
-
530
- An app/service qualifies if it **listens on a port, serves HTTP traffic, or runs as a deployed process** (e.g. an Express server, a Next.js app, a Django project, a FastAPI service, a background worker with an API).
531
-
532
- A **cloud resource** qualifies if it is an **owned infrastructure resource** referenced in the code — S3 buckets, GCS buckets, Azure Blob Storage, CloudFront distributions, Redis/ElastiCache instances, SQS queues, etc. These are part of the attack surface because they may have misconfigured permissions, public access, or sensitive data.
533
-
534
- ### Steps
535
- 1. List the root directory and read top-level config files (package.json, requirements.txt, Cargo.toml, go.mod, etc.)
536
- 2. **Check for git submodules** — run \`git submodule status\` or check for a \`.gitmodules\` file. Exclude all submodule directories.
537
- 3. Determine the **repo type**: monorepo (workspaces), single-app, multi-package, etc.
538
- 4. Determine the **package manager**: npm, yarn, pnpm, pip, cargo, go modules, etc.
539
- 5. Identify all **deployable** applications/services (ignoring submodules, libraries, and shared packages):
540
- - For monorepos: look at workspace packages that have their own server entry point, Dockerfile, or deploy config — skip packages that are libraries/utilities consumed by other packages
541
- - For multi-service repos: look at separate service directories with their own server startup
542
- - For single apps: the root is the app
543
- 6. **Discover cloud resources** referenced in the codebase:
544
- - Search for S3 bucket references (\`s3://\`, \`new S3Client\`, \`boto3.client('s3')\`, bucket name strings in config)
545
- - Search for cloud storage URLs (e.g. \`*.s3.amazonaws.com\`, \`storage.googleapis.com\`)
546
- - Search for CDN/distribution configs (CloudFront, Cloudflare, etc.)
547
- - Search for message queue references (SQS, SNS, RabbitMQ, etc.)
548
- - Search for cache/database endpoints (ElastiCache, Redis, DynamoDB, etc.)
549
- - Check infrastructure-as-code files (Terraform, CloudFormation, CDK, Pulumi, SST, serverless.yml)
550
- - Document each cloud resource as an app with \`appType: "cloud_resource"\` or \`appType: "storage"\`
551
- 7. For each app/resource, determine:
552
- - **name**: the application or service name
553
- - **framework**: the web framework or cloud service (e.g. "AWS S3", "CloudFront", "Express")
554
- - **description**: brief summary of what it does
555
- - **location**: path relative to the repository root (for code) or the resource identifier (for cloud resources)
556
- - **type**: classify as \`"web_application"\` for frontend-only apps, \`"api"\` for backend API services, \`"full_stack"\` for frameworks serving both UI and API (Next.js, Remix, Nuxt, SvelteKit, Django with templates, Rails), \`"database"\` for databases, \`"cloud_resource"\` for owned cloud infra (SQS, CDN, etc.), \`"storage"\` for S3/GCS/blob storage.
557
-
558
- ### Setting the \`domain\` field on \`document_app\` — CRITICAL
559
-
560
- **Only set \`domain\` when you can deterministically derive it from evidence** — Known Domains list, IaC resource definitions, configuration files, environment variables, or route definitions. Substituting a known environment/stage name into an IaC naming pattern IS deterministic (e.g. IaC defines \`\${stage}-bucket\` and the target environments include "production" → \`https://production-bucket.s3.amazonaws.com\` is valid). However, do NOT invent domains with no supporting evidence — if no domain can be derived from the source, **omit the \`domain\` field entirely**. A missing domain is far better than a hallucinated one.
561
-
562
- When you CAN determine the domain, each resource must have its OWN unique, resource-specific domain. Never reuse a generic domain or another application's domain.
563
-
564
- **For web apps and API services:** Use the public-facing URL from the Known Domains list, route configuration, or infrastructure definition (e.g., \`https://console.pensar.dev\`, \`https://api.example.com\`).
565
-
566
- **For S3 / GCS / blob storage buckets:** Derive the **actual bucket name** from the infrastructure-as-code. The bucket name is defined in the IaC resource definition (e.g., SST \`new sst.aws.Bucket("ProjectData")\` produces a bucket with a name like \`console-staging-projectdata-abc123\`). Set domain to \`https://{actual-bucket-name}.s3.amazonaws.com\`. If the IaC uses a stage/environment variable in the name, substitute the known environment name (e.g. \`\${stage}-projectdata\` with environment "production" → \`https://production-projectdata.s3.amazonaws.com\`). **NEVER use \`https://s3.amazonaws.com\`** — that is the S3 service, not a bucket. If you cannot determine the bucket name at all, omit the domain.
567
-
568
- **For databases (RDS, Aurora, DynamoDB):** Use the cluster/instance endpoint from IaC. If the exact endpoint isn't determinable, omit the domain.
569
-
570
- **For Redis / ElastiCache:** Use the cache cluster endpoint if determinable, otherwise omit.
571
-
572
- **For SQS queues:** Use \`https://sqs.{region}.amazonaws.com/{account}/{queue-name}\` if determinable, otherwise omit.
573
-
574
- **For Lambda functions:** Use the Lambda Function URL or the API Gateway route — NOT a generic API domain shared by all functions. If no concrete URL is determinable, omit.
575
-
576
- **For CloudFront / CDN:** Use the distribution domain or custom domain alias if determinable, otherwise omit.
577
-
578
- **For WebSocket APIs:** Use the WebSocket endpoint URL if determinable, otherwise omit.
579
-
580
- When finished, call the \`response\` tool with your structured findings.`;
581
- }
582
- function buildPagesDiscoveryObjective(codebasePath, appInfo) {
583
- return `# Find All Web Pages in ${appInfo.name}
584
-
585
- ## Codebase
586
- - **Repository root:** ${codebasePath}
587
- - **App location:** ${appInfo.location}
588
- - **Framework:** ${appInfo.framework}
589
-
590
- ## Scope — CRITICAL
591
- **Only document pages/views that are DEFINED within this application (\`${appInfo.location}\`).** A page belongs to this app if its route definition or component file lives inside \`${appInfo.location}\`.
592
-
593
- Do NOT document:
594
- - Routes defined in other applications or packages (even if this app imports/calls them)
595
- - External URLs or cloud resource endpoints that this app links to or fetches from
596
- - API endpoints (those are handled separately)
597
-
598
- ## Task
599
- Find ALL web pages, views, and routes that render HTML or serve client-side UI **defined in this application's source code**.
600
-
601
- ### What to look for (by framework)
602
- - **React/Next.js**: pages/ or app/ directory, route components, layout files
603
- - **Express**: res.render(), res.sendFile(), static file serving, template routes
604
- - **Django**: urls.py patterns pointing to template views, class-based views with template_name
605
- - **Rails**: routes.rb entries pointing to controller actions that render views
606
- - **Vue/Nuxt**: pages/ directory, router definitions
607
- - **FastAPI**: routes returning HTMLResponse, Jinja2 template responses
608
- - **Spring**: @Controller methods returning view names, Thymeleaf templates
609
-
610
- ### How to document each page
611
- For each page, call \`document_endpoint\` with:
612
- - **appName**: \`${appInfo.name}\`
613
- - **endpointType**: \`"web-endpoint"\`
614
- - **description**: Brief description of what this page shows
615
- - **routePath**: The HTTP route this page serves (e.g., \`/dashboard\`). This is the URL path a client requests — it is the endpoint's identity. NOT a file path.
616
- - **method**: \`"PAGE"\`
617
- - **file**: Source-code file where this page is defined (e.g., \`src/pages/dashboard.tsx\`). Must be inside \`${appInfo.location}\`. This is NOT the route.
618
- - **line**: Line number (if determinable)
619
- - **handler**: Component or handler name
620
- - **authRequired**: Whether the page requires authentication
621
- - **riskLevel**: CRITICAL for admin/auth pages, HIGH for user data, MEDIUM for general, LOW for static/public
622
-
623
- ### Required workflow — NO MANIFESTS, NO BATCHING
624
- **You MUST call \`document_endpoint\` directly, one page at a time, the moment you identify a route.** It is a hard error to:
625
- - Build a JSON file, array, or list of pages-to-document and then "process" it (e.g. \`cat > /tmp/pages.json << EOF [...] EOF\`).
626
- - Write a Python or shell script that emits \`document_endpoint\` calls.
627
- - Defer documentation until "the end" or until "you have the full picture."
628
- - Stop early because the calls feel repetitive or because you've documented "the important ones."
629
-
630
- These patterns hit per-message output-token limits and silently drop pages — usually the alphabetically-later ones. The only correct loop is: identify route → call \`document_endpoint\` → identify next route → call \`document_endpoint\` → ...
631
-
632
- You may use \`list_files\`, \`grep\`, or \`execute_command\` (e.g. \`find ... -name page.tsx\`) to **enumerate** the routes that exist. That enumeration step is fine and encouraged. What is not allowed is using a script to **emit the documentation calls themselves** — those must come directly from you, one tool call per route.
633
-
634
- Be thorough — examine every route file, every page directory, every template **within \`${appInfo.location}\`**. Every page surfaced by your enumeration must result in its own \`document_endpoint\` call. Repetitive calls are expected; do not summarize, deduplicate to "interesting" routes, or skip any.
635
-
636
- When finished, call \`response\` with a summary of how many pages you documented. The reported count must equal the number of successful \`document_endpoint\` calls you made.`;
637
- }
638
- function buildApiEndpointsDiscoveryObjective(codebasePath, appInfo) {
639
- return `# Find All API Endpoints in ${appInfo.name}
640
-
641
- ## Codebase
642
- - **Repository root:** ${codebasePath}
643
- - **App location:** ${appInfo.location}
644
- - **Framework:** ${appInfo.framework}
645
-
646
- ## Scope — CRITICAL
647
- **Only document API routes that are DEFINED within this application (\`${appInfo.location}\`).** An endpoint belongs to this app if its route handler or route definition file lives inside \`${appInfo.location}\`.
648
-
649
- Do NOT document:
650
- - Routes defined in other applications or packages
651
- - External API calls this app makes to third-party services or other internal services
652
- - S3 bucket URLs, cloud resource endpoints, or CDN URLs that this app interacts with — those belong to the cloud resource, not this API
653
- - Web pages/views (those are handled separately)
654
-
655
- ## Task
656
- Find ALL API endpoints **whose route definitions live in this application's source code**.
657
-
658
- ### What to look for (by framework)
659
- - **Express**: app.get(), app.post(), router.get(), router.post(), router.put(), router.delete(), etc.
660
- - **Next.js**: app/api/ or pages/api/ route handlers (GET, POST, PUT, DELETE exports)
661
- - **Django**: urls.py patterns pointing to API views, DRF viewsets, routers, @api_view decorators
662
- - **FastAPI**: @app.get(), @app.post(), @app.put(), @app.delete() decorators
663
- - **Rails**: routes.rb API namespaces, resources, controller actions
664
- - **Spring**: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @RequestMapping
665
- - **Go**: http.HandleFunc, mux.Handle, gin router methods
666
-
667
- ### How to document each endpoint
668
- For each **unique route path**, call \`document_endpoint\` with:
669
- - **appName**: \`${appInfo.name}\`
670
- - **endpointType**: \`"api-endpoint"\`
671
- - **description**: Brief description of what this endpoint does across all its methods
672
- - **routePath**: The HTTP route (e.g., \`/api/users\`, \`/api/orders/:id\`). This is the URL path a client requests — it is the endpoint's identity. NOT a source-file path.
673
- - **method**: Array of ALL HTTP methods this path supports (e.g., \`["GET", "POST"]\`). **Do NOT create separate entries for each method — consolidate them.**
674
- - **file**: Source-code file where the endpoint is defined (e.g., \`src/routes/users.ts\`). Must be inside \`${appInfo.location}\`. This is NOT the route.
675
- - **line**: Line number (if determinable)
676
- - **handler**: Handler function name (comma-separate if multiple handlers for different methods)
677
- - **authRequired**: Whether the endpoint requires authentication (true if ANY method requires it)
678
- - **riskLevel**: CRITICAL for auth/payment/admin, HIGH for user data mutations, MEDIUM for general, LOW for read-only public
679
-
680
- **CRITICAL: ONE entry per route path.** If \`/api/products\` has GET (list) and POST (create), document it as ONE entry with \`method: ["GET", "POST"]\`. Do NOT create two separate entries.
681
-
682
- **IMPORTANT — Method consolidation for document_endpoint:** When using the \`document_endpoint\` tool, do NOT create separate entries for different HTTP methods on the same route path. For example, if \`/api/users\` supports GET, POST, and DELETE, document it as ONE entry with \`method: ["GET", "POST", "DELETE"]\` and include pentest objectives covering all methods.
683
-
684
- ### Required workflow — NO MANIFESTS, NO BATCHING
685
- **You MUST call \`document_endpoint\` directly, one route at a time, the moment you identify it.** It is a hard error to:
686
- - Build a JSON file, array, or list of endpoints-to-document and then "process" it (e.g. \`cat > /tmp/endpoints.json << EOF [...] EOF\`).
687
- - Write a Python or shell script that emits \`document_endpoint\` calls.
688
- - Defer documentation until you've "mapped everything out."
689
- - Stop early because the calls feel repetitive or because you've covered "the important ones."
690
-
691
- These patterns hit per-message output-token limits and silently drop endpoints — usually the alphabetically-later ones. The only correct loop is: identify route → call \`document_endpoint\` → identify next route → call \`document_endpoint\` → ...
692
-
693
- You may use \`list_files\`, \`grep\`, or \`execute_command\` to **enumerate** routes (e.g. extracting all route registrations into a list to read). That enumeration step is fine. What is not allowed is using a script to **emit the documentation calls themselves** — those must come directly from you, one tool call per unique route path.
694
-
695
- Be thorough — trace through all route registrations, middleware chains, and controller files **within \`${appInfo.location}\`**. Every unique route path your enumeration surfaces must result in its own \`document_endpoint\` call.
696
-
697
- When finished, call \`response\` with a summary of how many endpoints you documented. The reported count must equal the number of successful \`document_endpoint\` calls you made.`;
698
- }
699
- function buildCloudResourceEndpointsObjective(codebasePath, appInfo, environments) {
700
- const envNote = environments?.length ? `
701
- ## Target Environments
702
- This resource may exist in the following environments: ${environments.join(", ")}. When documenting entry points, use the **environment-specific resource identifiers** (e.g. environment-prefixed bucket names, stage-scoped queue URLs, per-environment ARNs). If the app name already includes an environment prefix, use that environment's resource names in the endpoints.
703
- ` : "";
704
- return `# Document Entry Points for Cloud Resource: ${appInfo.name}
705
-
706
- ## Codebase
707
- - **Repository root:** ${codebasePath}
708
- - **Resource location:** ${appInfo.location}
709
- - **Service:** ${appInfo.framework}
710
- ${envNote}
711
- ## Context — Application Domain
712
- The parent application for this cloud resource already has a domain/URL associated with it (set via \`document_app\`). **Do NOT create endpoints that simply repeat the base domain URL.** The domain is already stored on the application record — endpoints should document **distinct access patterns** that go beyond the base domain.
713
-
714
- ## Scope — CRITICAL
715
- You are documenting the **distinct access patterns and resource identifiers** for this cloud resource — specific ways the resource can be accessed that are NOT just its base domain URL.
716
-
717
- Do NOT document:
718
- - The base domain URL of the resource (e.g. \`https://bucket-name.s3.amazonaws.com\`) — this is already the application's domain
719
- - Code locations where the app calls/uses this resource (e.g. "line 42 of api.ts calls S3.putObject" is NOT an endpoint)
720
- - API routes from other apps that happen to interact with this resource
721
- - Internal SDK calls or client instantiations
722
-
723
- DO document:
724
- - **Specific access patterns** beyond the base domain (e.g. pre-signed URL patterns, static website hosting paths, object key patterns)
725
- - **Resource ARNs** that represent programmatic access points (e.g. \`arn:aws:s3:::bucket-name\`, \`arn:aws:sqs:region:account:queue-name\`)
726
- - **Alternative access URLs** (e.g. CDN distribution URL, static website hosting URL, regional endpoint variants)
727
- - **Queue/topic URLs** for message-based resources
728
-
729
- ## Task
730
- Find the **distinct access patterns and resource identifiers** for this cloud resource by reading infrastructure-as-code and configuration.
731
-
732
- ### Where to find entry point information
733
- 1. **Infrastructure-as-code** — Terraform (*.tf), CloudFormation (*.yaml/*.json), CDK constructs, SST components (sst.config.ts, infra/), Pulumi, serverless.yml — these define the resource and its access configuration
734
- 2. **Configuration files** — .env files, config modules, environment variable definitions that contain resource URLs
735
- 3. **Resource policies** — bucket policies, CORS configs, access control settings that reveal how the resource is exposed
736
-
737
- ### What qualifies as an entry point (by resource type)
738
- - **S3 / GCS / Blob Storage**: Pre-signed URL patterns (e.g. \`/{objectKey}?X-Amz-Signature={sig}\`), static website hosting URL, ARN. Do NOT include the plain bucket HTTPS endpoint — that's the app domain.
739
- - **CloudFront / CDN**: Custom domain aliases, origin access patterns
740
- - **SQS / SNS / Message Queues**: Queue URL, topic ARN
741
- - **Lambda / Cloud Functions**: Function URL, API Gateway integration URL
742
- - **DynamoDB / ElastiCache / Redis**: Connection endpoint URL, ARN
743
-
744
- ### How to document each entry point
745
- For each entry point, call \`document_endpoint\` with:
746
- - **appName**: \`${appInfo.name}\`
747
- - **endpointType**: \`"asset"\`
748
- - **description**: What this entry point exposes (e.g., "Pre-signed HTTP URLs for temporary read access to objects", "AWS resource ARN for programmatic access through IAM policies")
749
- - **routePath**: The specific access pattern, path template, or ARN — this is the endpoint's identity. NOT the base domain URL. Examples: \`/{objectKey}?X-Amz-Signature={signature}&X-Amz-Credential={credential}\`, \`arn:aws:s3:::bucket-name\`, \`https://sqs.region.amazonaws.com/account/queue-name\`
750
- - **method**: Access methods on the resource (e.g., \`["GET", "PUT"]\` for S3, \`["SendMessage", "ReceiveMessage"]\` for SQS, \`["READ", "WRITE"]\` for generic)
751
- - **file**: Infrastructure or config file where this resource is defined (e.g., \`infra/storage.ts\`). NOT application code that calls it.
752
- - **line**: Line number if determinable
753
- - **authRequired**: Whether external access requires authentication
754
- - **riskLevel**: CRITICAL for publicly accessible storage with write access or sensitive data, HIGH for resources with broad IAM permissions, MEDIUM for internal resources, LOW for read-only public assets
755
-
756
- ### Required workflow — NO MANIFESTS, NO BATCHING
757
- **You MUST call \`document_endpoint\` directly, one entry point at a time, the moment you identify it.** Do not build a JSON file or list of entry points to "process later," and do not write a script that emits \`document_endpoint\` calls. Those patterns hit per-message output-token limits and silently drop entries. The only correct loop is: identify entry point → call \`document_endpoint\` → identify next → call \`document_endpoint\` → ...
758
-
759
- When finished, call \`response\` with a summary of how many entry points you documented. The reported count must equal the number of successful \`document_endpoint\` calls you made.`;
760
- }
761
-
762
- // src/core/agents/specialized/pentest/planPrompt.ts
763
- var PLAN_PHASE_SYSTEM_PROMPT = `You are an expert penetration tester performing reconnaissance and planning.
764
-
765
- Your job is to analyze the target, understand its attack surface, and create a comprehensive testing plan. You have READ-ONLY tools available — you can probe the target but NOT exploit it.
766
-
767
- Methodology:
768
- 1. RECALL — Call list_memories to check for relevant knowledge from previous engagements.
769
- 2. PROBE — Use http_request and browser tools to understand the target's behavior, technology stack, and input vectors.
770
- 3. ANALYZE — Identify technologies, endpoints, authentication mechanisms, input parameters, and potential vulnerability classes.
771
- 4. PLAN — Write a structured pentest plan via write_plan with the following sections:
772
- - **Objective** — engagement goals
773
- - **Scope** — in-scope targets, exclusions
774
- - **Reconnaissance Findings** — technology stack, endpoints, authentication mechanisms discovered
775
- - **Attack Surface Map** — entry points ranked by exposure
776
- - **Prioritized Attack Vectors** — ordered by impact, with endpoint, vulnerability class, rationale, and test approach per vector
777
- - **Testing Methodology** — concrete techniques and payloads per vector (OWASP categories)
778
- - **Estimated Action Tiers** — anticipated T1–T5 action counts for operator awareness
779
- 5. DECOMPOSE — Call create_task for each attack vector in the plan. One task per technique × endpoint combination.
780
- 6. SUBMIT — Call submit_plan when the plan is complete and all tasks are created.
781
-
782
- Guidelines:
783
- - Be thorough in reconnaissance — probe different endpoints, check response headers, test authentication flows
784
- - Rank attack vectors by likelihood of success and potential impact
785
- - Each task should be atomic and testable independently
786
- - Include authentication bypass, injection, access control, and business logic testing as appropriate`;
787
- function buildPlanPrompt(target, objectives) {
788
- const objectiveList = objectives.map((o, i) => `${i + 1}. ${o}`).join(`
789
- `);
790
- return `# Planning Assignment
791
-
792
- ## Target
793
- - **URL:** ${target}
794
-
795
- ## Objectives
796
- ${objectiveList}
797
-
798
- ## Instructions
799
- 1. Probe the target to understand its behavior and attack surface
800
- 2. Write a comprehensive pentest plan via write_plan
801
- 3. Create tasks for each attack vector via create_task
802
- 4. Submit the plan via submit_plan when complete`;
803
- }
804
-
805
- // src/core/workflows/pentest.ts
806
- var DEFAULT_CONCURRENCY2 = 10;
807
- function addUsageTotals(totals, usage) {
808
- if (!usage)
809
- return;
810
- const inputTokens = usage.inputTokens ?? 0;
811
- const outputTokens = usage.outputTokens ?? 0;
812
- const totalTokens = usage.totalTokens ?? inputTokens + outputTokens;
813
- totals.inputTokens += inputTokens;
814
- totals.outputTokens += outputTokens;
815
- totals.totalTokens += totalTokens;
816
- }
817
- async function runPentestSwarm(input) {
818
- const {
819
- targets,
820
- model,
821
- session,
822
- authConfig,
823
- abortSignal,
824
- findingsRegistry,
825
- eventBus,
826
- onError,
827
- concurrency = DEFAULT_CONCURRENCY2,
828
- onStepFinish,
829
- enableThinking
830
- } = input;
831
- const completedIds = getCompletedAgentIds(session);
832
- const existingManifest = readAgentManifest(session);
833
- const freshEntries = buildManifestEntries(targets);
834
- const manifestEntries = freshEntries.map((fresh) => {
835
- const existing = existingManifest.find((e) => e.id === fresh.id);
836
- if (existing && existing.status === "completed")
837
- return existing;
838
- return fresh;
839
- });
840
- writeAgentManifest(session, manifestEntries);
841
- const results = await runWithBoundedConcurrency(targets, concurrency, async (target, index) => {
842
- const subagentId = `pentest-agent-${index + 1}`;
843
- if (completedIds.has(subagentId))
844
- return null;
845
- const previousMessages = loadSubagentMessages(session, subagentId);
846
- let lastMessages = [];
847
- const handleStepFinish = (e) => {
848
- if (e.response.messages) {
849
- lastMessages = e.response.messages;
850
- }
851
- onStepFinish?.(e);
852
- };
853
- eventBus?.emit("subagent-spawn", {
854
- subagentId,
855
- name: target.name,
856
- input: { target: target.target, objectives: target.objectives }
857
- });
858
- if (session.config?.requirePlan && session.config?.taskDriven && previousMessages.length === 0) {
859
- try {
860
- const executionTasksDir = join3(session.rootPath, "subagents", `${subagentId}-tasks`);
861
- const planAgent = new OffensiveSecurityAgent({
862
- system: PLAN_PHASE_SYSTEM_PROMPT,
863
- prompt: buildPlanPrompt(target.target, target.objectives),
864
- model,
865
- session,
866
- target: target.target,
867
- mode: "plan",
868
- activeTools: [...PLAN_MODE_TOOL_NAMES],
869
- stopWhen: hasToolCall("submit_plan"),
870
- authConfig,
871
- abortSignal,
872
- findingsRegistry,
873
- subagentId: `${subagentId}-plan`,
874
- tasksDir: executionTasksDir,
875
- planSubagentId: subagentId,
876
- eventBus,
877
- enableThinking
878
- });
879
- await planAgent.consume();
880
- } catch (planErr) {
881
- console.error(`[pentest-swarm] Plan phase failed for ${subagentId}: ${planErr}`);
882
- }
883
- }
884
- const objectiveStr = target.objectives.join("; ");
885
- let agent;
886
- try {
887
- agent = new TargetedPentestAgent({
888
- target: target.target,
889
- objectives: target.objectives,
890
- model,
891
- session,
892
- authConfig,
893
- abortSignal,
894
- findingsRegistry,
895
- onStepFinish: handleStepFinish,
896
- messages: previousMessages.length > 0 ? previousMessages : undefined,
897
- eventBus,
898
- subagentId,
899
- enableThinking
900
- });
901
- const result = await agent.consume();
902
- saveSubagentData(session, {
903
- agentName: subagentId,
904
- target: target.target,
905
- objective: objectiveStr,
906
- status: "completed",
907
- findingsCount: result.findings.length,
908
- messages: [...previousMessages, ...lastMessages],
909
- systemPrompt: buildPentestSystemPrompt(session),
910
- userPrompt: agent.userPrompt
911
- });
912
- updateManifestEntryStatus(session, subagentId, "completed");
913
- eventBus?.emit("subagent-complete", {
914
- subagentId,
915
- status: "completed"
916
- });
917
- return result;
918
- } catch (error) {
919
- saveSubagentData(session, {
920
- agentName: subagentId,
921
- target: target.target,
922
- objective: objectiveStr,
923
- status: "failed",
924
- error: error instanceof Error ? error.message : String(error),
925
- messages: [...previousMessages, ...lastMessages],
926
- systemPrompt: buildPentestSystemPrompt(session),
927
- userPrompt: agent?.userPrompt ?? ""
928
- });
929
- updateManifestEntryStatus(session, subagentId, "failed");
930
- eventBus?.emit("subagent-complete", {
931
- subagentId,
932
- status: "failed"
933
- });
934
- eventBus?.emit("error", { error, subagentId });
935
- onError?.(error);
936
- throw error;
937
- }
938
- }, abortSignal);
939
- finalizeManifest(session, manifestEntries, results);
940
- return results;
941
- }
942
- async function runPentestWorkflow(input) {
943
- const {
944
- target,
945
- cwd,
946
- model,
947
- session,
948
- authConfig,
949
- abortSignal,
950
- eventBus,
951
- onStepFinish,
952
- onCacheMetrics,
953
- enableThinking,
954
- prompt,
955
- threatModel
956
- } = input;
957
- if (prompt || threatModel) {
958
- const parts = [];
959
- if (threatModel)
960
- parts.push(createThreatModelPrompt(threatModel));
961
- if (prompt)
962
- parts.push(prompt);
963
- const combined = parts.join(`
964
-
965
- `);
966
- if (!session.config) {
967
- session.config = {};
968
- }
969
- session.config.prompt = session.config.prompt ? `${session.config.prompt}
970
-
971
- ${combined}` : combined;
972
- }
973
- const startedAt = Date.now();
974
- const tokenUsageTotals = {
975
- inputTokens: 0,
976
- outputTokens: 0,
977
- totalTokens: 0
978
- };
979
- const wrappedOnStepFinish = (event) => {
980
- addUsageTotals(tokenUsageTotals, event.usage);
981
- if (onStepFinish) {
982
- onStepFinish(event);
983
- }
984
- };
985
- try {
986
- const mode = cwd ? "whitebox" : "blackbox";
987
- let swarmTargets;
988
- const existingResults = loadAttackSurfaceResults(session.rootPath);
989
- if (existingResults?.targets && existingResults.targets.length > 0) {
990
- swarmTargets = existingResults.targets.map((t) => ({
991
- target: t.target,
992
- objectives: [t.objective]
993
- }));
994
- eventBus?.emit("workflow-phase-start", {
995
- phase: "discovery",
996
- label: "Attack Surface Discovery (cached)"
997
- });
998
- eventBus?.emit("workflow-phase-complete", {
999
- phase: "discovery",
1000
- summary: {
1001
- targets: swarmTargets,
1002
- targetCount: swarmTargets.length,
1003
- cached: true
1004
- }
1005
- });
1006
- } else {
1007
- eventBus?.emit("workflow-phase-start", {
1008
- phase: "discovery",
1009
- label: `Attack Surface Discovery (${mode})`,
1010
- metadata: { mode, target }
1011
- });
1012
- if (mode === "whitebox") {
1013
- swarmTargets = await runWhiteboxPhase({
1014
- codebasePath: cwd,
1015
- baseTarget: target,
1016
- model,
1017
- session,
1018
- authConfig,
1019
- abortSignal,
1020
- eventBus,
1021
- onStepFinish: wrappedOnStepFinish,
1022
- onCacheMetrics
1023
- });
1024
- } else {
1025
- swarmTargets = await runBlackboxPhase({
1026
- target,
1027
- model,
1028
- session,
1029
- authConfig,
1030
- abortSignal,
1031
- eventBus,
1032
- onStepFinish: wrappedOnStepFinish,
1033
- onCacheMetrics
1034
- });
1035
- }
1036
- eventBus?.emit("workflow-phase-complete", {
1037
- phase: "discovery",
1038
- summary: {
1039
- targets: swarmTargets,
1040
- targetCount: swarmTargets.length
1041
- }
1042
- });
1043
- }
1044
- if (abortSignal?.aborted) {
1045
- throw new DOMException("Pentest aborted by user", "AbortError");
1046
- }
1047
- if (swarmTargets.length === 0) {
1048
- const report2 = buildPentestReport([], {
1049
- target,
1050
- model,
1051
- sessionId: session.id,
1052
- mode
1053
- });
1054
- const mdPath2 = join3(session.rootPath, REPORT_FILENAME_MD);
1055
- const jsonPath2 = join3(session.rootPath, REPORT_FILENAME_JSON);
1056
- writeFileSync2(mdPath2, renderMarkdown(report2));
1057
- writeFileSync2(jsonPath2, renderJson(report2));
1058
- return {
1059
- findings: [],
1060
- findingsPath: session.findingsPath,
1061
- pocsPath: session.pocsPath,
1062
- reportPath: mdPath2
1063
- };
1064
- }
1065
- eventBus?.emit("workflow-phase-start", {
1066
- phase: "pentesting",
1067
- label: `Pentest Swarm (${swarmTargets.length} targets)`
1068
- });
1069
- const findingsRegistry = FindingsRegistry.fromDirectory(session.findingsPath, {
1070
- model,
1071
- authConfig,
1072
- abortSignal
1073
- });
1074
- const completedCount = getCompletedAgentIds(session).size;
1075
- if (completedCount < swarmTargets.length) {
1076
- await runPentestSwarm({
1077
- targets: swarmTargets,
1078
- model,
1079
- session,
1080
- authConfig,
1081
- abortSignal,
1082
- findingsRegistry,
1083
- eventBus,
1084
- onStepFinish: wrappedOnStepFinish,
1085
- enableThinking
1086
- });
1087
- }
1088
- if (abortSignal?.aborted) {
1089
- throw new DOMException("Pentest aborted by user", "AbortError");
1090
- }
1091
- eventBus?.emit("workflow-phase-start", {
1092
- phase: "reporting",
1093
- label: "Report Generation"
1094
- });
1095
- await findingsRegistry.groupByRootCause();
1096
- const findings = [...findingsRegistry.getFindings()];
1097
- const report = buildPentestReport(findings, {
1098
- target,
1099
- model,
1100
- sessionId: session.id,
1101
- mode
1102
- });
1103
- const mdPath = join3(session.rootPath, REPORT_FILENAME_MD);
1104
- const jsonPath = join3(session.rootPath, REPORT_FILENAME_JSON);
1105
- writeFileSync2(mdPath, renderMarkdown(report));
1106
- writeFileSync2(jsonPath, renderJson(report));
1107
- const severityCounts = {};
1108
- for (const f of findings) {
1109
- const sev = (f.severity ?? "unknown").toLowerCase();
1110
- severityCounts[sev] = (severityCounts[sev] ?? 0) + 1;
1111
- }
1112
- eventBus?.emit("workflow-phase-complete", {
1113
- phase: "reporting",
1114
- summary: {
1115
- reportPath: mdPath,
1116
- findingsCount: findings.length,
1117
- findingsBySeverity: severityCounts
1118
- }
1119
- });
1120
- return {
1121
- findings,
1122
- findingsPath: session.findingsPath,
1123
- pocsPath: session.pocsPath,
1124
- reportPath: mdPath
1125
- };
1126
- } finally {
1127
- const runtime = formatDurationHmsFromMs(Date.now() - startedAt);
1128
- writeExecutionMetrics({
1129
- sessionRootPath: session.rootPath,
1130
- tokenUsage: tokenUsageTotals,
1131
- runtime
1132
- });
1133
- }
1134
- }
1135
- async function runWhiteboxPhase(opts) {
1136
- const workflowInput = {
1137
- codebasePath: opts.codebasePath,
1138
- model: opts.model,
1139
- session: opts.session,
1140
- authConfig: opts.authConfig,
1141
- abortSignal: opts.abortSignal,
1142
- eventBus: opts.eventBus,
1143
- onStepFinish: opts.onStepFinish,
1144
- onCacheMetrics: opts.onCacheMetrics
1145
- };
1146
- const result = await runWhiteboxAttackSurfaceWorkflow(workflowInput);
1147
- return result.apps.flatMap((app) => [...app.pages, ...app.apiEndpoints].map((ep) => ({
1148
- target: ep.path.startsWith("http") ? ep.path : `${opts.baseTarget}${ep.path}`,
1149
- objectives: ep.pentestObjectives
1150
- })));
1151
- }
1152
- async function runBlackboxPhase(opts) {
1153
- let lastMessages = [];
1154
- const agentInput = {
1155
- target: opts.target,
1156
- model: opts.model,
1157
- session: opts.session,
1158
- authConfig: opts.authConfig,
1159
- abortSignal: opts.abortSignal,
1160
- eventBus: opts.eventBus,
1161
- subagentId: "attack-surface-agent",
1162
- onStepFinish: (e) => {
1163
- if (e.response.messages) {
1164
- lastMessages = e.response.messages;
1165
- }
1166
- opts.onStepFinish?.(e);
1167
- },
1168
- onCacheMetrics: opts.onCacheMetrics
1169
- };
1170
- const agent = new BlackboxAttackSurfaceAgent(agentInput);
1171
- opts.eventBus?.emit("subagent-spawn", {
1172
- subagentId: "attack-surface-agent",
1173
- name: "Attack Surface Discovery",
1174
- input: { target: opts.target }
1175
- });
1176
- try {
1177
- const result = await agent.consume();
1178
- saveSubagentData(opts.session, {
1179
- agentName: "attack-surface-agent",
1180
- target: opts.target,
1181
- status: "completed",
1182
- messages: lastMessages
1183
- });
1184
- opts.eventBus?.emit("subagent-complete", {
1185
- subagentId: "attack-surface-agent",
1186
- status: "completed"
1187
- });
1188
- return result.targets.map((t) => ({
1189
- target: t.target,
1190
- objectives: [t.objective]
1191
- }));
1192
- } catch (e) {
1193
- saveSubagentData(opts.session, {
1194
- agentName: "attack-surface-agent",
1195
- target: opts.target,
1196
- status: "failed",
1197
- error: e instanceof Error ? e.message : String(e),
1198
- messages: lastMessages
1199
- });
1200
- opts.eventBus?.emit("subagent-complete", {
1201
- subagentId: "attack-surface-agent",
1202
- status: "failed"
1203
- });
1204
- throw e;
1205
- }
1206
- }
1207
- export { DEFAULT_CONCURRENCY2 as DEFAULT_CONCURRENCY, runPentestSwarm, runPentestWorkflow };