@pensar/apex 0.0.111 → 0.0.112

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 (64) hide show
  1. package/README.md +2 -3
  2. package/bin/pensar.js +31 -276
  3. package/build/agent-5qdmmchx.js +206 -0
  4. package/build/agent-s2z0dasf.js +16 -0
  5. package/build/auth-jvq72ekc.js +263 -0
  6. package/build/authentication-nya4td5k.js +310 -0
  7. package/build/blackboxAgent-qa9ze2hn.js +17 -0
  8. package/build/blackboxPentest-85hwznet.js +41 -0
  9. package/build/cli-15vxn9zj.js +1358 -0
  10. package/build/cli-2ckm5es2.js +50 -0
  11. package/build/cli-49cd9yfk.js +4475 -0
  12. package/build/cli-5d6cs4dq.js +53 -0
  13. package/build/cli-6gtnyaqf.js +109 -0
  14. package/build/cli-7ckctq7a.js +45 -0
  15. package/build/cli-8rxa073f.js +104 -0
  16. package/build/cli-bp6d08sg.js +110 -0
  17. package/build/cli-e20q3hqz.js +307 -0
  18. package/build/cli-f9shhcxf.js +1498 -0
  19. package/build/cli-hmrzx8am.js +507 -0
  20. package/build/cli-j66pect7.js +202 -0
  21. package/build/cli-jb0gcnrs.js +60 -0
  22. package/build/cli-jh38b6zv.js +1074 -0
  23. package/build/cli-kqtgcdzn.js +54784 -0
  24. package/build/cli-r8r90gka.js +96700 -0
  25. package/build/cli-va4y0089.js +395 -0
  26. package/build/cli-w04ggbe4.js +104 -0
  27. package/build/cli-x1msjf55.js +103 -0
  28. package/build/cli-yj3dy0vg.js +180 -0
  29. package/build/cli.js +509 -0
  30. package/build/doctor-b7612pzw.js +117 -0
  31. package/build/fixes-1r6v7kh2.js +49 -0
  32. package/build/index-5ke2yd32.js +17 -0
  33. package/build/index-9ze42wn7.js +68412 -0
  34. package/build/index-rd11fk7h.js +1257 -0
  35. package/build/index-tke6896d.js +1097 -0
  36. package/build/index-vwvh1rdw.js +535 -0
  37. package/build/issues-kx721wja.js +94 -0
  38. package/build/logs-hav7d0nm.js +77 -0
  39. package/build/main-2483qzbq.js +397 -0
  40. package/build/multipart-parser-r38qdp5v.js +350 -0
  41. package/build/pentest-zzebnfa0.js +25 -0
  42. package/build/pentests-s9fwd71b.js +70 -0
  43. package/build/projects-tr719twv.js +35 -0
  44. package/build/targetedPentest-w2c85whf.js +32 -0
  45. package/build/token-6x6aavpc.js +58 -0
  46. package/build/token-util-na95bqjj.js +6 -0
  47. package/build/uninstall-2j0pymb0.js +231 -0
  48. package/build/utils-jky0th19.js +107 -0
  49. package/package.json +3 -4
  50. package/build/auth.js +0 -625
  51. package/build/highlights-eq9cgrbb.scm +0 -604
  52. package/build/highlights-ghv9g403.scm +0 -205
  53. package/build/highlights-hk7bwhj4.scm +0 -284
  54. package/build/highlights-r812a2qc.scm +0 -150
  55. package/build/highlights-x6tmsnaa.scm +0 -115
  56. package/build/index.js +0 -292069
  57. package/build/injections-73j83es3.scm +0 -27
  58. package/build/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  59. package/build/tree-sitter-markdown-411r6y9b.wasm +0 -0
  60. package/build/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  61. package/build/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  62. package/build/tree-sitter-zig-e78zbjpm.wasm +0 -0
  63. package/src/core/installation/index.ts +0 -223
  64. package/src/core/installation/installation.test.ts +0 -454
