@vulcn/driver-browser 0.1.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 +756 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +298 -0
- package/dist/index.d.ts +298 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BROWSER_STEP_TYPES: () => BROWSER_STEP_TYPES,
|
|
24
|
+
BrowserRecorder: () => BrowserRecorder,
|
|
25
|
+
BrowserRunner: () => BrowserRunner,
|
|
26
|
+
BrowserStepSchema: () => BrowserStepSchema,
|
|
27
|
+
checkBrowsers: () => checkBrowsers,
|
|
28
|
+
configSchema: () => configSchema,
|
|
29
|
+
default: () => index_default,
|
|
30
|
+
installBrowsers: () => installBrowsers,
|
|
31
|
+
launchBrowser: () => launchBrowser
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
var import_zod = require("zod");
|
|
35
|
+
|
|
36
|
+
// src/browser.ts
|
|
37
|
+
var import_playwright = require("playwright");
|
|
38
|
+
var import_node_child_process = require("child_process");
|
|
39
|
+
var import_node_util = require("util");
|
|
40
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
|
|
41
|
+
var BrowserNotFoundError = class extends Error {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "BrowserNotFoundError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
async function launchBrowser(options = {}) {
|
|
48
|
+
const browserType = options.browser ?? "chromium";
|
|
49
|
+
const headless = options.headless ?? false;
|
|
50
|
+
if (browserType === "chromium") {
|
|
51
|
+
try {
|
|
52
|
+
const browser = await import_playwright.chromium.launch({
|
|
53
|
+
channel: "chrome",
|
|
54
|
+
headless
|
|
55
|
+
});
|
|
56
|
+
return { browser, channel: "chrome" };
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const browser = await import_playwright.chromium.launch({
|
|
61
|
+
channel: "msedge",
|
|
62
|
+
headless
|
|
63
|
+
});
|
|
64
|
+
return { browser, channel: "msedge" };
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const browser = await import_playwright.chromium.launch({ headless });
|
|
69
|
+
return { browser, channel: "chromium" };
|
|
70
|
+
} catch {
|
|
71
|
+
throw new BrowserNotFoundError(
|
|
72
|
+
"No Chromium browser found. Install Chrome or run: vulcn install chromium"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (browserType === "firefox") {
|
|
77
|
+
try {
|
|
78
|
+
const browser = await import_playwright.firefox.launch({ headless });
|
|
79
|
+
return { browser, channel: "firefox" };
|
|
80
|
+
} catch {
|
|
81
|
+
throw new BrowserNotFoundError(
|
|
82
|
+
"Firefox not found. Run: vulcn install firefox"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (browserType === "webkit") {
|
|
87
|
+
try {
|
|
88
|
+
const browser = await import_playwright.webkit.launch({ headless });
|
|
89
|
+
return { browser, channel: "webkit" };
|
|
90
|
+
} catch {
|
|
91
|
+
throw new BrowserNotFoundError(
|
|
92
|
+
"WebKit not found. Run: vulcn install webkit"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new BrowserNotFoundError(`Unknown browser type: ${browserType}`);
|
|
97
|
+
}
|
|
98
|
+
async function installBrowsers(browsers = ["chromium"]) {
|
|
99
|
+
const browserArg = browsers.join(" ");
|
|
100
|
+
await execAsync(`npx playwright install ${browserArg}`);
|
|
101
|
+
}
|
|
102
|
+
async function checkBrowsers() {
|
|
103
|
+
const results = {
|
|
104
|
+
systemChrome: false,
|
|
105
|
+
systemEdge: false,
|
|
106
|
+
playwrightChromium: false,
|
|
107
|
+
playwrightFirefox: false,
|
|
108
|
+
playwrightWebkit: false
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
const browser = await import_playwright.chromium.launch({
|
|
112
|
+
channel: "chrome",
|
|
113
|
+
headless: true
|
|
114
|
+
});
|
|
115
|
+
await browser.close();
|
|
116
|
+
results.systemChrome = true;
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const browser = await import_playwright.chromium.launch({
|
|
121
|
+
channel: "msedge",
|
|
122
|
+
headless: true
|
|
123
|
+
});
|
|
124
|
+
await browser.close();
|
|
125
|
+
results.systemEdge = true;
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const browser = await import_playwright.chromium.launch({ headless: true });
|
|
130
|
+
await browser.close();
|
|
131
|
+
results.playwrightChromium = true;
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const browser = await import_playwright.firefox.launch({ headless: true });
|
|
136
|
+
await browser.close();
|
|
137
|
+
results.playwrightFirefox = true;
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const browser = await import_playwright.webkit.launch({ headless: true });
|
|
142
|
+
await browser.close();
|
|
143
|
+
results.playwrightWebkit = true;
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/recorder.ts
|
|
150
|
+
var BrowserRecorder = class _BrowserRecorder {
|
|
151
|
+
/**
|
|
152
|
+
* Start a new recording session
|
|
153
|
+
*/
|
|
154
|
+
static async start(config, _options = {}) {
|
|
155
|
+
const { startUrl, browser: browserType, viewport, headless } = config;
|
|
156
|
+
if (!startUrl) {
|
|
157
|
+
throw new Error("startUrl is required for browser recording");
|
|
158
|
+
}
|
|
159
|
+
const { browser } = await launchBrowser({
|
|
160
|
+
browser: browserType,
|
|
161
|
+
headless
|
|
162
|
+
});
|
|
163
|
+
const context = await browser.newContext({ viewport });
|
|
164
|
+
const page = await context.newPage();
|
|
165
|
+
await page.goto(startUrl);
|
|
166
|
+
const startTime = Date.now();
|
|
167
|
+
const steps = [];
|
|
168
|
+
let stepCounter = 0;
|
|
169
|
+
const generateStepId = () => {
|
|
170
|
+
stepCounter++;
|
|
171
|
+
return `step_${String(stepCounter).padStart(3, "0")}`;
|
|
172
|
+
};
|
|
173
|
+
steps.push({
|
|
174
|
+
id: generateStepId(),
|
|
175
|
+
type: "browser.navigate",
|
|
176
|
+
url: startUrl,
|
|
177
|
+
timestamp: 0
|
|
178
|
+
});
|
|
179
|
+
_BrowserRecorder.attachListeners(page, steps, startTime, generateStepId);
|
|
180
|
+
return {
|
|
181
|
+
async stop() {
|
|
182
|
+
const session = {
|
|
183
|
+
name: `Recording ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
184
|
+
driver: "browser",
|
|
185
|
+
driverConfig: {
|
|
186
|
+
browser: browserType,
|
|
187
|
+
viewport,
|
|
188
|
+
startUrl
|
|
189
|
+
},
|
|
190
|
+
steps,
|
|
191
|
+
metadata: {
|
|
192
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
+
version: "1"
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
await browser.close();
|
|
197
|
+
return session;
|
|
198
|
+
},
|
|
199
|
+
async abort() {
|
|
200
|
+
await browser.close();
|
|
201
|
+
},
|
|
202
|
+
getSteps() {
|
|
203
|
+
return [...steps];
|
|
204
|
+
},
|
|
205
|
+
addStep(step) {
|
|
206
|
+
steps.push({
|
|
207
|
+
...step,
|
|
208
|
+
id: generateStepId(),
|
|
209
|
+
timestamp: Date.now() - startTime
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Attach event listeners to the page
|
|
216
|
+
*/
|
|
217
|
+
static attachListeners(page, steps, startTime, generateStepId) {
|
|
218
|
+
const getTimestamp = () => Date.now() - startTime;
|
|
219
|
+
const addStep = (step) => {
|
|
220
|
+
steps.push({
|
|
221
|
+
...step,
|
|
222
|
+
id: generateStepId(),
|
|
223
|
+
timestamp: getTimestamp()
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
page.on("framenavigated", (frame) => {
|
|
227
|
+
if (frame === page.mainFrame()) {
|
|
228
|
+
const url = frame.url();
|
|
229
|
+
const lastStep = steps[steps.length - 1];
|
|
230
|
+
if (steps.length > 0 && lastStep?.type === "browser.navigate" && lastStep.url === url) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
addStep({
|
|
234
|
+
type: "browser.navigate",
|
|
235
|
+
url
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
page.exposeFunction(
|
|
240
|
+
"__vulcn_record",
|
|
241
|
+
async (event) => {
|
|
242
|
+
switch (event.type) {
|
|
243
|
+
case "click": {
|
|
244
|
+
const data = event.data;
|
|
245
|
+
addStep({
|
|
246
|
+
type: "browser.click",
|
|
247
|
+
selector: data.selector,
|
|
248
|
+
position: { x: data.x, y: data.y }
|
|
249
|
+
});
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case "input": {
|
|
253
|
+
const data = event.data;
|
|
254
|
+
addStep({
|
|
255
|
+
type: "browser.input",
|
|
256
|
+
selector: data.selector,
|
|
257
|
+
value: data.value,
|
|
258
|
+
injectable: data.injectable
|
|
259
|
+
});
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "keypress": {
|
|
263
|
+
const data = event.data;
|
|
264
|
+
addStep({
|
|
265
|
+
type: "browser.keypress",
|
|
266
|
+
key: data.key,
|
|
267
|
+
modifiers: data.modifiers
|
|
268
|
+
});
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
page.on("load", async () => {
|
|
275
|
+
await _BrowserRecorder.injectRecordingScript(page);
|
|
276
|
+
});
|
|
277
|
+
_BrowserRecorder.injectRecordingScript(page);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Inject the recording script into the page
|
|
281
|
+
*/
|
|
282
|
+
static async injectRecordingScript(page) {
|
|
283
|
+
await page.evaluate(`
|
|
284
|
+
(function() {
|
|
285
|
+
if (window.__vulcn_injected) return;
|
|
286
|
+
window.__vulcn_injected = true;
|
|
287
|
+
|
|
288
|
+
var textInputTypes = ['text', 'password', 'email', 'search', 'url', 'tel', 'number'];
|
|
289
|
+
|
|
290
|
+
function getSelector(el) {
|
|
291
|
+
if (el.id) {
|
|
292
|
+
return '#' + CSS.escape(el.id);
|
|
293
|
+
}
|
|
294
|
+
if (el.name) {
|
|
295
|
+
var tag = el.tagName.toLowerCase();
|
|
296
|
+
var nameSelector = tag + '[name="' + el.name + '"]';
|
|
297
|
+
if (document.querySelectorAll(nameSelector).length === 1) {
|
|
298
|
+
return nameSelector;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (el.dataset && el.dataset.testid) {
|
|
302
|
+
return '[data-testid="' + el.dataset.testid + '"]';
|
|
303
|
+
}
|
|
304
|
+
if (el.tagName === 'INPUT' && el.type && el.name) {
|
|
305
|
+
var inputSelector = 'input[type="' + el.type + '"][name="' + el.name + '"]';
|
|
306
|
+
if (document.querySelectorAll(inputSelector).length === 1) {
|
|
307
|
+
return inputSelector;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (el.className && typeof el.className === 'string') {
|
|
311
|
+
var classes = el.className.trim().split(/\\s+/).filter(function(c) { return c.length > 0; });
|
|
312
|
+
if (classes.length > 0) {
|
|
313
|
+
var classSelector = el.tagName.toLowerCase() + '.' + classes.map(function(c) { return CSS.escape(c); }).join('.');
|
|
314
|
+
if (document.querySelectorAll(classSelector).length === 1) {
|
|
315
|
+
return classSelector;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
var path = [];
|
|
320
|
+
var current = el;
|
|
321
|
+
while (current && current !== document.body) {
|
|
322
|
+
var tag = current.tagName.toLowerCase();
|
|
323
|
+
var parent = current.parentElement;
|
|
324
|
+
if (parent) {
|
|
325
|
+
var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName; });
|
|
326
|
+
if (siblings.length > 1) {
|
|
327
|
+
var index = siblings.indexOf(current) + 1;
|
|
328
|
+
tag = tag + ':nth-of-type(' + index + ')';
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
path.unshift(tag);
|
|
332
|
+
current = parent;
|
|
333
|
+
}
|
|
334
|
+
return path.join(' > ');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getInputType(el) {
|
|
338
|
+
if (el.tagName === 'INPUT') return el.type || 'text';
|
|
339
|
+
if (el.tagName === 'TEXTAREA') return 'textarea';
|
|
340
|
+
if (el.tagName === 'SELECT') return 'select';
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isTextInjectable(el) {
|
|
345
|
+
var inputType = getInputType(el);
|
|
346
|
+
if (!inputType) return false;
|
|
347
|
+
if (inputType === 'textarea') return true;
|
|
348
|
+
if (inputType === 'select') return false;
|
|
349
|
+
return textInputTypes.indexOf(inputType) !== -1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
document.addEventListener('click', function(e) {
|
|
353
|
+
var target = e.target;
|
|
354
|
+
window.__vulcn_record({
|
|
355
|
+
type: 'click',
|
|
356
|
+
data: {
|
|
357
|
+
selector: getSelector(target),
|
|
358
|
+
x: e.clientX,
|
|
359
|
+
y: e.clientY
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}, true);
|
|
363
|
+
|
|
364
|
+
document.addEventListener('change', function(e) {
|
|
365
|
+
var target = e.target;
|
|
366
|
+
if ('value' in target) {
|
|
367
|
+
var inputType = getInputType(target);
|
|
368
|
+
window.__vulcn_record({
|
|
369
|
+
type: 'input',
|
|
370
|
+
data: {
|
|
371
|
+
selector: getSelector(target),
|
|
372
|
+
value: target.value,
|
|
373
|
+
inputType: inputType,
|
|
374
|
+
injectable: isTextInjectable(target)
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}, true);
|
|
379
|
+
|
|
380
|
+
document.addEventListener('keydown', function(e) {
|
|
381
|
+
if (e.ctrlKey || e.metaKey || e.altKey) {
|
|
382
|
+
var modifiers = [];
|
|
383
|
+
if (e.ctrlKey) modifiers.push('ctrl');
|
|
384
|
+
if (e.metaKey) modifiers.push('meta');
|
|
385
|
+
if (e.altKey) modifiers.push('alt');
|
|
386
|
+
if (e.shiftKey) modifiers.push('shift');
|
|
387
|
+
|
|
388
|
+
window.__vulcn_record({
|
|
389
|
+
type: 'keypress',
|
|
390
|
+
data: {
|
|
391
|
+
key: e.key,
|
|
392
|
+
modifiers: modifiers
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}, true);
|
|
397
|
+
})();
|
|
398
|
+
`);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/runner.ts
|
|
403
|
+
var BrowserRunner = class _BrowserRunner {
|
|
404
|
+
/**
|
|
405
|
+
* Execute a session with security payloads
|
|
406
|
+
*/
|
|
407
|
+
static async execute(session, ctx) {
|
|
408
|
+
const config = session.driverConfig;
|
|
409
|
+
const browserType = config.browser ?? "chromium";
|
|
410
|
+
const viewport = config.viewport ?? {
|
|
411
|
+
width: 1280,
|
|
412
|
+
height: 720
|
|
413
|
+
};
|
|
414
|
+
const startUrl = config.startUrl;
|
|
415
|
+
const headless = ctx.options.headless ?? true;
|
|
416
|
+
const startTime = Date.now();
|
|
417
|
+
const errors = [];
|
|
418
|
+
let payloadsTested = 0;
|
|
419
|
+
const payloads = ctx.payloads;
|
|
420
|
+
if (payloads.length === 0) {
|
|
421
|
+
return {
|
|
422
|
+
findings: [],
|
|
423
|
+
stepsExecuted: session.steps.length,
|
|
424
|
+
payloadsTested: 0,
|
|
425
|
+
duration: Date.now() - startTime,
|
|
426
|
+
errors: [
|
|
427
|
+
"No payloads loaded. Add a payload plugin or configure payloads."
|
|
428
|
+
]
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const { browser } = await launchBrowser({
|
|
432
|
+
browser: browserType,
|
|
433
|
+
headless
|
|
434
|
+
});
|
|
435
|
+
const context = await browser.newContext({ viewport });
|
|
436
|
+
const page = await context.newPage();
|
|
437
|
+
const eventFindings = [];
|
|
438
|
+
let currentPayloadInfo = null;
|
|
439
|
+
const dialogHandler = async (dialog) => {
|
|
440
|
+
if (currentPayloadInfo) {
|
|
441
|
+
const message = dialog.message();
|
|
442
|
+
if (message.includes("vulcn") || message === currentPayloadInfo.payloadValue) {
|
|
443
|
+
eventFindings.push({
|
|
444
|
+
type: "xss",
|
|
445
|
+
severity: "high",
|
|
446
|
+
title: "XSS Confirmed - Dialog Triggered",
|
|
447
|
+
description: `JavaScript dialog was triggered by payload injection`,
|
|
448
|
+
stepId: currentPayloadInfo.stepId,
|
|
449
|
+
payload: currentPayloadInfo.payloadValue,
|
|
450
|
+
url: page.url(),
|
|
451
|
+
evidence: `Dialog message: ${message}`,
|
|
452
|
+
metadata: {
|
|
453
|
+
dialogType: dialog.type(),
|
|
454
|
+
detectionMethod: "dialog"
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
await dialog.dismiss();
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
const consoleHandler = async (msg) => {
|
|
465
|
+
if (currentPayloadInfo && msg.type() === "log") {
|
|
466
|
+
const text = msg.text();
|
|
467
|
+
if (text.includes("vulcn") || text.includes(currentPayloadInfo.payloadValue)) {
|
|
468
|
+
eventFindings.push({
|
|
469
|
+
type: "xss",
|
|
470
|
+
severity: "high",
|
|
471
|
+
title: "XSS Confirmed - Console Output",
|
|
472
|
+
description: `JavaScript console.log was triggered by payload injection`,
|
|
473
|
+
stepId: currentPayloadInfo.stepId,
|
|
474
|
+
payload: currentPayloadInfo.payloadValue,
|
|
475
|
+
url: page.url(),
|
|
476
|
+
evidence: `Console output: ${text}`,
|
|
477
|
+
metadata: {
|
|
478
|
+
consoleType: msg.type(),
|
|
479
|
+
detectionMethod: "console"
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
page.on("dialog", dialogHandler);
|
|
486
|
+
page.on("console", consoleHandler);
|
|
487
|
+
try {
|
|
488
|
+
const injectableSteps = session.steps.filter(
|
|
489
|
+
(step) => step.type === "browser.input" && step.injectable !== false
|
|
490
|
+
);
|
|
491
|
+
const allPayloads = [];
|
|
492
|
+
for (const payloadSet of payloads) {
|
|
493
|
+
for (const value of payloadSet.payloads) {
|
|
494
|
+
allPayloads.push({ payloadSet, value });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const injectableStep of injectableSteps) {
|
|
498
|
+
for (const { payloadSet, value } of allPayloads) {
|
|
499
|
+
try {
|
|
500
|
+
currentPayloadInfo = {
|
|
501
|
+
stepId: injectableStep.id,
|
|
502
|
+
payloadSet,
|
|
503
|
+
payloadValue: value
|
|
504
|
+
};
|
|
505
|
+
await _BrowserRunner.replayWithPayload(
|
|
506
|
+
page,
|
|
507
|
+
session,
|
|
508
|
+
injectableStep,
|
|
509
|
+
value,
|
|
510
|
+
startUrl
|
|
511
|
+
);
|
|
512
|
+
const reflectionFinding = await _BrowserRunner.checkReflection(
|
|
513
|
+
page,
|
|
514
|
+
injectableStep,
|
|
515
|
+
payloadSet,
|
|
516
|
+
value
|
|
517
|
+
);
|
|
518
|
+
const allFindings = [...eventFindings];
|
|
519
|
+
if (reflectionFinding) {
|
|
520
|
+
allFindings.push(reflectionFinding);
|
|
521
|
+
}
|
|
522
|
+
for (const finding of allFindings) {
|
|
523
|
+
ctx.addFinding(finding);
|
|
524
|
+
}
|
|
525
|
+
eventFindings.length = 0;
|
|
526
|
+
payloadsTested++;
|
|
527
|
+
ctx.options.onStepComplete?.(injectableStep.id, payloadsTested);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
errors.push(`${injectableStep.id}: ${String(err)}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} finally {
|
|
534
|
+
page.off("dialog", dialogHandler);
|
|
535
|
+
page.off("console", consoleHandler);
|
|
536
|
+
currentPayloadInfo = null;
|
|
537
|
+
await browser.close();
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
findings: ctx.findings,
|
|
541
|
+
stepsExecuted: session.steps.length,
|
|
542
|
+
payloadsTested,
|
|
543
|
+
duration: Date.now() - startTime,
|
|
544
|
+
errors
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Replay session steps with payload injected at target step
|
|
549
|
+
*/
|
|
550
|
+
static async replayWithPayload(page, session, targetStep, payloadValue, startUrl) {
|
|
551
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
552
|
+
for (const step of session.steps) {
|
|
553
|
+
const browserStep = step;
|
|
554
|
+
try {
|
|
555
|
+
switch (browserStep.type) {
|
|
556
|
+
case "browser.navigate":
|
|
557
|
+
await page.goto(browserStep.url, { waitUntil: "domcontentloaded" });
|
|
558
|
+
break;
|
|
559
|
+
case "browser.click":
|
|
560
|
+
await page.click(browserStep.selector, { timeout: 5e3 });
|
|
561
|
+
break;
|
|
562
|
+
case "browser.input": {
|
|
563
|
+
const value = step.id === targetStep.id ? payloadValue : browserStep.value;
|
|
564
|
+
await page.fill(browserStep.selector, value, { timeout: 5e3 });
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
case "browser.keypress": {
|
|
568
|
+
const modifiers = browserStep.modifiers ?? [];
|
|
569
|
+
for (const mod of modifiers) {
|
|
570
|
+
await page.keyboard.down(
|
|
571
|
+
mod
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
await page.keyboard.press(browserStep.key);
|
|
575
|
+
for (const mod of modifiers.reverse()) {
|
|
576
|
+
await page.keyboard.up(
|
|
577
|
+
mod
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
case "browser.scroll":
|
|
583
|
+
if (browserStep.selector) {
|
|
584
|
+
await page.locator(browserStep.selector).evaluate((el, pos) => {
|
|
585
|
+
el.scrollTo(pos.x, pos.y);
|
|
586
|
+
}, browserStep.position);
|
|
587
|
+
} else {
|
|
588
|
+
await page.evaluate((pos) => {
|
|
589
|
+
window.scrollTo(pos.x, pos.y);
|
|
590
|
+
}, browserStep.position);
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
case "browser.wait":
|
|
594
|
+
await page.waitForTimeout(browserStep.duration);
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
} catch {
|
|
598
|
+
}
|
|
599
|
+
if (step.id === targetStep.id) {
|
|
600
|
+
await page.waitForTimeout(100);
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Check for payload reflection in page content
|
|
607
|
+
*/
|
|
608
|
+
static async checkReflection(page, step, payloadSet, payloadValue) {
|
|
609
|
+
const content = await page.content();
|
|
610
|
+
for (const pattern of payloadSet.detectPatterns) {
|
|
611
|
+
if (pattern.test(content)) {
|
|
612
|
+
return {
|
|
613
|
+
type: payloadSet.category,
|
|
614
|
+
severity: _BrowserRunner.getSeverity(payloadSet.category),
|
|
615
|
+
title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
|
|
616
|
+
description: `Payload pattern was reflected in page content`,
|
|
617
|
+
stepId: step.id,
|
|
618
|
+
payload: payloadValue,
|
|
619
|
+
url: page.url(),
|
|
620
|
+
evidence: content.match(pattern)?.[0]?.slice(0, 200)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (content.includes(payloadValue)) {
|
|
625
|
+
return {
|
|
626
|
+
type: payloadSet.category,
|
|
627
|
+
severity: "medium",
|
|
628
|
+
title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
|
|
629
|
+
description: `Payload was reflected in page without encoding`,
|
|
630
|
+
stepId: step.id,
|
|
631
|
+
payload: payloadValue,
|
|
632
|
+
url: page.url()
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
return void 0;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Determine severity based on vulnerability category
|
|
639
|
+
*/
|
|
640
|
+
static getSeverity(category) {
|
|
641
|
+
switch (category) {
|
|
642
|
+
case "sqli":
|
|
643
|
+
case "command-injection":
|
|
644
|
+
case "xxe":
|
|
645
|
+
return "critical";
|
|
646
|
+
case "xss":
|
|
647
|
+
case "ssrf":
|
|
648
|
+
case "path-traversal":
|
|
649
|
+
return "high";
|
|
650
|
+
case "open-redirect":
|
|
651
|
+
return "medium";
|
|
652
|
+
default:
|
|
653
|
+
return "medium";
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// src/index.ts
|
|
659
|
+
var configSchema = import_zod.z.object({
|
|
660
|
+
/** Starting URL for recording */
|
|
661
|
+
startUrl: import_zod.z.string().url().optional(),
|
|
662
|
+
/** Browser type */
|
|
663
|
+
browser: import_zod.z.enum(["chromium", "firefox", "webkit"]).default("chromium"),
|
|
664
|
+
/** Viewport size */
|
|
665
|
+
viewport: import_zod.z.object({
|
|
666
|
+
width: import_zod.z.number().default(1280),
|
|
667
|
+
height: import_zod.z.number().default(720)
|
|
668
|
+
}).default({ width: 1280, height: 720 }),
|
|
669
|
+
/** Run headless */
|
|
670
|
+
headless: import_zod.z.boolean().default(false)
|
|
671
|
+
});
|
|
672
|
+
var BROWSER_STEP_TYPES = [
|
|
673
|
+
"browser.navigate",
|
|
674
|
+
"browser.click",
|
|
675
|
+
"browser.input",
|
|
676
|
+
"browser.keypress",
|
|
677
|
+
"browser.scroll",
|
|
678
|
+
"browser.wait"
|
|
679
|
+
];
|
|
680
|
+
var BrowserStepSchema = import_zod.z.discriminatedUnion("type", [
|
|
681
|
+
import_zod.z.object({
|
|
682
|
+
id: import_zod.z.string(),
|
|
683
|
+
type: import_zod.z.literal("browser.navigate"),
|
|
684
|
+
url: import_zod.z.string(),
|
|
685
|
+
timestamp: import_zod.z.number()
|
|
686
|
+
}),
|
|
687
|
+
import_zod.z.object({
|
|
688
|
+
id: import_zod.z.string(),
|
|
689
|
+
type: import_zod.z.literal("browser.click"),
|
|
690
|
+
selector: import_zod.z.string(),
|
|
691
|
+
position: import_zod.z.object({ x: import_zod.z.number(), y: import_zod.z.number() }).optional(),
|
|
692
|
+
timestamp: import_zod.z.number()
|
|
693
|
+
}),
|
|
694
|
+
import_zod.z.object({
|
|
695
|
+
id: import_zod.z.string(),
|
|
696
|
+
type: import_zod.z.literal("browser.input"),
|
|
697
|
+
selector: import_zod.z.string(),
|
|
698
|
+
value: import_zod.z.string(),
|
|
699
|
+
injectable: import_zod.z.boolean().default(true),
|
|
700
|
+
timestamp: import_zod.z.number()
|
|
701
|
+
}),
|
|
702
|
+
import_zod.z.object({
|
|
703
|
+
id: import_zod.z.string(),
|
|
704
|
+
type: import_zod.z.literal("browser.keypress"),
|
|
705
|
+
key: import_zod.z.string(),
|
|
706
|
+
modifiers: import_zod.z.array(import_zod.z.string()).optional(),
|
|
707
|
+
timestamp: import_zod.z.number()
|
|
708
|
+
}),
|
|
709
|
+
import_zod.z.object({
|
|
710
|
+
id: import_zod.z.string(),
|
|
711
|
+
type: import_zod.z.literal("browser.scroll"),
|
|
712
|
+
selector: import_zod.z.string().optional(),
|
|
713
|
+
position: import_zod.z.object({ x: import_zod.z.number(), y: import_zod.z.number() }),
|
|
714
|
+
timestamp: import_zod.z.number()
|
|
715
|
+
}),
|
|
716
|
+
import_zod.z.object({
|
|
717
|
+
id: import_zod.z.string(),
|
|
718
|
+
type: import_zod.z.literal("browser.wait"),
|
|
719
|
+
duration: import_zod.z.number(),
|
|
720
|
+
timestamp: import_zod.z.number()
|
|
721
|
+
})
|
|
722
|
+
]);
|
|
723
|
+
var recorderDriver = {
|
|
724
|
+
async start(config, options) {
|
|
725
|
+
const parsedConfig = configSchema.parse(config);
|
|
726
|
+
return BrowserRecorder.start(parsedConfig, options);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
var runnerDriver = {
|
|
730
|
+
async execute(session, ctx) {
|
|
731
|
+
return BrowserRunner.execute(session, ctx);
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
var browserDriver = {
|
|
735
|
+
name: "browser",
|
|
736
|
+
version: "0.1.0",
|
|
737
|
+
apiVersion: 1,
|
|
738
|
+
description: "Browser recording driver using Playwright",
|
|
739
|
+
configSchema,
|
|
740
|
+
stepTypes: [...BROWSER_STEP_TYPES],
|
|
741
|
+
recorder: recorderDriver,
|
|
742
|
+
runner: runnerDriver
|
|
743
|
+
};
|
|
744
|
+
var index_default = browserDriver;
|
|
745
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
746
|
+
0 && (module.exports = {
|
|
747
|
+
BROWSER_STEP_TYPES,
|
|
748
|
+
BrowserRecorder,
|
|
749
|
+
BrowserRunner,
|
|
750
|
+
BrowserStepSchema,
|
|
751
|
+
checkBrowsers,
|
|
752
|
+
configSchema,
|
|
753
|
+
installBrowsers,
|
|
754
|
+
launchBrowser
|
|
755
|
+
});
|
|
756
|
+
//# sourceMappingURL=index.cjs.map
|