@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.
- package/README.md +2 -3
- package/bin/pensar.js +31 -276
- package/build/agent-5qdmmchx.js +206 -0
- package/build/agent-s2z0dasf.js +16 -0
- package/build/auth-jvq72ekc.js +263 -0
- package/build/authentication-nya4td5k.js +310 -0
- package/build/blackboxAgent-qa9ze2hn.js +17 -0
- package/build/blackboxPentest-85hwznet.js +41 -0
- package/build/cli-15vxn9zj.js +1358 -0
- package/build/cli-2ckm5es2.js +50 -0
- package/build/cli-49cd9yfk.js +4475 -0
- package/build/cli-5d6cs4dq.js +53 -0
- package/build/cli-6gtnyaqf.js +109 -0
- package/build/cli-7ckctq7a.js +45 -0
- package/build/cli-8rxa073f.js +104 -0
- package/build/cli-bp6d08sg.js +110 -0
- package/build/cli-e20q3hqz.js +307 -0
- package/build/cli-f9shhcxf.js +1498 -0
- package/build/cli-hmrzx8am.js +507 -0
- package/build/cli-j66pect7.js +202 -0
- package/build/cli-jb0gcnrs.js +60 -0
- package/build/cli-jh38b6zv.js +1074 -0
- package/build/cli-kqtgcdzn.js +54784 -0
- package/build/cli-r8r90gka.js +96700 -0
- package/build/cli-va4y0089.js +395 -0
- package/build/cli-w04ggbe4.js +104 -0
- package/build/cli-x1msjf55.js +103 -0
- package/build/cli-yj3dy0vg.js +180 -0
- package/build/cli.js +509 -0
- package/build/doctor-b7612pzw.js +117 -0
- package/build/fixes-1r6v7kh2.js +49 -0
- package/build/index-5ke2yd32.js +17 -0
- package/build/index-9ze42wn7.js +68412 -0
- package/build/index-rd11fk7h.js +1257 -0
- package/build/index-tke6896d.js +1097 -0
- package/build/index-vwvh1rdw.js +535 -0
- package/build/issues-kx721wja.js +94 -0
- package/build/logs-hav7d0nm.js +77 -0
- package/build/main-2483qzbq.js +397 -0
- package/build/multipart-parser-r38qdp5v.js +350 -0
- package/build/pentest-zzebnfa0.js +25 -0
- package/build/pentests-s9fwd71b.js +70 -0
- package/build/projects-tr719twv.js +35 -0
- package/build/targetedPentest-w2c85whf.js +32 -0
- package/build/token-6x6aavpc.js +58 -0
- package/build/token-util-na95bqjj.js +6 -0
- package/build/uninstall-2j0pymb0.js +231 -0
- package/build/utils-jky0th19.js +107 -0
- package/package.json +3 -4
- package/build/auth.js +0 -625
- package/build/highlights-eq9cgrbb.scm +0 -604
- package/build/highlights-ghv9g403.scm +0 -205
- package/build/highlights-hk7bwhj4.scm +0 -284
- package/build/highlights-r812a2qc.scm +0 -150
- package/build/highlights-x6tmsnaa.scm +0 -115
- package/build/index.js +0 -292069
- package/build/injections-73j83es3.scm +0 -27
- package/build/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/build/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/build/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/build/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/build/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/src/core/installation/index.ts +0 -223
- 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 };
|