@vulcn/engine 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,28 +30,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- BrowserNotFoundError: () => BrowserNotFoundError,
34
33
  DRIVER_API_VERSION: () => DRIVER_API_VERSION,
35
34
  DriverManager: () => DriverManager,
36
35
  PLUGIN_API_VERSION: () => PLUGIN_API_VERSION,
37
36
  PluginManager: () => PluginManager,
38
- Recorder: () => Recorder,
39
- Runner: () => Runner,
40
- SessionSchema: () => SessionSchema,
41
- StepSchema: () => StepSchema,
42
- checkBrowsers: () => checkBrowsers,
43
- createSession: () => createSession,
44
37
  driverManager: () => driverManager,
45
- installBrowsers: () => installBrowsers,
46
- launchBrowser: () => launchBrowser,
47
- parseSession: () => parseSession,
48
- pluginManager: () => pluginManager,
49
- serializeSession: () => serializeSession
38
+ pluginManager: () => pluginManager
50
39
  });
51
40
  module.exports = __toCommonJS(index_exports);
52
41
 
53
42
  // src/driver-manager.ts
54
43
  var import_node_path = require("path");
44
+ var import_yaml = require("yaml");
55
45
  var DriverManager = class {
56
46
  drivers = /* @__PURE__ */ new Map();
57
47
  defaultDriver = null;
@@ -130,6 +120,48 @@ var DriverManager = class {
130
120
  }
131
121
  return driver;
132
122
  }
123
+ /**
124
+ * Parse a YAML session string into a Session object.
125
+ *
126
+ * Handles both new driver-format sessions and legacy v1 sessions.
127
+ * Legacy sessions (those with non-namespaced step types like "click",
128
+ * "input", "navigate") are automatically converted to the driver format
129
+ * (e.g., "browser.click", "browser.input", "browser.navigate").
130
+ *
131
+ * @param yaml - Raw YAML string
132
+ * @param defaultDriver - Driver to assign for legacy sessions (default: "browser")
133
+ */
134
+ parseSession(yaml, defaultDriver = "browser") {
135
+ const data = (0, import_yaml.parse)(yaml);
136
+ if (data.driver && typeof data.driver === "string") {
137
+ return data;
138
+ }
139
+ const steps = data.steps ?? [];
140
+ const convertedSteps = steps.map((step) => {
141
+ const type = step.type;
142
+ if (type.includes(".")) {
143
+ return step;
144
+ }
145
+ return {
146
+ ...step,
147
+ type: `${defaultDriver}.${type}`
148
+ };
149
+ });
150
+ return {
151
+ name: data.name ?? "Untitled Session",
152
+ driver: defaultDriver,
153
+ driverConfig: {
154
+ browser: data.browser ?? "chromium",
155
+ viewport: data.viewport ?? { width: 1280, height: 720 },
156
+ startUrl: data.startUrl
157
+ },
158
+ steps: convertedSteps,
159
+ metadata: {
160
+ recordedAt: data.recordedAt,
161
+ version: data.version ?? "1"
162
+ }
163
+ };
164
+ }
133
165
  /**
134
166
  * Start recording with a driver
135
167
  */
@@ -142,6 +174,7 @@ var DriverManager = class {
142
174
  }
143
175
  /**
144
176
  * Execute a session
177
+ * Invokes plugin hooks (onRunStart, onRunEnd) around the driver runner.
145
178
  */
