@vulcn/engine 0.1.0 → 0.3.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 +63 -1
- package/LICENSE +662 -21
- package/README.md +41 -175
- package/dist/index.cjs +841 -240
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +658 -55
- package/dist/index.d.ts +658 -55
- package/dist/index.js +821 -232
- package/dist/index.js.map +1 -1
- package/package.json +33 -16
package/dist/index.js
CHANGED
|
@@ -1,60 +1,543 @@
|
|
|
1
|
-
//
|
|
1
|
+
// src/driver-manager.ts
|
|
2
|
+
import { isAbsolute, resolve } from "path";
|
|
3
|
+
var DriverManager = class {
|
|
4
|
+
drivers = /* @__PURE__ */ new Map();
|
|
5
|
+
defaultDriver = null;
|
|
6
|
+
/**
|
|
7
|
+
* Register a driver
|
|
8
|
+
*/
|
|
9
|
+
register(driver, source = "builtin") {
|
|
10
|
+
this.validateDriver(driver);
|
|
11
|
+
this.drivers.set(driver.name, { driver, source });
|
|
12
|
+
if (this.drivers.size === 1) {
|
|
13
|
+
this.defaultDriver = driver.name;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Load a driver from npm or local path
|
|
18
|
+
*/
|
|
19
|
+
async load(nameOrPath) {
|
|
20
|
+
let driver;
|
|
21
|
+
let source;
|
|
22
|
+
if (nameOrPath.startsWith("./") || nameOrPath.startsWith("../") || isAbsolute(nameOrPath)) {
|
|
23
|
+
const resolved = isAbsolute(nameOrPath) ? nameOrPath : resolve(process.cwd(), nameOrPath);
|
|
24
|
+
const module = await import(resolved);
|
|
25
|
+
driver = module.default || module;
|
|
26
|
+
source = "local";
|
|
27
|
+
} else {
|
|
28
|
+
const module = await import(nameOrPath);
|
|
29
|
+
driver = module.default || module;
|
|
30
|
+
source = "npm";
|
|
31
|
+
}
|
|
32
|
+
this.register(driver, source);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get a loaded driver by name
|
|
36
|
+
*/
|
|
37
|
+
get(name) {
|
|
38
|
+
return this.drivers.get(name)?.driver;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get the default driver
|
|
42
|
+
*/
|
|
43
|
+
getDefault() {
|
|
44
|
+
if (!this.defaultDriver) return void 0;
|
|
45
|
+
return this.get(this.defaultDriver);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Set the default driver
|
|
49
|
+
*/
|
|
50
|
+
setDefault(name) {
|
|
51
|
+
if (!this.drivers.has(name)) {
|
|
52
|
+
throw new Error(`Driver "${name}" is not registered`);
|
|
53
|
+
}
|
|
54
|
+
this.defaultDriver = name;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if a driver is registered
|
|
58
|
+
*/
|
|
59
|
+
has(name) {
|
|
60
|
+
return this.drivers.has(name);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get all registered drivers
|
|
64
|
+
*/
|
|
65
|
+
list() {
|
|
66
|
+
return Array.from(this.drivers.values());
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get driver for a session
|
|
70
|
+
*/
|
|
71
|
+
getForSession(session) {
|
|
72
|
+
const driverName = session.driver;
|
|
73
|
+
const driver = this.get(driverName);
|
|
74
|
+
if (!driver) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Driver "${driverName}" not found. Install @vulcn/driver-${driverName} or load it manually.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return driver;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Start recording with a driver
|
|
83
|
+
*/
|
|
84
|
+
async startRecording(driverName, config, options = {}) {
|
|
85
|
+
const driver = this.get(driverName);
|
|
86
|
+
if (!driver) {
|
|
87
|
+
throw new Error(`Driver "${driverName}" not found`);
|
|
88
|
+
}
|
|
89
|
+
return driver.recorder.start(config, options);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Execute a session
|
|
93
|
+
*/
|
|
94
|
+
async execute(session, pluginManager2, options = {}) {
|
|
95
|
+
const driver = this.getForSession(session);
|
|
96
|
+
const findings = [];
|
|
97
|
+
const logger = this.createLogger(driver.name);
|
|
98
|
+
const ctx = {
|
|
99
|
+
session,
|
|
100
|
+
pluginManager: pluginManager2,
|
|
101
|
+
payloads: pluginManager2.getPayloads(),
|
|
102
|
+
findings,
|
|
103
|
+
addFinding: (finding) => {
|
|
104
|
+
findings.push(finding);
|
|
105
|
+
pluginManager2.addFinding(finding);
|
|
106
|
+
options.onFinding?.(finding);
|
|
107
|
+
},
|
|
108
|
+
logger,
|
|
109
|
+
options
|
|
110
|
+
};
|
|
111
|
+
return driver.runner.execute(session, ctx);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Validate driver structure
|
|
115
|
+
*/
|
|
116
|
+
validateDriver(driver) {
|
|
117
|
+
if (!driver || typeof driver !== "object") {
|
|
118
|
+
throw new Error("Driver must be an object");
|
|
119
|
+
}
|
|
120
|
+
const d = driver;
|
|
121
|
+
if (typeof d.name !== "string" || !d.name) {
|
|
122
|
+
throw new Error("Driver must have a name");
|
|
123
|
+
}
|
|
124
|
+
if (typeof d.version !== "string" || !d.version) {
|
|
125
|
+
throw new Error("Driver must have a version");
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(d.stepTypes) || d.stepTypes.length === 0) {
|
|
128
|
+
throw new Error("Driver must define stepTypes");
|
|
129
|
+
}
|
|
130
|
+
if (!d.recorder || typeof d.recorder !== "object") {
|
|
131
|
+
throw new Error("Driver must have a recorder");
|
|
132
|
+
}
|
|
133
|
+
if (!d.runner || typeof d.runner !== "object") {
|
|
134
|
+
throw new Error("Driver must have a runner");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Create a scoped logger for a driver
|
|
139
|
+
*/
|
|
140
|
+
createLogger(name) {
|
|
141
|
+
const prefix = `[driver:${name}]`;
|
|
142
|
+
return {
|
|
143
|
+
debug: (msg, ...args) => console.debug(prefix, msg, ...args),
|
|
144
|
+
info: (msg, ...args) => console.info(prefix, msg, ...args),
|
|
145
|
+
warn: (msg, ...args) => console.warn(prefix, msg, ...args),
|
|
146
|
+
error: (msg, ...args) => console.error(prefix, msg, ...args)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
var driverManager = new DriverManager();
|
|
151
|
+
|
|
152
|
+
// src/driver-types.ts
|
|
153
|
+
var DRIVER_API_VERSION = 1;
|
|
154
|
+
|
|
155
|
+
// src/plugin-manager.ts
|
|
156
|
+
import { readFile } from "fs/promises";
|
|
157
|
+
import { existsSync } from "fs";
|
|
158
|
+
import { resolve as resolve2, isAbsolute as isAbsolute2 } from "path";
|
|
159
|
+
import YAML from "yaml";
|
|
2
160
|
import { z } from "zod";
|
|
161
|
+
|
|
162
|
+
// src/plugin-types.ts
|
|
163
|
+
var PLUGIN_API_VERSION = 1;
|
|
164
|
+
|
|
165
|
+
// src/plugin-manager.ts
|
|
166
|
+
var ENGINE_VERSION = "0.2.0";
|
|
167
|
+
var VulcnConfigSchema = z.object({
|
|
168
|
+
version: z.string().default("1"),
|
|
169
|
+
plugins: z.array(
|
|
170
|
+
z.object({
|
|
171
|
+
name: z.string(),
|
|
172
|
+
config: z.record(z.unknown()).optional(),
|
|
173
|
+
enabled: z.boolean().default(true)
|
|
174
|
+
})
|
|
175
|
+
).optional(),
|
|
176
|
+
settings: z.object({
|
|
177
|
+
browser: z.enum(["chromium", "firefox", "webkit"]).optional(),
|
|
178
|
+
headless: z.boolean().optional(),
|
|
179
|
+
timeout: z.number().optional()
|
|
180
|
+
}).optional()
|
|
181
|
+
});
|
|
182
|
+
var PluginManager = class {
|
|
183
|
+
plugins = [];
|
|
184
|
+
config = null;
|
|
185
|
+
initialized = false;
|
|
186
|
+
/**
|
|
187
|
+
* Shared context passed to all plugins
|
|
188
|
+
*/
|
|
189
|
+
sharedPayloads = [];
|
|
190
|
+
sharedFindings = [];
|
|
191
|
+
/**
|
|
192
|
+
* Load configuration from vulcn.config.yml
|
|
193
|
+
*/
|
|
194
|
+
async loadConfig(configPath) {
|
|
195
|
+
const paths = configPath ? [configPath] : [
|
|
196
|
+
"vulcn.config.yml",
|
|
197
|
+
"vulcn.config.yaml",
|
|
198
|
+
"vulcn.config.json",
|
|
199
|
+
".vulcnrc.yml",
|
|
200
|
+
".vulcnrc.yaml",
|
|
201
|
+
".vulcnrc.json"
|
|
202
|
+
];
|
|
203
|
+
for (const path of paths) {
|
|
204
|
+
const resolved = isAbsolute2(path) ? path : resolve2(process.cwd(), path);
|
|
205
|
+
if (existsSync(resolved)) {
|
|
206
|
+
const content = await readFile(resolved, "utf-8");
|
|
207
|
+
const parsed = path.endsWith(".json") ? JSON.parse(content) : YAML.parse(content);
|
|
208
|
+
this.config = VulcnConfigSchema.parse(parsed);
|
|
209
|
+
return this.config;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
this.config = { version: "1", plugins: [], settings: {} };
|
|
213
|
+
return this.config;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Load all plugins from config
|
|
217
|
+
*/
|
|
218
|
+
async loadPlugins() {
|
|
219
|
+
if (!this.config) {
|
|
220
|
+
await this.loadConfig();
|
|
221
|
+
}
|
|
222
|
+
const pluginConfigs = this.config?.plugins || [];
|
|
223
|
+
for (const pluginConfig of pluginConfigs) {
|
|
224
|
+
if (pluginConfig.enabled === false) continue;
|
|
225
|
+
try {
|
|
226
|
+
const loaded = await this.loadPlugin(pluginConfig);
|
|
227
|
+
this.plugins.push(loaded);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error(
|
|
230
|
+
`Failed to load plugin ${pluginConfig.name}:`,
|
|
231
|
+
err instanceof Error ? err.message : String(err)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Load a single plugin
|
|
238
|
+
*/
|
|
239
|
+
async loadPlugin(config) {
|
|
240
|
+
const { name, config: pluginConfig = {} } = config;
|
|
241
|
+
let plugin;
|
|
242
|
+
let source;
|
|
243
|
+
if (name.startsWith("./") || name.startsWith("../") || isAbsolute2(name)) {
|
|
244
|
+
const resolved = isAbsolute2(name) ? name : resolve2(process.cwd(), name);
|
|
245
|
+
const module = await import(resolved);
|
|
246
|
+
plugin = module.default || module;
|
|
247
|
+
source = "local";
|
|
248
|
+
} else if (name.startsWith("@vulcn/")) {
|
|
249
|
+
const module = await import(name);
|
|
250
|
+
plugin = module.default || module;
|
|
251
|
+
source = "npm";
|
|
252
|
+
} else {
|
|
253
|
+
const module = await import(name);
|
|
254
|
+
plugin = module.default || module;
|
|
255
|
+
source = "npm";
|
|
256
|
+
}
|
|
257
|
+
this.validatePlugin(plugin);
|
|
258
|
+
let resolvedConfig = pluginConfig;
|
|
259
|
+
if (plugin.configSchema) {
|
|
260
|
+
try {
|
|
261
|
+
resolvedConfig = plugin.configSchema.parse(pluginConfig);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Invalid config for plugin ${name}: ${err instanceof Error ? err.message : String(err)}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
plugin,
|
|
270
|
+
config: resolvedConfig,
|
|
271
|
+
source,
|
|
272
|
+
enabled: true
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Validate plugin structure
|
|
277
|
+
*/
|
|
278
|
+
validatePlugin(plugin) {
|
|
279
|
+
if (!plugin || typeof plugin !== "object") {
|
|
280
|
+
throw new Error("Plugin must be an object");
|
|
281
|
+
}
|
|
282
|
+
const p = plugin;
|
|
283
|
+
if (typeof p.name !== "string" || !p.name) {
|
|
284
|
+
throw new Error("Plugin must have a name");
|
|
285
|
+
}
|
|
286
|
+
if (typeof p.version !== "string" || !p.version) {
|
|
287
|
+
throw new Error("Plugin must have a version");
|
|
288
|
+
}
|
|
289
|
+
const apiVersion = p.apiVersion || 1;
|
|
290
|
+
if (apiVersion > PLUGIN_API_VERSION) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Plugin requires API version ${apiVersion}, but engine supports ${PLUGIN_API_VERSION}`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Add a plugin programmatically (for testing or dynamic loading)
|
|
298
|
+
*/
|
|
299
|
+
addPlugin(plugin, config = {}) {
|
|
300
|
+
this.validatePlugin(plugin);
|
|
301
|
+
this.plugins.push({
|
|
302
|
+
plugin,
|
|
303
|
+
config,
|
|
304
|
+
source: "custom",
|
|
305
|
+
enabled: true
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Initialize all plugins (call onInit hooks)
|
|
310
|
+
*/
|
|
311
|
+
async initialize() {
|
|
312
|
+
if (this.initialized) return;
|
|
313
|
+
for (const loaded of this.plugins) {
|
|
314
|
+
if (loaded.plugin.payloads) {
|
|
315
|
+
const payloads = typeof loaded.plugin.payloads === "function" ? await loaded.plugin.payloads() : loaded.plugin.payloads;
|
|
316
|
+
this.sharedPayloads.push(...payloads);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
await this.callHook("onInit", (hook, ctx) => hook(ctx));
|
|
320
|
+
this.initialized = true;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Destroy all plugins (call onDestroy hooks)
|
|
324
|
+
*/
|
|
325
|
+
async destroy() {
|
|
326
|
+
await this.callHook("onDestroy", (hook, ctx) => hook(ctx));
|
|
327
|
+
this.plugins = [];
|
|
328
|
+
this.sharedPayloads = [];
|
|
329
|
+
this.sharedFindings = [];
|
|
330
|
+
this.initialized = false;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Get all loaded payloads
|
|
334
|
+
*/
|
|
335
|
+
getPayloads() {
|
|
336
|
+
return this.sharedPayloads;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Get all collected findings
|
|
340
|
+
*/
|
|
341
|
+
getFindings() {
|
|
342
|
+
return this.sharedFindings;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Add a finding (used by detectors)
|
|
346
|
+
*/
|
|
347
|
+
addFinding(finding) {
|
|
348
|
+
this.sharedFindings.push(finding);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Add payloads (used by loaders)
|
|
352
|
+
*/
|
|
353
|
+
addPayloads(payloads) {
|
|
354
|
+
this.sharedPayloads.push(...payloads);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Clear findings (for new run)
|
|
358
|
+
*/
|
|
359
|
+
clearFindings() {
|
|
360
|
+
this.sharedFindings = [];
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Get loaded plugins
|
|
364
|
+
*/
|
|
365
|
+
getPlugins() {
|
|
366
|
+
return this.plugins;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Check if a plugin is loaded by name
|
|
370
|
+
*/
|
|
371
|
+
hasPlugin(name) {
|
|
372
|
+
return this.plugins.some((p) => p.plugin.name === name);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Create base context for plugins
|
|
376
|
+
*/
|
|
377
|
+
createContext(pluginConfig) {
|
|
378
|
+
const engineInfo = {
|
|
379
|
+
version: ENGINE_VERSION,
|
|
380
|
+
pluginApiVersion: PLUGIN_API_VERSION
|
|
381
|
+
};
|
|
382
|
+
return {
|
|
383
|
+
config: pluginConfig,
|
|
384
|
+
engine: engineInfo,
|
|
385
|
+
payloads: this.sharedPayloads,
|
|
386
|
+
findings: this.sharedFindings,
|
|
387
|
+
logger: this.createLogger("plugin"),
|
|
388
|
+
fetch: globalThis.fetch
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Create scoped logger for a plugin
|
|
393
|
+
*/
|
|
394
|
+
createLogger(name) {
|
|
395
|
+
const prefix = `[${name}]`;
|
|
396
|
+
return {
|
|
397
|
+
debug: (msg, ...args) => console.debug(prefix, msg, ...args),
|
|
398
|
+
info: (msg, ...args) => console.info(prefix, msg, ...args),
|
|
399
|
+
warn: (msg, ...args) => console.warn(prefix, msg, ...args),
|
|
400
|
+
error: (msg, ...args) => console.error(prefix, msg, ...args)
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Call a hook on all plugins sequentially
|
|
405
|
+
*/
|
|
406
|
+
async callHook(hookName, executor) {
|
|
407
|
+
for (const loaded of this.plugins) {
|
|
408
|
+
const hook = loaded.plugin.hooks?.[hookName];
|
|
409
|
+
if (hook) {
|
|
410
|
+
const ctx = this.createContext(loaded.config);
|
|
411
|
+
ctx.logger = this.createLogger(loaded.plugin.name);
|
|
412
|
+
try {
|
|
413
|
+
await executor(hook, ctx);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
console.error(
|
|
416
|
+
`Error in plugin ${loaded.plugin.name}.${hookName}:`,
|
|
417
|
+
err instanceof Error ? err.message : String(err)
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Call a hook and collect results
|
|
425
|
+
*/
|
|
426
|
+
async callHookCollect(hookName, executor) {
|
|
427
|
+
const results = [];
|
|
428
|
+
for (const loaded of this.plugins) {
|
|
429
|
+
const hook = loaded.plugin.hooks?.[hookName];
|
|
430
|
+
if (hook) {
|
|
431
|
+
const ctx = this.createContext(loaded.config);
|
|
432
|
+
ctx.logger = this.createLogger(loaded.plugin.name);
|
|
433
|
+
try {
|
|
434
|
+
const result = await executor(
|
|
435
|
+
hook,
|
|
436
|
+
ctx
|
|
437
|
+
);
|
|
438
|
+
if (result !== null && result !== void 0) {
|
|
439
|
+
if (Array.isArray(result)) {
|
|
440
|
+
results.push(...result);
|
|
441
|
+
} else {
|
|
442
|
+
results.push(result);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
console.error(
|
|
447
|
+
`Error in plugin ${loaded.plugin.name}.${hookName}:`,
|
|
448
|
+
err instanceof Error ? err.message : String(err)
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return results;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Call a hook that transforms a value through the pipeline
|
|
457
|
+
*/
|
|
458
|
+
async callHookPipe(hookName, initial, executor) {
|
|
459
|
+
let value = initial;
|
|
460
|
+
for (const loaded of this.plugins) {
|
|
461
|
+
const hook = loaded.plugin.hooks?.[hookName];
|
|
462
|
+
if (hook) {
|
|
463
|
+
const ctx = this.createContext(loaded.config);
|
|
464
|
+
ctx.logger = this.createLogger(loaded.plugin.name);
|
|
465
|
+
try {
|
|
466
|
+
value = await executor(
|
|
467
|
+
hook,
|
|
468
|
+
value,
|
|
469
|
+
ctx
|
|
470
|
+
);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.error(
|
|
473
|
+
`Error in plugin ${loaded.plugin.name}.${hookName}:`,
|
|
474
|
+
err instanceof Error ? err.message : String(err)
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
var pluginManager = new PluginManager();
|
|
483
|
+
|
|
484
|
+
// src/session.ts
|
|
485
|
+
import { z as z2 } from "zod";
|
|
3
486
|
import { parse, stringify } from "yaml";
|
|
4
|
-
var StepSchema =
|
|
5
|
-
|
|
6
|
-
id:
|
|
7
|
-
type:
|
|
8
|
-
url:
|
|
9
|
-
timestamp:
|
|
487
|
+
var StepSchema = z2.discriminatedUnion("type", [
|
|
488
|
+
z2.object({
|
|
489
|
+
id: z2.string(),
|
|
490
|
+
type: z2.literal("navigate"),
|
|
491
|
+
url: z2.string(),
|
|
492
|
+
timestamp: z2.number()
|
|
10
493
|
}),
|
|
11
|
-
|
|
12
|
-
id:
|
|
13
|
-
type:
|
|
14
|
-
selector:
|
|
15
|
-
position:
|
|
16
|
-
timestamp:
|
|
494
|
+
z2.object({
|
|
495
|
+
id: z2.string(),
|
|
496
|
+
type: z2.literal("click"),
|
|
497
|
+
selector: z2.string(),
|
|
498
|
+
position: z2.object({ x: z2.number(), y: z2.number() }).optional(),
|
|
499
|
+
timestamp: z2.number()
|
|
17
500
|
}),
|
|
18
|
-
|
|
19
|
-
id:
|
|
20
|
-
type:
|
|
21
|
-
selector:
|
|
22
|
-
value:
|
|
23
|
-
injectable:
|
|
24
|
-
timestamp:
|
|
501
|
+
z2.object({
|
|
502
|
+
id: z2.string(),
|
|
503
|
+
type: z2.literal("input"),
|
|
504
|
+
selector: z2.string(),
|
|
505
|
+
value: z2.string(),
|
|
506
|
+
injectable: z2.boolean().optional().default(true),
|
|
507
|
+
timestamp: z2.number()
|
|
25
508
|
}),
|
|
26
|
-
|
|
27
|
-
id:
|
|
28
|
-
type:
|
|
29
|
-
key:
|
|
30
|
-
modifiers:
|
|
31
|
-
timestamp:
|
|
509
|
+
z2.object({
|
|
510
|
+
id: z2.string(),
|
|
511
|
+
type: z2.literal("keypress"),
|
|
512
|
+
key: z2.string(),
|
|
513
|
+
modifiers: z2.array(z2.string()).optional(),
|
|
514
|
+
timestamp: z2.number()
|
|
32
515
|
}),
|
|
33
|
-
|
|
34
|
-
id:
|
|
35
|
-
type:
|
|
36
|
-
selector:
|
|
37
|
-
position:
|
|
38
|
-
timestamp:
|
|
516
|
+
z2.object({
|
|
517
|
+
id: z2.string(),
|
|
518
|
+
type: z2.literal("scroll"),
|
|
519
|
+
selector: z2.string().optional(),
|
|
520
|
+
position: z2.object({ x: z2.number(), y: z2.number() }),
|
|
521
|
+
timestamp: z2.number()
|
|
39
522
|
}),
|
|
40
|
-
|
|
41
|
-
id:
|
|
42
|
-
type:
|
|
43
|
-
duration:
|
|
44
|
-
timestamp:
|
|
523
|
+
z2.object({
|
|
524
|
+
id: z2.string(),
|
|
525
|
+
type: z2.literal("wait"),
|
|
526
|
+
duration: z2.number(),
|
|
527
|
+
timestamp: z2.number()
|
|
45
528
|
})
|
|
46
529
|
]);
|
|
47
|
-
var SessionSchema =
|
|
48
|
-
version:
|
|
49
|
-
name:
|
|
50
|
-
recordedAt:
|
|
51
|
-
browser:
|
|
52
|
-
viewport:
|
|
53
|
-
width:
|
|
54
|
-
height:
|
|
530
|
+
var SessionSchema = z2.object({
|
|
531
|
+
version: z2.string().default("1"),
|
|
532
|
+
name: z2.string(),
|
|
533
|
+
recordedAt: z2.string(),
|
|
534
|
+
browser: z2.enum(["chromium", "firefox", "webkit"]).default("chromium"),
|
|
535
|
+
viewport: z2.object({
|
|
536
|
+
width: z2.number(),
|
|
537
|
+
height: z2.number()
|
|
55
538
|
}),
|
|
56
|
-
startUrl:
|
|
57
|
-
steps:
|
|
539
|
+
startUrl: z2.string(),
|
|
540
|
+
steps: z2.array(StepSchema)
|
|
58
541
|
});
|
|
59
542
|
function createSession(options) {
|
|
60
543
|
return {
|
|
@@ -75,7 +558,7 @@ function serializeSession(session) {
|
|
|
75
558
|
return stringify(session, { lineWidth: 0 });
|
|
76
559
|
}
|
|
77
560
|
|
|
78
|
-
// browser.ts
|
|
561
|
+
// src/browser.ts
|
|
79
562
|
import { chromium, firefox, webkit } from "playwright";
|
|
80
563
|
import { exec } from "child_process";
|
|
81
564
|
import { promisify } from "util";
|
|
@@ -188,16 +671,18 @@ async function checkBrowsers() {
|
|
|
188
671
|
return results;
|
|
189
672
|
}
|
|
190
673
|
|
|
191
|
-
// recorder.ts
|
|
674
|
+
// src/recorder.ts
|
|
192
675
|
var Recorder = class _Recorder {
|
|
193
676
|
/**
|
|
194
677
|
* Start a new recording session
|
|
195
678
|
* Opens a browser window for the user to interact with
|
|
196
679
|
*/
|
|
197
|
-
static async start(startUrl, options = {}) {
|
|
680
|
+
static async start(startUrl, options = {}, config = {}) {
|
|
681
|
+
const manager = config.pluginManager ?? pluginManager;
|
|
198
682
|
const browserType = options.browser ?? "chromium";
|
|
199
683
|
const viewport = options.viewport ?? { width: 1280, height: 720 };
|
|
200
684
|
const headless = options.headless ?? false;
|
|
685
|
+
await manager.initialize();
|
|
201
686
|
const { browser } = await launchBrowser({
|
|
202
687
|
browser: browserType,
|
|
203
688
|
headless
|
|
@@ -218,18 +703,61 @@ var Recorder = class _Recorder {
|
|
|
218
703
|
stepCounter++;
|
|
219
704
|
return `step_${String(stepCounter).padStart(3, "0")}`;
|
|
220
705
|
};
|
|
221
|
-
|
|
706
|
+
const baseRecordContext = {
|
|
707
|
+
startUrl,
|
|
708
|
+
browser: browserType,
|
|
709
|
+
page,
|
|
710
|
+
engine: { version: "0.2.0", pluginApiVersion: 1 },
|
|
711
|
+
payloads: manager.getPayloads(),
|
|
712
|
+
findings: manager.getFindings(),
|
|
713
|
+
logger: {
|
|
714
|
+
debug: console.debug.bind(console),
|
|
715
|
+
info: console.info.bind(console),
|
|
716
|
+
warn: console.warn.bind(console),
|
|
717
|
+
error: console.error.bind(console)
|
|
718
|
+
},
|
|
719
|
+
fetch: globalThis.fetch
|
|
720
|
+
};
|
|
721
|
+
await manager.callHook("onRecordStart", async (hook, ctx) => {
|
|
722
|
+
const recordCtx = { ...baseRecordContext, ...ctx };
|
|
723
|
+
await hook(recordCtx);
|
|
724
|
+
});
|
|
725
|
+
const initialStep = {
|
|
222
726
|
id: generateStepId(),
|
|
223
727
|
type: "navigate",
|
|
224
728
|
url: startUrl,
|
|
225
729
|
timestamp: 0
|
|
226
|
-
}
|
|
227
|
-
_Recorder.
|
|
730
|
+
};
|
|
731
|
+
const transformedInitialStep = await _Recorder.transformStep(
|
|
732
|
+
initialStep,
|
|
733
|
+
manager,
|
|
734
|
+
baseRecordContext
|
|
735
|
+
);
|
|
736
|
+
if (transformedInitialStep) {
|
|
737
|
+
steps.push(transformedInitialStep);
|
|
738
|
+
}
|
|
739
|
+
_Recorder.attachListeners(
|
|
740
|
+
page,
|
|
741
|
+
steps,
|
|
742
|
+
startTime,
|
|
743
|
+
generateStepId,
|
|
744
|
+
manager,
|
|
745
|
+
baseRecordContext
|
|
746
|
+
);
|
|
228
747
|
return {
|
|
229
748
|
async stop() {
|
|
230
749
|
session.steps = steps;
|
|
750
|
+
let finalSession = session;
|
|
751
|
+
for (const loaded of manager.getPlugins()) {
|
|
752
|
+
const hook = loaded.plugin.hooks?.onRecordEnd;
|
|
753
|
+
if (hook) {
|
|
754
|
+
const ctx = manager.createContext(loaded.config);
|
|
755
|
+
const recordCtx = { ...baseRecordContext, ...ctx };
|
|
756
|
+
finalSession = await hook(finalSession, recordCtx);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
231
759
|
await browser.close();
|
|
232
|
-
return
|
|
760
|
+
return finalSession;
|
|
233
761
|
},
|
|
234
762
|
getSteps() {
|
|
235
763
|
return [...steps];
|
|
@@ -239,8 +767,34 @@ var Recorder = class _Recorder {
|
|
|
239
767
|
}
|
|
240
768
|
};
|
|
241
769
|
}
|
|
242
|
-
|
|
770
|
+
/**
|
|
771
|
+
* Transform a step through plugin hooks
|
|
772
|
+
* Returns null if the step should be filtered out
|
|
773
|
+
*/
|
|
774
|
+
static async transformStep(step, manager, baseContext) {
|
|
775
|
+
let transformedStep = step;
|
|
776
|
+
for (const loaded of manager.getPlugins()) {
|
|
777
|
+
const hook = loaded.plugin.hooks?.onRecordStep;
|
|
778
|
+
if (hook) {
|
|
779
|
+
const ctx = manager.createContext(loaded.config);
|
|
780
|
+
const recordCtx = { ...baseContext, ...ctx };
|
|
781
|
+
transformedStep = await hook(transformedStep, recordCtx);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return transformedStep;
|
|
785
|
+
}
|
|
786
|
+
static attachListeners(page, steps, startTime, generateStepId, manager, baseContext) {
|
|
243
787
|
const getTimestamp = () => Date.now() - startTime;
|
|
788
|
+
const addStep = async (step) => {
|
|
789
|
+
const transformed = await _Recorder.transformStep(
|
|
790
|
+
step,
|
|
791
|
+
manager,
|
|
792
|
+
baseContext
|
|
793
|
+
);
|
|
794
|
+
if (transformed) {
|
|
795
|
+
steps.push(transformed);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
244
798
|
page.on("framenavigated", (frame) => {
|
|
245
799
|
if (frame === page.mainFrame()) {
|
|
246
800
|
const url = frame.url();
|
|
@@ -248,7 +802,7 @@ var Recorder = class _Recorder {
|
|
|
248
802
|
if (steps.length > 0 && lastStep.type === "navigate" && lastStep.url === url) {
|
|
249
803
|
return;
|
|
250
804
|
}
|
|
251
|
-
|
|
805
|
+
addStep({
|
|
252
806
|
id: generateStepId(),
|
|
253
807
|
type: "navigate",
|
|
254
808
|
url,
|
|
@@ -258,12 +812,12 @@ var Recorder = class _Recorder {
|
|
|
258
812
|
});
|
|
259
813
|
page.exposeFunction(
|
|
260
814
|
"__vulcn_record",
|
|
261
|
-
(event) => {
|
|
815
|
+
async (event) => {
|
|
262
816
|
const timestamp = getTimestamp();
|
|
263
817
|
switch (event.type) {
|
|
264
818
|
case "click": {
|
|
265
819
|
const data = event.data;
|
|
266
|
-
|
|
820
|
+
await addStep({
|
|
267
821
|
id: generateStepId(),
|
|
268
822
|
type: "click",
|
|
269
823
|
selector: data.selector,
|
|
@@ -274,7 +828,7 @@ var Recorder = class _Recorder {
|
|
|
274
828
|
}
|
|
275
829
|
case "input": {
|
|
276
830
|
const data = event.data;
|
|
277
|
-
|
|
831
|
+
await addStep({
|
|
278
832
|
id: generateStepId(),
|
|
279
833
|
type: "input",
|
|
280
834
|
selector: data.selector,
|
|
@@ -286,7 +840,7 @@ var Recorder = class _Recorder {
|
|
|
286
840
|
}
|
|
287
841
|
case "keypress": {
|
|
288
842
|
const data = event.data;
|
|
289
|
-
|
|
843
|
+
await addStep({
|
|
290
844
|
id: generateStepId(),
|
|
291
845
|
type: "keypress",
|
|
292
846
|
key: data.key,
|
|
@@ -423,163 +977,156 @@ var Recorder = class _Recorder {
|
|
|
423
977
|
}
|
|
424
978
|
};
|
|
425
979
|
|
|
426
|
-
//
|
|
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
|
|
980
|
+
// src/runner.ts
|
|
539
981
|
var Runner = class _Runner {
|
|
540
982
|
/**
|
|
541
|
-
* Execute a session with security payloads
|
|
983
|
+
* Execute a session with security payloads from plugins
|
|
984
|
+
*
|
|
985
|
+
* @param session - The recorded session to replay
|
|
986
|
+
* @param options - Runner configuration
|
|
987
|
+
* @param config - Plugin manager configuration
|
|
542
988
|
*/
|
|
543
|
-
static async execute(session,
|
|
989
|
+
static async execute(session, options = {}, config = {}) {
|
|
990
|
+
const manager = config.pluginManager ?? pluginManager;
|
|
544
991
|
const browserType = options.browser ?? session.browser ?? "chromium";
|
|
545
992
|
const headless = options.headless ?? true;
|
|
546
993
|
const startTime = Date.now();
|
|
547
|
-
const findings = [];
|
|
548
994
|
const errors = [];
|
|
549
995
|
let payloadsTested = 0;
|
|
996
|
+
await manager.initialize();
|
|
997
|
+
manager.clearFindings();
|
|
998
|
+
const payloads = manager.getPayloads();
|
|
999
|
+
if (payloads.length === 0) {
|
|
1000
|
+
return {
|
|
1001
|
+
findings: [],
|
|
1002
|
+
stepsExecuted: session.steps.length,
|
|
1003
|
+
payloadsTested: 0,
|
|
1004
|
+
duration: Date.now() - startTime,
|
|
1005
|
+
errors: [
|
|
1006
|
+
"No payloads loaded. Add a payload plugin or configure payloads."
|
|
1007
|
+
]
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
550
1010
|
const { browser } = await launchBrowser({
|
|
551
1011
|
browser: browserType,
|
|
552
1012
|
headless
|
|
553
1013
|
});
|
|
554
1014
|
const context = await browser.newContext({ viewport: session.viewport });
|
|
555
1015
|
const page = await context.newPage();
|
|
1016
|
+
const baseRunContext = {
|
|
1017
|
+
session,
|
|
1018
|
+
page,
|
|
1019
|
+
browser: browserType,
|
|
1020
|
+
headless,
|
|
1021
|
+
engine: { version: "0.2.0", pluginApiVersion: 1 },
|
|
1022
|
+
payloads: manager.getPayloads(),
|
|
1023
|
+
findings: manager.getFindings(),
|
|
1024
|
+
logger: {
|
|
1025
|
+
debug: console.debug.bind(console),
|
|
1026
|
+
info: console.info.bind(console),
|
|
1027
|
+
warn: console.warn.bind(console),
|
|
1028
|
+
error: console.error.bind(console)
|
|
1029
|
+
},
|
|
1030
|
+
fetch: globalThis.fetch
|
|
1031
|
+
};
|
|
1032
|
+
await manager.callHook("onRunStart", async (hook, ctx) => {
|
|
1033
|
+
const runCtx = { ...baseRunContext, ...ctx };
|
|
1034
|
+
await hook(runCtx);
|
|
1035
|
+
});
|
|
1036
|
+
const eventFindings = [];
|
|
1037
|
+
let currentDetectContext = null;
|
|
1038
|
+
const dialogHandler = async (dialog) => {
|
|
1039
|
+
if (currentDetectContext) {
|
|
1040
|
+
const findings = await manager.callHookCollect(
|
|
1041
|
+
"onDialog",
|
|
1042
|
+
async (hook, ctx) => {
|
|
1043
|
+
const detectCtx = {
|
|
1044
|
+
...currentDetectContext,
|
|
1045
|
+
...ctx
|
|
1046
|
+
};
|
|
1047
|
+
return hook(dialog, detectCtx);
|
|
1048
|
+
}
|
|
1049
|
+
);
|
|
1050
|
+
eventFindings.push(...findings);
|
|
1051
|
+
}
|
|
1052
|
+
try {
|
|
1053
|
+
await dialog.dismiss();
|
|
1054
|
+
} catch {
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
const consoleHandler = async (msg) => {
|
|
1058
|
+
if (currentDetectContext) {
|
|
1059
|
+
const findings = await manager.callHookCollect("onConsoleMessage", async (hook, ctx) => {
|
|
1060
|
+
const detectCtx = { ...currentDetectContext, ...ctx };
|
|
1061
|
+
return hook(msg, detectCtx);
|
|
1062
|
+
});
|
|
1063
|
+
eventFindings.push(...findings);
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
page.on("dialog", dialogHandler);
|
|
1067
|
+
page.on("console", consoleHandler);
|
|
556
1068
|
try {
|
|
557
1069
|
const injectableSteps = session.steps.filter(
|
|
558
1070
|
(step) => step.type === "input" && step.injectable !== false
|
|
559
1071
|
);
|
|
560
1072
|
const allPayloads = [];
|
|
561
|
-
for (const
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
for (const value of payload.payloads) {
|
|
565
|
-
allPayloads.push({ name, value });
|
|
566
|
-
}
|
|
1073
|
+
for (const payloadSet of payloads) {
|
|
1074
|
+
for (const value of payloadSet.payloads) {
|
|
1075
|
+
allPayloads.push({ payloadSet, value });
|
|
567
1076
|
}
|
|
568
1077
|
}
|
|
569
1078
|
for (const injectableStep of injectableSteps) {
|
|
570
|
-
for (const
|
|
1079
|
+
for (const { payloadSet, value: originalValue } of allPayloads) {
|
|
571
1080
|
try {
|
|
572
|
-
|
|
1081
|
+
let transformedPayload = originalValue;
|
|
1082
|
+
for (const loaded of manager.getPlugins()) {
|
|
1083
|
+
const hook = loaded.plugin.hooks?.onBeforePayload;
|
|
1084
|
+
if (hook) {
|
|
1085
|
+
const ctx = manager.createContext(loaded.config);
|
|
1086
|
+
const runCtx = { ...baseRunContext, ...ctx };
|
|
1087
|
+
transformedPayload = await hook(
|
|
1088
|
+
transformedPayload,
|
|
1089
|
+
injectableStep,
|
|
1090
|
+
runCtx
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
currentDetectContext = {
|
|
1095
|
+
...baseRunContext,
|
|
1096
|
+
config: {},
|
|
1097
|
+
step: injectableStep,
|
|
1098
|
+
payloadSet,
|
|
1099
|
+
payloadValue: transformedPayload,
|
|
1100
|
+
stepId: injectableStep.id
|
|
1101
|
+
};
|
|
1102
|
+
await _Runner.replayWithPayload(
|
|
573
1103
|
page,
|
|
574
1104
|
session,
|
|
575
1105
|
injectableStep,
|
|
576
|
-
|
|
577
|
-
payload.value
|
|
1106
|
+
transformedPayload
|
|
578
1107
|
);
|
|
579
|
-
|
|
580
|
-
|
|
1108
|
+
const afterFindings = await manager.callHookCollect("onAfterPayload", async (hook, ctx) => {
|
|
1109
|
+
const detectCtx = {
|
|
1110
|
+
...currentDetectContext,
|
|
1111
|
+
...ctx
|
|
1112
|
+
};
|
|
1113
|
+
return hook(detectCtx);
|
|
1114
|
+
});
|
|
1115
|
+
const reflectionFinding = await _Runner.checkReflection(
|
|
1116
|
+
page,
|
|
1117
|
+
injectableStep,
|
|
1118
|
+
payloadSet,
|
|
1119
|
+
transformedPayload
|
|
1120
|
+
);
|
|
1121
|
+
const allFindings = [...afterFindings, ...eventFindings];
|
|
1122
|
+
if (reflectionFinding) {
|
|
1123
|
+
allFindings.push(reflectionFinding);
|
|
1124
|
+
}
|
|
1125
|
+
for (const finding of allFindings) {
|
|
1126
|
+
manager.addFinding(finding);
|
|
581
1127
|
options.onFinding?.(finding);
|
|
582
1128
|
}
|
|
1129
|
+
eventFindings.length = 0;
|
|
583
1130
|
payloadsTested++;
|
|
584
1131
|
} catch (err) {
|
|
585
1132
|
errors.push(`${injectableStep.id}: ${String(err)}`);
|
|
@@ -587,73 +1134,94 @@ var Runner = class _Runner {
|
|
|
587
1134
|
}
|
|
588
1135
|
}
|
|
589
1136
|
} finally {
|
|
1137
|
+
page.off("dialog", dialogHandler);
|
|
1138
|
+
page.off("console", consoleHandler);
|
|
1139
|
+
currentDetectContext = null;
|
|
590
1140
|
await browser.close();
|
|
591
1141
|
}
|
|
592
|
-
|
|
593
|
-
findings,
|
|
1142
|
+
let result = {
|
|
1143
|
+
findings: manager.getFindings(),
|
|
594
1144
|
stepsExecuted: session.steps.length,
|
|
595
1145
|
payloadsTested,
|
|
596
1146
|
duration: Date.now() - startTime,
|
|
597
1147
|
errors
|
|
598
1148
|
};
|
|
1149
|
+
for (const loaded of manager.getPlugins()) {
|
|
1150
|
+
const hook = loaded.plugin.hooks?.onRunEnd;
|
|
1151
|
+
if (hook) {
|
|
1152
|
+
const ctx = manager.createContext(loaded.config);
|
|
1153
|
+
const runCtx = { ...baseRunContext, ...ctx };
|
|
1154
|
+
result = await hook(result, runCtx);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return result;
|
|
599
1158
|
}
|
|
600
|
-
|
|
601
|
-
|
|
1159
|
+
/**
|
|
1160
|
+
* Execute with explicit payloads (legacy API, for backwards compatibility)
|
|
1161
|
+
*/
|
|
1162
|
+
static async executeWithPayloads(session, payloads, options = {}) {
|
|
1163
|
+
const manager = new PluginManager();
|
|
1164
|
+
manager.addPayloads(payloads);
|
|
1165
|
+
return _Runner.execute(session, options, { pluginManager: manager });
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Replay session steps with payload injected at target step
|
|
1169
|
+
*/
|
|
1170
|
+
static async replayWithPayload(page, session, targetStep, payloadValue) {
|
|
1171
|
+
await page.goto(session.startUrl, { waitUntil: "domcontentloaded" });
|
|
602
1172
|
for (const step of session.steps) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1173
|
+
try {
|
|
1174
|
+
if (step.type === "navigate") {
|
|
1175
|
+
await page.goto(step.url, { waitUntil: "domcontentloaded" });
|
|
1176
|
+
} else if (step.type === "click") {
|
|
1177
|
+
await page.click(step.selector, { timeout: 5e3 });
|
|
1178
|
+
} else if (step.type === "input") {
|
|
1179
|
+
const value = step.id === targetStep.id ? payloadValue : step.value;
|
|
1180
|
+
await page.fill(step.selector, value, { timeout: 5e3 });
|
|
1181
|
+
} else if (step.type === "keypress") {
|
|
1182
|
+
const modifiers = step.modifiers ?? [];
|
|
1183
|
+
for (const mod of modifiers) {
|
|
1184
|
+
await page.keyboard.down(
|
|
1185
|
+
mod
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
await page.keyboard.press(step.key);
|
|
1189
|
+
for (const mod of modifiers.reverse()) {
|
|
1190
|
+
await page.keyboard.up(mod);
|
|
1191
|
+
}
|
|
618
1192
|
}
|
|
1193
|
+
} catch {
|
|
619
1194
|
}
|
|
620
1195
|
if (step.id === targetStep.id) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
targetStep,
|
|
624
|
-
payloadName,
|
|
625
|
-
payloadValue
|
|
626
|
-
);
|
|
627
|
-
if (finding) {
|
|
628
|
-
return finding;
|
|
629
|
-
}
|
|
1196
|
+
await page.waitForTimeout(100);
|
|
1197
|
+
break;
|
|
630
1198
|
}
|
|
631
1199
|
}
|
|
632
|
-
return void 0;
|
|
633
1200
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1201
|
+
/**
|
|
1202
|
+
* Basic reflection check - fallback when no detection plugin is loaded
|
|
1203
|
+
*/
|
|
1204
|
+
static async checkReflection(page, step, payloadSet, payloadValue) {
|
|
637
1205
|
const content = await page.content();
|
|
638
|
-
for (const pattern of
|
|
1206
|
+
for (const pattern of payloadSet.detectPatterns) {
|
|
639
1207
|
if (pattern.test(content)) {
|
|
640
1208
|
return {
|
|
641
|
-
type:
|
|
642
|
-
severity:
|
|
643
|
-
title: `${
|
|
644
|
-
description: `Payload was reflected in page content`,
|
|
1209
|
+
type: payloadSet.category,
|
|
1210
|
+
severity: _Runner.getSeverity(payloadSet.category),
|
|
1211
|
+
title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
|
|
1212
|
+
description: `Payload pattern was reflected in page content`,
|
|
645
1213
|
stepId: step.id,
|
|
646
1214
|
payload: payloadValue,
|
|
647
1215
|
url: page.url(),
|
|
648
|
-
evidence: content.match(pattern)?.[0]
|
|
1216
|
+
evidence: content.match(pattern)?.[0]?.slice(0, 200)
|
|
649
1217
|
};
|
|
650
1218
|
}
|
|
651
1219
|
}
|
|
652
1220
|
if (content.includes(payloadValue)) {
|
|
653
1221
|
return {
|
|
654
|
-
type:
|
|
1222
|
+
type: payloadSet.category,
|
|
655
1223
|
severity: "medium",
|
|
656
|
-
title: `Potential ${
|
|
1224
|
+
title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
|
|
657
1225
|
description: `Payload was reflected in page without encoding`,
|
|
658
1226
|
stepId: step.id,
|
|
659
1227
|
payload: payloadValue,
|
|
@@ -662,22 +1230,43 @@ var Runner = class _Runner {
|
|
|
662
1230
|
}
|
|
663
1231
|
return void 0;
|
|
664
1232
|
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Determine severity based on vulnerability category
|
|
1235
|
+
*/
|
|
1236
|
+
static getSeverity(category) {
|
|
1237
|
+
switch (category) {
|
|
1238
|
+
case "sqli":
|
|
1239
|
+
case "command-injection":
|
|
1240
|
+
case "xxe":
|
|
1241
|
+
return "critical";
|
|
1242
|
+
case "xss":
|
|
1243
|
+
case "ssrf":
|
|
1244
|
+
case "path-traversal":
|
|
1245
|
+
return "high";
|
|
1246
|
+
case "open-redirect":
|
|
1247
|
+
return "medium";
|
|
1248
|
+
default:
|
|
1249
|
+
return "medium";
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
665
1252
|
};
|
|
666
1253
|
export {
|
|
667
|
-
BUILTIN_PAYLOADS,
|
|
668
1254
|
BrowserNotFoundError,
|
|
1255
|
+
DRIVER_API_VERSION,
|
|
1256
|
+
DriverManager,
|
|
1257
|
+
PLUGIN_API_VERSION,
|
|
1258
|
+
PluginManager,
|
|
669
1259
|
Recorder,
|
|
670
1260
|
Runner,
|
|
671
1261
|
SessionSchema,
|
|
672
1262
|
StepSchema,
|
|
673
1263
|
checkBrowsers,
|
|
674
1264
|
createSession,
|
|
675
|
-
|
|
676
|
-
getPayloadNames,
|
|
677
|
-
getPayloadsByCategory,
|
|
1265
|
+
driverManager,
|
|
678
1266
|
installBrowsers,
|
|
679
1267
|
launchBrowser,
|
|
680
1268
|
parseSession,
|
|
1269
|
+
pluginManager,
|
|
681
1270
|
serializeSession
|
|
682
1271
|
};
|
|
683
1272
|
//# sourceMappingURL=index.js.map
|