@vulcn/driver-browser 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +749 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +180 -22
- package/dist/index.d.ts +180 -22
- package/dist/index.js +743 -193
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -24,12 +24,17 @@ __export(index_exports, {
|
|
|
24
24
|
BrowserRecorder: () => BrowserRecorder,
|
|
25
25
|
BrowserRunner: () => BrowserRunner,
|
|
26
26
|
BrowserStepSchema: () => BrowserStepSchema,
|
|
27
|
+
buildCapturedRequests: () => buildCapturedRequests,
|
|
27
28
|
checkBrowsers: () => checkBrowsers,
|
|
29
|
+
checkSessionAlive: () => checkSessionAlive,
|
|
28
30
|
configSchema: () => configSchema,
|
|
29
31
|
crawlAndBuildSessions: () => crawlAndBuildSessions,
|
|
30
32
|
default: () => index_default,
|
|
33
|
+
detectLoginForm: () => detectLoginForm,
|
|
34
|
+
httpScan: () => httpScan,
|
|
31
35
|
installBrowsers: () => installBrowsers,
|
|
32
|
-
launchBrowser: () => launchBrowser
|
|
36
|
+
launchBrowser: () => launchBrowser,
|
|
37
|
+
performLogin: () => performLogin
|
|
33
38
|
});
|
|
34
39
|
module.exports = __toCommonJS(index_exports);
|
|
35
40
|
var import_zod = require("zod");
|
|
@@ -401,9 +406,12 @@ var BrowserRecorder = class _BrowserRecorder {
|
|
|
401
406
|
};
|
|
402
407
|
|
|
403
408
|
// src/runner.ts
|
|
404
|
-
var BrowserRunner = class
|
|
409
|
+
var BrowserRunner = class {
|
|
405
410
|
/**
|
|
406
|
-
* Execute a session with security payloads
|
|
411
|
+
* Execute a session with security payloads.
|
|
412
|
+
*
|
|
413
|
+
* If ctx.options.browser is provided, reuses that browser (persistent mode).
|
|
414
|
+
* Otherwise, launches and closes its own browser (standalone mode).
|
|
407
415
|
*/
|
|
408
416
|
static async execute(session, ctx) {
|
|
409
417
|
const config = session.driverConfig;
|
|
@@ -429,83 +437,39 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
429
437
|
]
|
|
430
438
|
};
|
|
431
439
|
}
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
440
|
+
const sharedBrowser = ctx.options.browser;
|
|
441
|
+
const ownBrowser = sharedBrowser ? null : (await launchBrowser({ browser: browserType, headless })).browser;
|
|
442
|
+
const browser = sharedBrowser ?? ownBrowser;
|
|
443
|
+
const storageState = ctx.options.storageState;
|
|
444
|
+
const contextOptions = { viewport };
|
|
445
|
+
if (storageState) {
|
|
446
|
+
contextOptions.storageState = JSON.parse(storageState);
|
|
447
|
+
}
|
|
448
|
+
const context = await browser.newContext(contextOptions);
|
|
437
449
|
const page = await context.newPage();
|
|
438
450
|
await ctx.options.onPageReady?.(page);
|
|
439
451
|
const eventFindings = [];
|
|
440
452
|
let currentPayloadInfo = null;
|
|
441
|
-
const dialogHandler =
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
|
|
451
|
-
stepId: currentPayloadInfo.stepId,
|
|
452
|
-
payload: currentPayloadInfo.payloadValue,
|
|
453
|
-
url: page.url(),
|
|
454
|
-
evidence: `Dialog type: ${dialogType}, Message: ${message}`,
|
|
455
|
-
metadata: {
|
|
456
|
-
dialogType,
|
|
457
|
-
dialogMessage: message,
|
|
458
|
-
detectionMethod: "dialog"
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
try {
|
|
464
|
-
await dialog.dismiss();
|
|
465
|
-
} catch {
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
const consoleHandler = async (msg) => {
|
|
469
|
-
if (currentPayloadInfo && msg.type() === "log") {
|
|
470
|
-
const text = msg.text();
|
|
471
|
-
if (text.includes("vulcn") || text.includes(currentPayloadInfo.payloadValue)) {
|
|
472
|
-
eventFindings.push({
|
|
473
|
-
type: "xss",
|
|
474
|
-
severity: "high",
|
|
475
|
-
title: "XSS Confirmed - Console Output",
|
|
476
|
-
description: `JavaScript console.log was triggered by payload injection`,
|
|
477
|
-
stepId: currentPayloadInfo.stepId,
|
|
478
|
-
payload: currentPayloadInfo.payloadValue,
|
|
479
|
-
url: page.url(),
|
|
480
|
-
evidence: `Console output: ${text}`,
|
|
481
|
-
metadata: {
|
|
482
|
-
consoleType: msg.type(),
|
|
483
|
-
detectionMethod: "console"
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
};
|
|
453
|
+
const dialogHandler = createDialogHandler(
|
|
454
|
+
page,
|
|
455
|
+
eventFindings,
|
|
456
|
+
() => currentPayloadInfo
|
|
457
|
+
);
|
|
458
|
+
const consoleHandler = createConsoleHandler(
|
|
459
|
+
eventFindings,
|
|
460
|
+
() => currentPayloadInfo
|
|
461
|
+
);
|
|
489
462
|
page.on("dialog", dialogHandler);
|
|
490
463
|
page.on("console", consoleHandler);
|
|
491
464
|
try {
|
|
492
465
|
const injectableSteps = session.steps.filter(
|
|
493
466
|
(step) => step.type === "browser.input" && step.injectable !== false
|
|
494
467
|
);
|
|
495
|
-
const allPayloads =
|
|
496
|
-
const payloadsByCategory = payloads.map(
|
|
497
|
-
(ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
|
|
498
|
-
);
|
|
499
|
-
const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
|
|
500
|
-
for (let i = 0; i < maxLen; i++) {
|
|
501
|
-
for (const category of payloadsByCategory) {
|
|
502
|
-
if (i < category.length) {
|
|
503
|
-
allPayloads.push(category[i]);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
468
|
+
const allPayloads = interleavePayloads(payloads);
|
|
507
469
|
const confirmedTypes = /* @__PURE__ */ new Set();
|
|
508
470
|
for (const injectableStep of injectableSteps) {
|
|
471
|
+
let isFirstPayload = true;
|
|
472
|
+
let formPageUrl = null;
|
|
509
473
|
for (const { payloadSet, value } of allPayloads) {
|
|
510
474
|
const stepTypeKey = `${injectableStep.id}::${payloadSet.category}`;
|
|
511
475
|
if (confirmedTypes.has(stepTypeKey)) {
|
|
@@ -517,14 +481,35 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
517
481
|
payloadSet,
|
|
518
482
|
payloadValue: value
|
|
519
483
|
};
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
484
|
+
if (isFirstPayload) {
|
|
485
|
+
await replayWithPayload(
|
|
486
|
+
page,
|
|
487
|
+
session,
|
|
488
|
+
injectableStep,
|
|
489
|
+
value,
|
|
490
|
+
startUrl
|
|
491
|
+
);
|
|
492
|
+
isFirstPayload = false;
|
|
493
|
+
formPageUrl = startUrl;
|
|
494
|
+
} else {
|
|
495
|
+
const cycled = await cyclePayload(
|
|
496
|
+
page,
|
|
497
|
+
session,
|
|
498
|
+
injectableStep,
|
|
499
|
+
value,
|
|
500
|
+
formPageUrl ?? startUrl
|
|
501
|
+
);
|
|
502
|
+
if (!cycled) {
|
|
503
|
+
await replayWithPayload(
|
|
504
|
+
page,
|
|
505
|
+
session,
|
|
506
|
+
injectableStep,
|
|
507
|
+
value,
|
|
508
|
+
startUrl
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const reflectionFinding = await checkReflection(
|
|
528
513
|
page,
|
|
529
514
|
injectableStep,
|
|
530
515
|
payloadSet,
|
|
@@ -550,6 +535,7 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
550
535
|
ctx.options.onStepComplete?.(injectableStep.id, payloadsTested);
|
|
551
536
|
} catch (err) {
|
|
552
537
|
errors.push(`${injectableStep.id}: ${String(err)}`);
|
|
538
|
+
isFirstPayload = true;
|
|
553
539
|
}
|
|
554
540
|
}
|
|
555
541
|
}
|
|
@@ -558,7 +544,10 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
558
544
|
page.off("console", consoleHandler);
|
|
559
545
|
currentPayloadInfo = null;
|
|
560
546
|
await ctx.options.onBeforeClose?.(page);
|
|
561
|
-
await
|
|
547
|
+
await context.close();
|
|
548
|
+
if (ownBrowser) {
|
|
549
|
+
await ownBrowser.close();
|
|
550
|
+
}
|
|
562
551
|
}
|
|
563
552
|
return {
|
|
564
553
|
findings: ctx.findings,
|
|
@@ -568,136 +557,506 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
568
557
|
errors
|
|
569
558
|
};
|
|
570
559
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
case "browser.click":
|
|
593
|
-
if (injected) {
|
|
594
|
-
await Promise.all([
|
|
595
|
-
page.waitForNavigation({
|
|
596
|
-
waitUntil: "domcontentloaded",
|
|
597
|
-
timeout: 5e3
|
|
598
|
-
}).catch(() => {
|
|
599
|
-
}),
|
|
600
|
-
page.click(browserStep.selector, { timeout: 5e3 })
|
|
601
|
-
]);
|
|
602
|
-
} else {
|
|
603
|
-
await page.click(browserStep.selector, { timeout: 5e3 });
|
|
604
|
-
}
|
|
605
|
-
break;
|
|
606
|
-
case "browser.input": {
|
|
607
|
-
const value = step.id === targetStep.id ? payloadValue : browserStep.value;
|
|
608
|
-
await page.fill(browserStep.selector, value, { timeout: 5e3 });
|
|
609
|
-
if (step.id === targetStep.id) {
|
|
610
|
-
injected = true;
|
|
611
|
-
}
|
|
612
|
-
break;
|
|
560
|
+
};
|
|
561
|
+
function createDialogHandler(page, eventFindings, getPayloadInfo) {
|
|
562
|
+
return async (dialog) => {
|
|
563
|
+
const info = getPayloadInfo();
|
|
564
|
+
if (info) {
|
|
565
|
+
const message = dialog.message();
|
|
566
|
+
const dialogType = dialog.type();
|
|
567
|
+
if (dialogType !== "beforeunload") {
|
|
568
|
+
eventFindings.push({
|
|
569
|
+
type: "xss",
|
|
570
|
+
severity: "high",
|
|
571
|
+
title: `XSS Confirmed - ${dialogType}() triggered`,
|
|
572
|
+
description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
|
|
573
|
+
stepId: info.stepId,
|
|
574
|
+
payload: info.payloadValue,
|
|
575
|
+
url: page.url(),
|
|
576
|
+
evidence: `Dialog type: ${dialogType}, Message: ${message}`,
|
|
577
|
+
metadata: {
|
|
578
|
+
dialogType,
|
|
579
|
+
dialogMessage: message,
|
|
580
|
+
detectionMethod: "dialog"
|
|
613
581
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
await dialog.dismiss();
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function createConsoleHandler(eventFindings, getPayloadInfo) {
|
|
592
|
+
return async (msg) => {
|
|
593
|
+
const info = getPayloadInfo();
|
|
594
|
+
if (info && msg.type() === "log") {
|
|
595
|
+
const text = msg.text();
|
|
596
|
+
if (text.includes("vulcn") || text.includes(info.payloadValue)) {
|
|
597
|
+
eventFindings.push({
|
|
598
|
+
type: "xss",
|
|
599
|
+
severity: "high",
|
|
600
|
+
title: "XSS Confirmed - Console Output",
|
|
601
|
+
description: `JavaScript console.log was triggered by payload injection`,
|
|
602
|
+
stepId: info.stepId,
|
|
603
|
+
payload: info.payloadValue,
|
|
604
|
+
url: "",
|
|
605
|
+
evidence: `Console output: ${text}`,
|
|
606
|
+
metadata: {
|
|
607
|
+
consoleType: msg.type(),
|
|
608
|
+
detectionMethod: "console"
|
|
628
609
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
} catch
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
async function cyclePayload(page, session, targetStep, payloadValue, formPageUrl) {
|
|
616
|
+
try {
|
|
617
|
+
await page.goBack({ waitUntil: "domcontentloaded", timeout: 5e3 });
|
|
618
|
+
const targetSelector = targetStep.selector;
|
|
619
|
+
const formPresent = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
|
|
620
|
+
if (!formPresent) {
|
|
621
|
+
await page.goto(formPageUrl, {
|
|
622
|
+
waitUntil: "domcontentloaded",
|
|
623
|
+
timeout: 5e3
|
|
624
|
+
});
|
|
625
|
+
const formPresentAfterNav = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
|
|
626
|
+
if (!formPresentAfterNav) {
|
|
627
|
+
return false;
|
|
645
628
|
}
|
|
646
629
|
}
|
|
630
|
+
await page.fill(targetSelector, payloadValue, { timeout: 5e3 });
|
|
631
|
+
await replayStepsAfter(page, session, targetStep);
|
|
647
632
|
await page.waitForTimeout(500);
|
|
633
|
+
return true;
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
648
636
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
637
|
+
}
|
|
638
|
+
async function replayWithPayload(page, session, targetStep, payloadValue, startUrl) {
|
|
639
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
640
|
+
let injected = false;
|
|
641
|
+
for (const step of session.steps) {
|
|
642
|
+
const browserStep = step;
|
|
643
|
+
try {
|
|
644
|
+
switch (browserStep.type) {
|
|
645
|
+
case "browser.navigate":
|
|
646
|
+
if (injected && browserStep.url.includes("sid=")) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
await page.goto(browserStep.url, { waitUntil: "domcontentloaded" });
|
|
650
|
+
break;
|
|
651
|
+
case "browser.click":
|
|
652
|
+
if (injected) {
|
|
653
|
+
await Promise.all([
|
|
654
|
+
page.waitForNavigation({
|
|
655
|
+
waitUntil: "domcontentloaded",
|
|
656
|
+
timeout: 5e3
|
|
657
|
+
}).catch(() => {
|
|
658
|
+
}),
|
|
659
|
+
page.click(browserStep.selector, { timeout: 5e3 })
|
|
660
|
+
]);
|
|
661
|
+
} else {
|
|
662
|
+
await page.click(browserStep.selector, { timeout: 5e3 });
|
|
663
|
+
}
|
|
664
|
+
break;
|
|
665
|
+
case "browser.input": {
|
|
666
|
+
const value = step.id === targetStep.id ? payloadValue : browserStep.value;
|
|
667
|
+
await page.fill(browserStep.selector, value, { timeout: 5e3 });
|
|
668
|
+
if (step.id === targetStep.id) {
|
|
669
|
+
injected = true;
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
case "browser.keypress": {
|
|
674
|
+
const modifiers = browserStep.modifiers ?? [];
|
|
675
|
+
for (const mod of modifiers) {
|
|
676
|
+
await page.keyboard.down(
|
|
677
|
+
mod
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
await page.keyboard.press(browserStep.key);
|
|
681
|
+
for (const mod of modifiers.reverse()) {
|
|
682
|
+
await page.keyboard.up(mod);
|
|
683
|
+
}
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case "browser.scroll":
|
|
687
|
+
if (browserStep.selector) {
|
|
688
|
+
await page.locator(browserStep.selector).evaluate((el, pos) => {
|
|
689
|
+
el.scrollTo(pos.x, pos.y);
|
|
690
|
+
}, browserStep.position);
|
|
691
|
+
} else {
|
|
692
|
+
await page.evaluate((pos) => {
|
|
693
|
+
window.scrollTo(pos.x, pos.y);
|
|
694
|
+
}, browserStep.position);
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
case "browser.wait":
|
|
698
|
+
await page.waitForTimeout(browserStep.duration);
|
|
699
|
+
break;
|
|
666
700
|
}
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
await page.waitForTimeout(500);
|
|
705
|
+
}
|
|
706
|
+
async function replayStepsAfter(page, session, targetStep) {
|
|
707
|
+
let pastTarget = false;
|
|
708
|
+
for (const step of session.steps) {
|
|
709
|
+
if (step.id === targetStep.id) {
|
|
710
|
+
pastTarget = true;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (!pastTarget) continue;
|
|
714
|
+
const browserStep = step;
|
|
715
|
+
try {
|
|
716
|
+
switch (browserStep.type) {
|
|
717
|
+
case "browser.navigate":
|
|
718
|
+
break;
|
|
719
|
+
case "browser.click":
|
|
720
|
+
await Promise.all([
|
|
721
|
+
page.waitForNavigation({
|
|
722
|
+
waitUntil: "domcontentloaded",
|
|
723
|
+
timeout: 5e3
|
|
724
|
+
}).catch(() => {
|
|
725
|
+
}),
|
|
726
|
+
page.click(browserStep.selector, { timeout: 5e3 })
|
|
727
|
+
]);
|
|
728
|
+
break;
|
|
729
|
+
case "browser.input":
|
|
730
|
+
await page.fill(browserStep.selector, browserStep.value, {
|
|
731
|
+
timeout: 5e3
|
|
732
|
+
});
|
|
733
|
+
break;
|
|
734
|
+
case "browser.keypress": {
|
|
735
|
+
const modifiers = browserStep.modifiers ?? [];
|
|
736
|
+
for (const mod of modifiers) {
|
|
737
|
+
await page.keyboard.down(
|
|
738
|
+
mod
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
await page.keyboard.press(browserStep.key);
|
|
742
|
+
for (const mod of modifiers.reverse()) {
|
|
743
|
+
await page.keyboard.up(mod);
|
|
744
|
+
}
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case "browser.scroll":
|
|
748
|
+
break;
|
|
749
|
+
// skip scrolls in fast path
|
|
750
|
+
case "browser.wait":
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
667
754
|
}
|
|
668
|
-
|
|
755
|
+
}
|
|
756
|
+
await page.waitForTimeout(500);
|
|
757
|
+
}
|
|
758
|
+
async function checkReflection(page, step, payloadSet, payloadValue) {
|
|
759
|
+
const content = await page.content();
|
|
760
|
+
for (const pattern of payloadSet.detectPatterns) {
|
|
761
|
+
if (pattern.test(content)) {
|
|
669
762
|
return {
|
|
670
763
|
type: payloadSet.category,
|
|
671
|
-
severity:
|
|
672
|
-
title:
|
|
673
|
-
description: `Payload was reflected in page
|
|
764
|
+
severity: getSeverity(payloadSet.category),
|
|
765
|
+
title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
|
|
766
|
+
description: `Payload pattern was reflected in page content`,
|
|
674
767
|
stepId: step.id,
|
|
675
768
|
payload: payloadValue,
|
|
676
|
-
url: page.url()
|
|
769
|
+
url: page.url(),
|
|
770
|
+
evidence: content.match(pattern)?.[0]?.slice(0, 200)
|
|
677
771
|
};
|
|
678
772
|
}
|
|
679
|
-
return void 0;
|
|
680
773
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
774
|
+
if (content.includes(payloadValue)) {
|
|
775
|
+
return {
|
|
776
|
+
type: payloadSet.category,
|
|
777
|
+
severity: "medium",
|
|
778
|
+
title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
|
|
779
|
+
description: `Payload was reflected in page without encoding`,
|
|
780
|
+
stepId: step.id,
|
|
781
|
+
payload: payloadValue,
|
|
782
|
+
url: page.url()
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return void 0;
|
|
786
|
+
}
|
|
787
|
+
function getSeverity(category) {
|
|
788
|
+
switch (category) {
|
|
789
|
+
case "sqli":
|
|
790
|
+
case "command-injection":
|
|
791
|
+
case "xxe":
|
|
792
|
+
return "critical";
|
|
793
|
+
case "xss":
|
|
794
|
+
case "ssrf":
|
|
795
|
+
case "path-traversal":
|
|
796
|
+
return "high";
|
|
797
|
+
case "open-redirect":
|
|
798
|
+
return "medium";
|
|
799
|
+
default:
|
|
800
|
+
return "medium";
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function interleavePayloads(payloads) {
|
|
804
|
+
const result = [];
|
|
805
|
+
const payloadsByCategory = payloads.map(
|
|
806
|
+
(ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
|
|
807
|
+
);
|
|
808
|
+
const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
|
|
809
|
+
for (let i = 0; i < maxLen; i++) {
|
|
810
|
+
for (const category of payloadsByCategory) {
|
|
811
|
+
if (i < category.length) {
|
|
812
|
+
result.push(category[i]);
|
|
813
|
+
}
|
|
698
814
|
}
|
|
699
815
|
}
|
|
700
|
-
|
|
816
|
+
return result;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/http-scanner.ts
|
|
820
|
+
async function httpScan(requests, payloads, options = {}) {
|
|
821
|
+
const timeout = options.timeout ?? 1e4;
|
|
822
|
+
const concurrency = options.concurrency ?? 10;
|
|
823
|
+
const start = Date.now();
|
|
824
|
+
const findings = [];
|
|
825
|
+
const reflectedRequests = [];
|
|
826
|
+
let requestsSent = 0;
|
|
827
|
+
const tasks = [];
|
|
828
|
+
for (const request of requests) {
|
|
829
|
+
if (!request.injectableField) continue;
|
|
830
|
+
for (const payloadSet of payloads) {
|
|
831
|
+
for (const value of payloadSet.payloads) {
|
|
832
|
+
tasks.push({ request, payloadSet, value });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const totalTasks = tasks.length;
|
|
837
|
+
if (totalTasks === 0) {
|
|
838
|
+
return { requestsSent: 0, duration: 0, findings, reflectedRequests };
|
|
839
|
+
}
|
|
840
|
+
for (let i = 0; i < tasks.length; i += concurrency) {
|
|
841
|
+
const batch = tasks.slice(i, i + concurrency);
|
|
842
|
+
const results = await Promise.allSettled(
|
|
843
|
+
batch.map(async ({ request, payloadSet, value }) => {
|
|
844
|
+
try {
|
|
845
|
+
const body = await sendPayload(request, value, {
|
|
846
|
+
timeout,
|
|
847
|
+
cookies: options.cookies,
|
|
848
|
+
headers: options.headers
|
|
849
|
+
});
|
|
850
|
+
requestsSent++;
|
|
851
|
+
const finding = checkHttpReflection(body, request, payloadSet, value);
|
|
852
|
+
if (finding) {
|
|
853
|
+
findings.push(finding);
|
|
854
|
+
reflectedRequests.push({
|
|
855
|
+
request,
|
|
856
|
+
payload: value,
|
|
857
|
+
category: payloadSet.category
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
} catch {
|
|
861
|
+
requestsSent++;
|
|
862
|
+
}
|
|
863
|
+
})
|
|
864
|
+
);
|
|
865
|
+
const completed = Math.min(i + batch.length, totalTasks);
|
|
866
|
+
options.onProgress?.(completed, totalTasks);
|
|
867
|
+
for (const result of results) {
|
|
868
|
+
if (result.status === "rejected") {
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
requestsSent,
|
|
874
|
+
duration: Date.now() - start,
|
|
875
|
+
findings,
|
|
876
|
+
reflectedRequests
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
async function sendPayload(request, payload, options) {
|
|
880
|
+
const { method, url, headers, body, contentType, injectableField } = request;
|
|
881
|
+
const reqHeaders = {
|
|
882
|
+
...headers,
|
|
883
|
+
...options.headers ?? {}
|
|
884
|
+
};
|
|
885
|
+
if (options.cookies) {
|
|
886
|
+
reqHeaders["Cookie"] = options.cookies;
|
|
887
|
+
}
|
|
888
|
+
delete reqHeaders["content-length"];
|
|
889
|
+
delete reqHeaders["Content-Length"];
|
|
890
|
+
let requestUrl = url;
|
|
891
|
+
let requestBody;
|
|
892
|
+
if (method.toUpperCase() === "GET") {
|
|
893
|
+
requestUrl = injectIntoUrl(url, injectableField, payload);
|
|
894
|
+
} else {
|
|
895
|
+
requestBody = injectIntoBody(body, contentType, injectableField, payload);
|
|
896
|
+
if (contentType) {
|
|
897
|
+
reqHeaders["Content-Type"] = contentType;
|
|
898
|
+
} else {
|
|
899
|
+
reqHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const controller = new AbortController();
|
|
903
|
+
const timer = setTimeout(() => controller.abort(), options.timeout);
|
|
904
|
+
try {
|
|
905
|
+
const response = await fetch(requestUrl, {
|
|
906
|
+
method: method.toUpperCase(),
|
|
907
|
+
headers: reqHeaders,
|
|
908
|
+
body: requestBody,
|
|
909
|
+
signal: controller.signal,
|
|
910
|
+
redirect: "follow"
|
|
911
|
+
});
|
|
912
|
+
return await response.text();
|
|
913
|
+
} finally {
|
|
914
|
+
clearTimeout(timer);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function injectIntoUrl(url, field, payload) {
|
|
918
|
+
try {
|
|
919
|
+
const parsed = new URL(url);
|
|
920
|
+
parsed.searchParams.set(field, payload);
|
|
921
|
+
return parsed.toString();
|
|
922
|
+
} catch {
|
|
923
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
924
|
+
return `${url}${separator}${encodeURIComponent(field)}=${encodeURIComponent(payload)}`;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function injectIntoBody(body, contentType, field, payload) {
|
|
928
|
+
if (!body) {
|
|
929
|
+
return `${encodeURIComponent(field)}=${encodeURIComponent(payload)}`;
|
|
930
|
+
}
|
|
931
|
+
const ct = (contentType ?? "").toLowerCase();
|
|
932
|
+
if (ct.includes("application/json")) {
|
|
933
|
+
return injectIntoJson(body, field, payload);
|
|
934
|
+
}
|
|
935
|
+
if (ct.includes("multipart/form-data")) {
|
|
936
|
+
return injectIntoMultipart(body, field, payload);
|
|
937
|
+
}
|
|
938
|
+
return injectIntoFormUrlEncoded(body, field, payload);
|
|
939
|
+
}
|
|
940
|
+
function injectIntoFormUrlEncoded(body, field, payload) {
|
|
941
|
+
const params = new URLSearchParams(body);
|
|
942
|
+
params.set(field, payload);
|
|
943
|
+
return params.toString();
|
|
944
|
+
}
|
|
945
|
+
function injectIntoJson(body, field, payload) {
|
|
946
|
+
try {
|
|
947
|
+
const parsed = JSON.parse(body);
|
|
948
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
949
|
+
parsed[field] = payload;
|
|
950
|
+
return JSON.stringify(parsed);
|
|
951
|
+
}
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
955
|
+
const regex = new RegExp(`("${escaped}"\\s*:\\s*)"[^"]*"`, "g");
|
|
956
|
+
const replaced = body.replace(regex, `$1"${payload}"`);
|
|
957
|
+
if (replaced !== body) return replaced;
|
|
958
|
+
return body;
|
|
959
|
+
}
|
|
960
|
+
function injectIntoMultipart(body, field, payload) {
|
|
961
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
962
|
+
const regex = new RegExp(
|
|
963
|
+
`(Content-Disposition:\\s*form-data;\\s*name="${escaped}"\\r?\\n\\r?\\n)[^\\r\\n-]*`,
|
|
964
|
+
"i"
|
|
965
|
+
);
|
|
966
|
+
return body.replace(regex, `$1${payload}`);
|
|
967
|
+
}
|
|
968
|
+
function checkHttpReflection(responseBody, request, payloadSet, payloadValue) {
|
|
969
|
+
for (const pattern of payloadSet.detectPatterns) {
|
|
970
|
+
if (pattern.test(responseBody)) {
|
|
971
|
+
return {
|
|
972
|
+
type: payloadSet.category,
|
|
973
|
+
severity: getSeverity2(payloadSet.category),
|
|
974
|
+
title: `${payloadSet.category.toUpperCase()} reflection detected (HTTP)`,
|
|
975
|
+
description: `Payload pattern was reflected in HTTP response body. Needs browser confirmation for execution proof.`,
|
|
976
|
+
stepId: `http-${request.sessionName}`,
|
|
977
|
+
payload: payloadValue,
|
|
978
|
+
url: request.url,
|
|
979
|
+
evidence: responseBody.match(pattern)?.[0]?.slice(0, 200),
|
|
980
|
+
metadata: {
|
|
981
|
+
detectionMethod: "tier1-http",
|
|
982
|
+
needsBrowserConfirmation: true,
|
|
983
|
+
requestMethod: request.method,
|
|
984
|
+
injectableField: request.injectableField
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (responseBody.includes(payloadValue)) {
|
|
990
|
+
return {
|
|
991
|
+
type: payloadSet.category,
|
|
992
|
+
severity: "medium",
|
|
993
|
+
title: `Potential ${payloadSet.category.toUpperCase()} \u2014 payload reflected in HTTP response`,
|
|
994
|
+
description: `Payload was reflected in HTTP response without encoding. Escalate to browser for execution proof.`,
|
|
995
|
+
stepId: `http-${request.sessionName}`,
|
|
996
|
+
payload: payloadValue,
|
|
997
|
+
url: request.url,
|
|
998
|
+
metadata: {
|
|
999
|
+
detectionMethod: "tier1-http",
|
|
1000
|
+
needsBrowserConfirmation: true,
|
|
1001
|
+
requestMethod: request.method,
|
|
1002
|
+
injectableField: request.injectableField
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
return void 0;
|
|
1007
|
+
}
|
|
1008
|
+
function getSeverity2(category) {
|
|
1009
|
+
switch (category) {
|
|
1010
|
+
case "sqli":
|
|
1011
|
+
case "command-injection":
|
|
1012
|
+
case "xxe":
|
|
1013
|
+
return "critical";
|
|
1014
|
+
case "xss":
|
|
1015
|
+
case "ssrf":
|
|
1016
|
+
case "path-traversal":
|
|
1017
|
+
return "high";
|
|
1018
|
+
case "open-redirect":
|
|
1019
|
+
return "medium";
|
|
1020
|
+
default:
|
|
1021
|
+
return "medium";
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function buildCapturedRequests(forms) {
|
|
1025
|
+
const requests = [];
|
|
1026
|
+
for (const form of forms) {
|
|
1027
|
+
const injectableInputs = form.inputs.filter((i) => i.injectable);
|
|
1028
|
+
if (injectableInputs.length === 0) continue;
|
|
1029
|
+
let actionUrl;
|
|
1030
|
+
try {
|
|
1031
|
+
actionUrl = new URL(form.action, form.pageUrl).toString();
|
|
1032
|
+
} catch {
|
|
1033
|
+
actionUrl = form.pageUrl;
|
|
1034
|
+
}
|
|
1035
|
+
const method = (form.method || "GET").toUpperCase();
|
|
1036
|
+
for (const input of injectableInputs) {
|
|
1037
|
+
const formParams = new URLSearchParams();
|
|
1038
|
+
for (const inp of form.inputs) {
|
|
1039
|
+
formParams.set(inp.name || inp.type, inp.injectable ? "test" : "");
|
|
1040
|
+
}
|
|
1041
|
+
const request = {
|
|
1042
|
+
method,
|
|
1043
|
+
url: method === "GET" ? actionUrl : actionUrl,
|
|
1044
|
+
headers: {
|
|
1045
|
+
"User-Agent": "Vulcn/1.0 (Security Scanner)",
|
|
1046
|
+
Accept: "text/html,application/xhtml+xml,*/*"
|
|
1047
|
+
},
|
|
1048
|
+
...method !== "GET" ? {
|
|
1049
|
+
body: formParams.toString(),
|
|
1050
|
+
contentType: "application/x-www-form-urlencoded"
|
|
1051
|
+
} : {},
|
|
1052
|
+
injectableField: input.name || input.type,
|
|
1053
|
+
sessionName: form.sessionName
|
|
1054
|
+
};
|
|
1055
|
+
requests.push(request);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return requests;
|
|
1059
|
+
}
|
|
701
1060
|
|
|
702
1061
|
// src/crawler.ts
|
|
703
1062
|
var INJECTABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -734,7 +1093,8 @@ async function crawlAndBuildSessions(config, options = {}) {
|
|
|
734
1093
|
headless: config.headless ?? true
|
|
735
1094
|
});
|
|
736
1095
|
const context = await browser.newContext({
|
|
737
|
-
viewport: config.viewport ?? { width: 1280, height: 720 }
|
|
1096
|
+
viewport: config.viewport ?? { width: 1280, height: 720 },
|
|
1097
|
+
...options.storageState ? { storageState: JSON.parse(options.storageState) } : {}
|
|
738
1098
|
});
|
|
739
1099
|
try {
|
|
740
1100
|
while (queue.length > 0 && visited.size < opts.maxPages) {
|
|
@@ -784,7 +1144,20 @@ async function crawlAndBuildSessions(config, options = {}) {
|
|
|
784
1144
|
console.log(
|
|
785
1145
|
`[crawler] Complete: ${visited.size} page(s), ${allForms.length} form(s)`
|
|
786
1146
|
);
|
|
787
|
-
|
|
1147
|
+
const sessions = buildSessions(allForms);
|
|
1148
|
+
const capturedRequests = buildCapturedRequests(
|
|
1149
|
+
allForms.filter((f) => f.inputs.some((i) => i.injectable)).map((form, idx) => ({
|
|
1150
|
+
pageUrl: form.pageUrl,
|
|
1151
|
+
action: form.action,
|
|
1152
|
+
method: form.method,
|
|
1153
|
+
inputs: form.inputs,
|
|
1154
|
+
sessionName: sessions[idx]?.name ?? `form-${idx + 1}`
|
|
1155
|
+
}))
|
|
1156
|
+
);
|
|
1157
|
+
console.log(
|
|
1158
|
+
`[crawler] Generated ${sessions.length} session(s), ${capturedRequests.length} HTTP request(s) for Tier 1`
|
|
1159
|
+
);
|
|
1160
|
+
return { sessions, capturedRequests };
|
|
788
1161
|
}
|
|
789
1162
|
async function discoverForms(page, pageUrl) {
|
|
790
1163
|
const forms = [];
|
|
@@ -1049,6 +1422,182 @@ function normalizeUrl(url) {
|
|
|
1049
1422
|
}
|
|
1050
1423
|
}
|
|
1051
1424
|
|
|
1425
|
+
// src/auth.ts
|
|
1426
|
+
async function detectLoginForm(page) {
|
|
1427
|
+
return page.evaluate(() => {
|
|
1428
|
+
function findUsernameInput(container) {
|
|
1429
|
+
const selectors = [
|
|
1430
|
+
'input[autocomplete="username"]',
|
|
1431
|
+
'input[autocomplete="email"]',
|
|
1432
|
+
'input[type="email"]',
|
|
1433
|
+
'input[name*="user" i]',
|
|
1434
|
+
'input[name*="login" i]',
|
|
1435
|
+
'input[name*="email" i]',
|
|
1436
|
+
'input[id*="user" i]',
|
|
1437
|
+
'input[id*="login" i]',
|
|
1438
|
+
'input[id*="email" i]',
|
|
1439
|
+
'input[name*="name" i]',
|
|
1440
|
+
'input[type="text"]'
|
|
1441
|
+
];
|
|
1442
|
+
for (const sel of selectors) {
|
|
1443
|
+
const el = container.querySelector(sel);
|
|
1444
|
+
if (el && el.type !== "password" && el.type !== "hidden") {
|
|
1445
|
+
return el;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
function findSubmitButton(container) {
|
|
1451
|
+
const selectors = [
|
|
1452
|
+
'button[type="submit"]',
|
|
1453
|
+
'input[type="submit"]',
|
|
1454
|
+
"button:not([type])",
|
|
1455
|
+
'button[type="button"]'
|
|
1456
|
+
];
|
|
1457
|
+
for (const sel of selectors) {
|
|
1458
|
+
const el = container.querySelector(sel);
|
|
1459
|
+
if (el) return el;
|
|
1460
|
+
}
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
function getSelector(el) {
|
|
1464
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
1465
|
+
if (el.getAttribute("name"))
|
|
1466
|
+
return `${el.tagName.toLowerCase()}[name="${CSS.escape(el.getAttribute("name"))}"]`;
|
|
1467
|
+
if (el.getAttribute("type") && el.tagName === "INPUT")
|
|
1468
|
+
return `input[type="${el.getAttribute("type")}"]`;
|
|
1469
|
+
const parent = el.parentElement;
|
|
1470
|
+
if (!parent) return el.tagName.toLowerCase();
|
|
1471
|
+
const siblings = Array.from(parent.children);
|
|
1472
|
+
const index = siblings.indexOf(el) + 1;
|
|
1473
|
+
return `${parent.tagName.toLowerCase()} > ${el.tagName.toLowerCase()}:nth-child(${index})`;
|
|
1474
|
+
}
|
|
1475
|
+
const forms = document.querySelectorAll("form");
|
|
1476
|
+
for (const form of forms) {
|
|
1477
|
+
const passwordInput2 = form.querySelector(
|
|
1478
|
+
'input[type="password"]'
|
|
1479
|
+
);
|
|
1480
|
+
if (!passwordInput2) continue;
|
|
1481
|
+
const usernameInput = findUsernameInput(form);
|
|
1482
|
+
const submitButton = findSubmitButton(form);
|
|
1483
|
+
return {
|
|
1484
|
+
usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
|
|
1485
|
+
passwordSelector: getSelector(passwordInput2),
|
|
1486
|
+
submitSelector: submitButton ? getSelector(submitButton) : null,
|
|
1487
|
+
autoDetected: true
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
const passwordInput = document.querySelector(
|
|
1491
|
+
'input[type="password"]'
|
|
1492
|
+
);
|
|
1493
|
+
if (passwordInput) {
|
|
1494
|
+
const usernameInput = findUsernameInput(document.body);
|
|
1495
|
+
const submitButton = findSubmitButton(document.body);
|
|
1496
|
+
return {
|
|
1497
|
+
usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
|
|
1498
|
+
passwordSelector: getSelector(passwordInput),
|
|
1499
|
+
submitSelector: submitButton ? getSelector(submitButton) : null,
|
|
1500
|
+
autoDetected: true
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
return null;
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
async function performLogin(page, context, credentials, options) {
|
|
1507
|
+
const loginUrl = credentials.loginUrl ?? options.targetUrl;
|
|
1508
|
+
await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
1509
|
+
let usernameSelector;
|
|
1510
|
+
let passwordSelector;
|
|
1511
|
+
let submitSelector;
|
|
1512
|
+
if (credentials.userSelector && credentials.passSelector) {
|
|
1513
|
+
usernameSelector = credentials.userSelector;
|
|
1514
|
+
passwordSelector = credentials.passSelector;
|
|
1515
|
+
submitSelector = null;
|
|
1516
|
+
} else {
|
|
1517
|
+
const form = await detectLoginForm(page);
|
|
1518
|
+
if (!form) {
|
|
1519
|
+
return {
|
|
1520
|
+
success: false,
|
|
1521
|
+
message: `No login form detected on ${loginUrl}. Use --user-field and --pass-field to specify selectors.`
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
usernameSelector = form.usernameSelector;
|
|
1525
|
+
passwordSelector = form.passwordSelector;
|
|
1526
|
+
submitSelector = form.submitSelector;
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
await page.fill(usernameSelector, credentials.username, { timeout: 5e3 });
|
|
1530
|
+
} catch {
|
|
1531
|
+
return {
|
|
1532
|
+
success: false,
|
|
1533
|
+
message: `Could not find username field: ${usernameSelector}`
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
await page.fill(passwordSelector, credentials.password, { timeout: 5e3 });
|
|
1538
|
+
} catch {
|
|
1539
|
+
return {
|
|
1540
|
+
success: false,
|
|
1541
|
+
message: `Could not find password field: ${passwordSelector}`
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
try {
|
|
1545
|
+
if (submitSelector) {
|
|
1546
|
+
await Promise.all([
|
|
1547
|
+
page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
|
|
1548
|
+
}),
|
|
1549
|
+
page.click(submitSelector, { timeout: 5e3 })
|
|
1550
|
+
]);
|
|
1551
|
+
} else {
|
|
1552
|
+
await Promise.all([
|
|
1553
|
+
page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
|
|
1554
|
+
}),
|
|
1555
|
+
page.press(passwordSelector, "Enter")
|
|
1556
|
+
]);
|
|
1557
|
+
}
|
|
1558
|
+
} catch {
|
|
1559
|
+
return {
|
|
1560
|
+
success: false,
|
|
1561
|
+
message: "Failed to submit login form"
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
await page.waitForTimeout(1e3);
|
|
1565
|
+
const bodyText = await page.textContent("body").catch(() => "");
|
|
1566
|
+
if (options.loggedOutIndicator && bodyText?.includes(options.loggedOutIndicator)) {
|
|
1567
|
+
return {
|
|
1568
|
+
success: false,
|
|
1569
|
+
message: `Login failed \u2014 "${options.loggedOutIndicator}" still visible on page`
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
if (options.loggedInIndicator && !bodyText?.includes(options.loggedInIndicator)) {
|
|
1573
|
+
return {
|
|
1574
|
+
success: false,
|
|
1575
|
+
message: `Login uncertain \u2014 "${options.loggedInIndicator}" not found on page`
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
const storageState = JSON.stringify(await context.storageState());
|
|
1579
|
+
return {
|
|
1580
|
+
success: true,
|
|
1581
|
+
message: "Login successful",
|
|
1582
|
+
storageState
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
async function checkSessionAlive(page, config) {
|
|
1586
|
+
try {
|
|
1587
|
+
const bodyText = await page.textContent("body");
|
|
1588
|
+
if (!bodyText) return true;
|
|
1589
|
+
if (config.loggedOutIndicator && bodyText.includes(config.loggedOutIndicator)) {
|
|
1590
|
+
return false;
|
|
1591
|
+
}
|
|
1592
|
+
if (config.loggedInIndicator && !bodyText.includes(config.loggedInIndicator)) {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
return true;
|
|
1596
|
+
} catch {
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1052
1601
|
// src/index.ts
|
|
1053
1602
|
var configSchema = import_zod.z.object({
|
|
1054
1603
|
/** Starting URL for recording */
|
|
@@ -1121,7 +1670,7 @@ var recorderDriver = {
|
|
|
1121
1670
|
},
|
|
1122
1671
|
async crawl(config, options) {
|
|
1123
1672
|
const parsedConfig = configSchema.parse(config);
|
|
1124
|
-
|
|
1673
|
+
const result = await crawlAndBuildSessions(
|
|
1125
1674
|
{
|
|
1126
1675
|
startUrl: parsedConfig.startUrl ?? "",
|
|
1127
1676
|
browser: parsedConfig.browser,
|
|
@@ -1130,6 +1679,7 @@ var recorderDriver = {
|
|
|
1130
1679
|
},
|
|
1131
1680
|
options
|
|
1132
1681
|
);
|
|
1682
|
+
return result.sessions;
|
|
1133
1683
|
}
|
|
1134
1684
|
};
|
|
1135
1685
|
var runnerDriver = {
|
|
@@ -1154,10 +1704,15 @@ var index_default = browserDriver;
|
|
|
1154
1704
|
BrowserRecorder,
|
|
1155
1705
|
BrowserRunner,
|
|
1156
1706
|
BrowserStepSchema,
|
|
1707
|
+
buildCapturedRequests,
|
|
1157
1708
|
checkBrowsers,
|
|
1709
|
+
checkSessionAlive,
|
|
1158
1710
|
configSchema,
|
|
1159
1711
|
crawlAndBuildSessions,
|
|
1712
|
+
detectLoginForm,
|
|
1713
|
+
httpScan,
|
|
1160
1714
|
installBrowsers,
|
|
1161
|
-
launchBrowser
|
|
1715
|
+
launchBrowser,
|
|
1716
|
+
performLogin
|
|
1162
1717
|
});
|
|
1163
1718
|
//# sourceMappingURL=index.cjs.map
|