@@ -0,0 +1,1498 @@
1
+ import {
2
+ TargetedPentestAgent
3
+ } from "./cli-e20q3hqz.js";
4
+ import {
5
+ CodeAgent
6
+ } from "./cli-w04ggbe4.js";
7
+ import {
8
+ EndpointSchema
9
+ } from "./cli-2ckm5es2.js";
10
+ import {
11
+ BlackboxAttackSurfaceAgent
12
+ } from "./cli-hmrzx8am.js";
13
+ import {
14
+ CweEntrySchema,
15
+ FindingsRegistry
16
+ } from "./cli-r8r90gka.js";
17
+ import {
18
+ exports_external,
19
+ init_zod
20
+ } from "./cli-kqtgcdzn.js";
21
+
22
+ // src/core/workflows/pentest.ts
23
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
24
+ import { join as join4 } from "path";
25
+
26
+ // src/core/report/schemas.ts
27
+ init_zod();
28
+ var PentestReportFindingSchema = exports_external.object({
29
+ title: exports_external.string(),
30
+ severity: exports_external.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
31
+ description: exports_external.string(),
32
+ impact: exports_external.string(),
33
+ evidence: exports_external.string(),
34
+ endpoint: exports_external.string(),
35
+ pocPath: exports_external.string(),
36
+ remediation: exports_external.string(),
37
+ references: exports_external.string().optional(),
38
+ cwes: exports_external.array(CweEntrySchema).optional()
39
+ });
40
+ var PentestReportSchema = exports_external.object({
41
+ version: exports_external.string().regex(/^1\.\d+$/),
42
+ metadata: exports_external.object({
43
+ target: exports_external.string(),
44
+ model: exports_external.string(),
45
+ timestamp: exports_external.string(),
46
+ sessionId: exports_external.string(),
47
+ mode: exports_external.enum(["blackbox", "whitebox", "targeted"])
48
+ }),
49
+ summary: exports_external.object({
50
+ totalFindings: exports_external.number(),
51
+ bySeverity: exports_external.object({
52
+ CRITICAL: exports_external.number(),
53
+ HIGH: exports_external.number(),
54
+ MEDIUM: exports_external.number(),
55
+ LOW: exports_external.number()
56
+ })
57
+ }),
58
+ findings: exports_external.array(PentestReportFindingSchema)
59
+ });
60
+ var REPORT_VERSION = "1.0";
61
+ // src/core/report/builder.ts
62
+ var SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
63
+ function buildPentestReport(findings, context) {
64
+ const sorted = [...findings].sort((a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity));
65
+ const bySeverity = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
66
+ for (const f of findings) {
67
+ bySeverity[f.severity]++;
68
+ }
69
+ return {
70
+ version: REPORT_VERSION,
71
+ metadata: {
72
+ target: context.target,
73
+ model: context.model,
74
+ timestamp: new Date().toISOString(),
75
+ sessionId: context.sessionId,
76
+ mode: context.mode
77
+ },
78
+ summary: {
79
+ totalFindings: findings.length,
80
+ bySeverity
81
+ },
82
+ findings: sorted.map((f) => ({
83
+ title: f.title,
84
+ severity: f.severity,
85
+ description: f.description,
86
+ impact: f.impact,
87
+ evidence: f.evidence,
88
+ endpoint: f.endpoint,
89
+ pocPath: f.pocPath,
90
+ remediation: f.remediation,
91
+ references: f.references
92
+ }))
93
+ };
94
+ }
95
+ // src/core/report/renderers/markdown.ts
96
+ function renderMarkdown(report) {
97
+ const { metadata, findings, summary } = report;
98
+ const header = [
99
+ `# Pentest Report — ${metadata.target}`,
100
+ "",
101
+ `**Date:** ${metadata.timestamp} `,
102
+ `**Session:** ${metadata.sessionId} `,
103
+ `**Model:** ${metadata.model} `,
104
+ `**Mode:** ${metadata.mode}`,
105
+ "",
106
+ `**Findings:** ${summary.totalFindings}`,
107
+ ""
108
+ ];
109
+ if (findings.length === 0) {
110
+ return [
111
+ ...header,
112
+ "No findings were identified during this assessment.",
113
+ ""
114
+ ].join(`
115
+ `);
116
+ }
117
+ const body = findings.map((finding) => renderFinding(finding, metadata)).join(`
118
+ `);
119
+ return [...header, body].join(`
120
+ `);
121
+ }
122
+ function renderFinding(finding, metadata) {
123
+ const lines = [
124
+ `# ${finding.title}`,
125
+ "",
126
+ `**Severity:** ${finding.severity} `,
127
+ `**Target:** ${metadata.target} `,
128
+ `**Endpoint:** ${finding.endpoint} `,
129
+ `**Date:** ${metadata.timestamp} `,
130
+ `**Session:** ${metadata.sessionId}`,
131
+ "",
132
+ "## Description",
133
+ "",
134
+ finding.description,
135
+ "",
136
+ "## Impact",
137
+ "",
138
+ finding.impact,
139
+ "",
140
+ "## Evidence",
141
+ "",
142
+ "```",
143
+ finding.evidence,
144
+ "```",
145
+ "",
146
+ ...finding.cwes?.length ? [
147
+ "## CWE Classification",
148
+ "",
149
+ ...finding.cwes.map((cwe) => `- **${cwe.id}** — ${cwe.reasoning}`),
150
+ ""
151
+ ] : [],
152
+ "## POC",
153
+ "",
154
+ `Path: \`${finding.pocPath}\``,
155
+ "",
156
+ "## Remediation",
157
+ "",
158
+ finding.remediation,
159
+ ...finding.references ? ["", `## References`, "", finding.references] : [],
160
+ "",
161
+ "---",
162
+ "",
163
+ "*This finding was automatically documented by the Pensar penetration testing agent.*",
164
+ ""
165
+ ];
166
+ return lines.join(`
167
+ `);
168
+ }
169
+ // src/core/report/renderers/json.ts
170
+ function renderJson(report) {
171
+ return JSON.stringify(report, null, 2);
172
+ }
173
+
174
+ // src/core/report/index.ts
175
+ var REPORT_FILENAME_MD = "pentest-report.md";
176
+ var REPORT_FILENAME_JSON = "pentest-report.json";
177
+
178
+ // src/core/session/persistence.ts
179
+ import {
180
+ existsSync,
181
+ mkdirSync,
182
+ writeFileSync,
183
+ readFileSync,
184
+ readdirSync
185
+ } from "fs";
186
+ import { join } from "path";
187
+ var SUBAGENTS_DIR = "subagents";
188
+ var MANIFEST_FILE = "agent-manifest.json";
189
+ function loadSubagentMessages(session, agentName) {
190
+ const filePath = join(session.rootPath, SUBAGENTS_DIR, `${agentName}.json`);
191
+ if (!existsSync(filePath))
192
+ return [];
193
+ try {
194
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
195
+ return data.messages;
196
+ } catch {
197
+ return [];
198
+ }
199
+ }
200
+ function saveSubagentData(session, data) {
201
+ const subagentsDir = join(session.rootPath, SUBAGENTS_DIR);
202
+ mkdirSync(subagentsDir, { recursive: true });
203
+ let toolCallCount = 0;
204
+ let stepCount = 0;
205
+ for (const msg of data.messages) {
206
+ if (msg.role === "assistant") {
207
+ stepCount++;
208
+ if (Array.isArray(msg.content)) {
209
+ for (const part of msg.content) {
210
+ if (part.type === "tool-call")
211
+ toolCallCount++;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ const savedData = {
217
+ agentName: data.agentName,
218
+ timestamp: new Date().toISOString(),
219
+ target: data.target,
220
+ objective: data.objective,
221
+ vulnerabilityClass: data.vulnerabilityClass,
222
+ toolCallCount,
223
+ stepCount,
224
+ findingsCount: data.findingsCount ?? 0,
225
+ status: data.status,
226
+ error: data.error,
227
+ messages: data.messages
228
+ };
229
+ writeFileSync(join(subagentsDir, `${data.agentName}.json`), JSON.stringify(savedData, null, 2));
230
+ }
231
+ function writeAgentManifest(session, entries) {
232
+ const manifestPath = join(session.rootPath, MANIFEST_FILE);
233
+ writeFileSync(manifestPath, JSON.stringify(entries, null, 2));
234
+ }
235
+ function readAgentManifest(session) {
236
+ const manifestPath = join(session.rootPath, MANIFEST_FILE);
237
+ if (!existsSync(manifestPath))
238
+ return [];
239
+ try {
240
+ return JSON.parse(readFileSync(manifestPath, "utf-8"));
241
+ } catch {
242
+ return [];
243
+ }
244
+ }
245
+ function buildManifestEntries(targets) {
246
+ return targets.map((t, i) => ({
247
+ id: `pentest-agent-${i + 1}`,
248
+ name: `Pentest Agent ${i + 1}`,
249
+ target: t.target,
250
+ vulnerabilityClass: t.objectives[0] || "general",
251
+ objective: t.objectives.join("; "),
252
+ status: "running",
253
+ spawnedAt: new Date().toISOString()
254
+ }));
255
+ }
256
+ function finalizeManifest(session, entries, results) {
257
+ const finalManifest = entries.map((entry, i) => {
258
+ if (entry.status === "completed")
259
+ return entry;
260
+ return {
261
+ ...entry,
262
+ status: results[i] != null ? "completed" : "failed",
263
+ completedAt: new Date().toISOString()
264
+ };
265
+ });
266
+ writeAgentManifest(session, finalManifest);
267
+ }
268
+ function updateManifestEntryStatus(session, agentId, status) {
269
+ const manifest = readAgentManifest(session);
270
+ const updated = manifest.map((e) => e.id === agentId ? { ...e, status, completedAt: new Date().toISOString() } : e);
271
+ writeAgentManifest(session, updated);
272
+ }
273
+ function getCompletedAgentIds(session) {
274
+ const manifest = readAgentManifest(session);
275
+ return new Set(manifest.filter((e) => e.id.startsWith("pentest-agent-") && e.status === "completed").map((e) => e.id));
276
+ }
277
+ function parseSubagentFilename(filename) {
278
+ if (filename.startsWith("attack-surface-agent")) {
279
+ return { agentType: "attack-surface", name: "Attack Surface Discovery" };
280
+ }
281
+ const pentestMatch = filename.match(/^pentest-agent-(\d+)/);
282
+ if (pentestMatch) {
283
+ return {
284
+ agentType: "pentest",
285
+ name: `Pentest Agent ${pentestMatch[1]}`
286
+ };
287
+ }
288
+ if (filename.startsWith("vuln-test-")) {
289
+ const parts = filename.replace("vuln-test-", "").split("-");
290
+ const vulnClass = parts[0] || "generic";
291
+ const vulnClassFormatted = vulnClass.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
292
+ return {
293
+ agentType: "pentest",
294
+ name: `${vulnClassFormatted} Test`
295
+ };
296
+ }
297
+ if (filename.startsWith("orchestrator-")) {
298
+ return { agentType: "pentest", name: "Orchestrator Summary" };
299
+ }
300
+ return { agentType: "pentest", name: filename.split("-")[0] || "Unknown" };
301
+ }
302
+ function convertMessagesToUI(messages, baseTime) {
303
+ const uiMessages = [];
304
+ let messageIndex = 0;
305
+ const toolResults = new Map;
306
+ for (const msg of messages) {
307
+ if (Array.isArray(msg.content)) {
308
+ for (const part of msg.content) {
309
+ if (part.type === "tool-result" && part.toolCallId) {
310
+ toolResults.set(part.toolCallId, part.output);
311
+ }
312
+ }
313
+ }
314
+ }
315
+ for (const msg of messages) {
316
+ const createdAt = new Date(baseTime.getTime() + messageIndex * 1000);
317
+ messageIndex++;
318
+ if (typeof msg.content === "string") {
319
+ uiMessages.push({
320
+ role: msg.role,
321
+ content: msg.content,
322
+ createdAt
323
+ });
324
+ } else if (Array.isArray(msg.content)) {
325
+ for (const part of msg.content) {
326
+ if (part.type === "text" && part.text) {
327
+ uiMessages.push({
328
+ role: "assistant",
329
+ content: part.text,
330
+ createdAt
331
+ });
332
+ } else if (part.type === "tool-call") {
333
+ const input = part.input;
334
+ const toolDescription = typeof input?.toolCallDescription === "string" ? input.toolCallDescription : part.toolName || "tool";
335
+ const result = part.toolCallId ? toolResults.get(part.toolCallId) : undefined;
336
+ const resultText = typeof result === "string" ? result : result != null && typeof result === "object" && ("value" in result) && typeof result.value === "string" ? result.value : undefined;
337
+ const cancelled = typeof resultText === "string" && resultText.toLowerCase().includes("cancelled");
338
+ uiMessages.push({
339
+ role: "tool",
340
+ content: toolDescription,
341
+ createdAt,
342
+ toolCallId: part.toolCallId,
343
+ toolName: part.toolName,
344
+ args: input,
345
+ result,
346
+ status: cancelled ? "error" : "completed"
347
+ });
348
+ }
349
+ }
350
+ }
351
+ }
352
+ return uiMessages;
353
+ }
354
+ function convertModelMessagesToUI(messages) {
355
+ return convertMessagesToUI(messages, new Date);
356
+ }
357
+ function loadSubagents(rootPath) {
358
+ const subagentsPath = join(rootPath, SUBAGENTS_DIR);
359
+ const subagents = [];
360
+ const agentNameIndex = new Map;
361
+ if (existsSync(subagentsPath)) {
362
+ const files = readdirSync(subagentsPath).filter((f) => f.endsWith(".json"));
363
+ files.sort();
364
+ for (const file of files) {
365
+ if (file.startsWith("orchestrator-"))
366
+ continue;
367
+ try {
368
+ const filePath = join(subagentsPath, file);
369
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
370
+ const { agentType, name } = parseSubagentFilename(file);
371
+ const timestamp = new Date(data.timestamp);
372
+ const messages = convertMessagesToUI(data.messages, timestamp);
373
+ let status = "completed";
374
+ switch (data.status) {
375
+ case "canceled":
376
+ case "failed":
377
+ case "completed":
378
+ case "pending":
379
+ status = data.status;
380
+ break;
381
+ default:
382
+ if (typeof data.findingsCount === "number" && data.findingsCount < 0) {
383
+ status = "failed";
384
+ }
385
+ break;
386
+ }
387
+ agentNameIndex.set(data.agentName, subagents.length);
388
+ subagents.push({
389
+ id: data.agentName,
390
+ name: data.agentName === "attack-surface-agent" ? "Attack Surface Discovery" : name,
391
+ type: agentType,
392
+ target: data.target || "Unknown",
393
+ messages,
394
+ createdAt: timestamp,
395
+ status
396
+ });
397
+ } catch (e) {
398
+ console.error(`Failed to load subagent file ${file}:`, e);
399
+ }
400
+ }
401
+ }
402
+ const manifestPath = join(rootPath, MANIFEST_FILE);
403
+ if (existsSync(manifestPath)) {
404
+ try {
405
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
406
+ for (const entry of manifest) {
407
+ if (entry.status !== "running")
408
+ continue;
409
+ const resumeInfo = {
410
+ target: entry.target,
411
+ objective: entry.objective,
412
+ vulnerabilityClass: entry.vulnerabilityClass,
413
+ authenticationInfo: entry.authInfo
414
+ };
415
+ const existingIndex = agentNameIndex.get(entry.id);
416
+ if (existingIndex !== undefined) {
417
+ subagents[existingIndex] = {
418
+ ...subagents[existingIndex],
419
+ status: "paused",
420
+ resumeInfo
421
+ };
422
+ } else {
423
+ subagents.push({
424
+ id: entry.id,
425
+ name: entry.name,
426
+ type: "pentest",
427
+ target: entry.target,
428
+ messages: [],
429
+ createdAt: new Date(entry.spawnedAt),
430
+ status: "paused",
431
+ resumeInfo
432
+ });
433
+ }
434
+ }
435
+ } catch (e) {
436
+ console.error("Failed to load agent manifest:", e);
437
+ }
438
+ }
439
+ return subagents;
440
+ }
441
+
442
+ // src/core/session/execution-metrics.ts
443
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
444
+ import { join as join2 } from "path";
445
+ var EXECUTION_METRICS_FILENAME = "execution-metrics.json";
446
+ function toNonNegativeInteger(value) {
447
+ const n = Number(value);
448
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
449
+ }
450
+ function normalizeTokenUsage(value) {
451
+ const inputTokens = toNonNegativeInteger(value?.inputTokens);
452
+ const outputTokens = toNonNegativeInteger(value?.outputTokens);
453
+ const derivedTotal = inputTokens + outputTokens;
454
+ const explicitTotal = toNonNegativeInteger(value?.totalTokens);
455
+ return {
456
+ inputTokens,
457
+ outputTokens,
458
+ totalTokens: explicitTotal > 0 ? explicitTotal : derivedTotal
459
+ };
460
+ }
461
+ function metricsPath(sessionRootPath) {
462
+ return join2(sessionRootPath, EXECUTION_METRICS_FILENAME);
463
+ }
464
+ function sessionJsonPath(sessionRootPath) {
465
+ return join2(sessionRootPath, "session.json");
466
+ }
467
+ function readExecutionMetrics(sessionRootPath) {
468
+ const path = metricsPath(sessionRootPath);
469
+ if (!existsSync2(path))
470
+ return null;
471
+ try {
472
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
473
+ return {
474
+ tokenUsage: normalizeTokenUsage(parsed.tokenUsage),
475
+ runtime: typeof parsed.runtime === "string" ? parsed.runtime : undefined,
476
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString()
477
+ };
478
+ } catch {
479
+ return null;
480
+ }
481
+ }
482
+ function writeSessionJsonTokenTotals(sessionRootPath, tokenUsage) {
483
+ const path = sessionJsonPath(sessionRootPath);
484
+ if (!existsSync2(path))
485
+ return;
486
+ try {
487
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
488
+ parsed.tokensIn = tokenUsage.inputTokens;
489
+ parsed.tokensOut = tokenUsage.outputTokens;
490
+ writeFileSync2(path, JSON.stringify(parsed, null, 2));
491
+ } catch {}
492
+ }
493
+ function writeExecutionMetrics(input) {
494
+ const existing = readExecutionMetrics(input.sessionRootPath);
495
+ const nextTokenUsage = input.tokenUsage ? normalizeTokenUsage(input.tokenUsage) : existing?.tokenUsage ?? {
496
+ inputTokens: 0,
497
+ outputTokens: 0,
498
+ totalTokens: 0
499
+ };
500
+ const next = {
501
+ tokenUsage: nextTokenUsage,
502
+ runtime: input.runtime ?? existing?.runtime,
503
+ updatedAt: new Date().toISOString()
504
+ };
505
+ writeFileSync2(metricsPath(input.sessionRootPath), JSON.stringify(next, null, 2), "utf-8");
506
+ writeSessionJsonTokenTotals(input.sessionRootPath, next.tokenUsage);
507
+ return next;
508
+ }
509
+ function formatDurationHmsFromMs(durationMs) {
510
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
511
+ const hours = Math.floor(totalSeconds / 3600);
512
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
513
+ const seconds = totalSeconds % 60;
514
+ return `${hours}h${minutes}m${seconds}s`;
515
+ }
516
+
517
+ // src/core/utils/concurrency.ts
518
+ async function runWithBoundedConcurrency(items, concurrency, fn, abortSignal) {
519
+ const results = new Array(items.length).fill(null);
520
+ let nextIdx = 0;
521
+ let completed = 0;
522
+ await new Promise((resolve) => {
523
+ if (items.length === 0) {
524
+ resolve();
525
+ return;
526
+ }
527
+ let active = 0;
528
+ function next() {
529
+ if (completed >= nextIdx && (nextIdx === items.length || abortSignal?.aborted)) {
530
+ resolve();
531
+ return;
532
+ }
533
+ while (active < concurrency && nextIdx < items.length && !abortSignal?.aborted) {
534
+ const idx = nextIdx++;
535
+ active++;
536
+ fn(items[idx], idx).then((r) => {
537
+ results[idx] = r;
538
+ }).catch(() => {
539
+ results[idx] = null;
540
+ }).finally(() => {
541
+ active--;
542
+ completed++;
543
+ next();
544
+ });
545
+ }
546
+ }
547
+ next();
548
+ });
549
+ return results;
550
+ }
551
+
552
+ // src/core/session/loader.ts
553
+ import { join as join3 } from "path";
554
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
555
+ function loadAttackSurfaceResults(rootPath) {
556
+ const resultsPath = join3(rootPath, "attack-surface-results.json");
557
+ if (!existsSync3(resultsPath)) {
558
+ return null;
559
+ }
560
+ try {
561
+ return JSON.parse(readFileSync3(resultsPath, "utf-8"));
562
+ } catch (e) {
563
+ console.error("Failed to load attack surface results:", e);
564
+ return null;
565
+ }
566
+ }
567
+ function hasReport(rootPath) {
568
+ const reportPath = join3(rootPath, REPORT_FILENAME_MD);
569
+ return existsSync3(reportPath);
570
+ }
571
+ function createDiscoveryFromLogs(rootPath, session) {
572
+ const logPath = join3(rootPath, "logs", "streamlined-pentest.log");
573
+ if (!existsSync3(logPath)) {
574
+ return null;
575
+ }
576
+ try {
577
+ const logContent = readFileSync3(logPath, "utf-8");
578
+ const lines = logContent.split(`
579
+ `).filter(Boolean);
580
+ const messages = [];
581
+ for (const line of lines) {
582
+ const match = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z) - \[(\w+)\] (.+)$/);
583
+ if (!match)
584
+ continue;
585
+ const [, timestamp, _level, content] = match;
586
+ const createdAt = new Date(timestamp);
587
+ if (content.startsWith("[Tool]")) {
588
+ const toolMatch = content.match(/\[Tool\] (\w+): (.+)/);
589
+ if (toolMatch) {
590
+ messages.push({
591
+ role: "tool",
592
+ content: `✓ ${toolMatch[2]}`,
593
+ createdAt,
594
+ toolName: toolMatch[1],
595
+ status: "completed"
596
+ });
597
+ }
598
+ } else if (content.startsWith("[Step")) {
599
+ const stepMatch = content.match(/\[Step \d+\] (.+)/);
600
+ if (stepMatch) {
601
+ messages.push({
602
+ role: "assistant",
603
+ content: stepMatch[1],
604
+ createdAt
605
+ });
606
+ }
607
+ }
608
+ }
609
+ if (messages.length === 0) {
610
+ return null;
611
+ }
612
+ return {
613
+ id: "discovery-from-logs",
614
+ name: "Attack Surface Discovery",
615
+ type: "attack-surface",
616
+ target: session.targets[0] || "Unknown",
617
+ messages,
618
+ createdAt: new Date(session.time.created),
619
+ status: "completed"
620
+ };
621
+ } catch (e) {
622
+ console.error("Failed to parse logs:", e);
623
+ return null;
624
+ }
625
+ }
626
+ async function loadSessionState(session) {
627
+ const rootPath = session.rootPath;
628
+ let subagents = loadSubagents(rootPath);
629
+ const hasAttackSurfaceAgent = subagents.some((s) => s.type === "attack-surface");
630
+ if (!hasAttackSurfaceAgent) {
631
+ const discoveryAgent = createDiscoveryFromLogs(rootPath, session);
632
+ if (discoveryAgent) {
633
+ subagents = [discoveryAgent, ...subagents];
634
+ }
635
+ }
636
+ const attackSurfaceResults = loadAttackSurfaceResults(rootPath);
637
+ const hasReportFile = hasReport(rootPath);
638
+ const hasDiscoverySubagent = subagents.some((s) => s.type === "attack-surface");
639
+ const interruptedDuringDiscovery = !attackSurfaceResults && !hasReportFile && hasDiscoverySubagent;
640
+ if (interruptedDuringDiscovery) {
641
+ for (let i = 0;i < subagents.length; i++) {
642
+ if (subagents[i].type === "attack-surface" && subagents[i].status === "completed") {
643
+ subagents[i] = { ...subagents[i], status: "paused" };
644
+ }
645
+ }
646
+ }
647
+ const isComplete = hasReportFile;
648
+ return {
649
+ session,
650
+ subagents,
651
+ attackSurfaceResults,
652
+ isComplete,
653
+ hasReport: hasReportFile,
654
+ interruptedDuringDiscovery
655
+ };
656
+ }
657
+
658
+ // src/core/workflows/whiteboxAttackSurface.ts
659
+ init_zod();
660
+
661
+ // src/core/workflows/riskScoring.ts
662
+ init_zod();
663
+ var DEFAULT_CONCURRENCY = 5;
664
+ var RiskScoreResultSchema = exports_external.object({
665
+ exposure: exports_external.number().min(0).max(3).describe("Exposure Level (0-3): 3=Public endpoint no auth, 2=Requires standard user login, 1=Requires privileged/admin access, 0=Private/internal-only"),
666
+ exposureReasoning: exports_external.string().describe("Brief explanation for the exposure score"),
667
+ dataSensitivity: exports_external.number().min(0).max(3).describe("Data Sensitivity (0-3): 3=PII/PHI/financial/passwords/tokens, 2=Business operations/configs, 1=Low-value user data, 0=No meaningful data"),
668
+ dataSensitivityReasoning: exports_external.string().describe("Brief explanation for the data sensitivity score"),
669
+ functionCriticality: exports_external.number().min(0).max(2).describe("Function Criticality (0-2): 2=Auth flows/password resets/payments/state-changing mutations, 1=Core product functionality, 0=Non-critical content"),
670
+ functionCriticalityReasoning: exports_external.string().describe("Brief explanation for the function criticality score"),
671
+ securityIndicators: exports_external.number().min(0).max(2).describe("Security Indicators (0-2): 2=Critical vulnerability patterns found (SQL injection, command injection, hardcoded secrets, path traversal), 1=Moderate security concerns (missing input validation, weak error handling), 0=No obvious security issues"),
672
+ securityIndicatorsReasoning: exports_external.string().describe("Brief explanation for the security indicators score, including specific vulnerability patterns observed if any"),
673
+ justification: exports_external.string().describe("Overall justification summarizing why this endpoint received this risk score")
674
+ });
675
+ var RISK_SCORE_SYSTEM_PROMPT = `You are an Endpoint Risk Scoring Agent. Your task is to evaluate an API or webpage endpoint and assign a Risk Score (0-10) that reflects how important it is to test this endpoint during a penetration test.
676
+
677
+ Read the source code at the specified location, analyze it, and provide a structured risk assessment. Be thorough but efficient — read only the relevant code.`;
678
+ async function scoreEndpoints(input) {
679
+ const {
680
+ codebasePath,
681
+ endpoints,
682
+ model,
683
+ session,
684
+ authConfig,
685
+ abortSignal,
686
+ callbacks,
687
+ concurrency = DEFAULT_CONCURRENCY
688
+ } = input;
689
+ const results = new Map;
690
+ const scored = await runWithBoundedConcurrency(endpoints, concurrency, async (ep) => {
691
+ const key = `${ep.method}:${ep.file}:${ep.path}`;
692
+ const subagentId = `risk-score-${ep.appName}-${ep.method}-${ep.path}`;
693
+ callbacks?.subagentCallbacks?.onSubagentSpawn?.({
694
+ subagentId,
695
+ input: { app: ep.appName, path: ep.path },
696
+ status: "pending"
697
+ });
698
+ try {
699
+ const score = await scoreEndpoint({
700
+ codebasePath,
701
+ endpoint: ep,
702
+ model,
703
+ session,
704
+ authConfig,
705
+ abortSignal,
706
+ callbacks
707
+ });
708
+ callbacks?.subagentCallbacks?.onSubagentComplete?.({
709
+ subagentId,
710
+ input: { app: ep.appName, path: ep.path },
711
+ status: "completed"
712
+ });
713
+ return { key, score };
714
+ } catch (error) {
715
+ console.error(`Risk scoring failed for ${ep.path}: ${error instanceof Error ? error.message : String(error)}`);
716
+ callbacks?.subagentCallbacks?.onSubagentComplete?.({
717
+ subagentId,
718
+ input: { app: ep.appName, path: ep.path },
719
+ status: "failed"
720
+ });
721
+ return null;
722
+ }
723
+ });
724
+ for (const r of scored) {
725
+ if (r)
726
+ results.set(r.key, r.score);
727
+ }
728
+ return results;
729
+ }
730
+ async function scoreEndpoint(opts) {
731
+ const {
732
+ codebasePath,
733
+ endpoint: ep,
734
+ model,
735
+ session,
736
+ authConfig,
737
+ abortSignal,
738
+ callbacks
739
+ } = opts;
740
+ const lineRange = ep.line ? `around line ${ep.line}` : "";
741
+ const authInfo = ep.authRequired ? "Authentication required" : "No authentication required";
742
+ const objective = `# Endpoint Risk Score Assessment
743
+
744
+ ## Endpoint
745
+ - **Method**: ${ep.method}
746
+ - **Path**: ${ep.path}
747
+ - **File**: ${ep.file} ${lineRange}
748
+ - **Handler**: ${ep.handler ?? "unknown"}
749
+ - **Auth**: ${authInfo}
750
+ - **Description**: ${ep.description ?? "N/A"}
751
+
752
+ ## Task
753
+ 1. Read the source file at \`${ep.file}\` ${lineRange ? `(${lineRange})` : ""} to understand the implementation
754
+ 2. Analyze authentication requirements, data handling, business logic, and security patterns
755
+ 3. Score each dimension and provide your assessment via the \`response\` tool
756
+
757
+ ## Scoring Model
758
+
759
+ ### 1. Exposure Level (0-3)
760
+ | Score | Description |
761
+ |-------|-------------|
762
+ | 3 | Public endpoint, no authentication required |
763
+ | 2 | Requires standard user login |
764
+ | 1 | Requires privileged/admin access |
765
+ | 0 | Private, IP-restricted, or internal-only |
766
+
767
+ ### 2. Data Sensitivity (0-3)
768
+ | Score | Example Data |
769
+ |-------|-------------|
770
+ | 3 | PII, PHI, financial data, passwords, tokens, secrets |
771
+ | 2 | Business operations data, configs, settings |
772
+ | 1 | Low-value or non-sensitive user data |
773
+ | 0 | No meaningful data (static content, health checks) |
774
+
775
+ ### 3. Function Criticality (0-2)
776
+ | Score | Examples |
777
+ |-------|---------|
778
+ | 2 | Auth flows, password resets, payments, permission changes |
779
+ | 1 | Core product functionality (CRUD on user data) |
780
+ | 0 | Non-critical content or utility endpoints |
781
+
782
+ ### 4. Security Indicators (0-2)
783
+ | Score | Indicators |
784
+ |-------|-----------|
785
+ | 2 | Critical vuln patterns: SQL injection, command injection, hardcoded secrets, path traversal, unsafe deserialization |
786
+ | 1 | Moderate concerns: missing input validation, weak error handling, missing output encoding, overly permissive CORS |
787
+ | 0 | No obvious security issues — code follows best practices |
788
+
789
+ **Final Score = Exposure + DataSensitivity + FunctionCriticality + SecurityIndicators (0-10)**
790
+
791
+ Begin by reading the source code, then call \`response\` with your assessment.`;
792
+ const agent = new CodeAgent({
793
+ codebasePath,
794
+ objective,
795
+ system: RISK_SCORE_SYSTEM_PROMPT,
796
+ model,
797
+ session,
798
+ authConfig,
799
+ abortSignal,
800
+ callbacks,
801
+ responseSchema: RiskScoreResultSchema
802
+ });
803
+ const result = await agent.consume({
804
+ onError: (e) => callbacks?.onError?.(e),
805
+ subagentCallbacks: callbacks?.subagentCallbacks
806
+ });
807
+ if (!result) {
808
+ return {
809
+ score: 0,
810
+ explanation: "Risk scoring agent did not return a result",
811
+ breakdown: {
812
+ exposure: 0,
813
+ dataSensitivity: 0,
814
+ functionCriticality: 0,
815
+ securityIndicators: 0
816
+ }
817
+ };
818
+ }
819
+ const totalScore = result.exposure + result.dataSensitivity + result.functionCriticality + result.securityIndicators;
820
+ return {
821
+ score: totalScore,
822
+ explanation: result.justification,
823
+ breakdown: {
824
+ exposure: result.exposure,
825
+ dataSensitivity: result.dataSensitivity,
826
+ functionCriticality: result.functionCriticality,
827
+ securityIndicators: result.securityIndicators
828
+ }
829
+ };
830
+ }
831
+
832
+ // src/core/workflows/whiteboxAttackSurface.ts
833
+ var DEFAULT_CONCURRENCY2 = 5;
834
+ 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.
835
+
836
+ Your focus is on **deployed applications and services** — APIs, web apps, microservices — that listen on a port and serve traffic. Ignore libraries, shared packages, SDKs, CLI tools, build scripts, and test suites unless they are part of a deployable service.
837
+
838
+ # Tool Usage Guide
839
+
840
+ ## read_file
841
+ Read the contents of any file. You can read the whole file or a specific line range.
842
+ - When a file is large, read it in chunks using startLine / endLine to stay focused.
843
+ - Follow imports and references — when you see an interesting function call, read its source.
844
+
845
+ ## list_files
846
+ List files and directories. Use this to orient yourself in the codebase.
847
+ - Start by listing the project root or relevant subdirectory to understand the structure.
848
+ - Use recursive=true sparingly on targeted subdirectories to avoid flooding context.
849
+
850
+ ## grep
851
+ Search file contents by pattern. This is your most powerful navigation tool.
852
+ - Use it to find route definitions, middleware, controllers, endpoint registrations, etc.
853
+ - Use -i for case-insensitive searches.
854
+ - Use --include="*.ext" to narrow to relevant file types.
855
+ - Use -C 3 or -C 5 to get context around matches.
856
+ - Use -rn (default for directories) for recursive search with line numbers.
857
+ - Use -l to get just file paths when you need a broad overview of where something appears.
858
+
859
+ ## execute_command
860
+ Run shell commands when needed.
861
+ - Use for build tools, git operations, package managers, linters, etc.
862
+
863
+ ## document_asset
864
+ **Use this to document every significant asset you discover.** Each call persists a JSON record to the session's assets directory. Document assets as you discover them — don't wait until the end.
865
+
866
+ Document these types of assets:
867
+ - **web_application**: Each application/service you identify (include framework and technology stack in details)
868
+ - **api**: API services or microservices (include base URL and authentication type in details)
869
+ - **admin_panel**: Admin interfaces, dashboards, management UIs
870
+ - **endpoint**: Notable endpoint groups — auth endpoints, file upload handlers, payment flows, admin routes
871
+ - **development_asset**: Dev/staging environments, CI/CD pipelines, internal tools
872
+
873
+ For each asset, include:
874
+ - **assetName**: A unique descriptive name (e.g., "user-api", "admin-dashboard", "payment-service")
875
+ - **assetType**: One of the types above
876
+ - **description**: What it is, what it does, why it matters for security
877
+ - **details**: Include \`technology\` (stack), \`endpoints\` (key routes), \`authentication\` (auth type), and \`url\` if known
878
+ - **riskLevel**: CRITICAL for auth/payment/admin, HIGH for user data, MEDIUM for general functionality, LOW for static/public
879
+
880
+ ## response
881
+ When your objective includes structured output, call \`response\` with your final results once you are done. This ends your run — make sure all data is included.
882
+
883
+ # Working Approach
884
+ 1. **Orient first** — list files and read key entry points to understand the structure.
885
+ 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.
886
+ 3. **Search, then read** — use grep to locate what you need, then read the relevant files.
887
+ 4. **Document as you go** — call document_asset for every significant asset you discover. Don't batch them up.
888
+ 5. **Follow the trail** — trace through imports, function calls, and references to build full understanding.
889
+ 6. **Be thorough** — don't stop at the first match. Cover everything relevant to the objective.
890
+ `;
891
+ var AppInfoSchema = exports_external.object({
892
+ name: exports_external.string().describe("Application or service name"),
893
+ framework: exports_external.string().describe("Framework in use (e.g. Express, Next.js, Django, FastAPI, Rails)"),
894
+ description: exports_external.string().describe("Brief description of what this app does"),
895
+ location: exports_external.string().describe("Path to the app root relative to the repository root")
896
+ });
897
+ var AppsDiscoveryResultSchema = exports_external.object({
898
+ repoType: exports_external.string().describe("e.g. monorepo, single-app, multi-package"),
899
+ packageManager: exports_external.string().describe("e.g. npm, yarn, pnpm, pip, cargo, go modules"),
900
+ apps: exports_external.array(AppInfoSchema).describe("All applications/services discovered in the repository")
901
+ });
902
+ var EndpointsDiscoveryResultSchema = exports_external.object({
903
+ endpoints: exports_external.array(EndpointSchema).describe("All discovered endpoints")
904
+ });
905
+ async function runWhiteboxAttackSurfaceWorkflow(input) {
906
+ const {
907
+ codebasePath,
908
+ model,
909
+ session,
910
+ authConfig,
911
+ abortSignal,
912
+ callbacks,
913
+ attackSurfaceRegistry,
914
+ onStepFinish
915
+ } = input;
916
+ const appsAgent = new CodeAgent({
917
+ codebasePath,
918
+ objective: buildAppsDiscoveryObjective(codebasePath),
919
+ system: WHITEBOX_CODE_AGENT_SYSTEM_PROMPT,
920
+ model,
921
+ session,
922
+ authConfig,
923
+ abortSignal,
924
+ attackSurfaceRegistry,
925
+ callbacks,
926
+ onStepFinish: (event) => onStepFinish?.(event),
927
+ responseSchema: AppsDiscoveryResultSchema
928
+ });
929
+ const appsResult = await appsAgent.consume({
930
+ onTextDelta: (d) => callbacks?.onTextDelta?.(d),
931
+ onToolCallStreaming: (d) => callbacks?.onToolCallStreaming?.(d),
932
+ onToolCallDelta: (d) => callbacks?.onToolCallDelta?.(d),
933
+ onToolCall: (d) => callbacks?.onToolCall?.(d),
934
+ onToolResult: (d) => callbacks?.onToolResult?.(d),
935
+ onError: (e) => callbacks?.onError?.(e),
936
+ subagentCallbacks: callbacks?.subagentCallbacks
937
+ });
938
+ if (!appsResult || appsResult.apps.length === 0) {
939
+ return {
940
+ repoType: appsResult?.repoType ?? "unknown",
941
+ packageManager: appsResult?.packageManager ?? "unknown",
942
+ apps: [],
943
+ summary: {
944
+ totalApps: 0,
945
+ totalPages: 0,
946
+ totalApiEndpoints: 0,
947
+ totalPentestObjectives: 0
948
+ }
949
+ };
950
+ }
951
+ const tasks = appsResult.apps.flatMap((app) => [
952
+ { appInfo: app, type: "pages" },
953
+ { appInfo: app, type: "apiEndpoints" }
954
+ ]);
955
+ const taskResults = await runWithBoundedConcurrency(tasks, DEFAULT_CONCURRENCY2, async (task, _index) => {
956
+ const subagentId = `${task.type}-${task.appInfo.name}`;
957
+ callbacks?.subagentCallbacks?.onSubagentSpawn?.({
958
+ subagentId,
959
+ input: { app: task.appInfo.name, type: task.type },
960
+ status: "pending"
961
+ });
962
+ const objective = task.type === "pages" ? buildPagesDiscoveryObjective(codebasePath, task.appInfo) : buildApiEndpointsDiscoveryObjective(codebasePath, task.appInfo);
963
+ const agent = new CodeAgent({
964
+ codebasePath,
965
+ objective,
966
+ system: WHITEBOX_CODE_AGENT_SYSTEM_PROMPT,
967
+ model,
968
+ session,
969
+ authConfig,
970
+ abortSignal,
971
+ attackSurfaceRegistry,
972
+ callbacks,
973
+ onStepFinish: (event) => onStepFinish?.(event),
974
+ responseSchema: EndpointsDiscoveryResultSchema
975
+ });
976
+ try {
977
+ const result = await agent.consume({
978
+ onError: (e) => callbacks?.onError?.(e),
979
+ subagentCallbacks: callbacks?.subagentCallbacks ? {
980
+ onTextDelta: (d) => callbacks.subagentCallbacks.onTextDelta?.({
981
+ ...d,
982
+ subagentId
983
+ }),
984
+ onToolCallStreaming: (d) => callbacks.subagentCallbacks.onToolCallStreaming?.({
985
+ ...d,
986
+ subagentId
987
+ }),
988
+ onToolCallDelta: (d) => callbacks.subagentCallbacks.onToolCallDelta?.({
989
+ ...d,
990
+ subagentId
991
+ }),
992
+ onToolCall: (d) => callbacks.subagentCallbacks.onToolCall?.({
993
+ ...d,
994
+ subagentId
995
+ }),
996
+ onToolResult: (d) => callbacks.subagentCallbacks.onToolResult?.({
997
+ ...d,
998
+ subagentId
999
+ }),
1000
+ onError: (e) => callbacks.subagentCallbacks.onError?.(e)
1001
+ } : undefined
1002
+ });
1003
+ callbacks?.subagentCallbacks?.onSubagentComplete?.({
1004
+ subagentId,
1005
+ input: { app: task.appInfo.name, type: task.type },
1006
+ status: "completed"
1007
+ });
1008
+ return {
1009
+ appName: task.appInfo.name,
1010
+ type: task.type,
1011
+ endpoints: result?.endpoints ?? []
1012
+ };
1013
+ } catch (error) {
1014
+ callbacks?.subagentCallbacks?.onSubagentComplete?.({
1015
+ subagentId,
1016
+ input: { app: task.appInfo.name, type: task.type },
1017
+ status: "failed"
1018
+ });
1019
+ return { appName: task.appInfo.name, type: task.type, endpoints: [] };
1020
+ }
1021
+ });
1022
+ const pagesByApp = new Map;
1023
+ const apiEndpointsByApp = new Map;
1024
+ for (const r of taskResults) {
1025
+ if (!r)
1026
+ continue;
1027
+ const map = r.type === "pages" ? pagesByApp : apiEndpointsByApp;
1028
+ map.set(r.appName, r.endpoints);
1029
+ }
1030
+ const allEndpointsForScoring = appsResult.apps.flatMap((appInfo) => {
1031
+ const pages = pagesByApp.get(appInfo.name) ?? [];
1032
+ const apiEps = apiEndpointsByApp.get(appInfo.name) ?? [];
1033
+ return [...pages, ...apiEps].map((ep) => ({
1034
+ ...ep,
1035
+ appName: appInfo.name
1036
+ }));
1037
+ });
1038
+ let riskScores = new Map;
1039
+ if (allEndpointsForScoring.length > 0) {
1040
+ try {
1041
+ riskScores = await scoreEndpoints({
1042
+ codebasePath,
1043
+ endpoints: allEndpointsForScoring,
1044
+ model,
1045
+ session,
1046
+ authConfig,
1047
+ abortSignal,
1048
+ callbacks
1049
+ });
1050
+ console.log(`Risk scoring complete: ${riskScores.size}/${allEndpointsForScoring.length} endpoints scored`);
1051
+ } catch (error) {
1052
+ console.error("Risk scoring phase failed, continuing without scores:", error);
1053
+ }
1054
+ }
1055
+ function attachRiskScore(ep) {
1056
+ const key = `${ep.method}:${ep.file}:${ep.path}`;
1057
+ const score = riskScores.get(key);
1058
+ return score ? { ...ep, riskScore: score } : ep;
1059
+ }
1060
+ const apps = appsResult.apps.map((appInfo) => ({
1061
+ name: appInfo.name,
1062
+ framework: appInfo.framework,
1063
+ description: appInfo.description,
1064
+ location: appInfo.location,
1065
+ pages: (pagesByApp.get(appInfo.name) ?? []).map(attachRiskScore),
1066
+ apiEndpoints: (apiEndpointsByApp.get(appInfo.name) ?? []).map(attachRiskScore)
1067
+ }));
1068
+ const totalPages = apps.reduce((sum, a) => sum + a.pages.length, 0);
1069
+ const totalApiEndpoints = apps.reduce((sum, a) => sum + a.apiEndpoints.length, 0);
1070
+ const totalPentestObjectives = apps.reduce((sum, a) => sum + [...a.pages, ...a.apiEndpoints].reduce((s, ep) => s + ep.pentestObjectives.length, 0), 0);
1071
+ return {
1072
+ repoType: appsResult.repoType,
1073
+ packageManager: appsResult.packageManager,
1074
+ apps,
1075
+ summary: {
1076
+ totalApps: apps.length,
1077
+ totalPages,
1078
+ totalApiEndpoints,
1079
+ totalPentestObjectives
1080
+ }
1081
+ };
1082
+ }
1083
+ function buildAppsDiscoveryObjective(codebasePath) {
1084
+ return `# Identify All Applications in the Repository
1085
+
1086
+ ## Codebase
1087
+ - **Path:** ${codebasePath}
1088
+
1089
+ ## Task
1090
+ Analyze the repository structure and identify every **deployed application or service** (APIs, web apps, microservices) defined within it.
1091
+
1092
+ **IMPORTANT: Only include deployable apps and services.** Exclude:
1093
+ - Libraries, SDKs, and shared packages that are consumed by other code but not deployed on their own
1094
+ - Git submodules (external dependencies)
1095
+ - Build tools, scripts, CLI utilities, and dev tooling
1096
+ - Test suites, fixtures, and test helpers
1097
+ - Documentation packages
1098
+
1099
+ 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).
1100
+
1101
+ ### Steps
1102
+ 1. List the root directory and read top-level config files (package.json, requirements.txt, Cargo.toml, go.mod, etc.)
1103
+ 2. **Check for git submodules** — run \`git submodule status\` or check for a \`.gitmodules\` file. Exclude all submodule directories.
1104
+ 3. Determine the **repo type**: monorepo (workspaces), single-app, multi-package, etc.
1105
+ 4. Determine the **package manager**: npm, yarn, pnpm, pip, cargo, go modules, etc.
1106
+ 5. Identify all **deployable** applications/services (ignoring submodules, libraries, and shared packages):
1107
+ - 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
1108
+ - For multi-service repos: look at separate service directories with their own server startup
1109
+ - For single apps: the root is the app
1110
+ 6. For each app, determine:
1111
+ - **name**: the application or service name
1112
+ - **framework**: the web framework (Express, Next.js, Django, FastAPI, Rails, Spring, etc.)
1113
+ - **description**: brief summary of what it does
1114
+ - **location**: path relative to the repository root
1115
+
1116
+ When finished, call the \`response\` tool with your structured findings.`;
1117
+ }
1118
+ function buildPagesDiscoveryObjective(codebasePath, appInfo) {
1119
+ return `# Find All Web Pages in ${appInfo.name}
1120
+
1121
+ ## Codebase
1122
+ - **Repository root:** ${codebasePath}
1123
+ - **App location:** ${appInfo.location}
1124
+ - **Framework:** ${appInfo.framework}
1125
+
1126
+ ## Task
1127
+ Find ALL web pages, views, and routes that render HTML or serve client-side UI in this application.
1128
+
1129
+ ### What to look for (by framework)
1130
+ - **React/Next.js**: pages/ or app/ directory, route components, layout files
1131
+ - **Express**: res.render(), res.sendFile(), static file serving, template routes
1132
+ - **Django**: urls.py patterns pointing to template views, class-based views with template_name
1133
+ - **Rails**: routes.rb entries pointing to controller actions that render views
1134
+ - **Vue/Nuxt**: pages/ directory, router definitions
1135
+ - **FastAPI**: routes returning HTMLResponse, Jinja2 template responses
1136
+ - **Spring**: @Controller methods returning view names, Thymeleaf templates
1137
+
1138
+ ### For each page, provide
1139
+ - **method**: "PAGE" or "GET"
1140
+ - **path**: the route path (e.g. /dashboard, /settings)
1141
+ - **handler**: the handler function or component name (if identifiable)
1142
+ - **file**: the file where this page is defined
1143
+ - **line**: line number (if determinable)
1144
+ - **authRequired**: whether the page requires authentication (look for middleware, guards, decorators)
1145
+ - **description**: brief description of what this page shows
1146
+ - **pentestObjectives**: specific testing goals, e.g.:
1147
+ - "Test for XSS in user-editable fields on the profile page"
1148
+ - "Test for authorization bypass — access admin dashboard as regular user"
1149
+ - "Test for CSRF on the settings update form"
1150
+
1151
+ Be thorough — examine every route file, every page directory, every template.
1152
+ When finished, call the \`response\` tool with your structured findings.`;
1153
+ }
1154
+ function buildApiEndpointsDiscoveryObjective(codebasePath, appInfo) {
1155
+ return `# Find All API Endpoints in ${appInfo.name}
1156
+
1157
+ ## Codebase
1158
+ - **Repository root:** ${codebasePath}
1159
+ - **App location:** ${appInfo.location}
1160
+ - **Framework:** ${appInfo.framework}
1161
+
1162
+ ## Task
1163
+ Find ALL API endpoints defined in this application.
1164
+
1165
+ ### What to look for (by framework)
1166
+ - **Express**: app.get(), app.post(), router.get(), router.post(), router.put(), router.delete(), etc.
1167
+ - **Next.js**: app/api/ or pages/api/ route handlers (GET, POST, PUT, DELETE exports)
1168
+ - **Django**: urls.py patterns pointing to API views, DRF viewsets, routers, @api_view decorators
1169
+ - **FastAPI**: @app.get(), @app.post(), @app.put(), @app.delete() decorators
1170
+ - **Rails**: routes.rb API namespaces, resources, controller actions
1171
+ - **Spring**: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @RequestMapping
1172
+ - **Go**: http.HandleFunc, mux.Handle, gin router methods
1173
+
1174
+ ### For each endpoint, provide
1175
+ - **method**: HTTP method (GET, POST, PUT, DELETE, PATCH, etc.)
1176
+ - **path**: the route path (e.g. /api/users/:id, /api/orders)
1177
+ - **handler**: the handler function name (if identifiable)
1178
+ - **file**: the file where this endpoint is defined
1179
+ - **line**: line number (if determinable)
1180
+ - **authRequired**: whether the endpoint requires authentication (look for auth middleware, decorators, guards)
1181
+ - **description**: brief description of what this endpoint does
1182
+ - **pentestObjectives**: specific testing goals, e.g.:
1183
+ - "Test for SQL injection in the 'search' query parameter"
1184
+ - "Test for IDOR by accessing /api/orders/{id} with other users' order IDs"
1185
+ - "Test for privilege escalation by calling admin-only endpoint as regular user"
1186
+ - "Test for mass assignment by sending extra fields in the POST body"
1187
+ - "Test for rate limiting on the login endpoint"
1188
+
1189
+ Be thorough — trace through all route registrations, middleware chains, and controller files.
1190
+ When finished, call the \`response\` tool with your structured findings.`;
1191
+ }
1192
+
1193
+ // src/core/workflows/pentest.ts
1194
+ var DEFAULT_CONCURRENCY3 = 10;
1195
+ function addUsageTotals(totals, usage) {
1196
+ if (!usage)
1197
+ return;
1198
+ const inputTokens = usage.inputTokens ?? 0;
1199
+ const outputTokens = usage.outputTokens ?? 0;
1200
+ const totalTokens = usage.totalTokens ?? inputTokens + outputTokens;
1201
+ totals.inputTokens += inputTokens;
1202
+ totals.outputTokens += outputTokens;
1203
+ totals.totalTokens += totalTokens;
1204
+ }
1205
+ async function runPentestSwarm(input) {
1206
+ const {
1207
+ targets,
1208
+ model,
1209
+ session,
1210
+ authConfig,
1211
+ abortSignal,
1212
+ findingsRegistry,
1213
+ subagentCallbacks,
1214
+ onError,
1215
+ concurrency = DEFAULT_CONCURRENCY3,
1216
+ onStepFinish
1217
+ } = input;
1218
+ const completedIds = getCompletedAgentIds(session);
1219
+ const existingManifest = readAgentManifest(session);
1220
+ const freshEntries = buildManifestEntries(targets);
1221
+ const manifestEntries = freshEntries.map((fresh) => {
1222
+ const existing = existingManifest.find((e) => e.id === fresh.id);
1223
+ if (existing && existing.status === "completed")
1224
+ return existing;
1225
+ return fresh;
1226
+ });
1227
+ writeAgentManifest(session, manifestEntries);
1228
+ const results = await runWithBoundedConcurrency(targets, concurrency, async (target, index) => {
1229
+ const subagentId = `pentest-agent-${index + 1}`;
1230
+ if (completedIds.has(subagentId))
1231
+ return null;
1232
+ const previousMessages = loadSubagentMessages(session, subagentId);
1233
+ let lastMessages = [];
1234
+ const handleStepFinish = (e) => {
1235
+ if (e.response.messages) {
1236
+ lastMessages = e.response.messages;
1237
+ }
1238
+ onStepFinish?.(e);
1239
+ };
1240
+ const wrappedCallbacks = subagentCallbacks ? {
1241
+ onTextDelta: (d) => subagentCallbacks.onTextDelta?.({ ...d, subagentId }),
1242
+ onToolCallStreaming: (d) => subagentCallbacks.onToolCallStreaming?.({
1243
+ ...d,
1244
+ subagentId
1245
+ }),
1246
+ onToolCallDelta: (d) => subagentCallbacks.onToolCallDelta?.({ ...d, subagentId }),
1247
+ onToolCall: (d) => subagentCallbacks.onToolCall?.({ ...d, subagentId }),
1248
+ onToolResult: (d) => subagentCallbacks.onToolResult?.({ ...d, subagentId }),
1249
+ onError: (e) => subagentCallbacks.onError?.(e)
1250
+ } : undefined;
1251
+ subagentCallbacks?.onSubagentSpawn?.({
1252
+ subagentId,
1253
+ name: target.name,
1254
+ input: { target: target.target, objectives: target.objectives },
1255
+ status: "pending"
1256
+ });
1257
+ try {
1258
+ const agent = new TargetedPentestAgent({
1259
+ target: target.target,
1260
+ objectives: target.objectives,
1261
+ model,
1262
+ session,
1263
+ authConfig,
1264
+ abortSignal,
1265
+ findingsRegistry,
1266
+ onStepFinish: handleStepFinish,
1267
+ messages: previousMessages.length > 0 ? previousMessages : undefined
1268
+ });
1269
+ const result = await agent.consume({
1270
+ onError: (e) => onError?.(e),
1271
+ subagentCallbacks: wrappedCallbacks
1272
+ });
1273
+ saveSubagentData(session, {
1274
+ agentName: subagentId,
1275
+ target: target.target,
1276
+ objective: target.objectives.join("; "),
1277
+ status: "completed",
1278
+ findingsCount: result.findings.length,
1279
+ messages: [...previousMessages, ...lastMessages]
1280
+ });
1281
+ updateManifestEntryStatus(session, subagentId, "completed");
1282
+ subagentCallbacks?.onSubagentComplete?.({
1283
+ subagentId,
1284
+ input: { target: target.target, objectives: target.objectives },
1285
+ status: "completed"
1286
+ });
1287
+ return result;
1288
+ } catch (error) {
1289
+ saveSubagentData(session, {
1290
+ agentName: subagentId,
1291
+ target: target.target,
1292
+ objective: target.objectives.join("; "),
1293
+ status: "failed",
1294
+ error: error instanceof Error ? error.message : String(error),
1295
+ messages: [...previousMessages, ...lastMessages]
1296
+ });
1297
+ updateManifestEntryStatus(session, subagentId, "failed");
1298
+ subagentCallbacks?.onSubagentComplete?.({
1299
+ subagentId,
1300
+ input: { target: target.target, objectives: target.objectives },
1301
+ status: "failed"
1302
+ });
1303
+ throw error;
1304
+ }
1305
+ }, abortSignal);
1306
+ finalizeManifest(session, manifestEntries, results);
1307
+ return results;
1308
+ }
1309
+ async function runPentestWorkflow(input) {
1310
+ const { target, cwd, model, session, authConfig, abortSignal, callbacks } = input;
1311
+ const startedAt = Date.now();
1312
+ const tokenUsageTotals = {
1313
+ inputTokens: 0,
1314
+ outputTokens: 0,
1315
+ totalTokens: 0
1316
+ };
1317
+ const onStepFinish = (event) => {
1318
+ addUsageTotals(tokenUsageTotals, event.usage);
1319
+ };
1320
+ try {
1321
+ const mode = cwd ? "whitebox" : "blackbox";
1322
+ let swarmTargets;
1323
+ const existingResults = loadAttackSurfaceResults(session.rootPath);
1324
+ if (existingResults?.targets && existingResults.targets.length > 0) {
1325
+ swarmTargets = existingResults.targets.map((t) => ({
1326
+ target: t.target,
1327
+ objectives: [t.objective]
1328
+ }));
1329
+ } else if (mode === "whitebox") {
1330
+ swarmTargets = await runWhiteboxPhase({
1331
+ codebasePath: cwd,
1332
+ baseTarget: target,
1333
+ model,
1334
+ session,
1335
+ authConfig,
1336
+ abortSignal,
1337
+ callbacks,
1338
+ onStepFinish
1339
+ });
1340
+ } else {
1341
+ swarmTargets = await runBlackboxPhase({
1342
+ target,
1343
+ model,
1344
+ session,
1345
+ authConfig,
1346
+ abortSignal,
1347
+ callbacks,
1348
+ onStepFinish
1349
+ });
1350
+ }
1351
+ if (abortSignal?.aborted) {
1352
+ throw new DOMException("Pentest aborted by user", "AbortError");
1353
+ }
1354
+ if (swarmTargets.length === 0) {
1355
+ const report2 = buildPentestReport([], {
1356
+ target,
1357
+ model,
1358
+ sessionId: session.id,
1359
+ mode
1360
+ });
1361
+ const mdPath2 = join4(session.rootPath, REPORT_FILENAME_MD);
1362
+ const jsonPath2 = join4(session.rootPath, REPORT_FILENAME_JSON);
1363
+ writeFileSync3(mdPath2, renderMarkdown(report2));
1364
+ writeFileSync3(jsonPath2, renderJson(report2));
1365
+ return {
1366
+ findings: [],
1367
+ findingsPath: session.findingsPath,
1368
+ pocsPath: session.pocsPath,
1369
+ reportPath: mdPath2
1370
+ };
1371
+ }
1372
+ const findingsRegistry = FindingsRegistry.fromDirectory(session.findingsPath, {
1373
+ model,
1374
+ authConfig,
1375
+ abortSignal
1376
+ });
1377
+ const completedCount = getCompletedAgentIds(session).size;
1378
+ if (completedCount < swarmTargets.length) {
1379
+ await runPentestSwarm({
1380
+ targets: swarmTargets,
1381
+ model,
1382
+ session,
1383
+ authConfig,
1384
+ abortSignal,
1385
+ findingsRegistry,
1386
+ subagentCallbacks: callbacks?.subagentCallbacks,
1387
+ onError: (e) => callbacks?.onError?.(e),
1388
+ onStepFinish
1389
+ });
1390
+ }
1391
+ if (abortSignal?.aborted) {
1392
+ throw new DOMException("Pentest aborted by user", "AbortError");
1393
+ }
1394
+ const findings = loadFindings(session.findingsPath);
1395
+ const report = buildPentestReport(findings, {
1396
+ target,
1397
+ model,
1398
+ sessionId: session.id,
1399
+ mode
1400
+ });
1401
+ const mdPath = join4(session.rootPath, REPORT_FILENAME_MD);
1402
+ const jsonPath = join4(session.rootPath, REPORT_FILENAME_JSON);
1403
+ writeFileSync3(mdPath, renderMarkdown(report));
1404
+ writeFileSync3(jsonPath, renderJson(report));
1405
+ return {
1406
+ findings,
1407
+ findingsPath: session.findingsPath,
1408
+ pocsPath: session.pocsPath,
1409
+ reportPath: mdPath
1410
+ };
1411
+ } finally {
1412
+ const runtime = formatDurationHmsFromMs(Date.now() - startedAt);
1413
+ writeExecutionMetrics({
1414
+ sessionRootPath: session.rootPath,
1415
+ tokenUsage: tokenUsageTotals,
1416
+ runtime
1417
+ });
1418
+ }
1419
+ }
1420
+ async function runWhiteboxPhase(opts) {
1421
+ const workflowInput = {
1422
+ codebasePath: opts.codebasePath,
1423
+ model: opts.model,
1424
+ session: opts.session,
1425
+ authConfig: opts.authConfig,
1426
+ abortSignal: opts.abortSignal,
1427
+ callbacks: opts.callbacks,
1428
+ onStepFinish: opts.onStepFinish
1429
+ };
1430
+ const result = await runWhiteboxAttackSurfaceWorkflow(workflowInput);
1431
+ return result.apps.flatMap((app) => [...app.pages, ...app.apiEndpoints].map((ep) => ({
1432
+ target: ep.path.startsWith("http") ? ep.path : `${opts.baseTarget}${ep.path}`,
1433
+ objectives: ep.pentestObjectives
1434
+ })));
1435
+ }
1436
+ async function runBlackboxPhase(opts) {
1437
+ let lastMessages = [];
1438
+ const agentInput = {
1439
+ target: opts.target,
1440
+ model: opts.model,
1441
+ session: opts.session,
1442
+ authConfig: opts.authConfig,
1443
+ abortSignal: opts.abortSignal,
1444
+ callbacks: opts.callbacks,
1445
+ onStepFinish: (e) => {
1446
+ if (e.response.messages) {
1447
+ lastMessages = e.response.messages;
1448
+ }
1449
+ opts.onStepFinish?.(e);
1450
+ }
1451
+ };
1452
+ const agent = new BlackboxAttackSurfaceAgent(agentInput);
1453
+ try {
1454
+ const result = await agent.consume({
1455
+ onTextDelta: (d) => opts.callbacks?.onTextDelta?.(d),
1456
+ onToolCallStreaming: (d) => opts.callbacks?.onToolCallStreaming?.(d),
1457
+ onToolCallDelta: (d) => opts.callbacks?.onToolCallDelta?.(d),
1458
+ onToolCall: (d) => opts.callbacks?.onToolCall?.(d),
1459
+ onToolResult: (d) => opts.callbacks?.onToolResult?.(d),
1460
+ onError: (e) => opts.callbacks?.onError?.(e),
1461
+ subagentCallbacks: opts.callbacks?.subagentCallbacks
1462
+ });
1463
+ saveSubagentData(opts.session, {
1464
+ agentName: "attack-surface-agent",
1465
+ target: opts.target,
1466
+ status: "completed",
1467
+ messages: lastMessages
1468
+ });
1469
+ return result.targets.map((t) => ({
1470
+ target: t.target,
1471
+ objectives: [t.objective]
1472
+ }));
1473
+ } catch (e) {
1474
+ saveSubagentData(opts.session, {
1475
+ agentName: "attack-surface-agent",
1476
+ target: opts.target,
1477
+ status: "failed",
1478
+ error: e instanceof Error ? e.message : String(e),
1479
+ messages: lastMessages
1480
+ });
1481
+ throw e;
1482
+ }
1483
+ }
1484
+ function loadFindings(findingsPath) {
1485
+ if (!existsSync4(findingsPath)) {
1486
+ return [];
1487
+ }
1488
+ return readdirSync2(findingsPath).filter((f) => f.endsWith(".json")).map((f) => {
1489
+ try {
1490
+ const content = readFileSync4(join4(findingsPath, f), "utf-8");
1491
+ return JSON.parse(content);
1492
+ } catch {
1493
+ return null;
1494
+ }
1495
+ }).filter((f) => f !== null);
1496
+ }
1497
+
1498
+ export { REPORT_FILENAME_MD, convertModelMessagesToUI, readExecutionMetrics, writeExecutionMetrics, loadSessionState, DEFAULT_CONCURRENCY3 as DEFAULT_CONCURRENCY, runPentestSwarm, runPentestWorkflow };