146
179
  async execute(session, pluginManager2, options = {}) {
147
180
  const driver = this.getForSession(session);
@@ -160,7 +193,44 @@ var DriverManager = class {
160
193
  logger,
161
194
  options
162
195
  };
163
- return driver.runner.execute(session, ctx);
196
+ const pluginCtx = {
197
+ session,
198
+ page: null,
199
+ headless: !!options.headless,
200
+ config: {},
201
+ engine: { version: "0.3.0", pluginApiVersion: 1 },
202
+ payloads: pluginManager2.getPayloads(),
203
+ findings,
204
+ logger,
205
+ fetch: globalThis.fetch
206
+ };
207
+ for (const loaded of pluginManager2.getPlugins()) {
208
+ if (loaded.enabled && loaded.plugin.hooks?.onRunStart) {
209
+ try {
210
+ await loaded.plugin.hooks.onRunStart({
211
+ ...pluginCtx,
212
+ config: loaded.config
213
+ });
214
+ } catch (err) {
215
+ logger.warn(`Plugin ${loaded.plugin.name} onRunStart failed: ${err}`);
216
+ }
217
+ }
218
+ }
219
+ let result = await driver.runner.execute(session, ctx);
220
+ for (const loaded of pluginManager2.getPlugins()) {
221
+ if (loaded.enabled && loaded.plugin.hooks?.onRunEnd) {
222
+ try {
223
+ result = await loaded.plugin.hooks.onRunEnd(result, {
224
+ ...pluginCtx,
225
+ config: loaded.config,
226
+ findings: result.findings
227
+ });
228
+ } catch (err) {
229
+ logger.warn(`Plugin ${loaded.plugin.name} onRunEnd failed: ${err}`);
230
+ }
231
+ }
232
+ }
233
+ return result;
164
234
  }
