@vulcn/engine 0.1.0 → 0.2.0

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
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,25 +17,32 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
23
- BUILTIN_PAYLOADS: () => BUILTIN_PAYLOADS,
24
33
  BrowserNotFoundError: () => BrowserNotFoundError,
34
+ PLUGIN_API_VERSION: () => PLUGIN_API_VERSION,
35
+ PluginManager: () => PluginManager,
25
36
  Recorder: () => Recorder,
26
37
  Runner: () => Runner,
27
38
  SessionSchema: () => SessionSchema,
28
39
  StepSchema: () => StepSchema,
29
40
  checkBrowsers: () => checkBrowsers,
30
41
  createSession: () => createSession,
31
- getPayload: () => getPayload,
32
- getPayloadNames: () => getPayloadNames,
33
- getPayloadsByCategory: () => getPayloadsByCategory,
34
42
  installBrowsers: () => installBrowsers,
35
43
  launchBrowser: () => launchBrowser,
36
44
  parseSession: () => parseSession,
45
+ pluginManager: () => pluginManager,
37
46
  serializeSession: () => serializeSession
38
47
  });
39
48
  module.exports = __toCommonJS(index_exports);
@@ -228,16 +237,347 @@ async function checkBrowsers() {
228
237
  return results;
229
238
  }
230
239
 
