@vulcn/engine 0.9.2 → 0.9.3
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/CHANGELOG.md +41 -0
- package/dist/index.cjs +707 -363
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +663 -148
- package/dist/index.d.ts +663 -148
- package/dist/index.js +685 -355
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/dist/index.cjs
CHANGED
|
@@ -30,10 +30,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
CONFIG_FILENAME: () => CONFIG_FILENAME,
|
|
34
|
+
DEFAULT_PROJECT_CONFIG: () => DEFAULT_PROJECT_CONFIG,
|
|
35
|
+
DIRS: () => DIRS,
|
|
33
36
|
DRIVER_API_VERSION: () => DRIVER_API_VERSION,
|
|
34
37
|
DriverManager: () => DriverManager,
|
|
38
|
+
ENGINE_VERSION: () => ENGINE_VERSION,
|
|
39
|
+
ErrorHandler: () => ErrorHandler,
|
|
40
|
+
ErrorSeverity: () => ErrorSeverity,
|
|
35
41
|
PLUGIN_API_VERSION: () => PLUGIN_API_VERSION,
|
|
36
42
|
PluginManager: () => PluginManager,
|
|
43
|
+
VulcnError: () => VulcnError,
|
|
44
|
+
VulcnProjectConfigSchema: () => VulcnProjectConfigSchema,
|
|
37
45
|
decrypt: () => decrypt,
|
|
38
46
|
decryptCredentials: () => decryptCredentials,
|
|
39
47
|
decryptStorageState: () => decryptStorageState,
|
|
@@ -41,20 +49,417 @@ __export(index_exports, {
|
|
|
41
49
|
encrypt: () => encrypt,
|
|
42
50
|
encryptCredentials: () => encryptCredentials,
|
|
43
51
|
encryptStorageState: () => encryptStorageState,
|
|
52
|
+
ensureProjectDirs: () => ensureProjectDirs,
|
|
53
|
+
error: () => error,
|
|
54
|
+
fatal: () => fatal,
|
|
55
|
+
findProjectRoot: () => findProjectRoot,
|
|
44
56
|
getPassphrase: () => getPassphrase,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
getSeverity: () => getSeverity,
|
|
58
|
+
loadProject: () => loadProject,
|
|
59
|
+
loadProjectFromFile: () => loadProjectFromFile,
|
|
60
|
+
parseProjectConfig: () => parseProjectConfig,
|
|
48
61
|
pluginManager: () => pluginManager,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
saveSessionDir: () => saveSessionDir
|
|
62
|
+
resolveProjectPaths: () => resolveProjectPaths,
|
|
63
|
+
warn: () => warn
|
|
52
64
|
});
|
|
53
65
|
module.exports = __toCommonJS(index_exports);
|
|
54
66
|
|
|
55
|
-
// src/
|
|
67
|
+
// src/config.ts
|
|
68
|
+
var import_zod = require("zod");
|
|
69
|
+
var ScanConfigSchema = import_zod.z.object({
|
|
70
|
+
/** Browser engine to use */
|
|
71
|
+
browser: import_zod.z.enum(["chromium", "firefox", "webkit"]).default("chromium"),
|
|
72
|
+
/** Run in headless mode */
|
|
73
|
+
headless: import_zod.z.boolean().default(true),
|
|
74
|
+
/** Per-step timeout in ms */
|
|
75
|
+
timeout: import_zod.z.number().positive().default(3e4)
|
|
76
|
+
}).default({});
|
|
77
|
+
var PayloadsConfigSchema = import_zod.z.object({
|
|
78
|
+
/** Payload types to use */
|
|
79
|
+
types: import_zod.z.array(import_zod.z.enum(["xss", "sqli", "xxe", "cmd", "redirect", "traversal"])).default(["xss"]),
|
|
80
|
+
/** Opt-in to PayloadsAllTheThings community payloads */
|
|
81
|
+
payloadbox: import_zod.z.boolean().default(false),
|
|
82
|
+
/** Max payloads per type from PayloadBox */
|
|
83
|
+
limit: import_zod.z.number().positive().default(100),
|
|
84
|
+
/** Path to custom payload YAML file (relative to project root) */
|
|
85
|
+
custom: import_zod.z.string().nullable().default(null)
|
|
86
|
+
}).default({});
|
|
87
|
+
var XssDetectionSchema = import_zod.z.object({
|
|
88
|
+
/** Monitor alert/confirm/prompt dialogs */
|
|
89
|
+
dialogs: import_zod.z.boolean().default(true),
|
|
90
|
+
/** Monitor console.log markers */
|
|
91
|
+
console: import_zod.z.boolean().default(true),
|
|
92
|
+
/** Console marker prefix */
|
|
93
|
+
consoleMarker: import_zod.z.string().default("VULCN_XSS:"),
|
|
94
|
+
/** Check for injected <script> elements */
|
|
95
|
+
domMutation: import_zod.z.boolean().default(false),
|
|
96
|
+
/** Finding severity level */
|
|
97
|
+
severity: import_zod.z.enum(["critical", "high", "medium", "low"]).default("high"),
|
|
98
|
+
/** Text patterns to match in alert messages */
|
|
99
|
+
alertPatterns: import_zod.z.array(import_zod.z.string()).default([
|
|
100
|
+
"XSS",
|
|
101
|
+
"1",
|
|
102
|
+
"document.domain",
|
|
103
|
+
"document.cookie",
|
|
104
|
+
"vulcn",
|
|
105
|
+
"pwned"
|
|
106
|
+
])
|
|
107
|
+
}).default({});
|
|
108
|
+
var ReflectionSeveritySchema = import_zod.z.object({
|
|
109
|
+
script: import_zod.z.enum(["critical", "high", "medium", "low"]).default("critical"),
|
|
110
|
+
attribute: import_zod.z.enum(["critical", "high", "medium", "low"]).default("medium"),
|
|
111
|
+
body: import_zod.z.enum(["critical", "high", "medium", "low"]).default("low")
|
|
112
|
+
}).default({});
|
|
113
|
+
var ReflectionContextsSchema = import_zod.z.object({
|
|
114
|
+
script: import_zod.z.boolean().default(true),
|
|
115
|
+
attribute: import_zod.z.boolean().default(true),
|
|
116
|
+
body: import_zod.z.boolean().default(true)
|
|
117
|
+
}).default({});
|
|
118
|
+
var ReflectionDetectionSchema = import_zod.z.object({
|
|
119
|
+
/** Enable reflection detection */
|
|
120
|
+
enabled: import_zod.z.boolean().default(true),
|
|
121
|
+
/** Minimum payload length to check */
|
|
122
|
+
minLength: import_zod.z.number().positive().default(4),
|
|
123
|
+
/** Which HTML contexts to check for reflections */
|
|
124
|
+
contexts: ReflectionContextsSchema,
|
|
125
|
+
/** Severity per context */
|
|
126
|
+
severity: ReflectionSeveritySchema
|
|
127
|
+
}).default({});
|
|
128
|
+
var DetectionConfigSchema = import_zod.z.object({
|
|
129
|
+
/** XSS detection settings */
|
|
130
|
+
xss: XssDetectionSchema,
|
|
131
|
+
/** Reflection detection settings */
|
|
132
|
+
reflection: ReflectionDetectionSchema,
|
|
133
|
+
/** Enable passive security checks (headers, cookies, info-disclosure) */
|
|
134
|
+
passive: import_zod.z.boolean().default(true)
|
|
135
|
+
}).default({});
|
|
136
|
+
var CrawlConfigSchema = import_zod.z.object({
|
|
137
|
+
/** Maximum crawl depth */
|
|
138
|
+
depth: import_zod.z.number().nonnegative().default(2),
|
|
139
|
+
/** Maximum pages to visit */
|
|
140
|
+
maxPages: import_zod.z.number().positive().default(20),
|
|
141
|
+
/** Stay on same origin */
|
|
142
|
+
sameOrigin: import_zod.z.boolean().default(true),
|
|
143
|
+
/** Per-page timeout in ms */
|
|
144
|
+
timeout: import_zod.z.number().positive().default(1e4)
|
|
145
|
+
}).default({});
|
|
146
|
+
var ReportConfigSchema = import_zod.z.object({
|
|
147
|
+
/** Report format to generate */
|
|
148
|
+
format: import_zod.z.enum(["html", "json", "yaml", "sarif", "all"]).nullable().default(null)
|
|
149
|
+
}).default({});
|
|
150
|
+
var FormAuthSchema = import_zod.z.object({
|
|
151
|
+
strategy: import_zod.z.literal("form"),
|
|
152
|
+
/** Login page URL */
|
|
153
|
+
loginUrl: import_zod.z.string().url().optional(),
|
|
154
|
+
/** CSS selector for username field */
|
|
155
|
+
userSelector: import_zod.z.string().nullable().default(null),
|
|
156
|
+
/** CSS selector for password field */
|
|
157
|
+
passSelector: import_zod.z.string().nullable().default(null)
|
|
158
|
+
});
|
|
159
|
+
var HeaderAuthSchema = import_zod.z.object({
|
|
160
|
+
strategy: import_zod.z.literal("header"),
|
|
161
|
+
/** Headers to include in requests */
|
|
162
|
+
headers: import_zod.z.record(import_zod.z.string())
|
|
163
|
+
});
|
|
164
|
+
var AuthConfigSchema = import_zod.z.discriminatedUnion("strategy", [FormAuthSchema, HeaderAuthSchema]).nullable().default(null);
|
|
165
|
+
var VulcnProjectConfigSchema = import_zod.z.object({
|
|
166
|
+
/** Target URL to scan */
|
|
167
|
+
target: import_zod.z.string().url().optional(),
|
|
168
|
+
/** Scan settings (browser, headless, timeout) */
|
|
169
|
+
scan: ScanConfigSchema,
|
|
170
|
+
/** Payload configuration */
|
|
171
|
+
payloads: PayloadsConfigSchema,
|
|
172
|
+
/** Detection configuration */
|
|
173
|
+
detection: DetectionConfigSchema,
|
|
174
|
+
/** Crawl configuration */
|
|
175
|
+
crawl: CrawlConfigSchema,
|
|
176
|
+
/** Report configuration */
|
|
177
|
+
report: ReportConfigSchema,
|
|
178
|
+
/** Authentication configuration */
|
|
179
|
+
auth: AuthConfigSchema
|
|
180
|
+
});
|
|
181
|
+
function parseProjectConfig(raw) {
|
|
182
|
+
return VulcnProjectConfigSchema.parse(raw);
|
|
183
|
+
}
|
|
184
|
+
var DEFAULT_PROJECT_CONFIG = {
|
|
185
|
+
target: "https://example.com",
|
|
186
|
+
scan: {
|
|
187
|
+
browser: "chromium",
|
|
188
|
+
headless: true,
|
|
189
|
+
timeout: 3e4
|
|
190
|
+
},
|
|
191
|
+
payloads: {
|
|
192
|
+
types: ["xss"]
|
|
193
|
+
},
|
|
194
|
+
detection: {
|
|
195
|
+
xss: {
|
|
196
|
+
dialogs: true,
|
|
197
|
+
console: true,
|
|
198
|
+
domMutation: false,
|
|
199
|
+
severity: "high"
|
|
200
|
+
},
|
|
201
|
+
reflection: {
|
|
202
|
+
enabled: true
|
|
203
|
+
},
|
|
204
|
+
passive: true
|
|
205
|
+
},
|
|
206
|
+
crawl: {
|
|
207
|
+
depth: 2,
|
|
208
|
+
maxPages: 20,
|
|
209
|
+
sameOrigin: true
|
|
210
|
+
},
|
|
211
|
+
report: {
|
|
212
|
+
format: "html"
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/project.ts
|
|
217
|
+
var import_promises = require("fs/promises");
|
|
218
|
+
var import_node_fs = require("fs");
|
|
56
219
|
var import_node_path = require("path");
|
|
57
|
-
var import_yaml = require("yaml");
|
|
220
|
+
var import_yaml = __toESM(require("yaml"), 1);
|
|
221
|
+
var CONFIG_FILENAME = ".vulcn.yml";
|
|
222
|
+
var DIRS = {
|
|
223
|
+
sessions: "sessions",
|
|
224
|
+
auth: "auth",
|
|
225
|
+
reports: "reports"
|
|
226
|
+
};
|
|
227
|
+
function findProjectRoot(startDir) {
|
|
228
|
+
let dir = (0, import_node_path.resolve)(startDir ?? process.cwd());
|
|
229
|
+
while (true) {
|
|
230
|
+
const configPath = (0, import_node_path.join)(dir, CONFIG_FILENAME);
|
|
231
|
+
if ((0, import_node_fs.existsSync)(configPath)) {
|
|
232
|
+
return dir;
|
|
233
|
+
}
|
|
234
|
+
const parent = (0, import_node_path.dirname)(dir);
|
|
235
|
+
if (parent === dir) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
dir = parent;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function resolveProjectPaths(root) {
|
|
242
|
+
return {
|
|
243
|
+
root,
|
|
244
|
+
config: (0, import_node_path.join)(root, CONFIG_FILENAME),
|
|
245
|
+
sessions: (0, import_node_path.join)(root, DIRS.sessions),
|
|
246
|
+
auth: (0, import_node_path.join)(root, DIRS.auth),
|
|
247
|
+
reports: (0, import_node_path.join)(root, DIRS.reports)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function loadProject(startDir) {
|
|
251
|
+
const root = findProjectRoot(startDir);
|
|
252
|
+
if (!root) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`No ${CONFIG_FILENAME} found. Run \`vulcn init\` to create one.`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const paths = resolveProjectPaths(root);
|
|
258
|
+
const raw = await (0, import_promises.readFile)(paths.config, "utf-8");
|
|
259
|
+
let parsed;
|
|
260
|
+
try {
|
|
261
|
+
parsed = import_yaml.default.parse(raw);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Invalid YAML in ${paths.config}: ${err instanceof Error ? err.message : String(err)}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (parsed === null || parsed === void 0) {
|
|
268
|
+
parsed = {};
|
|
269
|
+
}
|
|
270
|
+
const config = parseProjectConfig(parsed);
|
|
271
|
+
return { config, paths };
|
|
272
|
+
}
|
|
273
|
+
async function loadProjectFromFile(configPath) {
|
|
274
|
+
const absPath = (0, import_node_path.resolve)(configPath);
|
|
275
|
+
const root = (0, import_node_path.dirname)(absPath);
|
|
276
|
+
const paths = resolveProjectPaths(root);
|
|
277
|
+
const raw = await (0, import_promises.readFile)(absPath, "utf-8");
|
|
278
|
+
let parsed;
|
|
279
|
+
try {
|
|
280
|
+
parsed = import_yaml.default.parse(raw);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Invalid YAML in ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (parsed === null || parsed === void 0) {
|
|
287
|
+
parsed = {};
|
|
288
|
+
}
|
|
289
|
+
const config = parseProjectConfig(parsed);
|
|
290
|
+
return { config, paths };
|
|
291
|
+
}
|
|
292
|
+
async function ensureProjectDirs(paths, dirs = ["sessions"]) {
|
|
293
|
+
for (const dir of dirs) {
|
|
294
|
+
const dirPath = paths[dir];
|
|
295
|
+
if (!(0, import_node_fs.existsSync)(dirPath)) {
|
|
296
|
+
await (0, import_promises.mkdir)(dirPath, { recursive: true });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/driver-manager.ts
|
|
302
|
+
var import_node_path2 = require("path");
|
|
303
|
+
var import_node_module = require("module");
|
|
304
|
+
var import_yaml2 = require("yaml");
|
|
305
|
+
|
|
306
|
+
// src/errors.ts
|
|
307
|
+
var ErrorSeverity = /* @__PURE__ */ ((ErrorSeverity2) => {
|
|
308
|
+
ErrorSeverity2["FATAL"] = "fatal";
|
|
309
|
+
ErrorSeverity2["ERROR"] = "error";
|
|
310
|
+
ErrorSeverity2["WARN"] = "warn";
|
|
311
|
+
return ErrorSeverity2;
|
|
312
|
+
})(ErrorSeverity || {});
|
|
313
|
+
var VulcnError = class _VulcnError extends Error {
|
|
314
|
+
severity;
|
|
315
|
+
source;
|
|
316
|
+
context;
|
|
317
|
+
timestamp;
|
|
318
|
+
constructor(message, options) {
|
|
319
|
+
super(message, { cause: options.cause });
|
|
320
|
+
this.name = "VulcnError";
|
|
321
|
+
this.severity = options.severity;
|
|
322
|
+
this.source = options.source;
|
|
323
|
+
this.context = options.context;
|
|
324
|
+
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Wrap any caught error into a VulcnError.
|
|
328
|
+
* If it's already a VulcnError, returns it as-is.
|
|
329
|
+
*/
|
|
330
|
+
static from(err, defaults) {
|
|
331
|
+
if (err instanceof _VulcnError) return err;
|
|
332
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
333
|
+
return new _VulcnError(message, {
|
|
334
|
+
severity: defaults.severity,
|
|
335
|
+
source: defaults.source,
|
|
336
|
+
cause: err,
|
|
337
|
+
context: defaults.context
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
function fatal(message, source, options) {
|
|
342
|
+
return new VulcnError(message, {
|
|
343
|
+
severity: "fatal" /* FATAL */,
|
|
344
|
+
source,
|
|
345
|
+
...options
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function error(message, source, options) {
|
|
349
|
+
return new VulcnError(message, {
|
|
350
|
+
severity: "error" /* ERROR */,
|
|
351
|
+
source,
|
|
352
|
+
...options
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function warn(message, source, options) {
|
|
356
|
+
return new VulcnError(message, {
|
|
357
|
+
severity: "warn" /* WARN */,
|
|
358
|
+
source,
|
|
359
|
+
...options
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
var ErrorHandler = class {
|
|
363
|
+
errors = [];
|
|
364
|
+
listeners = [];
|
|
365
|
+
/**
|
|
366
|
+
* Handle an error based on its severity.
|
|
367
|
+
*
|
|
368
|
+
* - FATAL: logs, records, then THROWS (caller must not catch silently)
|
|
369
|
+
* - ERROR: logs and records
|
|
370
|
+
* - WARN: logs only
|
|
371
|
+
*/
|
|
372
|
+
handle(err) {
|
|
373
|
+
this.errors.push(err);
|
|
374
|
+
for (const listener of this.listeners) {
|
|
375
|
+
try {
|
|
376
|
+
listener(err);
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const ctx = err.context ? ` ${JSON.stringify(err.context)}` : "";
|
|
381
|
+
switch (err.severity) {
|
|
382
|
+
case "fatal" /* FATAL */:
|
|
383
|
+
console.error(`\u274C FATAL [${err.source}] ${err.message}${ctx}`);
|
|
384
|
+
if (err.cause instanceof Error) {
|
|
385
|
+
console.error(` Caused by: ${err.cause.message}`);
|
|
386
|
+
}
|
|
387
|
+
throw err;
|
|
388
|
+
// ← This is the whole point. FATAL stops execution.
|
|
389
|
+
case "error" /* ERROR */:
|
|
390
|
+
console.error(`\u26A0\uFE0F ERROR [${err.source}] ${err.message}${ctx}`);
|
|
391
|
+
break;
|
|
392
|
+
case "warn" /* WARN */:
|
|
393
|
+
console.warn(`\u26A1 WARN [${err.source}] ${err.message}${ctx}`);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Convenience: wrap a caught error and handle it.
|
|
399
|
+
*/
|
|
400
|
+
catch(err, defaults) {
|
|
401
|
+
this.handle(VulcnError.from(err, defaults));
|
|
402
|
+
}
|
|
403
|
+
// ── Query ──────────────────────────────────────────────────────────
|
|
404
|
+
/** All recorded errors (FATAL + ERROR + WARN) */
|
|
405
|
+
getAll() {
|
|
406
|
+
return [...this.errors];
|
|
407
|
+
}
|
|
408
|
+
/** Only ERROR and FATAL */
|
|
409
|
+
getErrors() {
|
|
410
|
+
return this.errors.filter(
|
|
411
|
+
(e) => e.severity === "error" /* ERROR */ || e.severity === "fatal" /* FATAL */
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
/** Were there any errors (not just warnings)? */
|
|
415
|
+
hasErrors() {
|
|
416
|
+
return this.errors.some(
|
|
417
|
+
(e) => e.severity === "error" /* ERROR */ || e.severity === "fatal" /* FATAL */
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
/** Count by severity */
|
|
421
|
+
counts() {
|
|
422
|
+
const counts = {
|
|
423
|
+
["fatal" /* FATAL */]: 0,
|
|
424
|
+
["error" /* ERROR */]: 0,
|
|
425
|
+
["warn" /* WARN */]: 0
|
|
426
|
+
};
|
|
427
|
+
for (const e of this.errors) {
|
|
428
|
+
counts[e.severity]++;
|
|
429
|
+
}
|
|
430
|
+
return counts;
|
|
431
|
+
}
|
|
432
|
+
/** Human-readable summary for end-of-run reporting */
|
|
433
|
+
getSummary() {
|
|
434
|
+
if (this.errors.length === 0) return "No errors.";
|
|
435
|
+
const c = this.counts();
|
|
436
|
+
const lines = [
|
|
437
|
+
`Error Summary: ${c.fatal} fatal, ${c.error} errors, ${c.warn} warnings`
|
|
438
|
+
];
|
|
439
|
+
for (const e of this.errors) {
|
|
440
|
+
const icon = e.severity === "fatal" /* FATAL */ ? "\u274C" : e.severity === "error" /* ERROR */ ? "\u26A0\uFE0F " : "\u26A1";
|
|
441
|
+
lines.push(` ${icon} [${e.source}] ${e.message}`);
|
|
442
|
+
}
|
|
443
|
+
return lines.join("\n");
|
|
444
|
+
}
|
|
445
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
446
|
+
/** Subscribe to errors as they happen */
|
|
447
|
+
onError(listener) {
|
|
448
|
+
this.listeners.push(listener);
|
|
449
|
+
return () => {
|
|
450
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/** Reset for a new run */
|
|
454
|
+
clear() {
|
|
455
|
+
this.errors = [];
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// src/driver-manager.ts
|
|
460
|
+
var import_meta = {};
|
|
461
|
+
var require2 = (0, import_node_module.createRequire)(import_meta.url);
|
|
462
|
+
var { version: ENGINE_VERSION } = require2("../package.json");
|
|
58
463
|
var DriverManager = class {
|
|
59
464
|
drivers = /* @__PURE__ */ new Map();
|
|
60
465
|
defaultDriver = null;
|
|
@@ -74,8 +479,8 @@ var DriverManager = class {
|
|
|
74
479
|
async load(nameOrPath) {
|
|
75
480
|
let driver;
|
|
76
481
|
let source;
|
|
77
|
-
if (nameOrPath.startsWith("./") || nameOrPath.startsWith("../") || (0,
|
|
78
|
-
const resolved = (0,
|
|
482
|
+
if (nameOrPath.startsWith("./") || nameOrPath.startsWith("../") || (0, import_node_path2.isAbsolute)(nameOrPath)) {
|
|
483
|
+
const resolved = (0, import_node_path2.isAbsolute)(nameOrPath) ? nameOrPath : (0, import_node_path2.resolve)(process.cwd(), nameOrPath);
|
|
79
484
|
const module2 = await import(resolved);
|
|
80
485
|
driver = module2.default || module2;
|
|
81
486
|
source = "local";
|
|
@@ -136,44 +541,18 @@ var DriverManager = class {
|
|
|
136
541
|
/**
|
|
137
542
|
* Parse a YAML session string into a Session object.
|
|
138
543
|
*
|
|
139
|
-
*
|
|
140
|
-
* Legacy sessions (those with non-namespaced step types like "click",
|
|
141
|
-
* "input", "navigate") are automatically converted to the driver format
|
|
142
|
-
* (e.g., "browser.click", "browser.input", "browser.navigate").
|
|
544
|
+
* Sessions must use the driver format with a `driver` field.
|
|
143
545
|
*
|
|
144
546
|
* @param yaml - Raw YAML string
|
|
145
|
-
* @param defaultDriver - Driver to assign for legacy sessions (default: "browser")
|
|
146
547
|
*/
|
|
147
|
-
parseSession(yaml
|
|
148
|
-
const data = (0,
|
|
149
|
-
if (data.driver
|
|
150
|
-
|
|
548
|
+
parseSession(yaml) {
|
|
549
|
+
const data = (0, import_yaml2.parse)(yaml);
|
|
550
|
+
if (!data.driver || typeof data.driver !== "string") {
|
|
551
|
+
throw new Error(
|
|
552
|
+
"Invalid session format: missing 'driver' field. Sessions must use the driver format."
|
|
553
|
+
);
|
|
151
554
|
}
|
|
152
|
-
|
|
153
|
-
const convertedSteps = steps.map((step) => {
|
|
154
|
-
const type = step.type;
|
|
155
|
-
if (type.includes(".")) {
|
|
156
|
-
return step;
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
...step,
|
|
160
|
-
type: `${defaultDriver}.${type}`
|
|
161
|
-
};
|
|
162
|
-
});
|
|
163
|
-
return {
|
|
164
|
-
name: data.name ?? "Untitled Session",
|
|
165
|
-
driver: defaultDriver,
|
|
166
|
-
driverConfig: {
|
|
167
|
-
browser: data.browser ?? "chromium",
|
|
168
|
-
viewport: data.viewport ?? { width: 1280, height: 720 },
|
|
169
|
-
startUrl: data.startUrl
|
|
170
|
-
},
|
|
171
|
-
steps: convertedSteps,
|
|
172
|
-
metadata: {
|
|
173
|
-
recordedAt: data.recordedAt,
|
|
174
|
-
version: data.version ?? "1"
|
|
175
|
-
}
|
|
176
|
-
};
|
|
555
|
+
return data;
|
|
177
556
|
}
|
|
178
557
|
/**
|
|
179
558
|
* Start recording with a driver
|
|
@@ -227,11 +606,12 @@ var DriverManager = class {
|
|
|
227
606
|
page: null,
|
|
228
607
|
headless: !!options.headless,
|
|
229
608
|
config: {},
|
|
230
|
-
engine: { version:
|
|
609
|
+
engine: { version: ENGINE_VERSION, pluginApiVersion: 1 },
|
|
231
610
|
payloads: pluginManager2.getPayloads(),
|
|
232
611
|
findings,
|
|
233
612
|
addFinding,
|
|
234
613
|
logger,
|
|
614
|
+
errors: pluginManager2.getErrorHandler(),
|
|
235
615
|
fetch: globalThis.fetch
|
|
236
616
|
};
|
|
237
617
|
const ctx = {
|
|
@@ -241,6 +621,7 @@ var DriverManager = class {
|
|
|
241
621
|
findings,
|
|
242
622
|
addFinding,
|
|
243
623
|
logger,
|
|
624
|
+
errors: pluginManager2.getErrorHandler(),
|
|
244
625
|
options: {
|
|
245
626
|
...options,
|
|
246
627
|
// Provide onPageReady callback — fires plugin onRunStart hooks
|
|
@@ -255,9 +636,11 @@ var DriverManager = class {
|
|
|
255
636
|
config: loaded.config
|
|
256
637
|
});
|
|
257
638
|
} catch (err) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
639
|
+
pluginManager2.getErrorHandler().catch(err, {
|
|
640
|
+
severity: "error" /* ERROR */,
|
|
641
|
+
source: `plugin:${loaded.plugin.name}`,
|
|
642
|
+
context: { hook: "onRunStart" }
|
|
643
|
+
});
|
|
261
644
|
}
|
|
262
645
|
}
|
|
263
646
|
}
|
|
@@ -272,9 +655,11 @@ var DriverManager = class {
|
|
|
272
655
|
config: loaded.config
|
|
273
656
|
});
|
|
274
657
|
} catch (err) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
658
|
+
pluginManager2.getErrorHandler().catch(err, {
|
|
659
|
+
severity: "warn" /* WARN */,
|
|
660
|
+
source: `plugin:${loaded.plugin.name}`,
|
|
661
|
+
context: { hook: "onBeforeClose" }
|
|
662
|
+
});
|
|
278
663
|
}
|
|
279
664
|
}
|
|
280
665
|
}
|
|
@@ -291,7 +676,11 @@ var DriverManager = class {
|
|
|
291
676
|
findings: result.findings
|
|
292
677
|
});
|
|
293
678
|
} catch (err) {
|
|
294
|
-
|
|
679
|
+
pluginManager2.getErrorHandler().catch(err, {
|
|
680
|
+
severity: "fatal" /* FATAL */,
|
|
681
|
+
source: `plugin:${loaded.plugin.name}`,
|
|
682
|
+
context: { hook: "onRunEnd" }
|
|
683
|
+
});
|
|
295
684
|
}
|
|
296
685
|
}
|
|
297
686
|
}
|
|
@@ -329,24 +718,23 @@ var DriverManager = class {
|
|
|
329
718
|
const allErrors = [];
|
|
330
719
|
const firstDriver = this.getForSession(sessions[0]);
|
|
331
720
|
let sharedBrowser = null;
|
|
332
|
-
if (firstDriver.
|
|
721
|
+
if (typeof firstDriver.createSharedResource === "function") {
|
|
333
722
|
try {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
723
|
+
const driverConfig = sessions[0].driverConfig;
|
|
724
|
+
sharedBrowser = await firstDriver.createSharedResource(
|
|
725
|
+
driverConfig,
|
|
726
|
+
options
|
|
338
727
|
);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
728
|
+
} catch (err) {
|
|
729
|
+
pluginManager2.getErrorHandler().catch(err, {
|
|
730
|
+
severity: "warn" /* WARN */,
|
|
731
|
+
source: `driver-manager:${firstDriver.name}`,
|
|
732
|
+
context: { action: "create-shared-resource" }
|
|
344
733
|
});
|
|
345
|
-
sharedBrowser = result.browser;
|
|
346
|
-
} catch {
|
|
347
734
|
}
|
|
348
735
|
}
|
|
349
736
|
try {
|
|
737
|
+
await pluginManager2.initialize();
|
|
350
738
|
await pluginManager2.callHook("onScanStart", async (hook, ctx) => {
|
|
351
739
|
const scanCtx = {
|
|
352
740
|
...ctx,
|
|
@@ -356,21 +744,63 @@ var DriverManager = class {
|
|
|
356
744
|
};
|
|
357
745
|
await hook(scanCtx);
|
|
358
746
|
});
|
|
359
|
-
for (
|
|
747
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
748
|
+
const session = sessions[i];
|
|
749
|
+
pluginManager2.clearFindings();
|
|
750
|
+
options.onSessionStart?.(session, i, sessions.length);
|
|
360
751
|
const sessionOptions = {
|
|
361
752
|
...options,
|
|
362
753
|
...sharedBrowser ? { browser: sharedBrowser } : {}
|
|
363
754
|
};
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
755
|
+
let result;
|
|
756
|
+
if (options.timeout && options.timeout > 0) {
|
|
757
|
+
const execPromise = this.execute(
|
|
758
|
+
session,
|
|
759
|
+
pluginManager2,
|
|
760
|
+
sessionOptions
|
|
761
|
+
);
|
|
762
|
+
const timeoutPromise = new Promise(
|
|
763
|
+
(_, reject) => setTimeout(
|
|
764
|
+
() => reject(
|
|
765
|
+
new Error(
|
|
766
|
+
`Session "${session.name}" timed out after ${options.timeout}ms`
|
|
767
|
+
)
|
|
768
|
+
),
|
|
769
|
+
options.timeout
|
|
770
|
+
)
|
|
771
|
+
);
|
|
772
|
+
try {
|
|
773
|
+
result = await Promise.race([execPromise, timeoutPromise]);
|
|
774
|
+
} catch (err) {
|
|
775
|
+
result = {
|
|
776
|
+
findings: [],
|
|
777
|
+
stepsExecuted: 0,
|
|
778
|
+
payloadsTested: 0,
|
|
779
|
+
duration: options.timeout,
|
|
780
|
+
errors: [err instanceof Error ? err.message : String(err)]
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
execPromise.catch(() => {
|
|
784
|
+
});
|
|
785
|
+
} else {
|
|
786
|
+
try {
|
|
787
|
+
result = await this.execute(session, pluginManager2, sessionOptions);
|
|
788
|
+
} catch (err) {
|
|
789
|
+
result = {
|
|
790
|
+
findings: [],
|
|
791
|
+
stepsExecuted: 0,
|
|
792
|
+
payloadsTested: 0,
|
|
793
|
+
duration: 0,
|
|
794
|
+
errors: [err instanceof Error ? err.message : String(err)]
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
}
|
|
369
798
|
results.push(result);
|
|
370
799
|
allFindings.push(...result.findings);
|
|
371
800
|
totalSteps += result.stepsExecuted;
|
|
372
801
|
totalPayloads += result.payloadsTested;
|
|
373
802
|
allErrors.push(...result.errors);
|
|
803
|
+
options.onSessionEnd?.(session, result, i, sessions.length);
|
|
374
804
|
}
|
|
375
805
|
} finally {
|
|
376
806
|
if (sharedBrowser && typeof sharedBrowser.close === "function") {
|
|
@@ -443,124 +873,30 @@ var driverManager = new DriverManager();
|
|
|
443
873
|
var DRIVER_API_VERSION = 1;
|
|
444
874
|
|
|
445
875
|
// src/plugin-manager.ts
|
|
446
|
-
var
|
|
447
|
-
var import_node_fs = require("fs");
|
|
448
|
-
var import_node_path2 = require("path");
|
|
449
|
-
var import_yaml2 = __toESM(require("yaml"), 1);
|
|
450
|
-
var import_zod = require("zod");
|
|
876
|
+
var import_node_module2 = require("module");
|
|
451
877
|
|
|
452
878
|
// src/plugin-types.ts
|
|
453
879
|
var PLUGIN_API_VERSION = 1;
|
|
454
880
|
|
|
455
881
|
// src/plugin-manager.ts
|
|
456
|
-
var
|
|
457
|
-
var
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
import_zod.z.object({
|
|
461
|
-
name: import_zod.z.string(),
|
|
462
|
-
config: import_zod.z.record(import_zod.z.unknown()).optional(),
|
|
463
|
-
enabled: import_zod.z.boolean().default(true)
|
|
464
|
-
})
|
|
465
|
-
).optional(),
|
|
466
|
-
settings: import_zod.z.object({
|
|
467
|
-
browser: import_zod.z.enum(["chromium", "firefox", "webkit"]).optional(),
|
|
468
|
-
headless: import_zod.z.boolean().optional(),
|
|
469
|
-
timeout: import_zod.z.number().optional()
|
|
470
|
-
}).optional()
|
|
471
|
-
});
|
|
472
|
-
var PluginManager = class {
|
|
882
|
+
var import_meta2 = {};
|
|
883
|
+
var _require = (0, import_node_module2.createRequire)(import_meta2.url);
|
|
884
|
+
var { version: ENGINE_VERSION2 } = _require("../package.json");
|
|
885
|
+
var PluginManager = class _PluginManager {
|
|
473
886
|
plugins = [];
|
|
474
|
-
config = null;
|
|
475
887
|
initialized = false;
|
|
888
|
+
errorHandler;
|
|
476
889
|
/**
|
|
477
890
|
* Shared context passed to all plugins
|
|
478
891
|
*/
|
|
479
892
|
sharedPayloads = [];
|
|
480
893
|
sharedFindings = [];
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
*/
|
|
484
|
-
async loadConfig(configPath) {
|
|
485
|
-
const paths = configPath ? [configPath] : [
|
|
486
|
-
"vulcn.config.yml",
|
|
487
|
-
"vulcn.config.yaml",
|
|
488
|
-
"vulcn.config.json",
|
|
489
|
-
".vulcnrc.yml",
|
|
490
|
-
".vulcnrc.yaml",
|
|
491
|
-
".vulcnrc.json"
|
|
492
|
-
];
|
|
493
|
-
for (const path of paths) {
|
|
494
|
-
const resolved = (0, import_node_path2.isAbsolute)(path) ? path : (0, import_node_path2.resolve)(process.cwd(), path);
|
|
495
|
-
if ((0, import_node_fs.existsSync)(resolved)) {
|
|
496
|
-
const content = await (0, import_promises.readFile)(resolved, "utf-8");
|
|
497
|
-
const parsed = path.endsWith(".json") ? JSON.parse(content) : import_yaml2.default.parse(content);
|
|
498
|
-
this.config = VulcnConfigSchema.parse(parsed);
|
|
499
|
-
return this.config;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
this.config = { version: "1", plugins: [], settings: {} };
|
|
503
|
-
return this.config;
|
|
894
|
+
constructor(errorHandler) {
|
|
895
|
+
this.errorHandler = errorHandler ?? new ErrorHandler();
|
|
504
896
|
}
|
|
505
|
-
/**
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
async loadPlugins() {
|
|
509
|
-
if (!this.config) {
|
|
510
|
-
await this.loadConfig();
|
|
511
|
-
}
|
|
512
|
-
const pluginConfigs = this.config?.plugins || [];
|
|
513
|
-
for (const pluginConfig of pluginConfigs) {
|
|
514
|
-
if (pluginConfig.enabled === false) continue;
|
|
515
|
-
try {
|
|
516
|
-
const loaded = await this.loadPlugin(pluginConfig);
|
|
517
|
-
this.plugins.push(loaded);
|
|
518
|
-
} catch (err) {
|
|
519
|
-
console.error(
|
|
520
|
-
`Failed to load plugin ${pluginConfig.name}:`,
|
|
521
|
-
err instanceof Error ? err.message : String(err)
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Load a single plugin
|
|
528
|
-
*/
|
|
529
|
-
async loadPlugin(config) {
|
|
530
|
-
const { name, config: pluginConfig = {} } = config;
|
|
531
|
-
let plugin;
|
|
532
|
-
let source;
|
|
533
|
-
if (name.startsWith("./") || name.startsWith("../") || (0, import_node_path2.isAbsolute)(name)) {
|
|
534
|
-
const resolved = (0, import_node_path2.isAbsolute)(name) ? name : (0, import_node_path2.resolve)(process.cwd(), name);
|
|
535
|
-
const module2 = await import(resolved);
|
|
536
|
-
plugin = module2.default || module2;
|
|
537
|
-
source = "local";
|
|
538
|
-
} else if (name.startsWith("@vulcn/")) {
|
|
539
|
-
const module2 = await import(name);
|
|
540
|
-
plugin = module2.default || module2;
|
|
541
|
-
source = "npm";
|
|
542
|
-
} else {
|
|
543
|
-
const module2 = await import(name);
|
|
544
|
-
plugin = module2.default || module2;
|
|
545
|
-
source = "npm";
|
|
546
|
-
}
|
|
547
|
-
this.validatePlugin(plugin);
|
|
548
|
-
let resolvedConfig = pluginConfig;
|
|
549
|
-
if (plugin.configSchema) {
|
|
550
|
-
try {
|
|
551
|
-
resolvedConfig = plugin.configSchema.parse(pluginConfig);
|
|
552
|
-
} catch (err) {
|
|
553
|
-
throw new Error(
|
|
554
|
-
`Invalid config for plugin ${name}: ${err instanceof Error ? err.message : String(err)}`
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return {
|
|
559
|
-
plugin,
|
|
560
|
-
config: resolvedConfig,
|
|
561
|
-
source,
|
|
562
|
-
enabled: true
|
|
563
|
-
};
|
|
897
|
+
/** Get the error handler for post-run inspection */
|
|
898
|
+
getErrorHandler() {
|
|
899
|
+
return this.errorHandler;
|
|
564
900
|
}
|
|
565
901
|
/**
|
|
566
902
|
* Validate plugin structure
|
|
@@ -619,6 +955,120 @@ var PluginManager = class {
|
|
|
619
955
|
this.sharedFindings = [];
|
|
620
956
|
this.initialized = false;
|
|
621
957
|
}
|
|
958
|
+
/**
|
|
959
|
+
* Load the engine from a flat VulcnProjectConfig (from `.vulcn.yml`).
|
|
960
|
+
*
|
|
961
|
+
* This is the primary entry point for the new config system.
|
|
962
|
+
* Maps user-facing config keys to internal plugin configs automatically.
|
|
963
|
+
*
|
|
964
|
+
* @param config - Parsed and validated VulcnProjectConfig
|
|
965
|
+
*/
|
|
966
|
+
async loadFromConfig(config) {
|
|
967
|
+
const { payloads, detection } = config;
|
|
968
|
+
if (payloads.custom) {
|
|
969
|
+
try {
|
|
970
|
+
const payloadPkg = "@vulcn/plugin-payloads";
|
|
971
|
+
const { loadFromFile } = await import(
|
|
972
|
+
/* @vite-ignore */
|
|
973
|
+
payloadPkg
|
|
974
|
+
);
|
|
975
|
+
const loaded = await loadFromFile(payloads.custom);
|
|
976
|
+
this.addPayloads(loaded);
|
|
977
|
+
} catch (err) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Failed to load custom payloads from ${payloads.custom}: ${err instanceof Error ? err.message : String(err)}`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
const payloadPkg = "@vulcn/plugin-payloads";
|
|
985
|
+
const { getCuratedPayloads, loadPayloadBox } = await import(
|
|
986
|
+
/* @vite-ignore */
|
|
987
|
+
payloadPkg
|
|
988
|
+
);
|
|
989
|
+
for (const name of payloads.types) {
|
|
990
|
+
const curated = getCuratedPayloads(name);
|
|
991
|
+
if (curated) {
|
|
992
|
+
this.addPayloads(curated);
|
|
993
|
+
}
|
|
994
|
+
if (payloads.payloadbox || !curated) {
|
|
995
|
+
try {
|
|
996
|
+
const payload = await loadPayloadBox(name, payloads.limit);
|
|
997
|
+
this.addPayloads([payload]);
|
|
998
|
+
} catch (err) {
|
|
999
|
+
if (!curated) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`No payloads for "${name}": no curated set and PayloadBox failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`Failed to load payloads: ${err instanceof Error ? err.message : String(err)}`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (payloads.types.includes("xss") && !this.hasPlugin("@vulcn/plugin-detect-xss")) {
|
|
1013
|
+
try {
|
|
1014
|
+
const pkg = "@vulcn/plugin-detect-xss";
|
|
1015
|
+
const mod = await import(
|
|
1016
|
+
/* @vite-ignore */
|
|
1017
|
+
pkg
|
|
1018
|
+
);
|
|
1019
|
+
this.addPlugin(mod.default, {
|
|
1020
|
+
detectDialogs: detection.xss.dialogs,
|
|
1021
|
+
detectConsole: detection.xss.console,
|
|
1022
|
+
consoleMarker: detection.xss.consoleMarker,
|
|
1023
|
+
detectDomMutation: detection.xss.domMutation,
|
|
1024
|
+
severity: detection.xss.severity,
|
|
1025
|
+
alertPatterns: detection.xss.alertPatterns
|
|
1026
|
+
});
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
this.errorHandler.catch(err, {
|
|
1029
|
+
severity: "warn" /* WARN */,
|
|
1030
|
+
source: "plugin-manager:loadFromConfig",
|
|
1031
|
+
context: { plugin: "@vulcn/plugin-detect-xss" }
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const hasSqli = payloads.types.some((t) => {
|
|
1036
|
+
const lower = t.toLowerCase();
|
|
1037
|
+
return lower === "sqli" || lower.includes("sql");
|
|
1038
|
+
});
|
|
1039
|
+
if (hasSqli && !this.hasPlugin("@vulcn/plugin-detect-sqli")) {
|
|
1040
|
+
try {
|
|
1041
|
+
const pkg = "@vulcn/plugin-detect-sqli";
|
|
1042
|
+
const mod = await import(
|
|
1043
|
+
/* @vite-ignore */
|
|
1044
|
+
pkg
|
|
1045
|
+
);
|
|
1046
|
+
this.addPlugin(mod.default);
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
this.errorHandler.catch(err, {
|
|
1049
|
+
severity: "warn" /* WARN */,
|
|
1050
|
+
source: "plugin-manager:loadFromConfig",
|
|
1051
|
+
context: { plugin: "@vulcn/plugin-detect-sqli" }
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (detection.passive && !this.hasPlugin("@vulcn/plugin-passive")) {
|
|
1056
|
+
try {
|
|
1057
|
+
const pkg = "@vulcn/plugin-passive";
|
|
1058
|
+
const mod = await import(
|
|
1059
|
+
/* @vite-ignore */
|
|
1060
|
+
pkg
|
|
1061
|
+
);
|
|
1062
|
+
this.addPlugin(mod.default);
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
this.errorHandler.catch(err, {
|
|
1065
|
+
severity: "warn" /* WARN */,
|
|
1066
|
+
source: "plugin-manager:loadFromConfig",
|
|
1067
|
+
context: { plugin: "@vulcn/plugin-passive" }
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
622
1072
|
/**
|
|
623
1073
|
* Get all loaded payloads
|
|
624
1074
|
*/
|
|
@@ -664,9 +1114,9 @@ var PluginManager = class {
|
|
|
664
1114
|
/**
|
|
665
1115
|
* Create base context for plugins
|
|
666
1116
|
*/
|
|
667
|
-
createContext(pluginConfig) {
|
|
1117
|
+
createContext(pluginConfig, pluginName) {
|
|
668
1118
|
const engineInfo = {
|
|
669
|
-
version:
|
|
1119
|
+
version: ENGINE_VERSION2,
|
|
670
1120
|
pluginApiVersion: PLUGIN_API_VERSION
|
|
671
1121
|
};
|
|
672
1122
|
return {
|
|
@@ -675,9 +1125,13 @@ var PluginManager = class {
|
|
|
675
1125
|
payloads: this.sharedPayloads,
|
|
676
1126
|
findings: this.sharedFindings,
|
|
677
1127
|
addFinding: (finding) => {
|
|
1128
|
+
console.log(
|
|
1129
|
+
`[DEBUG-PM] Plugin ${pluginName || "?"} adding finding: ${finding.type}`
|
|
1130
|
+
);
|
|
678
1131
|
this.sharedFindings.push(finding);
|
|
679
1132
|
},
|
|
680
|
-
logger: this.createLogger("plugin"),
|
|
1133
|
+
logger: this.createLogger(pluginName || "plugin"),
|
|
1134
|
+
errors: this.errorHandler,
|
|
681
1135
|
fetch: globalThis.fetch
|
|
682
1136
|
};
|
|
683
1137
|
}
|
|
@@ -693,6 +1147,26 @@ var PluginManager = class {
|
|
|
693
1147
|
error: (msg, ...args) => console.error(prefix, msg, ...args)
|
|
694
1148
|
};
|
|
695
1149
|
}
|
|
1150
|
+
// ── Hook severity classification ────────────────────────────────────
|
|
1151
|
+
//
|
|
1152
|
+
// Hooks that produce OUTPUT (reports, results) are FATAL on failure.
|
|
1153
|
+
// Hooks that set up state are ERROR. Everything else is WARN.
|
|
1154
|
+
//
|
|
1155
|
+
static FATAL_HOOKS = /* @__PURE__ */ new Set([
|
|
1156
|
+
"onRunEnd",
|
|
1157
|
+
"onScanEnd"
|
|
1158
|
+
]);
|
|
1159
|
+
static ERROR_HOOKS = /* @__PURE__ */ new Set([
|
|
1160
|
+
"onInit",
|
|
1161
|
+
"onRunStart",
|
|
1162
|
+
"onScanStart",
|
|
1163
|
+
"onAfterPayload"
|
|
1164
|
+
]);
|
|
1165
|
+
hookSeverity(hookName) {
|
|
1166
|
+
if (_PluginManager.FATAL_HOOKS.has(hookName)) return "fatal" /* FATAL */;
|
|
1167
|
+
if (_PluginManager.ERROR_HOOKS.has(hookName)) return "error" /* ERROR */;
|
|
1168
|
+
return "warn" /* WARN */;
|
|
1169
|
+
}
|
|
696
1170
|
/**
|
|
697
1171
|
* Call a hook on all plugins sequentially
|
|
698
1172
|
*/
|
|
@@ -700,15 +1174,16 @@ var PluginManager = class {
|
|
|
700
1174
|
for (const loaded of this.plugins) {
|
|
701
1175
|
const hook = loaded.plugin.hooks?.[hookName];
|
|
702
1176
|
if (hook) {
|
|
703
|
-
const ctx = this.createContext(loaded.config);
|
|
1177
|
+
const ctx = this.createContext(loaded.config, loaded.plugin.name);
|
|
704
1178
|
ctx.logger = this.createLogger(loaded.plugin.name);
|
|
705
1179
|
try {
|
|
706
1180
|
await executor(hook, ctx);
|
|
707
1181
|
} catch (err) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1182
|
+
this.errorHandler.catch(err, {
|
|
1183
|
+
severity: this.hookSeverity(hookName),
|
|
1184
|
+
source: `plugin:${loaded.plugin.name}`,
|
|
1185
|
+
context: { hook: hookName }
|
|
1186
|
+
});
|
|
712
1187
|
}
|
|
713
1188
|
}
|
|
714
1189
|
}
|
|
@@ -721,7 +1196,7 @@ var PluginManager = class {
|
|
|
721
1196
|
for (const loaded of this.plugins) {
|
|
722
1197
|
const hook = loaded.plugin.hooks?.[hookName];
|
|
723
1198
|
if (hook) {
|
|
724
|
-
const ctx = this.createContext(loaded.config);
|
|
1199
|
+
const ctx = this.createContext(loaded.config, loaded.plugin.name);
|
|
725
1200
|
ctx.logger = this.createLogger(loaded.plugin.name);
|
|
726
1201
|
try {
|
|
727
1202
|
const result = await executor(
|
|
@@ -736,10 +1211,11 @@ var PluginManager = class {
|
|
|
736
1211
|
}
|
|
737
1212
|
}
|
|
738
1213
|
} catch (err) {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1214
|
+
this.errorHandler.catch(err, {
|
|
1215
|
+
severity: this.hookSeverity(hookName),
|
|
1216
|
+
source: `plugin:${loaded.plugin.name}`,
|
|
1217
|
+
context: { hook: hookName }
|
|
1218
|
+
});
|
|
743
1219
|
}
|
|
744
1220
|
}
|
|
745
1221
|
}
|
|
@@ -753,7 +1229,7 @@ var PluginManager = class {
|
|
|
753
1229
|
for (const loaded of this.plugins) {
|
|
754
1230
|
const hook = loaded.plugin.hooks?.[hookName];
|
|
755
1231
|
if (hook) {
|
|
756
|
-
const ctx = this.createContext(loaded.config);
|
|
1232
|
+
const ctx = this.createContext(loaded.config, loaded.plugin.name);
|
|
757
1233
|
ctx.logger = this.createLogger(loaded.plugin.name);
|
|
758
1234
|
try {
|
|
759
1235
|
value = await executor(
|
|
@@ -762,10 +1238,11 @@ var PluginManager = class {
|
|
|
762
1238
|
ctx
|
|
763
1239
|
);
|
|
764
1240
|
} catch (err) {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1241
|
+
this.errorHandler.catch(err, {
|
|
1242
|
+
severity: this.hookSeverity(hookName),
|
|
1243
|
+
source: `plugin:${loaded.plugin.name}`,
|
|
1244
|
+
context: { hook: hookName }
|
|
1245
|
+
});
|
|
769
1246
|
}
|
|
770
1247
|
}
|
|
771
1248
|
}
|
|
@@ -846,178 +1323,41 @@ function getPassphrase(interactive) {
|
|
|
846
1323
|
);
|
|
847
1324
|
}
|
|
848
1325
|
|
|
849
|
-
// src/
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
let authConfig;
|
|
869
|
-
if (manifest.auth?.configFile) {
|
|
870
|
-
const authPath = (0, import_node_path3.join)(dirPath, manifest.auth.configFile);
|
|
871
|
-
if ((0, import_node_fs2.existsSync)(authPath)) {
|
|
872
|
-
const authYaml = await (0, import_promises2.readFile)(authPath, "utf-8");
|
|
873
|
-
authConfig = (0, import_yaml3.parse)(authYaml);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
const sessions = [];
|
|
877
|
-
for (const ref of manifest.sessions) {
|
|
878
|
-
if (ref.injectable === false) continue;
|
|
879
|
-
const sessionPath = (0, import_node_path3.join)(dirPath, ref.file);
|
|
880
|
-
if (!(0, import_node_fs2.existsSync)(sessionPath)) {
|
|
881
|
-
console.warn(`Session file not found: ${sessionPath}, skipping`);
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
const sessionYaml = await (0, import_promises2.readFile)(sessionPath, "utf-8");
|
|
885
|
-
const sessionData = (0, import_yaml3.parse)(sessionYaml);
|
|
886
|
-
const session = {
|
|
887
|
-
name: sessionData.name ?? (0, import_node_path3.basename)(ref.file, ".yml"),
|
|
888
|
-
driver: manifest.driver,
|
|
889
|
-
driverConfig: {
|
|
890
|
-
...manifest.driverConfig,
|
|
891
|
-
startUrl: resolveUrl(
|
|
892
|
-
manifest.target,
|
|
893
|
-
sessionData.page
|
|
894
|
-
)
|
|
895
|
-
},
|
|
896
|
-
steps: sessionData.steps ?? [],
|
|
897
|
-
metadata: {
|
|
898
|
-
recordedAt: manifest.recordedAt,
|
|
899
|
-
version: "2",
|
|
900
|
-
manifestDir: dirPath
|
|
901
|
-
}
|
|
902
|
-
};
|
|
903
|
-
sessions.push(session);
|
|
904
|
-
}
|
|
905
|
-
return { manifest, sessions, authConfig };
|
|
906
|
-
}
|
|
907
|
-
function isSessionDir(path) {
|
|
908
|
-
return (0, import_node_fs2.existsSync)((0, import_node_path3.join)(path, "manifest.yml"));
|
|
909
|
-
}
|
|
910
|
-
function looksLikeSessionDir(path) {
|
|
911
|
-
return path.endsWith(".vulcn") || path.endsWith(".vulcn/");
|
|
912
|
-
}
|
|
913
|
-
async function saveSessionDir(dirPath, options) {
|
|
914
|
-
await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "sessions"), { recursive: true });
|
|
915
|
-
const sessionRefs = [];
|
|
916
|
-
for (const session of options.sessions) {
|
|
917
|
-
const safeName = slugify(session.name);
|
|
918
|
-
const fileName = `sessions/${safeName}.yml`;
|
|
919
|
-
const sessionPath = (0, import_node_path3.join)(dirPath, fileName);
|
|
920
|
-
const startUrl = session.driverConfig.startUrl;
|
|
921
|
-
const page = startUrl ? startUrl.replace(options.target, "").replace(/^\//, "/") : void 0;
|
|
922
|
-
const sessionData = {
|
|
923
|
-
name: session.name,
|
|
924
|
-
...page ? { page } : {},
|
|
925
|
-
steps: session.steps
|
|
926
|
-
};
|
|
927
|
-
await (0, import_promises2.writeFile)(sessionPath, (0, import_yaml3.stringify)(sessionData), "utf-8");
|
|
928
|
-
const hasInjectable = session.steps.some(
|
|
929
|
-
(s) => s.type === "browser.input" && s.injectable !== false
|
|
930
|
-
);
|
|
931
|
-
sessionRefs.push({
|
|
932
|
-
file: fileName,
|
|
933
|
-
injectable: hasInjectable
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
if (options.authConfig) {
|
|
937
|
-
await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "auth"), { recursive: true });
|
|
938
|
-
await (0, import_promises2.writeFile)(
|
|
939
|
-
(0, import_node_path3.join)(dirPath, "auth", "config.yml"),
|
|
940
|
-
(0, import_yaml3.stringify)(options.authConfig),
|
|
941
|
-
"utf-8"
|
|
942
|
-
);
|
|
943
|
-
}
|
|
944
|
-
if (options.encryptedState) {
|
|
945
|
-
await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "auth"), { recursive: true });
|
|
946
|
-
await (0, import_promises2.writeFile)(
|
|
947
|
-
(0, import_node_path3.join)(dirPath, "auth", "state.enc"),
|
|
948
|
-
options.encryptedState,
|
|
949
|
-
"utf-8"
|
|
950
|
-
);
|
|
951
|
-
}
|
|
952
|
-
if (options.requests && options.requests.length > 0) {
|
|
953
|
-
await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "requests"), { recursive: true });
|
|
954
|
-
for (const req of options.requests) {
|
|
955
|
-
const safeName = slugify(req.sessionName);
|
|
956
|
-
await (0, import_promises2.writeFile)(
|
|
957
|
-
(0, import_node_path3.join)(dirPath, "requests", `${safeName}.json`),
|
|
958
|
-
JSON.stringify(req, null, 2),
|
|
959
|
-
"utf-8"
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
const manifest = {
|
|
964
|
-
version: "2",
|
|
965
|
-
name: options.name,
|
|
966
|
-
target: options.target,
|
|
967
|
-
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
968
|
-
driver: options.driver,
|
|
969
|
-
driverConfig: options.driverConfig,
|
|
970
|
-
...options.authConfig ? {
|
|
971
|
-
auth: {
|
|
972
|
-
strategy: options.authConfig.strategy,
|
|
973
|
-
configFile: "auth/config.yml",
|
|
974
|
-
stateFile: options.encryptedState ? "auth/state.enc" : void 0,
|
|
975
|
-
loggedInIndicator: options.authConfig.loggedInIndicator,
|
|
976
|
-
loggedOutIndicator: options.authConfig.loggedOutIndicator
|
|
977
|
-
}
|
|
978
|
-
} : {},
|
|
979
|
-
sessions: sessionRefs,
|
|
980
|
-
scan: {
|
|
981
|
-
tier: "auto",
|
|
982
|
-
parallel: 1,
|
|
983
|
-
timeout: 12e4
|
|
984
|
-
}
|
|
985
|
-
};
|
|
986
|
-
await (0, import_promises2.writeFile)((0, import_node_path3.join)(dirPath, "manifest.yml"), (0, import_yaml3.stringify)(manifest), "utf-8");
|
|
987
|
-
}
|
|
988
|
-
async function readAuthState(dirPath) {
|
|
989
|
-
const statePath = (0, import_node_path3.join)(dirPath, "auth", "state.enc");
|
|
990
|
-
if (!(0, import_node_fs2.existsSync)(statePath)) return null;
|
|
991
|
-
return (0, import_promises2.readFile)(statePath, "utf-8");
|
|
992
|
-
}
|
|
993
|
-
async function readCapturedRequests(dirPath) {
|
|
994
|
-
const requestsDir = (0, import_node_path3.join)(dirPath, "requests");
|
|
995
|
-
if (!(0, import_node_fs2.existsSync)(requestsDir)) return [];
|
|
996
|
-
const files = await (0, import_promises2.readdir)(requestsDir);
|
|
997
|
-
const requests = [];
|
|
998
|
-
for (const file of files) {
|
|
999
|
-
if (!file.endsWith(".json")) continue;
|
|
1000
|
-
const content = await (0, import_promises2.readFile)((0, import_node_path3.join)(requestsDir, file), "utf-8");
|
|
1001
|
-
requests.push(JSON.parse(content));
|
|
1326
|
+
// src/payload-types.ts
|
|
1327
|
+
function getSeverity(category) {
|
|
1328
|
+
switch (category) {
|
|
1329
|
+
case "sqli":
|
|
1330
|
+
case "command-injection":
|
|
1331
|
+
case "xxe":
|
|
1332
|
+
return "critical";
|
|
1333
|
+
case "xss":
|
|
1334
|
+
case "ssrf":
|
|
1335
|
+
case "path-traversal":
|
|
1336
|
+
return "high";
|
|
1337
|
+
case "open-redirect":
|
|
1338
|
+
return "medium";
|
|
1339
|
+
case "security-misconfiguration":
|
|
1340
|
+
return "low";
|
|
1341
|
+
case "information-disclosure":
|
|
1342
|
+
return "info";
|
|
1343
|
+
default:
|
|
1344
|
+
return "medium";
|
|
1002
1345
|
}
|
|
1003
|
-
return requests;
|
|
1004
|
-
}
|
|
1005
|
-
function resolveUrl(target, page) {
|
|
1006
|
-
if (!page) return target;
|
|
1007
|
-
if (page.startsWith("http")) return page;
|
|
1008
|
-
const base = target.replace(/\/$/, "");
|
|
1009
|
-
const path = page.startsWith("/") ? page : `/${page}`;
|
|
1010
|
-
return `${base}${path}`;
|
|
1011
|
-
}
|
|
1012
|
-
function slugify(text) {
|
|
1013
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
1014
1346
|
}
|
|
1015
1347
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1016
1348
|
0 && (module.exports = {
|
|
1349
|
+
CONFIG_FILENAME,
|
|
1350
|
+
DEFAULT_PROJECT_CONFIG,
|
|
1351
|
+
DIRS,
|
|
1017
1352
|
DRIVER_API_VERSION,
|
|
1018
1353
|
DriverManager,
|
|
1354
|
+
ENGINE_VERSION,
|
|
1355
|
+
ErrorHandler,
|
|
1356
|
+
ErrorSeverity,
|
|
1019
1357
|
PLUGIN_API_VERSION,
|
|
1020
1358
|
PluginManager,
|
|
1359
|
+
VulcnError,
|
|
1360
|
+
VulcnProjectConfigSchema,
|
|
1021
1361
|
decrypt,
|
|
1022
1362
|
decryptCredentials,
|
|
1023
1363
|
decryptStorageState,
|
|
@@ -1025,13 +1365,17 @@ function slugify(text) {
|
|
|
1025
1365
|
encrypt,
|
|
1026
1366
|
encryptCredentials,
|
|
1027
1367
|
encryptStorageState,
|
|
1368
|
+
ensureProjectDirs,
|
|
1369
|
+
error,
|
|
1370
|
+
fatal,
|
|
1371
|
+
findProjectRoot,
|
|
1028
1372
|
getPassphrase,
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1373
|
+
getSeverity,
|
|
1374
|
+
loadProject,
|
|
1375
|
+
loadProjectFromFile,
|
|
1376
|
+
parseProjectConfig,
|
|
1032
1377
|
pluginManager,
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
saveSessionDir
|
|
1378
|
+
resolveProjectPaths,
|
|
1379
|
+
warn
|
|
1036
1380
|
});
|
|
1037
1381
|
//# sourceMappingURL=index.cjs.map
|