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