@vulcn/engine 0.5.0 → 0.8.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 +50 -0
- package/dist/index.cjs +425 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +273 -2
- package/dist/index.d.ts +273 -2
- package/dist/index.js +416 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 15d8504: ### Authenticated Scanning
|
|
8
|
+
|
|
9
|
+
End-to-end support for scanning applications behind login pages.
|
|
10
|
+
|
|
11
|
+
#### `@vulcn/engine`
|
|
12
|
+
- **Credential encryption module** (`src/auth.ts`): AES-256-GCM encryption/decryption for credentials and Playwright storage state, with PBKDF2 key derivation (600k iterations)
|
|
13
|
+
- **Auth types**: `FormCredentials`, `HeaderCredentials`, `AuthConfig` with session expiry detection config
|
|
14
|
+
- **Scan-level hooks**: `onScanStart` / `onScanEnd` — fire once per scan wrapping all sessions, with `ScanContext` providing full session list and scan metadata
|
|
15
|
+
- **`onScanEnd` result transformation**: uses `callHookPipe` so plugins can transform the aggregate `RunResult` (e.g. deduplication, risk scoring)
|
|
16
|
+
- **v2 session format**: `.vulcn/` directory structure with manifest, encrypted auth state, and config alongside session files
|
|
17
|
+
- **`CrawlOptions.storageState`**: pass authenticated browser state (cookies + localStorage) to the crawler
|
|
18
|
+
- **New exports**: `ScanContext`, `encryptCredentials`, `decryptCredentials`, `encryptStorageState`, `decryptStorageState`, `getPassphrase`
|
|
19
|
+
|
|
20
|
+
#### `@vulcn/driver-browser`
|
|
21
|
+
- **Authenticated crawling**: `crawlAndBuildSessions` accepts `storageState` via `CrawlOptions` and injects it into the Playwright browser context
|
|
22
|
+
- **Authenticated scanning**: `BrowserRunner` reads `storageState` from `RunOptions` and applies it to the scanner's browser context
|
|
23
|
+
- **Login form auto-detection**: `performLogin` navigates to the login URL, auto-detects username/password fields, fills credentials, and submits
|
|
24
|
+
- **Storage state capture**: after successful login, captures full browser storage state (cookies, localStorage, sessionStorage)
|
|
25
|
+
|
|
26
|
+
#### `vulcn` (CLI)
|
|
27
|
+
- **`vulcn store`**: new command to encrypt and save credentials (form-based or header-based) to `.vulcn/auth.enc`
|
|
28
|
+
- **`vulcn crawl --creds`**: decrypt credentials → perform login → capture storage state → crawl all authenticated pages
|
|
29
|
+
- **`vulcn run --creds`**: decrypt credentials → perform login → inject storage state into scanner browser context → run all payloads authenticated
|
|
30
|
+
- **Auth state persistence**: crawl saves encrypted auth state + config alongside sessions in the output directory
|
|
31
|
+
|
|
32
|
+
## 0.7.0
|
|
33
|
+
|
|
34
|
+
### Minor Changes
|
|
35
|
+
|
|
36
|
+
- 458572e: ### @vulcn/engine
|
|
37
|
+
- **`addFinding` on PluginContext**: Plugins now have `ctx.addFinding()` to report findings through the proper callback chain. This ensures consumers are notified via `onFinding` and findings survive timeouts. Plugins should use this instead of `ctx.findings.push()`.
|
|
38
|
+
- **`onPageReady` callback**: New `RunOptions.onPageReady` callback fires after the driver creates the browser page. The engine uses this to defer `onRunStart` plugin hooks until the page is ready, so plugins receive a real page object (not `null`).
|
|
39
|
+
- **`onBeforeClose` hook**: New plugin lifecycle hook called before the browser is closed. Plugins can flush in-flight async work here (e.g., pending response handlers that need browser access).
|
|
40
|
+
- **`onBeforeClose` callback**: New `RunOptions.onBeforeClose` callback fires before browser teardown, triggering plugin `onBeforeClose` hooks.
|
|
41
|
+
|
|
42
|
+
### @vulcn/driver-browser
|
|
43
|
+
- **`onPageReady` signal**: Runner now calls `ctx.options.onPageReady(page)` after creating the browser page, enabling plugins to attach event listeners before any navigation occurs.
|
|
44
|
+
- **`onBeforeClose` signal**: Runner now calls `ctx.options.onBeforeClose(page)` before `browser.close()`, giving plugins time to drain pending async work.
|
|
45
|
+
- **Payload interleaving**: Payloads are now ordered round-robin across categories (e.g., `[sqli1, xss1, sqli2, xss2, ...]`) instead of sequentially. This ensures faster category coverage and earlier dedup early-breaks on slow SPAs.
|
|
46
|
+
- **Extended dedup early-break**: The per-step category dedup now treats any finding (dialog, console, or reflection) as confirmation, not just dialog-based detections. One confirmed finding per category per step is sufficient.
|
|
47
|
+
|
|
48
|
+
### @vulcn/plugin-passive
|
|
49
|
+
- **Uses `ctx.addFinding()`**: All findings are now reported through the proper callback chain instead of pushing to `ctx.findings` directly. This fixes passive findings being invisible to `onFinding` consumers.
|
|
50
|
+
- **Cross-session dedup**: `reportedFindings` is no longer cleared between sessions, so the same passive finding (e.g., "Missing CSP" on the same origin) is reported once per scan, not once per crawled form.
|
|
51
|
+
- **Async handler drain**: Response handlers are tracked as promises and drained in the new `onBeforeClose` hook, preventing findings from being lost when the browser closes before async `response.allHeaders()` calls complete.
|
|
52
|
+
|
|
3
53
|
## 0.5.0
|
|
4
54
|
|
|
5
55
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -34,8 +34,21 @@ __export(index_exports, {
|
|
|
34
34
|
DriverManager: () => DriverManager,
|
|
35
35
|
PLUGIN_API_VERSION: () => PLUGIN_API_VERSION,
|
|
36
36
|
PluginManager: () => PluginManager,
|
|
37
|
+
decrypt: () => decrypt,
|
|
38
|
+
decryptCredentials: () => decryptCredentials,
|
|
39
|
+
decryptStorageState: () => decryptStorageState,
|
|
37
40
|
driverManager: () => driverManager,
|
|
38
|
-
|
|
41
|
+
encrypt: () => encrypt,
|
|
42
|
+
encryptCredentials: () => encryptCredentials,
|
|
43
|
+
encryptStorageState: () => encryptStorageState,
|
|
44
|
+
getPassphrase: () => getPassphrase,
|
|
45
|
+
isSessionDir: () => isSessionDir,
|
|
46
|
+
loadSessionDir: () => loadSessionDir,
|
|
47
|
+
looksLikeSessionDir: () => looksLikeSessionDir,
|
|
48
|
+
pluginManager: () => pluginManager,
|
|
49
|
+
readAuthState: () => readAuthState,
|
|
50
|
+
readCapturedRequests: () => readCapturedRequests,
|
|
51
|
+
saveSessionDir: () => saveSessionDir
|
|
39
52
|
});
|
|
40
53
|
module.exports = __toCommonJS(index_exports);
|
|
41
54
|
|
|
@@ -197,23 +210,17 @@ var DriverManager = class {
|
|
|
197
210
|
/**
|
|
198
211
|
* Execute a session
|
|
199
212
|
* Invokes plugin hooks (onRunStart, onRunEnd) around the driver runner.
|
|
213
|
+
* Plugin onRunStart is deferred until the driver signals the page is ready
|
|
214
|
+
* via the onPageReady callback, ensuring plugins get a real page object.
|
|
200
215
|
*/
|
|
201
216
|
async execute(session, pluginManager2, options = {}) {
|
|
202
217
|
const driver = this.getForSession(session);
|
|
203
218
|
const findings = [];
|
|
204
219
|
const logger = this.createLogger(driver.name);
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
findings,
|
|
210
|
-
addFinding: (finding) => {
|
|
211
|
-
findings.push(finding);
|
|
212
|
-
pluginManager2.addFinding(finding);
|
|
213
|
-
options.onFinding?.(finding);
|
|
214
|
-
},
|
|
215
|
-
logger,
|
|
216
|
-
options
|
|
220
|
+
const addFinding = (finding) => {
|
|
221
|
+
findings.push(finding);
|
|
222
|
+
pluginManager2.addFinding(finding);
|
|
223
|
+
options.onFinding?.(finding);
|
|
217
224
|
};
|
|
218
225
|
const pluginCtx = {
|
|
219
226
|
session,
|
|
@@ -223,21 +230,57 @@ var DriverManager = class {
|
|
|
223
230
|
engine: { version: "0.3.0", pluginApiVersion: 1 },
|
|
224
231
|
payloads: pluginManager2.getPayloads(),
|
|
225
232
|
findings,
|
|
233
|
+
addFinding,
|
|
226
234
|
logger,
|
|
227
235
|
fetch: globalThis.fetch
|
|
228
236
|
};
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
const ctx = {
|
|
238
|
+
session,
|
|
239
|
+
pluginManager: pluginManager2,
|
|
240
|
+
payloads: pluginManager2.getPayloads(),
|
|
241
|
+
findings,
|
|
242
|
+
addFinding,
|
|
243
|
+
logger,
|
|
244
|
+
options: {
|
|
245
|
+
...options,
|
|
246
|
+
// Provide onPageReady callback — fires plugin onRunStart hooks
|
|
247
|
+
// with the real page object once the driver has created it
|
|
248
|
+
onPageReady: async (page) => {
|
|
249
|
+
pluginCtx.page = page;
|
|
250
|
+
for (const loaded of pluginManager2.getPlugins()) {
|
|
251
|
+
if (loaded.enabled && loaded.plugin.hooks?.onRunStart) {
|
|
252
|
+
try {
|
|
253
|
+
await loaded.plugin.hooks.onRunStart({
|
|
254
|
+
...pluginCtx,
|
|
255
|
+
config: loaded.config
|
|
256
|
+
});
|
|
257
|
+
} catch (err) {
|
|
258
|
+
logger.warn(
|
|
259
|
+
`Plugin ${loaded.plugin.name} onRunStart failed: ${err}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
// Fires before browser closes — lets plugins flush pending async work
|
|
266
|
+
onBeforeClose: async (_page) => {
|
|
267
|
+
for (const loaded of pluginManager2.getPlugins()) {
|
|
268
|
+
if (loaded.enabled && loaded.plugin.hooks?.onBeforeClose) {
|
|
269
|
+
try {
|
|
270
|
+
await loaded.plugin.hooks.onBeforeClose({
|
|
271
|
+
...pluginCtx,
|
|
272
|
+
config: loaded.config
|
|
273
|
+
});
|
|
274
|
+
} catch (err) {
|
|
275
|
+
logger.warn(
|
|
276
|
+
`Plugin ${loaded.plugin.name} onBeforeClose failed: ${err}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
238
281
|
}
|
|
239
282
|
}
|
|
240
|
-
}
|
|
283
|
+
};
|
|
241
284
|
let result = await driver.runner.execute(session, ctx);
|
|
242
285
|
for (const loaded of pluginManager2.getPlugins()) {
|
|
243
286
|
if (loaded.enabled && loaded.plugin.hooks?.onRunEnd) {
|
|
@@ -254,6 +297,109 @@ var DriverManager = class {
|
|
|
254
297
|
}
|
|
255
298
|
return result;
|
|
256
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Execute multiple sessions with a shared browser (scan-level orchestration).
|
|
302
|
+
*
|
|
303
|
+
* This is the preferred entry point for running a full scan. It:
|
|
304
|
+
* 1. Launches ONE browser for the entire scan
|
|
305
|
+
* 2. Passes the browser to each session's runner via options.browser
|
|
306
|
+
* 3. Each session creates its own context (lightweight, isolated cookies)
|
|
307
|
+
* 4. Aggregates results across all sessions
|
|
308
|
+
* 5. Closes the browser once at the end
|
|
309
|
+
*
|
|
310
|
+
* This is 5-10x faster than calling execute() per session because
|
|
311
|
+
* launching a browser takes 2-3 seconds.
|
|
312
|
+
*/
|
|
313
|
+
async executeScan(sessions, pluginManager2, options = {}) {
|
|
314
|
+
if (sessions.length === 0) {
|
|
315
|
+
const empty = {
|
|
316
|
+
findings: [],
|
|
317
|
+
stepsExecuted: 0,
|
|
318
|
+
payloadsTested: 0,
|
|
319
|
+
duration: 0,
|
|
320
|
+
errors: ["No sessions to execute"]
|
|
321
|
+
};
|
|
322
|
+
return { results: [], aggregate: empty };
|
|
323
|
+
}
|
|
324
|
+
const startTime = Date.now();
|
|
325
|
+
const results = [];
|
|
326
|
+
const allFindings = [];
|
|
327
|
+
let totalSteps = 0;
|
|
328
|
+
let totalPayloads = 0;
|
|
329
|
+
const allErrors = [];
|
|
330
|
+
const firstDriver = this.getForSession(sessions[0]);
|
|
331
|
+
let sharedBrowser = null;
|
|
332
|
+
if (firstDriver.name === "browser") {
|
|
333
|
+
try {
|
|
334
|
+
const driverPkg = "@vulcn/driver-browser";
|
|
335
|
+
const { launchBrowser } = await import(
|
|
336
|
+
/* @vite-ignore */
|
|
337
|
+
driverPkg
|
|
338
|
+
);
|
|
339
|
+
const browserType = sessions[0].driverConfig.browser ?? "chromium";
|
|
340
|
+
const headless = options.headless ?? true;
|
|
341
|
+
const result = await launchBrowser({
|
|
342
|
+
browser: browserType,
|
|
343
|
+
headless
|
|
344
|
+
});
|
|
345
|
+
sharedBrowser = result.browser;
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
await pluginManager2.callHook("onScanStart", async (hook, ctx) => {
|
|
351
|
+
const scanCtx = {
|
|
352
|
+
...ctx,
|
|
353
|
+
sessions,
|
|
354
|
+
headless: options.headless ?? true,
|
|
355
|
+
sessionCount: sessions.length
|
|
356
|
+
};
|
|
357
|
+
await hook(scanCtx);
|
|
358
|
+
});
|
|
359
|
+
for (const session of sessions) {
|
|
360
|
+
const sessionOptions = {
|
|
361
|
+
...options,
|
|
362
|
+
...sharedBrowser ? { browser: sharedBrowser } : {}
|
|
363
|
+
};
|
|
364
|
+
const result = await this.execute(
|
|
365
|
+
session,
|
|
366
|
+
pluginManager2,
|
|
367
|
+
sessionOptions
|
|
368
|
+
);
|
|
369
|
+
results.push(result);
|
|
370
|
+
allFindings.push(...result.findings);
|
|
371
|
+
totalSteps += result.stepsExecuted;
|
|
372
|
+
totalPayloads += result.payloadsTested;
|
|
373
|
+
allErrors.push(...result.errors);
|
|
374
|
+
}
|
|
375
|
+
} finally {
|
|
376
|
+
if (sharedBrowser && typeof sharedBrowser.close === "function") {
|
|
377
|
+
await sharedBrowser.close();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const aggregate = {
|
|
381
|
+
findings: allFindings,
|
|
382
|
+
stepsExecuted: totalSteps,
|
|
383
|
+
payloadsTested: totalPayloads,
|
|
384
|
+
duration: Date.now() - startTime,
|
|
385
|
+
errors: allErrors
|
|
386
|
+
};
|
|
387
|
+
let finalAggregate = aggregate;
|
|
388
|
+
finalAggregate = await pluginManager2.callHookPipe(
|
|
389
|
+
"onScanEnd",
|
|
390
|
+
finalAggregate,
|
|
391
|
+
async (hook, value, ctx) => {
|
|
392
|
+
const scanCtx = {
|
|
393
|
+
...ctx,
|
|
394
|
+
sessions,
|
|
395
|
+
headless: options.headless ?? true,
|
|
396
|
+
sessionCount: sessions.length
|
|
397
|
+
};
|
|
398
|
+
return await hook(value, scanCtx);
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
return { results, aggregate: finalAggregate };
|
|
402
|
+
}
|
|
257
403
|
/**
|
|
258
404
|
* Validate driver structure
|
|
259
405
|
*/
|
|
@@ -528,6 +674,9 @@ var PluginManager = class {
|
|
|
528
674
|
engine: engineInfo,
|
|
529
675
|
payloads: this.sharedPayloads,
|
|
530
676
|
findings: this.sharedFindings,
|
|
677
|
+
addFinding: (finding) => {
|
|
678
|
+
this.sharedFindings.push(finding);
|
|
679
|
+
},
|
|
531
680
|
logger: this.createLogger("plugin"),
|
|
532
681
|
fetch: globalThis.fetch
|
|
533
682
|
};
|
|
@@ -624,13 +773,265 @@ var PluginManager = class {
|
|
|
624
773
|
}
|
|
625
774
|
};
|
|
626
775
|
var pluginManager = new PluginManager();
|
|
776
|
+
|
|
777
|
+
// src/auth.ts
|
|
778
|
+
var import_node_crypto = require("crypto");
|
|
779
|
+
var ALGORITHM = "aes-256-gcm";
|
|
780
|
+
var KEY_LENGTH = 32;
|
|
781
|
+
var IV_LENGTH = 16;
|
|
782
|
+
var SALT_LENGTH = 32;
|
|
783
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
784
|
+
var PBKDF2_DIGEST = "sha512";
|
|
785
|
+
function deriveKey(passphrase, salt) {
|
|
786
|
+
return (0, import_node_crypto.pbkdf2Sync)(
|
|
787
|
+
passphrase,
|
|
788
|
+
salt,
|
|
789
|
+
PBKDF2_ITERATIONS,
|
|
790
|
+
KEY_LENGTH,
|
|
791
|
+
PBKDF2_DIGEST
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
function encrypt(data, passphrase) {
|
|
795
|
+
const salt = (0, import_node_crypto.randomBytes)(SALT_LENGTH);
|
|
796
|
+
const iv = (0, import_node_crypto.randomBytes)(IV_LENGTH);
|
|
797
|
+
const key = deriveKey(passphrase, salt);
|
|
798
|
+
const cipher = (0, import_node_crypto.createCipheriv)(ALGORITHM, key, iv);
|
|
799
|
+
let encrypted = cipher.update(data, "utf8", "hex");
|
|
800
|
+
encrypted += cipher.final("hex");
|
|
801
|
+
const tag = cipher.getAuthTag();
|
|
802
|
+
const payload = {
|
|
803
|
+
version: 1,
|
|
804
|
+
salt: salt.toString("hex"),
|
|
805
|
+
iv: iv.toString("hex"),
|
|
806
|
+
tag: tag.toString("hex"),
|
|
807
|
+
data: encrypted,
|
|
808
|
+
iterations: PBKDF2_ITERATIONS
|
|
809
|
+
};
|
|
810
|
+
return JSON.stringify(payload);
|
|
811
|
+
}
|
|
812
|
+
function decrypt(encrypted, passphrase) {
|
|
813
|
+
const payload = JSON.parse(encrypted);
|
|
814
|
+
if (payload.version !== 1) {
|
|
815
|
+
throw new Error(`Unsupported encryption version: ${payload.version}`);
|
|
816
|
+
}
|
|
817
|
+
const salt = Buffer.from(payload.salt, "hex");
|
|
818
|
+
const iv = Buffer.from(payload.iv, "hex");
|
|
819
|
+
const tag = Buffer.from(payload.tag, "hex");
|
|
820
|
+
const key = deriveKey(passphrase, salt);
|
|
821
|
+
const decipher = (0, import_node_crypto.createDecipheriv)(ALGORITHM, key, iv);
|
|
822
|
+
decipher.setAuthTag(tag);
|
|
823
|
+
let decrypted = decipher.update(payload.data, "hex", "utf8");
|
|
824
|
+
decrypted += decipher.final("utf8");
|
|
825
|
+
return decrypted;
|
|
826
|
+
}
|
|
827
|
+
function encryptCredentials(credentials, passphrase) {
|
|
828
|
+
return encrypt(JSON.stringify(credentials), passphrase);
|
|
829
|
+
}
|
|
830
|
+
function decryptCredentials(encrypted, passphrase) {
|
|
831
|
+
const json = decrypt(encrypted, passphrase);
|
|
832
|
+
return JSON.parse(json);
|
|
833
|
+
}
|
|
834
|
+
function encryptStorageState(storageState, passphrase) {
|
|
835
|
+
return encrypt(storageState, passphrase);
|
|
836
|
+
}
|
|
837
|
+
function decryptStorageState(encrypted, passphrase) {
|
|
838
|
+
return decrypt(encrypted, passphrase);
|
|
839
|
+
}
|
|
840
|
+
function getPassphrase(interactive) {
|
|
841
|
+
if (interactive) return interactive;
|
|
842
|
+
const envKey = process.env.VULCN_KEY;
|
|
843
|
+
if (envKey) return envKey;
|
|
844
|
+
throw new Error(
|
|
845
|
+
"No passphrase provided. Set VULCN_KEY environment variable or pass --passphrase."
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/session.ts
|
|
850
|
+
var import_promises2 = require("fs/promises");
|
|
851
|
+
var import_node_fs2 = require("fs");
|
|
852
|
+
var import_node_path3 = require("path");
|
|
853
|
+
var import_yaml3 = require("yaml");
|
|
854
|
+
async function loadSessionDir(dirPath) {
|
|
855
|
+
const manifestPath = (0, import_node_path3.join)(dirPath, "manifest.yml");
|
|
856
|
+
if (!(0, import_node_fs2.existsSync)(manifestPath)) {
|
|
857
|
+
throw new Error(
|
|
858
|
+
`No manifest.yml found in ${dirPath}. Is this a v2 session directory?`
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
const manifestYaml = await (0, import_promises2.readFile)(manifestPath, "utf-8");
|
|
862
|
+
const manifest = (0, import_yaml3.parse)(manifestYaml);
|
|
863
|
+
if (manifest.version !== "2") {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`Unsupported session format version: ${manifest.version}. Expected "2".`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
let authConfig;
|
|
869
|
+
if (manifest.auth?.configFile) {
|
|
870
|
+
const authPath = (0, import_node_path3.join)(dirPath, manifest.auth.configFile);
|
|
871
|
+
if ((0, import_node_fs2.existsSync)(authPath)) {
|
|
872
|
+
const authYaml = await (0, import_promises2.readFile)(authPath, "utf-8");
|
|
873
|
+
authConfig = (0, import_yaml3.parse)(authYaml);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const sessions = [];
|
|
877
|
+
for (const ref of manifest.sessions) {
|
|
878
|
+
if (ref.injectable === false) continue;
|
|
879
|
+
const sessionPath = (0, import_node_path3.join)(dirPath, ref.file);
|
|
880
|
+
if (!(0, import_node_fs2.existsSync)(sessionPath)) {
|
|
881
|
+
console.warn(`Session file not found: ${sessionPath}, skipping`);
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const sessionYaml = await (0, import_promises2.readFile)(sessionPath, "utf-8");
|
|
885
|
+
const sessionData = (0, import_yaml3.parse)(sessionYaml);
|
|
886
|
+
const session = {
|
|
887
|
+
name: sessionData.name ?? (0, import_node_path3.basename)(ref.file, ".yml"),
|
|
888
|
+
driver: manifest.driver,
|
|
889
|
+
driverConfig: {
|
|
890
|
+
...manifest.driverConfig,
|
|
891
|
+
startUrl: resolveUrl(
|
|
892
|
+
manifest.target,
|
|
893
|
+
sessionData.page
|
|
894
|
+
)
|
|
895
|
+
},
|
|
896
|
+
steps: sessionData.steps ?? [],
|
|
897
|
+
metadata: {
|
|
898
|
+
recordedAt: manifest.recordedAt,
|
|
899
|
+
version: "2",
|
|
900
|
+
manifestDir: dirPath
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
sessions.push(session);
|
|
904
|
+
}
|
|
905
|
+
return { manifest, sessions, authConfig };
|
|
906
|
+
}
|
|
907
|
+
function isSessionDir(path) {
|
|
908
|
+
return (0, import_node_fs2.existsSync)((0, import_node_path3.join)(path, "manifest.yml"));
|
|
909
|
+
}
|
|
910
|
+
function looksLikeSessionDir(path) {
|
|
911
|
+
return path.endsWith(".vulcn") || path.endsWith(".vulcn/");
|
|
912
|
+
}
|
|
913
|
+
async function saveSessionDir(dirPath, options) {
|
|
914
|
+
await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "sessions"), { recursive: true });
|
|
915
|
+
const sessionRefs = [];
|
|
916
|
+
for (const session of options.sessions) {
|
|
917
|
+
const safeName = slugify(session.name);
|
|
918
|
+
const fileName = `sessions/${safeName}.yml`;
|
|
919
|
+
const sessionPath = (0, import_node_path3.join)(dirPath, fileName);
|
|
920
|
+
const startUrl = session.driverConfig.startUrl;
|
|
921
|
+
const page = startUrl ? startUrl.replace(options.target, "").replace(/^\//, "/") : void 0;
|
|
922
|
+
const sessionData = {
|
|
923
|
+
name: session.name,
|
|
924
|
+
...page ? { page } : {},
|
|
925
|
+
steps: session.steps
|
|
926
|
+
};
|
|
927
|
+
await (0, import_promises2.writeFile)(sessionPath, (0, import_yaml3.stringify)(sessionData), "utf-8");
|
|
928
|
+
const hasInjectable = session.steps.some(
|
|
929
|
+
(s) => s.type === "browser.input" && s.injectable !== false
|
|
930
|
+
);
|
|
931
|
+
sessionRefs.push({
|
|
932
|
+
file: fileName,
|
|
933
|
+
injectable: hasInjectable
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
if (options.authConfig) {
|
|
937
|
+
await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "auth"), { recursive: true });
|
|
938
|
+
await (0, import_promises2.writeFile)(
|
|
939
|
+
(0, import_node_path3.join)(dirPath, "auth", "config.yml"),
|
|
940
|
+
(0, import_yaml3.stringify)(options.authConfig),
|
|
941
|
+
"utf-8"
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
if (options.encryptedState) {
|
|
945
|
+
await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "auth"), { recursive: true });
|
|
946
|
+
await (0, import_promises2.writeFile)(
|
|
947
|
+
(0, import_node_path3.join)(dirPath, "auth", "state.enc"),
|
|
948
|
+
options.encryptedState,
|
|
949
|
+
"utf-8"
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (options.requests && options.requests.length > 0) {
|
|
953
|
+
await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "requests"), { recursive: true });
|
|
954
|
+
for (const req of options.requests) {
|
|
955
|
+
const safeName = slugify(req.sessionName);
|
|
956
|
+
await (0, import_promises2.writeFile)(
|
|
957
|
+
(0, import_node_path3.join)(dirPath, "requests", `${safeName}.json`),
|
|
958
|
+
JSON.stringify(req, null, 2),
|
|
959
|
+
"utf-8"
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const manifest = {
|
|
964
|
+
version: "2",
|
|
965
|
+
name: options.name,
|
|
966
|
+
target: options.target,
|
|
967
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
968
|
+
driver: options.driver,
|
|
969
|
+
driverConfig: options.driverConfig,
|
|
970
|
+
...options.authConfig ? {
|
|
971
|
+
auth: {
|
|
972
|
+
strategy: options.authConfig.strategy,
|
|
973
|
+
configFile: "auth/config.yml",
|
|
974
|
+
stateFile: options.encryptedState ? "auth/state.enc" : void 0,
|
|
975
|
+
loggedInIndicator: options.authConfig.loggedInIndicator,
|
|
976
|
+
loggedOutIndicator: options.authConfig.loggedOutIndicator
|
|
977
|
+
}
|
|
978
|
+
} : {},
|
|
979
|
+
sessions: sessionRefs,
|
|
980
|
+
scan: {
|
|
981
|
+
tier: "auto",
|
|
982
|
+
parallel: 1,
|
|
983
|
+
timeout: 12e4
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
await (0, import_promises2.writeFile)((0, import_node_path3.join)(dirPath, "manifest.yml"), (0, import_yaml3.stringify)(manifest), "utf-8");
|
|
987
|
+
}
|
|
988
|
+
async function readAuthState(dirPath) {
|
|
989
|
+
const statePath = (0, import_node_path3.join)(dirPath, "auth", "state.enc");
|
|
990
|
+
if (!(0, import_node_fs2.existsSync)(statePath)) return null;
|
|
991
|
+
return (0, import_promises2.readFile)(statePath, "utf-8");
|
|
992
|
+
}
|
|
993
|
+
async function readCapturedRequests(dirPath) {
|
|
994
|
+
const requestsDir = (0, import_node_path3.join)(dirPath, "requests");
|
|
995
|
+
if (!(0, import_node_fs2.existsSync)(requestsDir)) return [];
|
|
996
|
+
const files = await (0, import_promises2.readdir)(requestsDir);
|
|
997
|
+
const requests = [];
|
|
998
|
+
for (const file of files) {
|
|
999
|
+
if (!file.endsWith(".json")) continue;
|
|
1000
|
+
const content = await (0, import_promises2.readFile)((0, import_node_path3.join)(requestsDir, file), "utf-8");
|
|
1001
|
+
requests.push(JSON.parse(content));
|
|
1002
|
+
}
|
|
1003
|
+
return requests;
|
|
1004
|
+
}
|
|
1005
|
+
function resolveUrl(target, page) {
|
|
1006
|
+
if (!page) return target;
|
|
1007
|
+
if (page.startsWith("http")) return page;
|
|
1008
|
+
const base = target.replace(/\/$/, "");
|
|
1009
|
+
const path = page.startsWith("/") ? page : `/${page}`;
|
|
1010
|
+
return `${base}${path}`;
|
|
1011
|
+
}
|
|
1012
|
+
function slugify(text) {
|
|
1013
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
1014
|
+
}
|
|
627
1015
|
// Annotate the CommonJS export names for ESM import in node:
|
|
628
1016
|
0 && (module.exports = {
|
|
629
1017
|
DRIVER_API_VERSION,
|
|
630
1018
|
DriverManager,
|
|
631
1019
|
PLUGIN_API_VERSION,
|
|
632
1020
|
PluginManager,
|
|
1021
|
+
decrypt,
|
|
1022
|
+
decryptCredentials,
|
|
1023
|
+
decryptStorageState,
|
|
633
1024
|
driverManager,
|
|
634
|
-
|
|
1025
|
+
encrypt,
|
|
1026
|
+
encryptCredentials,
|
|
1027
|
+
encryptStorageState,
|
|
1028
|
+
getPassphrase,
|
|
1029
|
+
isSessionDir,
|
|
1030
|
+
loadSessionDir,
|
|
1031
|
+
looksLikeSessionDir,
|
|
1032
|
+
pluginManager,
|
|
1033
|
+
readAuthState,
|
|
1034
|
+
readCapturedRequests,
|
|
1035
|
+
saveSessionDir
|
|
635
1036
|
});
|
|
636
1037
|
//# sourceMappingURL=index.cjs.map
|