240
+ // plugin-manager.ts
241
+ var import_promises = require("fs/promises");
242
+ var import_node_fs = require("fs");
243
+ var import_node_path = require("path");
244
+ var import_yaml2 = __toESM(require("yaml"), 1);
245
+ var import_zod2 = require("zod");
246
+
247
+ // plugin-types.ts
248
+ var PLUGIN_API_VERSION = 1;
249
+
250
+ // plugin-manager.ts
251
+ var ENGINE_VERSION = "0.2.0";
252
+ var VulcnConfigSchema = import_zod2.z.object({
253
+ version: import_zod2.z.string().default("1"),
254
+ plugins: import_zod2.z.array(
255
+ import_zod2.z.object({
256
+ name: import_zod2.z.string(),
257
+ config: import_zod2.z.record(import_zod2.z.unknown()).optional(),
258
+ enabled: import_zod2.z.boolean().default(true)
259
+ })
260
+ ).optional(),
261
+ settings: import_zod2.z.object({
262
+ browser: import_zod2.z.enum(["chromium", "firefox", "webkit"]).optional(),
263
+ headless: import_zod2.z.boolean().optional(),
264
+ timeout: import_zod2.z.number().optional()
265
+ }).optional()
266
+ });
267
+ var PluginManager = class {
268
+ plugins = [];
269
+ config = null;
270
+ initialized = false;
271
+ /**
272
+ * Shared context passed to all plugins
273
+ */
274
+ sharedPayloads = [];
275
+ sharedFindings = [];
276
+ /**
277
+ * Load configuration from vulcn.config.yml
278
+ */
279
+ async loadConfig(configPath) {
280
+ const paths = configPath ? [configPath] : [
281
+ "vulcn.config.yml",
282
+ "vulcn.config.yaml",
283
+ "vulcn.config.json",
284
+ ".vulcnrc.yml",
285
+ ".vulcnrc.yaml",
286
+ ".vulcnrc.json"
287
+ ];
288
+ for (const path of paths) {
289
+ const resolved = (0, import_node_path.isAbsolute)(path) ? path : (0, import_node_path.resolve)(process.cwd(), path);
290
+ if ((0, import_node_fs.existsSync)(resolved)) {
291
+ const content = await (0, import_promises.readFile)(resolved, "utf-8");
292
+ const parsed = path.endsWith(".json") ? JSON.parse(content) : import_yaml2.default.parse(content);
293
+ this.config = VulcnConfigSchema.parse(parsed);
294
+ return this.config;
295
+ }
296
+ }
297
+ this.config = { version: "1", plugins: [], settings: {} };
298
+ return this.config;
299
+ }
300
+ /**
301
+ * Load all plugins from config
302
+ */
303
+ async loadPlugins() {
304
+ if (!this.config) {
305
+ await this.loadConfig();
306
+ }
307
+ const pluginConfigs = this.config?.plugins || [];
308
+ for (const pluginConfig of pluginConfigs) {
309
+ if (pluginConfig.enabled === false) continue;
310
+ try {
311
+ const loaded = await this.loadPlugin(pluginConfig);
312
+ this.plugins.push(loaded);
313
+ } catch (err) {
314
+ console.error(
315
+ `Failed to load plugin ${pluginConfig.name}:`,
316
+ err instanceof Error ? err.message : String(err)
317
+ );
318
+ }
319
+ }
320
+ }
321
+ /**
322
+ * Load a single plugin
323
+ */
324
+ async loadPlugin(config) {
325
+ const { name, config: pluginConfig = {} } = config;
326
+ let plugin;
327
+ let source;
328
+ if (name.startsWith("./") || name.startsWith("../") || (0, import_node_path.isAbsolute)(name)) {
329
+ const resolved = (0, import_node_path.isAbsolute)(name) ? name : (0, import_node_path.resolve)(process.cwd(), name);
330
+ const module2 = await import(resolved);
331
+ plugin = module2.default || module2;
332
+ source = "local";
333
+ } else if (name.startsWith("@vulcn/")) {
334
+ const module2 = await import(name);
335
+ plugin = module2.default || module2;
336
+ source = "npm";
337
+ } else {
338
+ const module2 = await import(name);
339
+ plugin = module2.default || module2;
340
+ source = "npm";
341
+ }
342
+ this.validatePlugin(plugin);
343
+ let resolvedConfig = pluginConfig;
344
+ if (plugin.configSchema) {
345
+ try {
346
+ resolvedConfig = plugin.configSchema.parse(pluginConfig);
347
+ } catch (err) {
348
+ throw new Error(
349
+ `Invalid config for plugin ${name}: ${err instanceof Error ? err.message : String(err)}`
350
+ );
351
+ }
352
+ }
353
+ return {
354
+ plugin,
355
+ config: resolvedConfig,
356
+ source,
357
+ enabled: true
358
+ };
359
+ }
360
+ /**
361
+ * Validate plugin structure
362
+ */
363
+ validatePlugin(plugin) {
364
+ if (!plugin || typeof plugin !== "object") {
365
+ throw new Error("Plugin must be an object");
366
+ }
367
+ const p = plugin;
368
+ if (typeof p.name !== "string" || !p.name) {
369
+ throw new Error("Plugin must have a name");
370
+ }
371
+ if (typeof p.version !== "string" || !p.version) {
372
+ throw new Error("Plugin must have a version");
373
+ }
374
+ const apiVersion = p.apiVersion || 1;
375
+ if (apiVersion > PLUGIN_API_VERSION) {
376
+ throw new Error(
377
+ `Plugin requires API version ${apiVersion}, but engine supports ${PLUGIN_API_VERSION}`
378
+ );
379
+ }
380
+ }
381
+ /**
382
+ * Add a plugin programmatically (for testing or dynamic loading)
383
+ */
384
+ addPlugin(plugin, config = {}) {
385
+ this.validatePlugin(plugin);
386
+ this.plugins.push({
387
+ plugin,
388
+ config,
389
+ source: "custom",
390
+ enabled: true
391
+ });
392
+ }
393
+ /**
394
+ * Initialize all plugins (call onInit hooks)
395
+ */
396
+ async initialize() {
397
+ if (this.initialized) return;
398
+ for (const loaded of this.plugins) {
399
+ if (loaded.plugin.payloads) {
400
+ const payloads = typeof loaded.plugin.payloads === "function" ? await loaded.plugin.payloads() : loaded.plugin.payloads;
401
+ this.sharedPayloads.push(...payloads);
402
+ }
403
+ }
404
+ await this.callHook("onInit", (hook, ctx) => hook(ctx));
405
+ this.initialized = true;
406
+ }
407
+ /**
408
+ * Destroy all plugins (call onDestroy hooks)
409
+ */
410
+ async destroy() {
411
+ await this.callHook("onDestroy", (hook, ctx) => hook(ctx));
412
+ this.plugins = [];
413
+ this.sharedPayloads = [];
414
+ this.sharedFindings = [];
415
+ this.initialized = false;
416
+ }
417
+ /**
418
+ * Get all loaded payloads
419
+ */
420
+ getPayloads() {
421
+ return this.sharedPayloads;
422
+ }
423
+ /**
424
+ * Get all collected findings
425
+ */
426
+ getFindings() {
427
+ return this.sharedFindings;
428
+ }
429
+ /**
430
+ * Add a finding (used by detectors)
431
+ */
432
+ addFinding(finding) {
433
+ this.sharedFindings.push(finding);
434
+ }
435
+ /**
436
+ * Add payloads (used by loaders)
437
+ */
438
+ addPayloads(payloads) {
439
+ this.sharedPayloads.push(...payloads);
440
+ }
441
+ /**
442
+ * Clear findings (for new run)
443
+ */
444
+ clearFindings() {
445
+ this.sharedFindings = [];
446
+ }
447
+ /**
448
+ * Get loaded plugins
449
+ */
450
+ getPlugins() {
451
+ return this.plugins;
452
+ }
453
+ /**
454
+ * Check if a plugin is loaded by name
455
+ */
456
+ hasPlugin(name) {
457
+ return this.plugins.some((p) => p.plugin.name === name);
458
+ }
459
+ /**
460
+ * Create base context for plugins
461
+ */
462
+ createContext(pluginConfig) {
463
+ const engineInfo = {
464
+ version: ENGINE_VERSION,
465
+ pluginApiVersion: PLUGIN_API_VERSION
466
+ };
467
+ return {
468
+ config: pluginConfig,
469
+ engine: engineInfo,
470
+ payloads: this.sharedPayloads,
471
+ findings: this.sharedFindings,
472
+ logger: this.createLogger("plugin"),
473
+ fetch: globalThis.fetch
474
+ };
475
+ }
476
+ /**
477
+ * Create scoped logger for a plugin
478
+ */
479
+ createLogger(name) {
480
+ const prefix = `[${name}]`;
481
+ return {
482
+ debug: (msg, ...args) => console.debug(prefix, msg, ...args),
483
+ info: (msg, ...args) => console.info(prefix, msg, ...args),
484
+ warn: (msg, ...args) => console.warn(prefix, msg, ...args),
485
+ error: (msg, ...args) => console.error(prefix, msg, ...args)
486
+ };
487
+ }
488
+ /**
489
+ * Call a hook on all plugins sequentially
490
+ */
491
+ async callHook(hookName, executor) {
492
+ for (const loaded of this.plugins) {
493
+ const hook = loaded.plugin.hooks?.[hookName];
494
+ if (hook) {
495
+ const ctx = this.createContext(loaded.config);
496
+ ctx.logger = this.createLogger(loaded.plugin.name);
497
+ try {
498
+ await executor(hook, ctx);
499
+ } catch (err) {
500
+ console.error(
501
+ `Error in plugin ${loaded.plugin.name}.${hookName}:`,
502
+ err instanceof Error ? err.message : String(err)
503
+ );
504
+ }
505
+ }
506
+ }
507
+ }
508
+ /**
509
+ * Call a hook and collect results
510
+ */
511
+ async callHookCollect(hookName, executor) {
512
+ const results = [];
513
+ for (const loaded of this.plugins) {
514
+ const hook = loaded.plugin.hooks?.[hookName];
515
+ if (hook) {
516
+ const ctx = this.createContext(loaded.config);
517
+ ctx.logger = this.createLogger(loaded.plugin.name);
518
+ try {
519
+ const result = await executor(
520
+ hook,
521
+ ctx
522
+ );
523
+ if (result !== null && result !== void 0) {
524
+ if (Array.isArray(result)) {
525
+ results.push(...result);
526
+ } else {
527
+ results.push(result);
528
+ }
529
+ }
530
+ } catch (err) {
531
+ console.error(
532
+ `Error in plugin ${loaded.plugin.name}.${hookName}:`,
533
+ err instanceof Error ? err.message : String(err)
534
+ );
535
+ }
536
+ }
537
+ }
538
+ return results;
539
+ }
540
+ /**
541
+ * Call a hook that transforms a value through the pipeline
542
+ */
543
+ async callHookPipe(hookName, initial, executor) {
544
+ let value = initial;
545
+ for (const loaded of this.plugins) {
546
+ const hook = loaded.plugin.hooks?.[hookName];
547
+ if (hook) {
548
+ const ctx = this.createContext(loaded.config);
549
+ ctx.logger = this.createLogger(loaded.plugin.name);
550
+ try {
551
+ value = await executor(
552
+ hook,
553
+ value,
554
+ ctx
555
+ );
556
+ } catch (err) {
557
+ console.error(
558
+ `Error in plugin ${loaded.plugin.name}.${hookName}:`,
559
+ err instanceof Error ? err.message : String(err)
560
+ );
561
+ }
562
+ }
563
+ }
564
+ return value;
565
+ }
566
+ };
567
+ var pluginManager = new PluginManager();
568
+
231
569
  // recorder.ts
