@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.d.ts CHANGED
@@ -1,5 +1,479 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ /**
4
+ * Vulcn Project Configuration — `.vulcn.yml` schema
5
+ *
6
+ * Single source of truth for all Vulcn configuration.
7
+ * The CLI is a thin layer that passes this config to the engine.
8
+ * Plugin names never appear here — the engine maps flat keys to plugins internally.
9
+ */
10
+
11
+ declare const VulcnProjectConfigSchema: z.ZodObject<{
12
+ /** Target URL to scan */
13
+ target: z.ZodOptional<z.ZodString>;
14
+ /** Scan settings (browser, headless, timeout) */
15
+ scan: z.ZodDefault<z.ZodObject<{
16
+ /** Browser engine to use */
17
+ browser: z.ZodDefault<z.ZodEnum<["chromium", "firefox", "webkit"]>>;
18
+ /** Run in headless mode */
19
+ headless: z.ZodDefault<z.ZodBoolean>;
20
+ /** Per-step timeout in ms */
21
+ timeout: z.ZodDefault<z.ZodNumber>;
22
+ }, "strip", z.ZodTypeAny, {
23
+ browser: "chromium" | "firefox" | "webkit";
24
+ headless: boolean;
25
+ timeout: number;
26
+ }, {
27
+ browser?: "chromium" | "firefox" | "webkit" | undefined;
28
+ headless?: boolean | undefined;
29
+ timeout?: number | undefined;
30
+ }>>;
31
+ /** Payload configuration */
32
+ payloads: z.ZodDefault<z.ZodObject<{
33
+ /** Payload types to use */
34
+ types: z.ZodDefault<z.ZodArray<z.ZodEnum<["xss", "sqli", "xxe", "cmd", "redirect", "traversal"]>, "many">>;
35
+ /** Opt-in to PayloadsAllTheThings community payloads */
36
+ payloadbox: z.ZodDefault<z.ZodBoolean>;
37
+ /** Max payloads per type from PayloadBox */
38
+ limit: z.ZodDefault<z.ZodNumber>;
39
+ /** Path to custom payload YAML file (relative to project root) */
40
+ custom: z.ZodDefault<z.ZodNullable<z.ZodString>>;
41
+ }, "strip", z.ZodTypeAny, {
42
+ custom: string | null;
43
+ types: ("xss" | "sqli" | "xxe" | "cmd" | "redirect" | "traversal")[];
44
+ payloadbox: boolean;
45
+ limit: number;
46
+ }, {
47
+ custom?: string | null | undefined;
48
+ types?: ("xss" | "sqli" | "xxe" | "cmd" | "redirect" | "traversal")[] | undefined;
49
+ payloadbox?: boolean | undefined;
50
+ limit?: number | undefined;
51
+ }>>;
52
+ /** Detection configuration */
53
+ detection: z.ZodDefault<z.ZodObject<{
54
+ /** XSS detection settings */
55
+ xss: z.ZodDefault<z.ZodObject<{
56
+ /** Monitor alert/confirm/prompt dialogs */
57
+ dialogs: z.ZodDefault<z.ZodBoolean>;
58
+ /** Monitor console.log markers */
59
+ console: z.ZodDefault<z.ZodBoolean>;
60
+ /** Console marker prefix */
61
+ consoleMarker: z.ZodDefault<z.ZodString>;
62
+ /** Check for injected <script> elements */
63
+ domMutation: z.ZodDefault<z.ZodBoolean>;
64
+ /** Finding severity level */
65
+ severity: z.ZodDefault<z.ZodEnum<["critical", "high", "medium", "low"]>>;
66
+ /** Text patterns to match in alert messages */
67
+ alertPatterns: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
68
+ }, "strip", z.ZodTypeAny, {
69
+ dialogs: boolean;
70
+ console: boolean;
71
+ consoleMarker: string;
72
+ domMutation: boolean;
73
+ severity: "critical" | "high" | "medium" | "low";
74
+ alertPatterns: string[];
75
+ }, {
76
+ dialogs?: boolean | undefined;
77
+ console?: boolean | undefined;
78
+ consoleMarker?: string | undefined;
79
+ domMutation?: boolean | undefined;
80
+ severity?: "critical" | "high" | "medium" | "low" | undefined;
81
+ alertPatterns?: string[] | undefined;
82
+ }>>;
83
+ /** Reflection detection settings */
84
+ reflection: z.ZodDefault<z.ZodObject<{
85
+ /** Enable reflection detection */
86
+ enabled: z.ZodDefault<z.ZodBoolean>;
87
+ /** Minimum payload length to check */
88
+ minLength: z.ZodDefault<z.ZodNumber>;
89
+ /** Which HTML contexts to check for reflections */
90
+ contexts: z.ZodDefault<z.ZodObject<{
91
+ script: z.ZodDefault<z.ZodBoolean>;
92
+ attribute: z.ZodDefault<z.ZodBoolean>;
93
+ body: z.ZodDefault<z.ZodBoolean>;
94
+ }, "strip", z.ZodTypeAny, {
95
+ script: boolean;
96
+ attribute: boolean;
97
+ body: boolean;
98
+ }, {
99
+ script?: boolean | undefined;
100
+ attribute?: boolean | undefined;
101
+ body?: boolean | undefined;
102
+ }>>;
103
+ /** Severity per context */
104
+ severity: z.ZodDefault<z.ZodObject<{
105
+ script: z.ZodDefault<z.ZodEnum<["critical", "high", "medium", "low"]>>;
106
+ attribute: z.ZodDefault<z.ZodEnum<["critical", "high", "medium", "low"]>>;
107
+ body: z.ZodDefault<z.ZodEnum<["critical", "high", "medium", "low"]>>;
108
+ }, "strip", z.ZodTypeAny, {
109
+ script: "critical" | "high" | "medium" | "low";
110
+ attribute: "critical" | "high" | "medium" | "low";
111
+ body: "critical" | "high" | "medium" | "low";
112
+ }, {
113
+ script?: "critical" | "high" | "medium" | "low" | undefined;
114
+ attribute?: "critical" | "high" | "medium" | "low" | undefined;
115
+ body?: "critical" | "high" | "medium" | "low" | undefined;
116
+ }>>;
117
+ }, "strip", z.ZodTypeAny, {
118
+ severity: {
119
+ script: "critical" | "high" | "medium" | "low";
120
+ attribute: "critical" | "high" | "medium" | "low";
121
+ body: "critical" | "high" | "medium" | "low";
122
+ };
123
+ enabled: boolean;
124
+ minLength: number;
125
+ contexts: {
126
+ script: boolean;
127
+ attribute: boolean;
128
+ body: boolean;
129
+ };
130
+ }, {
131
+ severity?: {
132
+ script?: "critical" | "high" | "medium" | "low" | undefined;
133
+ attribute?: "critical" | "high" | "medium" | "low" | undefined;
134
+ body?: "critical" | "high" | "medium" | "low" | undefined;
135
+ } | undefined;
136
+ enabled?: boolean | undefined;
137
+ minLength?: number | undefined;
138
+ contexts?: {
139
+ script?: boolean | undefined;
140
+ attribute?: boolean | undefined;
141
+ body?: boolean | undefined;
142
+ } | undefined;
143
+ }>>;
144
+ /** Enable passive security checks (headers, cookies, info-disclosure) */
145
+ passive: z.ZodDefault<z.ZodBoolean>;
146
+ }, "strip", z.ZodTypeAny, {
147
+ xss: {
148
+ dialogs: boolean;
149
+ console: boolean;
150
+ consoleMarker: string;
151
+ domMutation: boolean;
152
+ severity: "critical" | "high" | "medium" | "low";
153
+ alertPatterns: string[];
154
+ };
155
+ reflection: {
156
+ severity: {
157
+ script: "critical" | "high" | "medium" | "low";
158
+ attribute: "critical" | "high" | "medium" | "low";
159
+ body: "critical" | "high" | "medium" | "low";
160
+ };
161
+ enabled: boolean;
162
+ minLength: number;
163
+ contexts: {
164
+ script: boolean;
165
+ attribute: boolean;
166
+ body: boolean;
167
+ };
168
+ };
169
+ passive: boolean;
170
+ }, {
171
+ xss?: {
172
+ dialogs?: boolean | undefined;
173
+ console?: boolean | undefined;
174
+ consoleMarker?: string | undefined;
175
+ domMutation?: boolean | undefined;
176
+ severity?: "critical" | "high" | "medium" | "low" | undefined;
177
+ alertPatterns?: string[] | undefined;
178
+ } | undefined;
179
+ reflection?: {
180
+ severity?: {
181
+ script?: "critical" | "high" | "medium" | "low" | undefined;
182
+ attribute?: "critical" | "high" | "medium" | "low" | undefined;
183
+ body?: "critical" | "high" | "medium" | "low" | undefined;
184
+ } | undefined;
185
+ enabled?: boolean | undefined;
186
+ minLength?: number | undefined;
187
+ contexts?: {
188
+ script?: boolean | undefined;
189
+ attribute?: boolean | undefined;
190
+ body?: boolean | undefined;
191
+ } | undefined;
192
+ } | undefined;
193
+ passive?: boolean | undefined;
194
+ }>>;
195
+ /** Crawl configuration */
196
+ crawl: z.ZodDefault<z.ZodObject<{
197
+ /** Maximum crawl depth */
198
+ depth: z.ZodDefault<z.ZodNumber>;
199
+ /** Maximum pages to visit */
200
+ maxPages: z.ZodDefault<z.ZodNumber>;
201
+ /** Stay on same origin */
202
+ sameOrigin: z.ZodDefault<z.ZodBoolean>;
203
+ /** Per-page timeout in ms */
204
+ timeout: z.ZodDefault<z.ZodNumber>;
205
+ }, "strip", z.ZodTypeAny, {
206
+ timeout: number;
207
+ depth: number;
208
+ maxPages: number;
209
+ sameOrigin: boolean;
210
+ }, {
211
+ timeout?: number | undefined;
212
+ depth?: number | undefined;
213
+ maxPages?: number | undefined;
214
+ sameOrigin?: boolean | undefined;
215
+ }>>;
216
+ /** Report configuration */
217
+ report: z.ZodDefault<z.ZodObject<{
218
+ /** Report format to generate */
219
+ format: z.ZodDefault<z.ZodNullable<z.ZodEnum<["html", "json", "yaml", "sarif", "all"]>>>;
220
+ }, "strip", z.ZodTypeAny, {
221
+ format: "html" | "json" | "yaml" | "sarif" | "all" | null;
222
+ }, {
223
+ format?: "html" | "json" | "yaml" | "sarif" | "all" | null | undefined;
224
+ }>>;
225
+ /** Authentication configuration */
226
+ auth: z.ZodDefault<z.ZodNullable<z.ZodDiscriminatedUnion<"strategy", [z.ZodObject<{
227
+ strategy: z.ZodLiteral<"form">;
228
+ /** Login page URL */
229
+ loginUrl: z.ZodOptional<z.ZodString>;
230
+ /** CSS selector for username field */
231
+ userSelector: z.ZodDefault<z.ZodNullable<z.ZodString>>;
232
+ /** CSS selector for password field */
233
+ passSelector: z.ZodDefault<z.ZodNullable<z.ZodString>>;
234
+ }, "strip", z.ZodTypeAny, {
235
+ strategy: "form";
236
+ userSelector: string | null;
237
+ passSelector: string | null;
238
+ loginUrl?: string | undefined;
239
+ }, {
240
+ strategy: "form";
241
+ loginUrl?: string | undefined;
242
+ userSelector?: string | null | undefined;
243
+ passSelector?: string | null | undefined;
244
+ }>, z.ZodObject<{
245
+ strategy: z.ZodLiteral<"header">;
246
+ /** Headers to include in requests */
247
+ headers: z.ZodRecord<z.ZodString, z.ZodString>;
248
+ }, "strip", z.ZodTypeAny, {
249
+ strategy: "header";
250
+ headers: Record<string, string>;
251
+ }, {
252
+ strategy: "header";
253
+ headers: Record<string, string>;
254
+ }>]>>>;
255
+ }, "strip", z.ZodTypeAny, {
256
+ scan: {
257
+ browser: "chromium" | "firefox" | "webkit";
258
+ headless: boolean;
259
+ timeout: number;
260
+ };
261
+ payloads: {
262
+ custom: string | null;
263
+ types: ("xss" | "sqli" | "xxe" | "cmd" | "redirect" | "traversal")[];
264
+ payloadbox: boolean;
265
+ limit: number;
266
+ };
267
+ detection: {
268
+ xss: {
269
+ dialogs: boolean;
270
+ console: boolean;
271
+ consoleMarker: string;
272
+ domMutation: boolean;
273
+ severity: "critical" | "high" | "medium" | "low";
274
+ alertPatterns: string[];
275
+ };
276
+ reflection: {
277
+ severity: {
278
+ script: "critical" | "high" | "medium" | "low";
279
+ attribute: "critical" | "high" | "medium" | "low";
280
+ body: "critical" | "high" | "medium" | "low";
281
+ };
282
+ enabled: boolean;
283
+ minLength: number;
284
+ contexts: {
285
+ script: boolean;
286
+ attribute: boolean;
287
+ body: boolean;
288
+ };
289
+ };
290
+ passive: boolean;
291
+ };
292
+ crawl: {
293
+ timeout: number;
294
+ depth: number;
295
+ maxPages: number;
296
+ sameOrigin: boolean;
297
+ };
298
+ report: {
299
+ format: "html" | "json" | "yaml" | "sarif" | "all" | null;
300
+ };
301
+ auth: {
302
+ strategy: "form";
303
+ userSelector: string | null;
304
+ passSelector: string | null;
305
+ loginUrl?: string | undefined;
306
+ } | {
307
+ strategy: "header";
308
+ headers: Record<string, string>;
309
+ } | null;
310
+ target?: string | undefined;
311
+ }, {
312
+ target?: string | undefined;
313
+ scan?: {
314
+ browser?: "chromium" | "firefox" | "webkit" | undefined;
315
+ headless?: boolean | undefined;
316
+ timeout?: number | undefined;
317
+ } | undefined;
318
+ payloads?: {
319
+ custom?: string | null | undefined;
320
+ types?: ("xss" | "sqli" | "xxe" | "cmd" | "redirect" | "traversal")[] | undefined;
321
+ payloadbox?: boolean | undefined;
322
+ limit?: number | undefined;
323
+ } | undefined;
324
+ detection?: {
325
+ xss?: {
326
+ dialogs?: boolean | undefined;
327
+ console?: boolean | undefined;
328
+ consoleMarker?: string | undefined;
329
+ domMutation?: boolean | undefined;
330
+ severity?: "critical" | "high" | "medium" | "low" | undefined;
331
+ alertPatterns?: string[] | undefined;
332
+ } | undefined;
333
+ reflection?: {
334
+ severity?: {
335
+ script?: "critical" | "high" | "medium" | "low" | undefined;
336
+ attribute?: "critical" | "high" | "medium" | "low" | undefined;
337
+ body?: "critical" | "high" | "medium" | "low" | undefined;
338
+ } | undefined;
339
+ enabled?: boolean | undefined;
340
+ minLength?: number | undefined;
341
+ contexts?: {
342
+ script?: boolean | undefined;
343
+ attribute?: boolean | undefined;
344
+ body?: boolean | undefined;
345
+ } | undefined;
346
+ } | undefined;
347
+ passive?: boolean | undefined;
348
+ } | undefined;
349
+ crawl?: {
350
+ timeout?: number | undefined;
351
+ depth?: number | undefined;
352
+ maxPages?: number | undefined;
353
+ sameOrigin?: boolean | undefined;
354
+ } | undefined;
355
+ report?: {
356
+ format?: "html" | "json" | "yaml" | "sarif" | "all" | null | undefined;
357
+ } | undefined;
358
+ auth?: {
359
+ strategy: "form";
360
+ loginUrl?: string | undefined;
361
+ userSelector?: string | null | undefined;
362
+ passSelector?: string | null | undefined;
363
+ } | {
364
+ strategy: "header";
365
+ headers: Record<string, string>;
366
+ } | null | undefined;
367
+ }>;
368
+ /** Parsed and validated project config */
369
+ type VulcnProjectConfig = z.infer<typeof VulcnProjectConfigSchema>;
370
+ /**
371
+ * Parse and validate a raw config object (from YAML.parse).
372
+ * All fields have defaults, so an empty object is valid.
373
+ */
374
+ declare function parseProjectConfig(raw: unknown): VulcnProjectConfig;
375
+ /**
376
+ * Default config for `vulcn init`.
377
+ * Only includes fields that users are likely to customize.
378
+ */
379
+ declare const DEFAULT_PROJECT_CONFIG: {
380
+ readonly target: "https://example.com";
381
+ readonly scan: {
382
+ readonly browser: "chromium";
383
+ readonly headless: true;
384
+ readonly timeout: 30000;
385
+ };
386
+ readonly payloads: {
387
+ readonly types: readonly ["xss"];
388
+ };
389
+ readonly detection: {
390
+ readonly xss: {
391
+ readonly dialogs: true;
392
+ readonly console: true;
393
+ readonly domMutation: false;
394
+ readonly severity: "high";
395
+ };
396
+ readonly reflection: {
397
+ readonly enabled: true;
398
+ };
399
+ readonly passive: true;
400
+ };
401
+ readonly crawl: {
402
+ readonly depth: 2;
403
+ readonly maxPages: 20;
404
+ readonly sameOrigin: true;
405
+ };
406
+ readonly report: {
407
+ readonly format: "html";
408
+ };
409
+ };
410
+
411
+ /**
412
+ * Vulcn Project Discovery & Resolution
413
+ *
414
+ * Finds `.vulcn.yml` by walking up from `cwd`, parses the config,
415
+ * and resolves convention-based paths (sessions/, auth/, reports/).
416
+ */
417
+
418
+ /** Config filename — presence marks a directory as a Vulcn project */
419
+ declare const CONFIG_FILENAME = ".vulcn.yml";
420
+ /** Convention-based subdirectory names */
421
+ declare const DIRS: {
422
+ readonly sessions: "sessions";
423
+ readonly auth: "auth";
424
+ readonly reports: "reports";
425
+ };
426
+ /** Resolved project paths */
427
+ interface ProjectPaths {
428
+ /** Absolute path to the project root (directory containing .vulcn.yml) */
429
+ root: string;
430
+ /** Absolute path to .vulcn.yml */
431
+ config: string;
432
+ /** Absolute path to sessions/ directory */
433
+ sessions: string;
434
+ /** Absolute path to auth/ directory */
435
+ auth: string;
436
+ /** Absolute path to reports/ directory */
437
+ reports: string;
438
+ }
439
+ /** Loaded project — config + paths */
440
+ interface VulcnProject {
441
+ /** Parsed and validated config */
442
+ config: VulcnProjectConfig;
443
+ /** Resolved absolute paths */
444
+ paths: ProjectPaths;
445
+ }
446
+ /**
447
+ * Find the project root by walking up from `startDir` looking for `.vulcn.yml`.
448
+ *
449
+ * @param startDir - Directory to start searching from (default: `cwd()`)
450
+ * @returns Absolute path to the project root, or `null` if not found
451
+ */
452
+ declare function findProjectRoot(startDir?: string): string | null;
453
+ /**
454
+ * Resolve convention-based paths from a project root.
455
+ */
456
+ declare function resolveProjectPaths(root: string): ProjectPaths;
457
+ /**
458
+ * Load a Vulcn project from a directory.
459
+ *
460
+ * Finds `.vulcn.yml`, parses it, validates with Zod, and resolves paths.
461
+ *
462
+ * @param startDir - Directory to start searching from (default: `cwd()`)
463
+ * @throws If `.vulcn.yml` is not found or invalid
464
+ */
465
+ declare function loadProject(startDir?: string): Promise<VulcnProject>;
466
+ /**
467
+ * Load project config from a specific file path (no discovery).
468
+ * Useful for testing or when the path is already known.
469
+ */
470
+ declare function loadProjectFromFile(configPath: string): Promise<VulcnProject>;
471
+ /**
472
+ * Ensure convention directories exist (sessions/, auth/, reports/).
473
+ * Called during init and before operations that write to these dirs.
474
+ */
475
+ declare function ensureProjectDirs(paths: ProjectPaths, dirs?: Array<keyof typeof DIRS>): Promise<void>;
476
+
3
477
  /**
4
478
  * Payload Types for Vulcn
5
479
  * Core types used by the engine and plugins
@@ -11,7 +485,7 @@ type PayloadCategory = "xss" | "sqli" | "ssrf" | "xxe" | "command-injection" | "
11
485
  /**
12
486
  * Payload source types
13
487
  */
