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