232
570
  var Recorder = class _Recorder {
233
571
  /**
234
572
  * Start a new recording session
235
573
  * Opens a browser window for the user to interact with
236
574
  */
237
- static async start(startUrl, options = {}) {
575
+ static async start(startUrl, options = {}, config = {}) {
576
+ const manager = config.pluginManager ?? pluginManager;
238
577
  const browserType = options.browser ?? "chromium";
239
578
  const viewport = options.viewport ?? { width: 1280, height: 720 };
240
579
  const headless = options.headless ?? false;
580
+ await manager.initialize();
241
581
  const { browser } = await launchBrowser({
242
582
  browser: browserType,
243
583
  headless
@@ -258,18 +598,61 @@ var Recorder = class _Recorder {
258
598
  stepCounter++;
259
599
  return `step_${String(stepCounter).padStart(3, "0")}`;
260
600
  };
261
- steps.push({
601
+ const baseRecordContext = {
602
+ startUrl,
603
+ browser: browserType,
604
+ page,
605
+ engine: { version: "0.2.0", pluginApiVersion: 1 },
606
+ payloads: manager.getPayloads(),
607
+ findings: manager.getFindings(),
608
+ logger: {
609
+ debug: console.debug.bind(console),
610
+ info: console.info.bind(console),
611
+ warn: console.warn.bind(console),
612
+ error: console.error.bind(console)
613
+ },
614
+ fetch: globalThis.fetch
615
+ };
616
+ await manager.callHook("onRecordStart", async (hook, ctx) => {
617
+ const recordCtx = { ...baseRecordContext, ...ctx };
618
+ await hook(recordCtx);
619
+ });
620
+ const initialStep = {
262
621
  id: generateStepId(),
263
622
  type: "navigate",
264
623
  url: startUrl,
265
624
  timestamp: 0
266
- });
267
- _Recorder.attachListeners(page, steps, startTime, generateStepId);
625
+ };
626
+ const transformedInitialStep = await _Recorder.transformStep(
627
+ initialStep,
628
+ manager,
629
+ baseRecordContext
630
+ );
631
+ if (transformedInitialStep) {
632
+ steps.push(transformedInitialStep);
633
+ }
634
+ _Recorder.attachListeners(
635
+ page,
636
+ steps,
637
+ startTime,
638
+ generateStepId,
639
+ manager,
640
+ baseRecordContext
641
+ );
268
642
  return {
269
643
  async stop() {
270
644
  session.steps = steps;
645
+ let finalSession = session;
646
+ for (const loaded of manager.getPlugins()) {
647
+ const hook = loaded.plugin.hooks?.onRecordEnd;
648
+ if (hook) {
649
+ const ctx = manager.createContext(loaded.config);
650
+ const recordCtx = { ...baseRecordContext, ...ctx };
651
+ finalSession = await hook(finalSession, recordCtx);
652
+ }
653
+ }
271
654
  await browser.close();
272
- return session;
655
+ return finalSession;
273
656
  },
274
657
  getSteps() {
275
658
  return [...steps];
@@ -279,8 +662,34 @@ var Recorder = class _Recorder {
279
662
  }
280
663
  };
281
664
  }
282
- static attachListeners(page, steps, startTime, generateStepId) {
665
+ /**
666
+ * Transform a step through plugin hooks
667
+ * Returns null if the step should be filtered out
668
+ */
669
+ static async transformStep(step, manager, baseContext) {
670
+ let transformedStep = step;
671
+ for (const loaded of manager.getPlugins()) {
672
+ const hook = loaded.plugin.hooks?.onRecordStep;
673
+ if (hook) {
674
+ const ctx = manager.createContext(loaded.config);
675
+ const recordCtx = { ...baseContext, ...ctx };
676
+ transformedStep = await hook(transformedStep, recordCtx);
677
+ }
678
+ }
679
+ return transformedStep;
680
+ }
681
+ static attachListeners(page, steps, startTime, generateStepId, manager, baseContext) {
283
682
  const getTimestamp = () => Date.now() - startTime;
683
+ const addStep = async (step) => {
684
+ const transformed = await _Recorder.transformStep(
685
+ step,
686
+ manager,
687
+ baseContext
688
+ );
689
+ if (transformed) {
690
+ steps.push(transformed);
691
+ }
692
+ };
284
693
  page.on("framenavigated", (frame) => {
285
694
  if (frame === page.mainFrame()) {
286
695
  const url = frame.url();
@@ -288,7 +697,7 @@ var Recorder = class _Recorder {
288
697
  if (steps.length > 0 && lastStep.type === "navigate" && lastStep.url === url) {
289
698
  return;
290
699
  }
291
- steps.push({
700
+ addStep({
292
701
  id: generateStepId(),
293
702
  type: "navigate",
294
703
  url,
@@ -298,12 +707,12 @@ var Recorder = class _Recorder {
298
707
  });
299
708
  page.exposeFunction(
300
709
  "__vulcn_record",
301
- (event) => {
710
+ async (event) => {
302
711
  const timestamp = getTimestamp();
303
712
  switch (event.type) {
304
713
  case "click": {
305
714
  const data = event.data;
306
- steps.push({
715
+ await addStep({
307
716
  id: generateStepId(),
308
717
  type: "click",
309
718
  selector: data.selector,
@@ -314,7 +723,7 @@ var Recorder = class _Recorder {
314
723
  }
315
724
  case "input": {
316
725
  const data = event.data;
317
- steps.push({
726
+ await addStep({
318
727
  id: generateStepId(),
319
728
  type: "input",
320
729
  selector: data.selector,
@@ -326,7 +735,7 @@ var Recorder = class _Recorder {
326
735
  }
327
736
  case "keypress": {
328
737
  const data = event.data;
329
- steps.push({
738
+ await addStep({
330
739
  id: generateStepId(),
331
740
  type: "keypress",
332
741
  key: data.key,
@@ -463,163 +872,156 @@ var Recorder = class _Recorder {
463
872
  }
464
873
  };
465
874
 
466
- // payloads.ts
467
- var BUILTIN_PAYLOADS = {
468
- "xss-basic": {
469
- name: "xss-basic",
470
- category: "xss",
471
- description: "Basic XSS payloads with script tags and event handlers",
472
- payloads: [
473
- '<script>alert("XSS")</script>',
474
- '<img src=x onerror=alert("XSS")>',
475
- '"><script>alert("XSS")</script>',
476
- "javascript:alert('XSS')",
477
- '<svg onload=alert("XSS")>'
478
- ],
479
- detectPatterns: [
480
- /<script[^>]*>alert\(/i,
481
- /onerror\s*=\s*alert\(/i,
482
- /onload\s*=\s*alert\(/i,
483
- /javascript:alert\(/i
484
- ]
485
- },
486
- "xss-event": {
487
- name: "xss-event",
488
- category: "xss",
489
- description: "XSS via event handlers",
490
- payloads: [
491
- '" onfocus="alert(1)" autofocus="',
492
- "' onmouseover='alert(1)'",
493
- '<body onload=alert("XSS")>',
494
- "<input onfocus=alert(1) autofocus>",
495
- "<marquee onstart=alert(1)>"
496
- ],
497
- detectPatterns: [
498
- /onfocus\s*=\s*["']?alert/i,
499
- /onmouseover\s*=\s*["']?alert/i,
500
- /onload\s*=\s*["']?alert/i,
501
- /onstart\s*=\s*["']?alert/i
502
- ]
503
- },
504
- "xss-svg": {
505
- name: "xss-svg",
506
- category: "xss",
507
- description: "XSS via SVG elements",
508
- payloads: [
509
- '<svg/onload=alert("XSS")>',
510
- "<svg><script>alert(1)</script></svg>",
511
- "<svg><animate onbegin=alert(1)>",
512
- "<svg><set onbegin=alert(1)>"
513
- ],
514
- detectPatterns: [
515
- /<svg[^>]*onload\s*=/i,
516
- /<svg[^>]*>.*<script>/i,
517
- /onbegin\s*=\s*alert/i
518
- ]
519
- },
520
- "sqli-basic": {
521
- name: "sqli-basic",
522
- category: "sqli",
523
- description: "Basic SQL injection payloads",
524
- payloads: [
525
- "' OR '1'='1",
526
- "' OR '1'='1' --",
527
- "1' OR '1'='1",
528
- "admin'--",
529
- "' UNION SELECT NULL--"
530
- ],
531
- detectPatterns: [
532
- /sql.*syntax/i,
533
- /mysql.*error/i,
534
- /ORA-\d{5}/i,
535
- /pg_query/i,
536
- /sqlite.*error/i,
537
- /unclosed.*quotation/i
538
- ]
539
- },
540
- "sqli-error": {
541
- name: "sqli-error",
542
- category: "sqli",
543
- description: "SQL injection payloads to trigger errors",
544
- payloads: ["'", "''", "`", '"', "')", `'"`, "1' AND '1'='2", "1 AND 1=2"],
545
- detectPatterns: [
546
- /sql.*syntax/i,
547
- /mysql.*error/i,
548
- /ORA-\d{5}/i,
549
- /postgresql.*error/i,
550
- /sqlite.*error/i,
551
- /quoted.*string.*properly.*terminated/i
552
- ]
553
- },
554
- "sqli-blind": {
555
- name: "sqli-blind",
556
- category: "sqli",
557
- description: "Blind SQL injection payloads",
558
- payloads: [
559
- "1' AND SLEEP(5)--",
560
- "1; WAITFOR DELAY '0:0:5'--",
561
- "1' AND (SELECT COUNT(*) FROM information_schema.tables)>0--"
562
- ],
563
- detectPatterns: [
564
- // Blind SQLi is detected by timing, not content
565
- ]
566
- }
567
- };
568
- function getPayload(name) {
569
- return BUILTIN_PAYLOADS[name];
570
- }
571
- function getPayloadNames() {
572
- return Object.keys(BUILTIN_PAYLOADS);
573
- }
574
- function getPayloadsByCategory(category) {
575
- return Object.values(BUILTIN_PAYLOADS).filter((p) => p.category === category);
576
- }
577
-
578
875
  // runner.ts
579
876
  var Runner = class _Runner {
580
877
  /**
581
- * Execute a session with security payloads
878
+ * Execute a session with security payloads from plugins
879
+ *
880
+ * @param session - The recorded session to replay
881
+ * @param options - Runner configuration
882
+ * @param config - Plugin manager configuration
582
883
  */
583
- static async execute(session, payloadNames, options = {}) {
884
+ static async execute(session, options = {}, config = {}) {
885
+ const manager = config.pluginManager ?? pluginManager;
584
886
  const browserType = options.browser ?? session.browser ?? "chromium";
585
887
  const headless = options.headless ?? true;
586
888
  const startTime = Date.now();
587
- const findings = [];
588
889
  const errors = [];
589
890
  let payloadsTested = 0;
891
+ await manager.initialize();
892
+ manager.clearFindings();
893
+ const payloads = manager.getPayloads();
894
+ if (payloads.length === 0) {
895
+ return {
896
+ findings: [],
897
+ stepsExecuted: session.steps.length,
898
+ payloadsTested: 0,
899
+ duration: Date.now() - startTime,
900
+ errors: [
901
+ "No payloads loaded. Add a payload plugin or configure payloads."
902
+ ]
903
+ };
904
+ }
590
905
  const { browser } = await launchBrowser({
591
906
  browser: browserType,
592
907
  headless
593
908
  });
594
909
  const context = await browser.newContext({ viewport: session.viewport });
595
910
  const page = await context.newPage();
911
+ const baseRunContext = {
912
+ session,
913
+ page,
914
+ browser: browserType,
915
+ headless,
916
+ engine: { version: "0.2.0", pluginApiVersion: 1 },
917
+ payloads: manager.getPayloads(),
918
+ findings: manager.getFindings(),
919
+ logger: {
920
+ debug: console.debug.bind(console),
921
+ info: console.info.bind(console),
922
+ warn: console.warn.bind(console),
923
+ error: console.error.bind(console)
924
+ },
925
+ fetch: globalThis.fetch
926
+ };
927
+ await manager.callHook("onRunStart", async (hook, ctx) => {
928
+ const runCtx = { ...baseRunContext, ...ctx };
929
+ await hook(runCtx);
930
+ });
931
+ const eventFindings = [];
932
+ let currentDetectContext = null;
933
+ const dialogHandler = async (dialog) => {
934
+ if (currentDetectContext) {
935
+ const findings = await manager.callHookCollect(
936
+ "onDialog",
937
+ async (hook, ctx) => {
938
+ const detectCtx = {
939
+ ...currentDetectContext,
940
+ ...ctx
941
+ };
942
+ return hook(dialog, detectCtx);
943
+ }
944
+ );
945
+ eventFindings.push(...findings);
946
+ }
947
+ try {
948
+ await dialog.dismiss();
949
+ } catch {
950
+ }
951
+ };
952
+ const consoleHandler = async (msg) => {
953
+ if (currentDetectContext) {
954
+ const findings = await manager.callHookCollect("onConsoleMessage", async (hook, ctx) => {
955
+ const detectCtx = { ...currentDetectContext, ...ctx };
956
+ return hook(msg, detectCtx);
957
+ });
958
+ eventFindings.push(...findings);
959
+ }
960
+ };
961
+ page.on("dialog", dialogHandler);
962
+ page.on("console", consoleHandler);
596
963
  try {
597
964
  const injectableSteps = session.steps.filter(
598
965
  (step) => step.type === "input" && step.injectable !== false
599
966
  );
600
967
  const allPayloads = [];
601
- for (const name of payloadNames) {
602
- const payload = BUILTIN_PAYLOADS[name];
603
- if (payload) {
604
- for (const value of payload.payloads) {
605
- allPayloads.push({ name, value });
606
- }
968
+ for (const payloadSet of payloads) {
969
+ for (const value of payloadSet.payloads) {
970
+ allPayloads.push({ payloadSet, value });
607
971
  }
608
972
  }
609
973
  for (const injectableStep of injectableSteps) {
610
- for (const payload of allPayloads) {
974
+ for (const { payloadSet, value: originalValue } of allPayloads) {
611
975
  try {
612
- const finding = await _Runner.replayWithPayload(
976
+ let transformedPayload = originalValue;
977
+ for (const loaded of manager.getPlugins()) {
978
+ const hook = loaded.plugin.hooks?.onBeforePayload;
979
+ if (hook) {
980
+ const ctx = manager.createContext(loaded.config);
981
+ const runCtx = { ...baseRunContext, ...ctx };
982
+ transformedPayload = await hook(
983
+ transformedPayload,
984
+ injectableStep,
985
+ runCtx
986
+ );
987
+ }
988
+ }
989
+ currentDetectContext = {
990
+ ...baseRunContext,
991
+ config: {},
992
+ step: injectableStep,
993
+ payloadSet,
994
+ payloadValue: transformedPayload,
995
+ stepId: injectableStep.id
996
+ };
997
+ await _Runner.replayWithPayload(
613
998
  page,
614
999
  session,
615
1000
  injectableStep,
616
- payload.name,
617
- payload.value
1001
+ transformedPayload
618
1002
  );
619
- if (finding) {
620
- findings.push(finding);
1003
+ const afterFindings = await manager.callHookCollect("onAfterPayload", async (hook, ctx) => {
1004
+ const detectCtx = {
1005
+ ...currentDetectContext,
1006
+ ...ctx
1007
+ };
1008
+ return hook(detectCtx);
1009
+ });
1010
+ const reflectionFinding = await _Runner.checkReflection(
1011
+ page,
1012
+ injectableStep,
1013
+ payloadSet,
1014
+ transformedPayload
1015
+ );
1016
+ const allFindings = [...afterFindings, ...eventFindings];
1017
+ if (reflectionFinding) {
1018
+ allFindings.push(reflectionFinding);
1019
+ }
1020
+ for (const finding of allFindings) {
1021
+ manager.addFinding(finding);
621
1022
  options.onFinding?.(finding);
622
1023
  }
1024
+ eventFindings.length = 0;
623
1025
  payloadsTested++;
624
1026
  } catch (err) {
625
1027
  errors.push(`${injectableStep.id}: ${String(err)}`);
@@ -627,73 +1029,94 @@ var Runner = class _Runner {
627
1029
  }
628
1030
  }
629
1031
  } finally {
1032
+ page.off("dialog", dialogHandler);
1033
+ page.off("console", consoleHandler);
1034
+ currentDetectContext = null;
630
1035
  await browser.close();
631
1036
  }
632
- return {
633
- findings,
1037
+ let result = {
1038
+ findings: manager.getFindings(),
634
1039
  stepsExecuted: session.steps.length,
635
1040
  payloadsTested,
636
1041
  duration: Date.now() - startTime,
637
1042
  errors
638
1043
  };
1044
+ for (const loaded of manager.getPlugins()) {
1045
+ const hook = loaded.plugin.hooks?.onRunEnd;
1046
+ if (hook) {
1047
+ const ctx = manager.createContext(loaded.config);
1048
+ const runCtx = { ...baseRunContext, ...ctx };
1049
+ result = await hook(result, runCtx);
1050
+ }
1051
+ }
1052
+ return result;
1053
+ }
1054
+ /**
1055
+ * Execute with explicit payloads (legacy API, for backwards compatibility)
1056
+ */
1057
+ static async executeWithPayloads(session, payloads, options = {}) {
1058
+ const manager = new PluginManager();
1059
+ manager.addPayloads(payloads);
1060
+ return _Runner.execute(session, options, { pluginManager: manager });
639
1061
  }
640
- static async replayWithPayload(page, session, targetStep, payloadName, payloadValue) {
641
- await page.goto(session.startUrl);
1062
+ /**
1063
+ * Replay session steps with payload injected at target step
1064
+ */
1065
+ static async replayWithPayload(page, session, targetStep, payloadValue) {
1066
+ await page.goto(session.startUrl, { waitUntil: "domcontentloaded" });
642
1067
  for (const step of session.steps) {
643
- if (step.type === "navigate") {
644
- await page.goto(step.url);
645
- } else if (step.type === "click") {
646
- await page.click(step.selector, { timeout: 5e3 });
647
- } else if (step.type === "input") {
648
- const value = step.id === targetStep.id ? payloadValue : step.value;
649
- await page.fill(step.selector, value, { timeout: 5e3 });
650
- } else if (step.type === "keypress") {
651
- const modifiers = step.modifiers ?? [];
652
- for (const mod of modifiers) {
653
- await page.keyboard.down(mod);
654
- }
655
- await page.keyboard.press(step.key);
656
- for (const mod of modifiers.reverse()) {
657
- await page.keyboard.up(mod);
1068
+ try {
1069
+ if (step.type === "navigate") {
1070
+ await page.goto(step.url, { waitUntil: "domcontentloaded" });
1071
+ } else if (step.type === "click") {
1072
+ await page.click(step.selector, { timeout: 5e3 });
1073
+ } else if (step.type === "input") {
1074
+ const value = step.id === targetStep.id ? payloadValue : step.value;
1075
+ await page.fill(step.selector, value, { timeout: 5e3 });
1076
+ } else if (step.type === "keypress") {
1077
+ const modifiers = step.modifiers ?? [];
1078
+ for (const mod of modifiers) {
1079
+ await page.keyboard.down(
1080
+ mod
1081
+ );
1082
+ }
1083
+ await page.keyboard.press(step.key);
1084
+ for (const mod of modifiers.reverse()) {
1085
+ await page.keyboard.up(mod);
1086
+ }
658
1087
  }
1088
+ } catch {
659
1089
  }
660
1090
  if (step.id === targetStep.id) {
661
- const finding = await _Runner.checkForVulnerability(
662
- page,
663
- targetStep,
664
- payloadName,
665
- payloadValue
666
- );
667
- if (finding) {
668
- return finding;
669
- }
1091
+ await page.waitForTimeout(100);
1092
+ break;
670
1093
  }
671
1094
  }
672
- return void 0;
673
1095
  }
674
- static async checkForVulnerability(page, step, payloadName, payloadValue) {
675
- const payload = BUILTIN_PAYLOADS[payloadName];
676
- if (!payload) return void 0;
1096
+ /**
1097
+ * Basic reflection check - fallback when no detection plugin is loaded
1098
+ */
1099
+ static async checkReflection(page, step, payloadSet, payloadValue) {
677
1100
  const content = await page.content();
678
- for (const pattern of payload.detectPatterns) {
1101
+ for (const pattern of payloadSet.detectPatterns) {
679
1102
  if (pattern.test(content)) {
680
1103
  return {
681
- type: payload.category,
682
- severity: payload.category === "xss" ? "high" : "critical",
683
- title: `${payload.category.toUpperCase()} vulnerability detected`,
684
- description: `Payload was reflected in page content`,
1104
+ type: payloadSet.category,
1105
+ severity: _Runner.getSeverity(payloadSet.category),
1106
+ title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
1107
+ description: `Payload pattern was reflected in page content`,
685
1108
  stepId: step.id,
686
1109
  payload: payloadValue,
687
1110
  url: page.url(),
688
- evidence: content.match(pattern)?.[0]
1111
+ evidence: content.match(pattern)?.[0]?.slice(0, 200)
689
1112
  };
690
1113
  }
691
1114
  }
692
1115
  if (content.includes(payloadValue)) {
693
1116
  return {
694
- type: payload.category,
1117
+ type: payloadSet.category,
695
1118
  severity: "medium",
696
- title: `Potential ${payload.category.toUpperCase()} - payload reflection`,
1119
+ title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
697
1120
  description: `Payload was reflected in page without encoding`,
698
1121
  stepId: step.id,
699
1122
  payload: payloadValue,
@@ -702,23 +1125,41 @@ var Runner = class _Runner {
702
1125
  }
703
1126
  return void 0;
704
1127
  }
1128
+ /**
1129
+ * Determine severity based on vulnerability category
1130
+ */
1131
+ static getSeverity(category) {
1132
+ switch (category) {
1133
+ case "sqli":
1134
+ case "command-injection":
1135
+ case "xxe":
1136
+ return "critical";
1137
+ case "xss":
1138
+ case "ssrf":
1139
+ case "path-traversal":
1140
+ return "high";
1141
+ case "open-redirect":
1142
+ return "medium";
1143
+ default:
1144
+ return "medium";
1145
+ }
1146
+ }
705
1147
  };
706
1148
  // Annotate the CommonJS export names for ESM import in node:
707
1149
  0 && (module.exports = {
708
- BUILTIN_PAYLOADS,
709
1150
  BrowserNotFoundError,
1151
+ PLUGIN_API_VERSION,
1152
+ PluginManager,
710
1153
  Recorder,
711
1154
  Runner,
712
1155
  SessionSchema,
713
1156
  StepSchema,
714
1157
  checkBrowsers,
715
1158
  createSession,
716
- getPayload,
717
- getPayloadNames,
718
- getPayloadsByCategory,
719
1159
  installBrowsers,
720
1160
  launchBrowser,
721
1161
  parseSession,
1162
+ pluginManager,
722
1163
  serializeSession
723
1164
  });
724
1165
  //# sourceMappingURL=index.cjs.map