14
- type PayloadSource = "custom" | "payloadbox" | "plugin";
488
+ type PayloadSource = "curated" | "custom" | "payloadbox" | "plugin";
15
489
  /**
16
490
  * Runtime payload structure - used by plugins and the runner
17
491
  */
@@ -46,9 +520,18 @@ interface CustomPayloadFile {
46
520
  version?: string;
47
521
  payloads: CustomPayload[];
48
522
  }
523
+ /**
524
+ * Determine finding severity based on vulnerability category.
525
+ *
526
+ * Central mapping used by all drivers and plugins — single source of truth
527
+ * so severity ratings remain consistent across Tier 1 (HTTP) and Tier 2 (browser) scans.
528
+ */
529
+ declare function getSeverity(category: PayloadCategory): "critical" | "high" | "medium" | "low" | "info";
49
530
 
50
531
  interface Finding {
51
532
  type: PayloadCategory;
533
+ /** CWE identifier (e.g., "CWE-79" for XSS, "CWE-89" for SQLi) */
534
+ cwe?: string;
52
535
  severity: "critical" | "high" | "medium" | "low" | "info";
53
536
  title: string;
54
537
  description: string;
@@ -60,6 +543,111 @@ interface Finding {
60
543
  metadata?: Record<string, unknown>;
61
544
  }
62
545
 
546
+ /**
547
+ * Vulcn Error System
548
+ *
549
+ * Centralized error handling with severity classification.
550
+ * Components emit errors here instead of making local catch/swallow decisions.
551
+ *
552
+ * Severity levels:
553
+ * FATAL — Stop execution immediately. The operation cannot continue.
554
+ * Examples: report plugin fails to write, payload loading fails,
555
+ * driver can't launch browser.
556
+ *
557
+ * ERROR — Something broke but execution can continue. Record it.
558
+ * Examples: a single session times out, a plugin hook fails
559
+ * on a non-critical lifecycle event.
560
+ *
561
+ * WARN — Expected or recoverable. Log and move on.
562
+ * Examples: optional plugin not installed, browser page navigation
563
+ * intermittently fails, auth state not found.
564
+ */
565
+ declare enum ErrorSeverity {
566
+ /** Stop execution — unrecoverable */
567
+ FATAL = "fatal",
568
+ /** Record and continue — something broke but others can proceed */
569
+ ERROR = "error",
570
+ /** Log and move on — expected or minor */
571
+ WARN = "warn"
572
+ }
573
+ declare class VulcnError extends Error {
574
+ readonly severity: ErrorSeverity;
575
+ readonly source: string;
576
+ readonly context?: Record<string, unknown>;
577
+ readonly timestamp: string;
578
+ constructor(message: string, options: {
579
+ severity: ErrorSeverity;
580
+ source: string;
581
+ cause?: unknown;
582
+ context?: Record<string, unknown>;
583
+ });
584
+ /**
585
+ * Wrap any caught error into a VulcnError.
586
+ * If it's already a VulcnError, returns it as-is.
587
+ */
588
+ static from(err: unknown, defaults: {
589
+ severity: ErrorSeverity;
590
+ source: string;
591
+ context?: Record<string, unknown>;
592
+ }): VulcnError;
593
+ }
594
+ declare function fatal(message: string, source: string, options?: {
595
+ cause?: unknown;
596
+ context?: Record<string, unknown>;
597
+ }): VulcnError;
598
+ declare function error(message: string, source: string, options?: {
599
+ cause?: unknown;
600
+ context?: Record<string, unknown>;
601
+ }): VulcnError;
602
+ declare function warn(message: string, source: string, options?: {
603
+ cause?: unknown;
604
+ context?: Record<string, unknown>;
605
+ }): VulcnError;
606
+ type ErrorListener = (error: VulcnError) => void;
607
+ /**
608
+ * Central error handler for the Vulcn engine.
609
+ *
610
+ * - FATAL errors throw immediately (halt execution)
611
+ * - ERROR errors are recorded and logged
612
+ * - WARN errors are logged only
613
+ *
614
+ * At the end of a run/scan, call `getSummary()` to see everything that went wrong.
615
+ */
616
+ declare class ErrorHandler {
617
+ private errors;
618
+ private listeners;
619
+ /**
620
+ * Handle an error based on its severity.
621
+ *
622
+ * - FATAL: logs, records, then THROWS (caller must not catch silently)
623
+ * - ERROR: logs and records
624
+ * - WARN: logs only
625
+ */
626
+ handle(err: VulcnError): void;
627
+ /**
628
+ * Convenience: wrap a caught error and handle it.
629
+ */
630
+ catch(err: unknown, defaults: {
631
+ severity: ErrorSeverity;
632
+ source: string;
633
+ context?: Record<string, unknown>;
634
+ }): void;
635
+ /** All recorded errors (FATAL + ERROR + WARN) */
636
+ getAll(): VulcnError[];
637
+ /** Only ERROR and FATAL */
638
+ getErrors(): VulcnError[];
639
+ /** Were there any errors (not just warnings)? */
640
+ hasErrors(): boolean;
641
+ /** Count by severity */
642
+ counts(): Record<ErrorSeverity, number>;
643
+ /** Human-readable summary for end-of-run reporting */
644
+ getSummary(): string;
645
+ /** Subscribe to errors as they happen */
646
+ onError(listener: ErrorListener): () => void;
647
+ /** Reset for a new run */
648
+ clear(): void;
649
+ }
650
+
63
651
  /**
64
652
  * Vulcn Plugin System Types
65
653
  * @module @vulcn/engine/plugin
@@ -173,7 +761,7 @@ interface EngineInfo {
173
761
  * Base context available to all plugin hooks
174
762
  */
175
763
  interface PluginContext {
176
- /** Plugin-specific config from vulcn.config.yml */
764
+ /** Plugin-specific configuration */
177
765
  config: Record<string, unknown>;
178
766
  /** Engine information */
179
767
  engine: EngineInfo;
@@ -189,6 +777,13 @@ interface PluginContext {
189
777
  addFinding: (finding: Finding) => void;
190
778
  /** Scoped logger */
191
779
  logger: PluginLogger;
780
+ /**
781
+ * Centralized error handler.
782
+ * Plugins MUST use this to surface errors instead of swallowing them:
783
+ * ctx.errors.fatal("can't write report", "plugin:report", { cause: err })
784
+ * ctx.errors.warn("optional feature unavailable", "plugin:passive")
785
+ */
786
+ errors: ErrorHandler;
192
787
  /** Fetch API for network requests */
193
788
  fetch: typeof fetch;
194
789
  }
@@ -234,31 +829,6 @@ interface DetectContext extends RunContext$1 {
234
829
  /** Step ID for reporting */
235
830
  stepId: string;
236
831
  }
237
- /**
238
- * Plugin configuration in vulcn.config.yml
239
- */
240
- interface PluginConfig {
241
- /** Plugin name/path */
242
- name: string;
243
- /** Plugin-specific configuration */
244
- config?: Record<string, unknown>;
245
- /** Whether plugin is enabled (default: true) */
246
- enabled?: boolean;
247
- }
248
- /**
249
- * Vulcn configuration file schema
250
- */
251
- interface VulcnConfig {
252
- /** Config version */
253
- version: string;
254
- /** Plugins to load */
255
- plugins?: PluginConfig[];
256
- /** Global settings */
257
- settings?: {
258
- headless?: boolean;
259
- timeout?: number;
260
- };
261
- }
262
832
  /**
263
833
  * Loaded plugin instance with resolved config
264
834
  */
@@ -275,7 +845,11 @@ interface LoadedPlugin {
275
845
 
276
846
  /**
277
847
  * Vulcn Plugin Manager
278
- * Handles plugin loading, lifecycle, and hook execution
848
+ * Handles plugin loading, lifecycle, and hook execution.
849
+ *
850
+ * The primary entry point is `loadFromConfig(config)` which takes
851
+ * a flat `VulcnProjectConfig` (from `.vulcn.yml`) and maps it to
852
+ * internal plugin configs automatically.
279
853
  */
280
854
 
281
855
  /**
@@ -283,25 +857,16 @@ interface LoadedPlugin {
283
857
  */
284
858
  declare class PluginManager {
285
859
  private plugins;
286
- private config;
287
860
  private initialized;
861
+ private errorHandler;
288
862
  /**
289
863
  * Shared context passed to all plugins
290
864
  */
291
865
  private sharedPayloads;
292
866
  private sharedFindings;
293
- /**
294
- * Load configuration from vulcn.config.yml
295
- */
296
- loadConfig(configPath?: string): Promise<VulcnConfig>;
297
- /**
298
- * Load all plugins from config
299
- */
300
- loadPlugins(): Promise<void>;
301
- /**
302
- * Load a single plugin
303
- */
304
- private loadPlugin;
867
+ constructor(errorHandler?: ErrorHandler);
868
+ /** Get the error handler for post-run inspection */
869
+ getErrorHandler(): ErrorHandler;
305
870
  /**
306
871
  * Validate plugin structure
307
872
  */
@@ -318,6 +883,15 @@ declare class PluginManager {
318
883
  * Destroy all plugins (call onDestroy hooks)
319
884
  */
320
885
  destroy(): Promise<void>;
886
+ /**
887
+ * Load the engine from a flat VulcnProjectConfig (from `.vulcn.yml`).
888
+ *
889
+ * This is the primary entry point for the new config system.
890
+ * Maps user-facing config keys to internal plugin configs automatically.
891
+ *
892
+ * @param config - Parsed and validated VulcnProjectConfig
893
+ */
894
+ loadFromConfig(config: VulcnProjectConfig): Promise<void>;
321
895
  /**
322
896
  * Get all loaded payloads
323
897
  */
@@ -349,11 +923,14 @@ declare class PluginManager {
349
923
  /**
350
924
  * Create base context for plugins
351
925
  */
352
- createContext(pluginConfig: Record<string, unknown>): PluginContext;
926
+ createContext(pluginConfig: Record<string, unknown>, pluginName?: string): PluginContext;
353
927
  /**
354
928
  * Create scoped logger for a plugin
355
929
  */
356
930
  private createLogger;
931
+ private static readonly FATAL_HOOKS;
932
+ private static readonly ERROR_HOOKS;
933
+ private hookSeverity;
357
934
  /**
358
935
  * Call a hook on all plugins sequentially
359
936
  */
@@ -435,6 +1012,13 @@ interface RunContext {
435
1012
  addFinding(finding: Finding): void;
436
1013
  /** Logger */
437
1014
  logger: DriverLogger;
1015
+ /**
1016
+ * Centralized error handler.
1017
+ * Drivers MUST use this to surface errors:
1018
+ * ctx.errors.fatal("session data malformed", "driver:browser")
1019
+ * ctx.errors.warn("page timeout", "driver:browser")
1020
+ */
1021
+ errors: ErrorHandler;
438
1022
  /** Running options */
439
1023
  options: RunOptions;
440
1024
  }
@@ -480,6 +1064,16 @@ interface RunOptions {
480
1064
  onFinding?: (finding: Finding) => void;
481
1065
  /** Callback for step completion */
482
1066
  onStepComplete?: (stepId: string, payloadCount: number) => void;
1067
+ /**
1068
+ * Called by executeScan before each session starts.
1069
+ * Provides the session name, index, and total count for progress tracking.
1070
+ */
1071
+ onSessionStart?: (session: Session, index: number, total: number) => void;
1072
+ /**
1073
+ * Called by executeScan after each session completes.
1074
+ * Provides the result for that session.
1075
+ */
1076
+ onSessionEnd?: (session: Session, result: RunResult, index: number, total: number) => void;
483
1077
  /**
484
1078
  * Called by the driver runner after the page/environment is ready.
485
1079
  * The driver-manager uses this to fire plugin onRunStart hooks
@@ -492,7 +1086,19 @@ interface RunOptions {
492
1086
  * so plugins can flush pending async work.
493
1087
  */
494
1088
  onBeforeClose?: (page: unknown) => Promise<void>;
495
- /** Driver-specific options */
1089
+ /**
1090
+ * Per-session timeout in milliseconds.
1091
+ * If a session exceeds this duration, it will be aborted with a timeout error.
1092
+ * Both CLI and Worker benefit from this when using `executeScan`.
1093
+ */
1094
+ timeout?: number;
1095
+ /** Shared browser instance (passed by executeScan for persistent mode) */
1096
+ browser?: unknown;
1097
+ /** JSON-stringified browser storage state (cookies, localStorage) for authenticated scans */
1098
+ storageState?: string;
1099
+ /** Extra HTTP headers to inject into every request (for header-based auth) */
1100
+ extraHeaders?: Record<string, string>;
1101
+ /** Allow additional driver-specific options */
496
1102
  [key: string]: unknown;
497
1103
  }
498
1104
  /**
@@ -580,6 +1186,14 @@ interface VulcnDriver {
580
1186
  recorder: RecorderDriver;
581
1187
  /** Runner implementation */
582
1188
  runner: RunnerDriver;
1189
+ /**
1190
+ * Create a shared resource (e.g., a browser instance) that can be
1191
+ * passed to execute() via ctx.options.
1192
+ *
1193
+ * Used by executeScan() to improve performance by reusing resources
1194
+ * across multiple sessions.
1195
+ */
1196
+ createSharedResource?: (config: Record<string, unknown>, options: RunOptions) => Promise<unknown>;
583
1197
  }
584
1198
  /**
585
1199
  * Driver source for loading
@@ -599,6 +1213,7 @@ interface LoadedDriver {
599
1213
  * Handles driver loading, registration, and lifecycle.
600
1214
  * Drivers are loaded from npm packages or local files.
601
1215
  */
1216
+ declare const ENGINE_VERSION: any;
602
1217
 
603
1218
  /**
604
1219
  * Driver Manager - loads and manages recording/running drivers
@@ -641,15 +1256,11 @@ declare class DriverManager {
641
1256
  /**
642
1257
  * Parse a YAML session string into a Session object.
643
1258
  *
644
- * Handles both new driver-format sessions and legacy v1 sessions.
645
- * Legacy sessions (those with non-namespaced step types like "click",
646
- * "input", "navigate") are automatically converted to the driver format
647
- * (e.g., "browser.click", "browser.input", "browser.navigate").
1259
+ * Sessions must use the driver format with a `driver` field.
648
1260
  *
649
1261
  * @param yaml - Raw YAML string
650
- * @param defaultDriver - Driver to assign for legacy sessions (default: "browser")
651
1262
  */
652
- parseSession(yaml: string, defaultDriver?: string): Session;
1263
+ parseSession(yaml: string): Session;
653
1264
  /**
654
1265
  * Start recording with a driver
655
1266
  */
@@ -796,57 +1407,10 @@ declare function decryptStorageState(encrypted: string, passphrase: string): str
796
1407
  declare function getPassphrase(interactive?: string): string;
797
1408
 
798
1409
  /**
799
- * Vulcn Session Format v2
1410
+ * Vulcn Session Utilities
800
1411
  *
801
- * Directory-based session format: `.vulcn/` or `<name>.vulcn/`
802
- *
803
- * Structure:
804
- * manifest.yml - scan config, session list, auth config
805
- * auth/config.yml - login strategy, indicators
806
- * auth/state.enc - encrypted storageState (cookies/localStorage)
807
- * sessions/*.yml - individual session files (one per form)
808
- * requests/*.json - captured HTTP metadata (for Tier 1 fast scan)
1412
+ * Types and utilities used by the driver system for HTTP request metadata.
809
1413
  */
810
-
811
- /** Manifest file schema (manifest.yml) */
812
- interface ScanManifest {
813
- /** Format version */
814
- version: "2";
815
- /** Human-readable scan name */
816
- name: string;
817
- /** Target URL */
818
- target: string;
819
- /** When the scan was recorded */
820
- recordedAt: string;
821
- /** Driver name */
822
- driver: string;
823
- /** Driver configuration */
824
- driverConfig: Record<string, unknown>;
825
- /** Auth configuration (optional) */
826
- auth?: {
827
- strategy: string;
828
- configFile?: string;
829
- stateFile?: string;
830
- loggedInIndicator?: string;
831
- loggedOutIndicator?: string;
832
- reAuthOn?: Array<Record<string, unknown>>;
833
- };
834
- /** Session file references */
835
- sessions: SessionRef[];
836
- /** Scan configuration */
837
- scan?: {
838
- tier?: "auto" | "http-only" | "browser-only";
839
- parallel?: number;
840
- timeout?: number;
841
- };
842
- }
843
- /** Reference to a session file within the manifest */
844
- interface SessionRef {
845
- /** Relative path to session file */
846
- file: string;
847
- /** Whether this session has injectable inputs */
848
- injectable?: boolean;
849
- }
850
1414
  /** HTTP request metadata for Tier 1 fast scanning */
851
1415
  interface CapturedRequest {
852
1416
  /** Request method */
@@ -864,54 +1428,5 @@ interface CapturedRequest {
864
1428
  /** Session name this request belongs to */
865
1429
  sessionName: string;
866
1430
  }
867
- /**
868
- * Load a v2 session directory into Session[] ready for execution.
869
- *
870
- * @param dirPath - Path to the .vulcn/ directory
871
- * @returns Array of sessions with manifest metadata attached
872
- */
873
- declare function loadSessionDir(dirPath: string): Promise<{
874
- manifest: ScanManifest;
875
- sessions: Session[];
876
- authConfig?: AuthConfig;
877
- }>;
878
- /**
879
- * Check if a path is a v2 session directory.
880
- */
881
- declare function isSessionDir(path: string): boolean;
882
- /**
883
- * Check if a path looks like a v2 session directory (by extension).
884
- */
885
- declare function looksLikeSessionDir(path: string): boolean;
886
- /**
887
- * Save sessions to a v2 session directory.
888
- *
889
- * Creates the directory structure:
890
- * <dirPath>/
891
- * ├── manifest.yml
892
- * ├── sessions/
893
- * │ ├── <session-name>.yml
894
- * │ └── ...
895
- * └── requests/ (if HTTP metadata provided)
896
- * └── ...
897
- */
898
- declare function saveSessionDir(dirPath: string, options: {
899
- name: string;
900
- target: string;
901
- driver: string;
902
- driverConfig: Record<string, unknown>;
903
- sessions: Session[];
904
- authConfig?: AuthConfig;
905
- encryptedState?: string;
906
- requests?: CapturedRequest[];
907
- }): Promise<void>;
908
- /**
909
- * Read encrypted auth state from a session directory.
910
- */
911
- declare function readAuthState(dirPath: string): Promise<string | null>;
912
- /**
913
- * Read captured HTTP requests from a session directory.
914
- */
915
- declare function readCapturedRequests(dirPath: string): Promise<CapturedRequest[]>;
916
1431
 
917
- export { type AuthConfig, type CapturedRequest, type CrawlOptions, type Credentials, type CustomPayload, type CustomPayloadFile, DRIVER_API_VERSION, type DetectContext, type DriverLogger, DriverManager, type DriverSource, type EngineInfo, type Finding, type FormCredentials, type HeaderCredentials, type LoadedDriver, type LoadedPlugin as LoadedPluginInfo, PLUGIN_API_VERSION, type PayloadCategory, type PayloadSource, type PluginConfig, type PluginContext, type PluginHooks, type PluginLogger, PluginManager, type RunContext$1 as PluginRunContext, type PluginSource, type RecordContext, type RecordOptions, type RecorderDriver, type RecordingHandle, type RunContext, type RunOptions, type RunResult, type RunnerDriver, type RuntimePayload, type ScanContext, type ScanManifest, type Session, type SessionRef, type Step, type VulcnConfig, type VulcnDriver, type VulcnPlugin, decrypt, decryptCredentials, decryptStorageState, driverManager, encrypt, encryptCredentials, encryptStorageState, getPassphrase, isSessionDir, loadSessionDir, looksLikeSessionDir, pluginManager, readAuthState, readCapturedRequests, saveSessionDir };
1432
+ export { type AuthConfig, CONFIG_FILENAME, type CapturedRequest, type CrawlOptions, type Credentials, type CustomPayload, type CustomPayloadFile, DEFAULT_PROJECT_CONFIG, DIRS, DRIVER_API_VERSION, type DetectContext, type DriverLogger, DriverManager, type DriverSource, ENGINE_VERSION, type EngineInfo, ErrorHandler, type ErrorListener, ErrorSeverity, type Finding, type FormCredentials, type HeaderCredentials, type LoadedDriver, type LoadedPlugin as LoadedPluginInfo, PLUGIN_API_VERSION, type PayloadCategory, type PayloadSource, type PluginContext, type PluginHooks, type PluginLogger, PluginManager, type RunContext$1 as PluginRunContext, type PluginSource, type ProjectPaths, type RecordContext, type RecordOptions, type RecorderDriver, type RecordingHandle, type RunContext, type RunOptions, type RunResult, type RunnerDriver, type RuntimePayload, type ScanContext, type Session, type Step, type VulcnDriver, VulcnError, type VulcnPlugin, type VulcnProject, type VulcnProjectConfig, VulcnProjectConfigSchema, decrypt, decryptCredentials, decryptStorageState, driverManager, encrypt, encryptCredentials, encryptStorageState, ensureProjectDirs, error, fatal, findProjectRoot, getPassphrase, getSeverity, loadProject, loadProjectFromFile, parseProjectConfig, pluginManager, resolveProjectPaths, warn };