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