@vulcn/engine 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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/index.cjs +724 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +546 -0
- package/dist/index.d.ts +546 -0
- package/dist/index.js +683 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
// session.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { parse, stringify } from "yaml";
|
|
4
|
+
var StepSchema = z.discriminatedUnion("type", [
|
|
5
|
+
z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
type: z.literal("navigate"),
|
|
8
|
+
url: z.string(),
|
|
9
|
+
timestamp: z.number()
|
|
10
|
+
}),
|
|
11
|
+
z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
type: z.literal("click"),
|
|
14
|
+
selector: z.string(),
|
|
15
|
+
position: z.object({ x: z.number(), y: z.number() }).optional(),
|
|
16
|
+
timestamp: z.number()
|
|
17
|
+
}),
|
|
18
|
+
z.object({
|
|
19
|
+
id: z.string(),
|
|
20
|
+
type: z.literal("input"),
|
|
21
|
+
selector: z.string(),
|
|
22
|
+
value: z.string(),
|
|
23
|
+
injectable: z.boolean().optional().default(true),
|
|
24
|
+
timestamp: z.number()
|
|
25
|
+
}),
|
|
26
|
+
z.object({
|
|
27
|
+
id: z.string(),
|
|
28
|
+
type: z.literal("keypress"),
|
|
29
|
+
key: z.string(),
|
|
30
|
+
modifiers: z.array(z.string()).optional(),
|
|
31
|
+
timestamp: z.number()
|
|
32
|
+
}),
|
|
33
|
+
z.object({
|
|
34
|
+
id: z.string(),
|
|
35
|
+
type: z.literal("scroll"),
|
|
36
|
+
selector: z.string().optional(),
|
|
37
|
+
position: z.object({ x: z.number(), y: z.number() }),
|
|
38
|
+
timestamp: z.number()
|
|
39
|
+
}),
|
|
40
|
+
z.object({
|
|
41
|
+
id: z.string(),
|
|
42
|
+
type: z.literal("wait"),
|
|
43
|
+
duration: z.number(),
|
|
44
|
+
timestamp: z.number()
|
|
45
|
+
})
|
|
46
|
+
]);
|
|
47
|
+
var SessionSchema = z.object({
|
|
48
|
+
version: z.string().default("1"),
|
|
49
|
+
name: z.string(),
|
|
50
|
+
recordedAt: z.string(),
|
|
51
|
+
browser: z.enum(["chromium", "firefox", "webkit"]).default("chromium"),
|
|
52
|
+
viewport: z.object({
|
|
53
|
+
width: z.number(),
|
|
54
|
+
height: z.number()
|
|
55
|
+
}),
|
|
56
|
+
startUrl: z.string(),
|
|
57
|
+
steps: z.array(StepSchema)
|
|
58
|
+
});
|
|
59
|
+
function createSession(options) {
|
|
60
|
+
return {
|
|
61
|
+
version: "1",
|
|
62
|
+
name: options.name,
|
|
63
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
64
|
+
browser: options.browser ?? "chromium",
|
|
65
|
+
viewport: options.viewport ?? { width: 1280, height: 720 },
|
|
66
|
+
startUrl: options.startUrl,
|
|
67
|
+
steps: []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function parseSession(yaml) {
|
|
71
|
+
const data = parse(yaml);
|
|
72
|
+
return SessionSchema.parse(data);
|
|
73
|
+
}
|
|
74
|
+
function serializeSession(session) {
|
|
75
|
+
return stringify(session, { lineWidth: 0 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// browser.ts
|
|
79
|
+
import { chromium, firefox, webkit } from "playwright";
|
|
80
|
+
import { exec } from "child_process";
|
|
81
|
+
import { promisify } from "util";
|
|
82
|
+
var execAsync = promisify(exec);
|
|
83
|
+
var BrowserNotFoundError = class extends Error {
|
|
84
|
+
constructor(message) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "BrowserNotFoundError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
async function launchBrowser(options = {}) {
|
|
90
|
+
const browserType = options.browser ?? "chromium";
|
|
91
|
+
const headless = options.headless ?? false;
|
|
92
|
+
if (browserType === "chromium") {
|
|
93
|
+
try {
|
|
94
|
+
const browser = await chromium.launch({
|
|
95
|
+
channel: "chrome",
|
|
96
|
+
headless
|
|
97
|
+
});
|
|
98
|
+
return { browser, channel: "chrome" };
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const browser = await chromium.launch({
|
|
103
|
+
channel: "msedge",
|
|
104
|
+
headless
|
|
105
|
+
});
|
|
106
|
+
return { browser, channel: "msedge" };
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const browser = await chromium.launch({ headless });
|
|
111
|
+
return { browser, channel: "chromium" };
|
|
112
|
+
} catch {
|
|
113
|
+
throw new BrowserNotFoundError(
|
|
114
|
+
"No Chromium browser found. Install Chrome or run: vulcn install chromium"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (browserType === "firefox") {
|
|
119
|
+
try {
|
|
120
|
+
const browser = await firefox.launch({ headless });
|
|
121
|
+
return { browser, channel: "firefox" };
|
|
122
|
+
} catch {
|
|
123
|
+
throw new BrowserNotFoundError(
|
|
124
|
+
"Firefox not found. Run: vulcn install firefox"
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (browserType === "webkit") {
|
|
129
|
+
try {
|
|
130
|
+
const browser = await webkit.launch({ headless });
|
|
131
|
+
return { browser, channel: "webkit" };
|
|
132
|
+
} catch {
|
|
133
|
+
throw new BrowserNotFoundError(
|
|
134
|
+
"WebKit not found. Run: vulcn install webkit"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw new BrowserNotFoundError(`Unknown browser type: ${browserType}`);
|
|
139
|
+
}
|
|
140
|
+
async function installBrowsers(browsers = ["chromium"]) {
|
|
141
|
+
const browserArg = browsers.join(" ");
|
|
142
|
+
await execAsync(`npx playwright install ${browserArg}`);
|
|
143
|
+
}
|
|
144
|
+
async function checkBrowsers() {
|
|
145
|
+
const results = {
|
|
146
|
+
systemChrome: false,
|
|
147
|
+
systemEdge: false,
|
|
148
|
+
playwrightChromium: false,
|
|
149
|
+
playwrightFirefox: false,
|
|
150
|
+
playwrightWebkit: false
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
const browser = await chromium.launch({
|
|
154
|
+
channel: "chrome",
|
|
155
|
+
headless: true
|
|
156
|
+
});
|
|
157
|
+
await browser.close();
|
|
158
|
+
results.systemChrome = true;
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const browser = await chromium.launch({
|
|
163
|
+
channel: "msedge",
|
|
164
|
+
headless: true
|
|
165
|
+
});
|
|
166
|
+
await browser.close();
|
|
167
|
+
results.systemEdge = true;
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const browser = await chromium.launch({ headless: true });
|
|
172
|
+
await browser.close();
|
|
173
|
+
results.playwrightChromium = true;
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const browser = await firefox.launch({ headless: true });
|
|
178
|
+
await browser.close();
|
|
179
|
+
results.playwrightFirefox = true;
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const browser = await webkit.launch({ headless: true });
|
|
184
|
+
await browser.close();
|
|
185
|
+
results.playwrightWebkit = true;
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// recorder.ts
|
|
192
|
+
var Recorder = class _Recorder {
|
|
193
|
+
/**
|
|
194
|
+
* Start a new recording session
|
|
195
|
+
* Opens a browser window for the user to interact with
|
|
196
|
+
*/
|
|
197
|
+
static async start(startUrl, options = {}) {
|
|
198
|
+
const browserType = options.browser ?? "chromium";
|
|
199
|
+
const viewport = options.viewport ?? { width: 1280, height: 720 };
|
|
200
|
+
const headless = options.headless ?? false;
|
|
201
|
+
const { browser } = await launchBrowser({
|
|
202
|
+
browser: browserType,
|
|
203
|
+
headless
|
|
204
|
+
});
|
|
205
|
+
const context = await browser.newContext({ viewport });
|
|
206
|
+
const page = await context.newPage();
|
|
207
|
+
await page.goto(startUrl);
|
|
208
|
+
const session = createSession({
|
|
209
|
+
name: `Recording ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
210
|
+
startUrl,
|
|
211
|
+
browser: browserType,
|
|
212
|
+
viewport
|
|
213
|
+
});
|
|
214
|
+
const startTime = Date.now();
|
|
215
|
+
const steps = [];
|
|
216
|
+
let stepCounter = 0;
|
|
217
|
+
const generateStepId = () => {
|
|
218
|
+
stepCounter++;
|
|
219
|
+
return `step_${String(stepCounter).padStart(3, "0")}`;
|
|
220
|
+
};
|
|
221
|
+
steps.push({
|
|
222
|
+
id: generateStepId(),
|
|
223
|
+
type: "navigate",
|
|
224
|
+
url: startUrl,
|
|
225
|
+
timestamp: 0
|
|
226
|
+
});
|
|
227
|
+
_Recorder.attachListeners(page, steps, startTime, generateStepId);
|
|
228
|
+
return {
|
|
229
|
+
async stop() {
|
|
230
|
+
session.steps = steps;
|
|
231
|
+
await browser.close();
|
|
232
|
+
return session;
|
|
233
|
+
},
|
|
234
|
+
getSteps() {
|
|
235
|
+
return [...steps];
|
|
236
|
+
},
|
|
237
|
+
getPage() {
|
|
238
|
+
return page;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
static attachListeners(page, steps, startTime, generateStepId) {
|
|
243
|
+
const getTimestamp = () => Date.now() - startTime;
|
|
244
|
+
page.on("framenavigated", (frame) => {
|
|
245
|
+
if (frame === page.mainFrame()) {
|
|
246
|
+
const url = frame.url();
|
|
247
|
+
const lastStep = steps[steps.length - 1];
|
|
248
|
+
if (steps.length > 0 && lastStep.type === "navigate" && lastStep.url === url) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
steps.push({
|
|
252
|
+
id: generateStepId(),
|
|
253
|
+
type: "navigate",
|
|
254
|
+
url,
|
|
255
|
+
timestamp: getTimestamp()
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
page.exposeFunction(
|
|
260
|
+
"__vulcn_record",
|
|
261
|
+
(event) => {
|
|
262
|
+
const timestamp = getTimestamp();
|
|
263
|
+
switch (event.type) {
|
|
264
|
+
case "click": {
|
|
265
|
+
const data = event.data;
|
|
266
|
+
steps.push({
|
|
267
|
+
id: generateStepId(),
|
|
268
|
+
type: "click",
|
|
269
|
+
selector: data.selector,
|
|
270
|
+
position: { x: data.x, y: data.y },
|
|
271
|
+
timestamp
|
|
272
|
+
});
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case "input": {
|
|
276
|
+
const data = event.data;
|
|
277
|
+
steps.push({
|
|
278
|
+
id: generateStepId(),
|
|
279
|
+
type: "input",
|
|
280
|
+
selector: data.selector,
|
|
281
|
+
value: data.value,
|
|
282
|
+
injectable: data.injectable,
|
|
283
|
+
timestamp
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "keypress": {
|
|
288
|
+
const data = event.data;
|
|
289
|
+
steps.push({
|
|
290
|
+
id: generateStepId(),
|
|
291
|
+
type: "keypress",
|
|
292
|
+
key: data.key,
|
|
293
|
+
modifiers: data.modifiers,
|
|
294
|
+
timestamp
|
|
295
|
+
});
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
page.on("load", async () => {
|
|
302
|
+
await _Recorder.injectRecordingScript(page);
|
|
303
|
+
});
|
|
304
|
+
_Recorder.injectRecordingScript(page);
|
|
305
|
+
}
|
|
306
|
+
static async injectRecordingScript(page) {
|
|
307
|
+
await page.evaluate(`
|
|
308
|
+
(function() {
|
|
309
|
+
if (window.__vulcn_injected) return;
|
|
310
|
+
window.__vulcn_injected = true;
|
|
311
|
+
|
|
312
|
+
var textInputTypes = ['text', 'password', 'email', 'search', 'url', 'tel', 'number'];
|
|
313
|
+
|
|
314
|
+
function getSelector(el) {
|
|
315
|
+
if (el.id) {
|
|
316
|
+
return '#' + CSS.escape(el.id);
|
|
317
|
+
}
|
|
318
|
+
if (el.name) {
|
|
319
|
+
var tag = el.tagName.toLowerCase();
|
|
320
|
+
var nameSelector = tag + '[name="' + el.name + '"]';
|
|
321
|
+
if (document.querySelectorAll(nameSelector).length === 1) {
|
|
322
|
+
return nameSelector;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (el.dataset && el.dataset.testid) {
|
|
326
|
+
return '[data-testid="' + el.dataset.testid + '"]';
|
|
327
|
+
}
|
|
328
|
+
if (el.tagName === 'INPUT' && el.type && el.name) {
|
|
329
|
+
var inputSelector = 'input[type="' + el.type + '"][name="' + el.name + '"]';
|
|
330
|
+
if (document.querySelectorAll(inputSelector).length === 1) {
|
|
331
|
+
return inputSelector;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (el.className && typeof el.className === 'string') {
|
|
335
|
+
var classes = el.className.trim().split(/\\s+/).filter(function(c) { return c.length > 0; });
|
|
336
|
+
if (classes.length > 0) {
|
|
337
|
+
var classSelector = el.tagName.toLowerCase() + '.' + classes.map(function(c) { return CSS.escape(c); }).join('.');
|
|
338
|
+
if (document.querySelectorAll(classSelector).length === 1) {
|
|
339
|
+
return classSelector;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
var path = [];
|
|
344
|
+
var current = el;
|
|
345
|
+
while (current && current !== document.body) {
|
|
346
|
+
var tag = current.tagName.toLowerCase();
|
|
347
|
+
var parent = current.parentElement;
|
|
348
|
+
if (parent) {
|
|
349
|
+
var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName; });
|
|
350
|
+
if (siblings.length > 1) {
|
|
351
|
+
var index = siblings.indexOf(current) + 1;
|
|
352
|
+
tag = tag + ':nth-of-type(' + index + ')';
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
path.unshift(tag);
|
|
356
|
+
current = parent;
|
|
357
|
+
}
|
|
358
|
+
return path.join(' > ');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function getInputType(el) {
|
|
362
|
+
if (el.tagName === 'INPUT') return el.type || 'text';
|
|
363
|
+
if (el.tagName === 'TEXTAREA') return 'textarea';
|
|
364
|
+
if (el.tagName === 'SELECT') return 'select';
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function isTextInjectable(el) {
|
|
369
|
+
var inputType = getInputType(el);
|
|
370
|
+
if (!inputType) return false;
|
|
371
|
+
if (inputType === 'textarea') return true;
|
|
372
|
+
if (inputType === 'select') return false;
|
|
373
|
+
return textInputTypes.indexOf(inputType) !== -1;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
document.addEventListener('click', function(e) {
|
|
377
|
+
var target = e.target;
|
|
378
|
+
window.__vulcn_record({
|
|
379
|
+
type: 'click',
|
|
380
|
+
data: {
|
|
381
|
+
selector: getSelector(target),
|
|
382
|
+
x: e.clientX,
|
|
383
|
+
y: e.clientY
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}, true);
|
|
387
|
+
|
|
388
|
+
document.addEventListener('change', function(e) {
|
|
389
|
+
var target = e.target;
|
|
390
|
+
if ('value' in target) {
|
|
391
|
+
var inputType = getInputType(target);
|
|
392
|
+
window.__vulcn_record({
|
|
393
|
+
type: 'input',
|
|
394
|
+
data: {
|
|
395
|
+
selector: getSelector(target),
|
|
396
|
+
value: target.value,
|
|
397
|
+
inputType: inputType,
|
|
398
|
+
injectable: isTextInjectable(target)
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}, true);
|
|
403
|
+
|
|
404
|
+
document.addEventListener('keydown', function(e) {
|
|
405
|
+
if (e.ctrlKey || e.metaKey || e.altKey) {
|
|
406
|
+
var modifiers = [];
|
|
407
|
+
if (e.ctrlKey) modifiers.push('ctrl');
|
|
408
|
+
if (e.metaKey) modifiers.push('meta');
|
|
409
|
+
if (e.altKey) modifiers.push('alt');
|
|
410
|
+
if (e.shiftKey) modifiers.push('shift');
|
|
411
|
+
|
|
412
|
+
window.__vulcn_record({
|
|
413
|
+
type: 'keypress',
|
|
414
|
+
data: {
|
|
415
|
+
key: e.key,
|
|
416
|
+
modifiers: modifiers
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}, true);
|
|
421
|
+
})();
|
|
422
|
+
`);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
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
|
+
// runner.ts
|
|
539
|
+
var Runner = class _Runner {
|
|
540
|
+
/**
|
|
541
|
+
* Execute a session with security payloads
|
|
542
|
+
*/
|
|
543
|
+
static async execute(session, payloadNames, options = {}) {
|
|
544
|
+
const browserType = options.browser ?? session.browser ?? "chromium";
|
|
545
|
+
const headless = options.headless ?? true;
|
|
546
|
+
const startTime = Date.now();
|
|
547
|
+
const findings = [];
|
|
548
|
+
const errors = [];
|
|
549
|
+
let payloadsTested = 0;
|
|
550
|
+
const { browser } = await launchBrowser({
|
|
551
|
+
browser: browserType,
|
|
552
|
+
headless
|
|
553
|
+
});
|
|
554
|
+
const context = await browser.newContext({ viewport: session.viewport });
|
|
555
|
+
const page = await context.newPage();
|
|
556
|
+
try {
|
|
557
|
+
const injectableSteps = session.steps.filter(
|
|
558
|
+
(step) => step.type === "input" && step.injectable !== false
|
|
559
|
+
);
|
|
560
|
+
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
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
for (const injectableStep of injectableSteps) {
|
|
570
|
+
for (const payload of allPayloads) {
|
|
571
|
+
try {
|
|
572
|
+
const finding = await _Runner.replayWithPayload(
|
|
573
|
+
page,
|
|
574
|
+
session,
|
|
575
|
+
injectableStep,
|
|
576
|
+
payload.name,
|
|
577
|
+
payload.value
|
|
578
|
+
);
|
|
579
|
+
if (finding) {
|
|
580
|
+
findings.push(finding);
|
|
581
|
+
options.onFinding?.(finding);
|
|
582
|
+
}
|
|
583
|
+
payloadsTested++;
|
|
584
|
+
} catch (err) {
|
|
585
|
+
errors.push(`${injectableStep.id}: ${String(err)}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
} finally {
|
|
590
|
+
await browser.close();
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
findings,
|
|
594
|
+
stepsExecuted: session.steps.length,
|
|
595
|
+
payloadsTested,
|
|
596
|
+
duration: Date.now() - startTime,
|
|
597
|
+
errors
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
static async replayWithPayload(page, session, targetStep, payloadName, payloadValue) {
|
|
601
|
+
await page.goto(session.startUrl);
|
|
602
|
+
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);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
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
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return void 0;
|
|
633
|
+
}
|
|
634
|
+
static async checkForVulnerability(page, step, payloadName, payloadValue) {
|
|
635
|
+
const payload = BUILTIN_PAYLOADS[payloadName];
|
|
636
|
+
if (!payload) return void 0;
|
|
637
|
+
const content = await page.content();
|
|
638
|
+
for (const pattern of payload.detectPatterns) {
|
|
639
|
+
if (pattern.test(content)) {
|
|
640
|
+
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`,
|
|
645
|
+
stepId: step.id,
|
|
646
|
+
payload: payloadValue,
|
|
647
|
+
url: page.url(),
|
|
648
|
+
evidence: content.match(pattern)?.[0]
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (content.includes(payloadValue)) {
|
|
653
|
+
return {
|
|
654
|
+
type: payload.category,
|
|
655
|
+
severity: "medium",
|
|
656
|
+
title: `Potential ${payload.category.toUpperCase()} - payload reflection`,
|
|
657
|
+
description: `Payload was reflected in page without encoding`,
|
|
658
|
+
stepId: step.id,
|
|
659
|
+
payload: payloadValue,
|
|
660
|
+
url: page.url()
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
return void 0;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
export {
|
|
667
|
+
BUILTIN_PAYLOADS,
|
|
668
|
+
BrowserNotFoundError,
|
|
669
|
+
Recorder,
|
|
670
|
+
Runner,
|
|
671
|
+
SessionSchema,
|
|
672
|
+
StepSchema,
|
|
673
|
+
checkBrowsers,
|
|
674
|
+
createSession,
|
|
675
|
+
getPayload,
|
|
676
|
+
getPayloadNames,
|
|
677
|
+
getPayloadsByCategory,
|
|
678
|
+
installBrowsers,
|
|
679
|
+
launchBrowser,
|
|
680
|
+
parseSession,
|
|
681
|
+
serializeSession
|
|
682
|
+
};
|
|
683
|
+
//# sourceMappingURL=index.js.map
|