@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/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
- isSessionDir: () => isSessionDir,
46
- loadSessionDir: () => loadSessionDir,
47
- looksLikeSessionDir: () => looksLikeSessionDir,
57
+ getSeverity: () => getSeverity,
58
+ loadProject: () => loadProject,
59
+ loadProjectFromFile: () => loadProjectFromFile,
60
+ parseProjectConfig: () => parseProjectConfig,
48
61
  pluginManager: () => pluginManager,
49
- readAuthState: () => readAuthState,
50
- readCapturedRequests: () => readCapturedRequests,
51
- saveSessionDir: () => saveSessionDir
62
+ resolveProjectPaths: () => resolveProjectPaths,
63
+ warn: () => warn
52
64
  });
53
65
  module.exports = __toCommonJS(index_exports);
54
66
 
55
- // src/driver-manager.ts
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, import_node_path.isAbsolute)(nameOrPath)) {
78
- const resolved = (0, import_node_path.isAbsolute)(nameOrPath) ? nameOrPath : (0, import_node_path.resolve)(process.cwd(), nameOrPath);
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
- * Handles both new driver-format sessions and legacy v1 sessions.
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, defaultDriver = "browser") {
148
- const data = (0, import_yaml.parse)(yaml);
149
- if (data.driver && typeof data.driver === "string") {
150
- return data;
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
- const steps = data.steps ?? [];
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: "0.3.0", pluginApiVersion: 1 },
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
- logger.warn(
259
- `Plugin ${loaded.plugin.name} onRunStart failed: ${err}`
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
- logger.warn(
276
- `Plugin ${loaded.plugin.name} onBeforeClose failed: ${err}`
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
- logger.warn(`Plugin ${loaded.plugin.name} onRunEnd failed: ${err}`);
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.name === "browser") {
721
+ if (typeof firstDriver.createSharedResource === "function") {
333
722
  try {
334
- const driverPkg = "@vulcn/driver-browser";
335
- const { launchBrowser } = await import(
336
- /* @vite-ignore */
337
- driverPkg
723
+ const driverConfig = sessions[0].driverConfig;
724
+ sharedBrowser = await firstDriver.createSharedResource(
725
+ driverConfig,
726
+ options
338
727
  );
339
- const browserType = sessions[0].driverConfig.browser ?? "chromium";
340
- const headless = options.headless ?? true;
341
- const result = await launchBrowser({
342
- browser: browserType,
343
- headless
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 (const session of sessions) {
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
- const result = await this.execute(
365
- session,
366
- pluginManager2,
367
- sessionOptions
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 import_promises = require("fs/promises");
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 ENGINE_VERSION = "0.2.0";
457
- var VulcnConfigSchema = import_zod.z.object({
458
- version: import_zod.z.string().default("1"),
459
- plugins: import_zod.z.array(
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
- * Load configuration from vulcn.config.yml
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
- * Load all plugins from config
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: ENGINE_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
- console.error(
709
- `Error in plugin ${loaded.plugin.name}.${hookName}:`,
710
- err instanceof Error ? err.message : String(err)
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
- console.error(
740
- `Error in plugin ${loaded.plugin.name}.${hookName}:`,
741
- err instanceof Error ? err.message : String(err)
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
- console.error(
766
- `Error in plugin ${loaded.plugin.name}.${hookName}:`,
767
- err instanceof Error ? err.message : String(err)
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/session.ts
850
- var import_promises2 = require("fs/promises");
851
- var import_node_fs2 = require("fs");
852
- var import_node_path3 = require("path");
853
- var import_yaml3 = require("yaml");
854
- async function loadSessionDir(dirPath) {
855
- const manifestPath = (0, import_node_path3.join)(dirPath, "manifest.yml");
856
- if (!(0, import_node_fs2.existsSync)(manifestPath)) {
857
- throw new Error(
858
- `No manifest.yml found in ${dirPath}. Is this a v2 session directory?`
859
- );
860
- }
861
- const manifestYaml = await (0, import_promises2.readFile)(manifestPath, "utf-8");
862
- const manifest = (0, import_yaml3.parse)(manifestYaml);
863
- if (manifest.version !== "2") {
864
- throw new Error(
865
- `Unsupported session format version: ${manifest.version}. Expected "2".`
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
- isSessionDir,
1030
- loadSessionDir,
1031
- looksLikeSessionDir,
1373
+ getSeverity,
1374
+ loadProject,
1375
+ loadProjectFromFile,
1376
+ parseProjectConfig,
1032
1377
  pluginManager,
1033
- readAuthState,
1034
- readCapturedRequests,
1035
- saveSessionDir
1378
+ resolveProjectPaths,
1379
+ warn
1036
1380
  });
1037
1381
  //# sourceMappingURL=index.cjs.map