165
235
  /**
166
236
  * Validate driver structure
@@ -208,7 +278,7 @@ var DRIVER_API_VERSION = 1;
208
278
  var import_promises = require("fs/promises");
209
279
  var import_node_fs = require("fs");
210
280
  var import_node_path2 = require("path");
211
- var import_yaml = __toESM(require("yaml"), 1);
281
+ var import_yaml2 = __toESM(require("yaml"), 1);
212
282
  var import_zod = require("zod");
213
283
 
214
284
  // src/plugin-types.ts
@@ -256,7 +326,7 @@ var PluginManager = class {
256
326
  const resolved = (0, import_node_path2.isAbsolute)(path) ? path : (0, import_node_path2.resolve)(process.cwd(), path);
257
327
  if ((0, import_node_fs.existsSync)(resolved)) {
258
328
  const content = await (0, import_promises.readFile)(resolved, "utf-8");
259
- const parsed = path.endsWith(".json") ? JSON.parse(content) : import_yaml.default.parse(content);
329
+ const parsed = path.endsWith(".json") ? JSON.parse(content) : import_yaml2.default.parse(content);
260
330
  this.config = VulcnConfigSchema.parse(parsed);
261
331
  return this.config;
262
332
  }
@@ -532,794 +602,13 @@ var PluginManager = class {
532
602
  }
533
603
  };
534
604
  var pluginManager = new PluginManager();
535
-
536
- // src/session.ts
537
- var import_zod2 = require("zod");
538
- var import_yaml2 = require("yaml");
539
- var StepSchema = import_zod2.z.discriminatedUnion("type", [
540
- import_zod2.z.object({
541
- id: import_zod2.z.string(),
542
- type: import_zod2.z.literal("navigate"),
543
- url: import_zod2.z.string(),
544
- timestamp: import_zod2.z.number()
545
- }),
546
- import_zod2.z.object({
547
- id: import_zod2.z.string(),
548
- type: import_zod2.z.literal("click"),
549
- selector: import_zod2.z.string(),
550
- position: import_zod2.z.object({ x: import_zod2.z.number(), y: import_zod2.z.number() }).optional(),
551
- timestamp: import_zod2.z.number()
552
- }),
553
- import_zod2.z.object({
554
- id: import_zod2.z.string(),
555
- type: import_zod2.z.literal("input"),
556
- selector: import_zod2.z.string(),
557
- value: import_zod2.z.string(),
558
- injectable: import_zod2.z.boolean().optional().default(true),
559
- timestamp: import_zod2.z.number()
560
- }),
561
- import_zod2.z.object({
562
- id: import_zod2.z.string(),
563
- type: import_zod2.z.literal("keypress"),
564
- key: import_zod2.z.string(),
565
- modifiers: import_zod2.z.array(import_zod2.z.string()).optional(),
566
- timestamp: import_zod2.z.number()
567
- }),
568
- import_zod2.z.object({
569
- id: import_zod2.z.string(),
570
- type: import_zod2.z.literal("scroll"),
571
- selector: import_zod2.z.string().optional(),
572
- position: import_zod2.z.object({ x: import_zod2.z.number(), y: import_zod2.z.number() }),
573
- timestamp: import_zod2.z.number()
574
- }),
575
- import_zod2.z.object({
576
- id: import_zod2.z.string(),
577
- type: import_zod2.z.literal("wait"),
578
- duration: import_zod2.z.number(),
579
- timestamp: import_zod2.z.number()
580
- })
581
- ]);
582
- var SessionSchema = import_zod2.z.object({
583
- version: import_zod2.z.string().default("1"),
584
- name: import_zod2.z.string(),
585
- recordedAt: import_zod2.z.string(),
586
- browser: import_zod2.z.enum(["chromium", "firefox", "webkit"]).default("chromium"),
587
- viewport: import_zod2.z.object({
588
- width: import_zod2.z.number(),
589
- height: import_zod2.z.number()
590
- }),
591
- startUrl: import_zod2.z.string(),
592
- steps: import_zod2.z.array(StepSchema)
593
- });
594
- function createSession(options) {
595
- return {
596
- version: "1",
597
- name: options.name,
598
- recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
599
- browser: options.browser ?? "chromium",
600
- viewport: options.viewport ?? { width: 1280, height: 720 },
601
- startUrl: options.startUrl,
602
- steps: []
603
- };
604
- }
605
- function parseSession(yaml) {
606
- const data = (0, import_yaml2.parse)(yaml);
607
- return SessionSchema.parse(data);
608
- }
609
- function serializeSession(session) {
610
- return (0, import_yaml2.stringify)(session, { lineWidth: 0 });
611
- }
612
-
613
- // src/browser.ts
614
- var import_playwright = require("playwright");
615
- var import_node_child_process = require("child_process");
616
- var import_node_util = require("util");
617
- var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
618
- var BrowserNotFoundError = class extends Error {
619
- constructor(message) {
620
- super(message);
621
- this.name = "BrowserNotFoundError";
622
- }
623
- };
624
- async function launchBrowser(options = {}) {
625
- const browserType = options.browser ?? "chromium";
626
- const headless = options.headless ?? false;
627
- if (browserType === "chromium") {
628
- try {
629
- const browser = await import_playwright.chromium.launch({
630
- channel: "chrome",
631
- headless
632
- });
633
- return { browser, channel: "chrome" };
634
- } catch {
635
- }
636
- try {
637
- const browser = await import_playwright.chromium.launch({
638
- channel: "msedge",
639
- headless
640
- });
641
- return { browser, channel: "msedge" };
642
- } catch {
643
- }
644
- try {
645
- const browser = await import_playwright.chromium.launch({ headless });
646
- return { browser, channel: "chromium" };
647
- } catch {
648
- throw new BrowserNotFoundError(
649
- "No Chromium browser found. Install Chrome or run: vulcn install chromium"
650
- );
651
- }
652
- }
653
- if (browserType === "firefox") {
654
- try {
655
- const browser = await import_playwright.firefox.launch({ headless });
656
- return { browser, channel: "firefox" };
657
- } catch {
658
- throw new BrowserNotFoundError(
659
- "Firefox not found. Run: vulcn install firefox"
660
- );
661
- }
662
- }
663
- if (browserType === "webkit") {
664
- try {
665
- const browser = await import_playwright.webkit.launch({ headless });
666
- return { browser, channel: "webkit" };
667
- } catch {
668
- throw new BrowserNotFoundError(
669
- "WebKit not found. Run: vulcn install webkit"
670
- );
671
- }
672
- }
673
- throw new BrowserNotFoundError(`Unknown browser type: ${browserType}`);
674
- }
675
- async function installBrowsers(browsers = ["chromium"]) {
676
- const browserArg = browsers.join(" ");
677
- await execAsync(`npx playwright install ${browserArg}`);
678
- }
679
- async function checkBrowsers() {
680
- const results = {
681
- systemChrome: false,
682
- systemEdge: false,
683
- playwrightChromium: false,
684
- playwrightFirefox: false,
685
- playwrightWebkit: false
686
- };
687
- try {
688
- const browser = await import_playwright.chromium.launch({
689
- channel: "chrome",
690
- headless: true
691
- });
692
- await browser.close();
693
- results.systemChrome = true;
694
- } catch {
695
- }
696
- try {
697
- const browser = await import_playwright.chromium.launch({
698
- channel: "msedge",
699
- headless: true
700
- });
701
- await browser.close();
702
- results.systemEdge = true;
703
- } catch {
704
- }
705
- try {
706
- const browser = await import_playwright.chromium.launch({ headless: true });
707
- await browser.close();
708
- results.playwrightChromium = true;
709
- } catch {
710
- }
711
- try {
712
- const browser = await import_playwright.firefox.launch({ headless: true });
713
- await browser.close();
714
- results.playwrightFirefox = true;
715
- } catch {
716
- }
717
- try {
718
- const browser = await import_playwright.webkit.launch({ headless: true });
719
- await browser.close();
720
- results.playwrightWebkit = true;
721
- } catch {
722
- }
723
- return results;
724
- }
725
-
726
- // src/recorder.ts
727
- var Recorder = class _Recorder {
728
- /**
729
- * Start a new recording session
730
- * Opens a browser window for the user to interact with
731
- */
732
- static async start(startUrl, options = {}, config = {}) {
733
- const manager = config.pluginManager ?? pluginManager;
734
- const browserType = options.browser ?? "chromium";
735
- const viewport = options.viewport ?? { width: 1280, height: 720 };
736
- const headless = options.headless ?? false;
737
- await manager.initialize();
738
- const { browser } = await launchBrowser({
739
- browser: browserType,
740
- headless
741
- });
742
- const context = await browser.newContext({ viewport });
743
- const page = await context.newPage();
744
- await page.goto(startUrl);
745
- const session = createSession({
746
- name: `Recording ${(/* @__PURE__ */ new Date()).toISOString()}`,
747
- startUrl,
748
- browser: browserType,
749
- viewport
750
- });
751
- const startTime = Date.now();
752
- const steps = [];
753
- let stepCounter = 0;
754
- const generateStepId = () => {
755
- stepCounter++;
756
- return `step_${String(stepCounter).padStart(3, "0")}`;
757
- };
758
- const baseRecordContext = {
759
- startUrl,
760
- browser: browserType,
761
- page,
762
- engine: { version: "0.2.0", pluginApiVersion: 1 },
763
- payloads: manager.getPayloads(),
764
- findings: manager.getFindings(),
765
- logger: {
766
- debug: console.debug.bind(console),
767
- info: console.info.bind(console),
768
- warn: console.warn.bind(console),
769
- error: console.error.bind(console)
770
- },
771
- fetch: globalThis.fetch
772
- };
773
- await manager.callHook("onRecordStart", async (hook, ctx) => {
774
- const recordCtx = { ...baseRecordContext, ...ctx };
775
- await hook(recordCtx);
776
- });
777
- const initialStep = {
778
- id: generateStepId(),
779
- type: "navigate",
780
- url: startUrl,
781
- timestamp: 0
782
- };
783
- const transformedInitialStep = await _Recorder.transformStep(
784
- initialStep,
785
- manager,
786
- baseRecordContext
787
- );
788
- if (transformedInitialStep) {
789
- steps.push(transformedInitialStep);
790
- }
791
- _Recorder.attachListeners(
792
- page,
793
- steps,
794
- startTime,
795
- generateStepId,
796
- manager,
797
- baseRecordContext
798
- );
799
- return {
800
- async stop() {
801
- session.steps = steps;
802
- let finalSession = session;
803
- for (const loaded of manager.getPlugins()) {
804
- const hook = loaded.plugin.hooks?.onRecordEnd;
805
- if (hook) {
806
- const ctx = manager.createContext(loaded.config);
807
- const recordCtx = { ...baseRecordContext, ...ctx };
808
- finalSession = await hook(finalSession, recordCtx);
809
- }
810
- }
811
- await browser.close();
812
- return finalSession;
813
- },
814
- getSteps() {
815
- return [...steps];
816
- },
817
- getPage() {
818
- return page;
819
- }
820
- };
821
- }
822
- /**
823
- * Transform a step through plugin hooks
824
- * Returns null if the step should be filtered out
825
- */
826
- static async transformStep(step, manager, baseContext) {
827
- let transformedStep = step;
828
- for (const loaded of manager.getPlugins()) {
829
- const hook = loaded.plugin.hooks?.onRecordStep;
830
- if (hook) {
831
- const ctx = manager.createContext(loaded.config);
832
- const recordCtx = { ...baseContext, ...ctx };
833
- transformedStep = await hook(transformedStep, recordCtx);
834
- }
835
- }
836
- return transformedStep;
837
- }
838
- static attachListeners(page, steps, startTime, generateStepId, manager, baseContext) {
839
- const getTimestamp = () => Date.now() - startTime;
840
- const addStep = async (step) => {
841
- const transformed = await _Recorder.transformStep(
842
- step,
843
- manager,
844
- baseContext
845
- );
846
- if (transformed) {
847
- steps.push(transformed);
848
- }
849
- };
850
- page.on("framenavigated", (frame) => {
851
- if (frame === page.mainFrame()) {
852
- const url = frame.url();
853
- const lastStep = steps[steps.length - 1];
854
- if (steps.length > 0 && lastStep.type === "navigate" && lastStep.url === url) {
855
- return;
856
- }
857
- addStep({
858
- id: generateStepId(),
859
- type: "navigate",
860
- url,
861
- timestamp: getTimestamp()
862
- });
863
- }
864
- });
865
- page.exposeFunction(
866
- "__vulcn_record",
867
- async (event) => {
868
- const timestamp = getTimestamp();
869
- switch (event.type) {
870
- case "click": {
871
- const data = event.data;
872
- await addStep({
873
- id: generateStepId(),
874
- type: "click",
875
- selector: data.selector,
876
- position: { x: data.x, y: data.y },
877
- timestamp
878
- });
879
- break;
880
- }
881
- case "input": {
882
- const data = event.data;
883
- await addStep({
884
- id: generateStepId(),
885
- type: "input",
886
- selector: data.selector,
887
- value: data.value,
888
- injectable: data.injectable,
889
- timestamp
890
- });
891
- break;
892
- }
893
- case "keypress": {
894
- const data = event.data;
895
- await addStep({
896
- id: generateStepId(),
897
- type: "keypress",
898
- key: data.key,
899
- modifiers: data.modifiers,
900
- timestamp
901
- });
902
- break;
903
- }
904
- }
905
- }
906
- );
907
- page.on("load", async () => {
908
- await _Recorder.injectRecordingScript(page);
909
- });
910
- _Recorder.injectRecordingScript(page);
911
- }
912
- static async injectRecordingScript(page) {
913
- await page.evaluate(`
914
- (function() {
915
- if (window.__vulcn_injected) return;
916
- window.__vulcn_injected = true;
917
-
918
- var textInputTypes = ['text', 'password', 'email', 'search', 'url', 'tel', 'number'];
919
-
920
- function getSelector(el) {
921
- if (el.id) {
922
- return '#' + CSS.escape(el.id);
923
- }
924
- if (el.name) {
925
- var tag = el.tagName.toLowerCase();
926
- var nameSelector = tag + '[name="' + el.name + '"]';
927
- if (document.querySelectorAll(nameSelector).length === 1) {
928
- return nameSelector;
929
- }
930
- }
931
- if (el.dataset && el.dataset.testid) {
932
- return '[data-testid="' + el.dataset.testid + '"]';
933
- }
934
- if (el.tagName === 'INPUT' && el.type && el.name) {
935
- var inputSelector = 'input[type="' + el.type + '"][name="' + el.name + '"]';
936
- if (document.querySelectorAll(inputSelector).length === 1) {
937
- return inputSelector;
938
- }
939
- }
940
- if (el.className && typeof el.className === 'string') {
941
- var classes = el.className.trim().split(/\\s+/).filter(function(c) { return c.length > 0; });
942
- if (classes.length > 0) {
943
- var classSelector = el.tagName.toLowerCase() + '.' + classes.map(function(c) { return CSS.escape(c); }).join('.');
944
- if (document.querySelectorAll(classSelector).length === 1) {
945
- return classSelector;
946
- }
947
- }
948
- }
949
- var path = [];
950
- var current = el;
951
- while (current && current !== document.body) {
952
- var tag = current.tagName.toLowerCase();
953
- var parent = current.parentElement;
954
- if (parent) {
955
- var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName; });
956
- if (siblings.length > 1) {
957
- var index = siblings.indexOf(current) + 1;
958
- tag = tag + ':nth-of-type(' + index + ')';
959
- }
960
- }
961
- path.unshift(tag);
962
- current = parent;
963
- }
964
- return path.join(' > ');
965
- }
966
-
967
- function getInputType(el) {
968
- if (el.tagName === 'INPUT') return el.type || 'text';
969
- if (el.tagName === 'TEXTAREA') return 'textarea';
970
- if (el.tagName === 'SELECT') return 'select';
971
- return null;
972
- }
973
-
974
- function isTextInjectable(el) {
975
- var inputType = getInputType(el);
976
- if (!inputType) return false;
977
- if (inputType === 'textarea') return true;
978
- if (inputType === 'select') return false;
979
- return textInputTypes.indexOf(inputType) !== -1;
980
- }
981
-
982
- document.addEventListener('click', function(e) {
983
- var target = e.target;
984
- window.__vulcn_record({
985
- type: 'click',
986
- data: {
987
- selector: getSelector(target),
988
- x: e.clientX,
989
- y: e.clientY
990
- }
991
- });
992
- }, true);
993
-
994
- document.addEventListener('change', function(e) {
995
- var target = e.target;
996
- if ('value' in target) {
997
- var inputType = getInputType(target);
998
- window.__vulcn_record({
999
- type: 'input',
1000
- data: {
1001
- selector: getSelector(target),
1002
- value: target.value,
1003
- inputType: inputType,
1004
- injectable: isTextInjectable(target)
1005
- }
1006
- });
1007
- }
1008
- }, true);
1009
-
1010
- document.addEventListener('keydown', function(e) {
1011
- if (e.ctrlKey || e.metaKey || e.altKey) {
1012
- var modifiers = [];
1013
- if (e.ctrlKey) modifiers.push('ctrl');
1014
- if (e.metaKey) modifiers.push('meta');
1015
- if (e.altKey) modifiers.push('alt');
1016
- if (e.shiftKey) modifiers.push('shift');
1017
-
1018
- window.__vulcn_record({
1019
- type: 'keypress',
1020
- data: {
1021
- key: e.key,
1022
- modifiers: modifiers
1023
- }
1024
- });
1025
- }
1026
- }, true);
1027
- })();
1028
- `);
1029
- }
1030
- };
1031
-
1032
- // src/runner.ts
1033
- var Runner = class _Runner {
1034
- /**
1035
- * Execute a session with security payloads from plugins
1036
- *
1037
- * @param session - The recorded session to replay
1038
- * @param options - Runner configuration
1039
- * @param config - Plugin manager configuration
1040
- */
1041
- static async execute(session, options = {}, config = {}) {
1042
- const manager = config.pluginManager ?? pluginManager;
1043
- const browserType = options.browser ?? session.browser ?? "chromium";
1044
- const headless = options.headless ?? true;
1045
- const startTime = Date.now();
1046
- const errors = [];
1047
- let payloadsTested = 0;
1048
- await manager.initialize();
1049
- manager.clearFindings();
1050
- const payloads = manager.getPayloads();
1051
- if (payloads.length === 0) {
1052
- return {
1053
- findings: [],
1054
- stepsExecuted: session.steps.length,
1055
- payloadsTested: 0,
1056
- duration: Date.now() - startTime,
1057
- errors: [
1058
- "No payloads loaded. Add a payload plugin or configure payloads."
1059
- ]
1060
- };
1061
- }
1062
- const { browser } = await launchBrowser({
1063
- browser: browserType,
1064
- headless
1065
- });
1066
- const context = await browser.newContext({ viewport: session.viewport });
1067
- const page = await context.newPage();
1068
- const baseRunContext = {
1069
- session,
1070
- page,
1071
- browser: browserType,
1072
- headless,
1073
- engine: { version: "0.2.0", pluginApiVersion: 1 },
1074
- payloads: manager.getPayloads(),
1075
- findings: manager.getFindings(),
1076
- logger: {
1077
- debug: console.debug.bind(console),
1078
- info: console.info.bind(console),
1079
- warn: console.warn.bind(console),
1080
- error: console.error.bind(console)
1081
- },
1082
- fetch: globalThis.fetch
1083
- };
1084
- await manager.callHook("onRunStart", async (hook, ctx) => {
1085
- const runCtx = { ...baseRunContext, ...ctx };
1086
- await hook(runCtx);
1087
- });
1088
- const eventFindings = [];
1089
- let currentDetectContext = null;
1090
- const dialogHandler = async (dialog) => {
1091
- if (currentDetectContext) {
1092
- const findings = await manager.callHookCollect(
1093
- "onDialog",
1094
- async (hook, ctx) => {
1095
- const detectCtx = {
1096
- ...currentDetectContext,
1097
- ...ctx
1098
- };
1099
- return hook(dialog, detectCtx);
1100
- }
1101
- );
1102
- eventFindings.push(...findings);
1103
- }
1104
- try {
1105
- await dialog.dismiss();
1106
- } catch {
1107
- }
1108
- };
1109
- const consoleHandler = async (msg) => {
1110
- if (currentDetectContext) {
1111
- const findings = await manager.callHookCollect("onConsoleMessage", async (hook, ctx) => {
1112
- const detectCtx = { ...currentDetectContext, ...ctx };
1113
- return hook(msg, detectCtx);
1114
- });
1115
- eventFindings.push(...findings);
1116
- }
1117
- };
1118
- page.on("dialog", dialogHandler);
1119
- page.on("console", consoleHandler);
1120
- try {
1121
- const injectableSteps = session.steps.filter(
1122
- (step) => step.type === "input" && step.injectable !== false
1123
- );
1124
- const allPayloads = [];
1125
- for (const payloadSet of payloads) {
1126
- for (const value of payloadSet.payloads) {
1127
- allPayloads.push({ payloadSet, value });
1128
- }
1129
- }
1130
- for (const injectableStep of injectableSteps) {
1131
- for (const { payloadSet, value: originalValue } of allPayloads) {
1132
- try {
1133
- let transformedPayload = originalValue;
1134
- for (const loaded of manager.getPlugins()) {
1135
- const hook = loaded.plugin.hooks?.onBeforePayload;
1136
- if (hook) {
1137
- const ctx = manager.createContext(loaded.config);
1138
- const runCtx = { ...baseRunContext, ...ctx };
1139
- transformedPayload = await hook(
1140
- transformedPayload,
1141
- injectableStep,
1142
- runCtx
1143
- );
1144
- }
1145
- }
1146
- currentDetectContext = {
1147
- ...baseRunContext,
1148
- config: {},
1149
- step: injectableStep,
1150
- payloadSet,
1151
- payloadValue: transformedPayload,
1152
- stepId: injectableStep.id
1153
- };
1154
- await _Runner.replayWithPayload(
1155
- page,
1156
- session,
1157
- injectableStep,
1158
- transformedPayload
1159
- );
1160
- const afterFindings = await manager.callHookCollect("onAfterPayload", async (hook, ctx) => {
1161
- const detectCtx = {
1162
- ...currentDetectContext,
1163
- ...ctx
1164
- };
1165
- return hook(detectCtx);
1166
- });
1167
- const reflectionFinding = await _Runner.checkReflection(
1168
- page,
1169
- injectableStep,
1170
- payloadSet,
1171
- transformedPayload
1172
- );
1173
- const allFindings = [...afterFindings, ...eventFindings];
1174
- if (reflectionFinding) {
1175
- allFindings.push(reflectionFinding);
1176
- }
1177
- for (const finding of allFindings) {
1178
- manager.addFinding(finding);
1179
- options.onFinding?.(finding);
1180
- }
1181
- eventFindings.length = 0;
1182
- payloadsTested++;
1183
- } catch (err) {
1184
- errors.push(`${injectableStep.id}: ${String(err)}`);
1185
- }
1186
- }
1187
- }
1188
- } finally {
1189
- page.off("dialog", dialogHandler);
1190
- page.off("console", consoleHandler);
1191
- currentDetectContext = null;
1192
- await browser.close();
1193
- }
1194
- let result = {
1195
- findings: manager.getFindings(),
1196
- stepsExecuted: session.steps.length,
1197
- payloadsTested,
1198
- duration: Date.now() - startTime,
1199
- errors
1200
- };
1201
- for (const loaded of manager.getPlugins()) {
1202
- const hook = loaded.plugin.hooks?.onRunEnd;
1203
- if (hook) {
1204
- const ctx = manager.createContext(loaded.config);
1205
- const runCtx = { ...baseRunContext, ...ctx };
1206
- result = await hook(result, runCtx);
1207
- }
1208
- }
1209
- return result;
1210
- }
1211
- /**
1212
- * Execute with explicit payloads (legacy API, for backwards compatibility)
1213
- */
1214
- static async executeWithPayloads(session, payloads, options = {}) {
1215
- const manager = new PluginManager();
1216
- manager.addPayloads(payloads);
1217
- return _Runner.execute(session, options, { pluginManager: manager });
1218
- }
1219
- /**
1220
- * Replay session steps with payload injected at target step
1221
- */
1222
- static async replayWithPayload(page, session, targetStep, payloadValue) {
1223
- await page.goto(session.startUrl, { waitUntil: "domcontentloaded" });
1224
- for (const step of session.steps) {
1225
- try {
1226
- if (step.type === "navigate") {
1227
- await page.goto(step.url, { waitUntil: "domcontentloaded" });
1228
- } else if (step.type === "click") {
1229
- await page.click(step.selector, { timeout: 5e3 });
1230
- } else if (step.type === "input") {
1231
- const value = step.id === targetStep.id ? payloadValue : step.value;
1232
- await page.fill(step.selector, value, { timeout: 5e3 });
1233
- } else if (step.type === "keypress") {
1234
- const modifiers = step.modifiers ?? [];
1235
- for (const mod of modifiers) {
1236
- await page.keyboard.down(
1237
- mod
1238
- );
1239
- }
1240
- await page.keyboard.press(step.key);
1241
- for (const mod of modifiers.reverse()) {
1242
- await page.keyboard.up(mod);
1243
- }
1244
- }
1245
- } catch {
1246
- }
1247
- if (step.id === targetStep.id) {
1248
- await page.waitForTimeout(100);
1249
- break;
1250
- }
1251
- }
1252
- }
1253
- /**
1254
- * Basic reflection check - fallback when no detection plugin is loaded
1255
- */
1256
- static async checkReflection(page, step, payloadSet, payloadValue) {
1257
- const content = await page.content();
1258
- for (const pattern of payloadSet.detectPatterns) {
1259
- if (pattern.test(content)) {
1260
- return {
1261
- type: payloadSet.category,
1262
- severity: _Runner.getSeverity(payloadSet.category),
1263
- title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
1264
- description: `Payload pattern was reflected in page content`,
1265
- stepId: step.id,
1266
- payload: payloadValue,
1267
- url: page.url(),
1268
- evidence: content.match(pattern)?.[0]?.slice(0, 200)
1269
- };
1270
- }
1271
- }
1272
- if (content.includes(payloadValue)) {
1273
- return {
1274
- type: payloadSet.category,
1275
- severity: "medium",
1276
- title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
1277
- description: `Payload was reflected in page without encoding`,
1278
- stepId: step.id,
1279
- payload: payloadValue,
1280
- url: page.url()
1281
- };
1282
- }
1283
- return void 0;
1284
- }
1285
- /**
1286
- * Determine severity based on vulnerability category
1287
- */
1288
- static getSeverity(category) {
1289
- switch (category) {
1290
- case "sqli":
1291
- case "command-injection":
1292
- case "xxe":
1293
- return "critical";
1294
- case "xss":
1295
- case "ssrf":
1296
- case "path-traversal":
1297
- return "high";
1298
- case "open-redirect":
1299
- return "medium";
1300
- default:
1301
- return "medium";
1302
- }
1303
- }
1304
- };
1305
605
  // Annotate the CommonJS export names for ESM import in node:
1306
606
  0 && (module.exports = {
1307
- BrowserNotFoundError,
1308
607
  DRIVER_API_VERSION,
1309
608
  DriverManager,
1310
609
  PLUGIN_API_VERSION,
1311
610
  PluginManager,
1312
- Recorder,
1313
- Runner,
1314
- SessionSchema,
1315
- StepSchema,
1316
- checkBrowsers,
1317
- createSession,
1318
611
  driverManager,
1319
- installBrowsers,
1320
- launchBrowser,
1321
- parseSession,
1322
- pluginManager,
1323
- serializeSession
612
+ pluginManager
1324
613
  });
1325
614
  //# sourceMappingURL=index.cjs.map