@xbrowser/cli 1.10.0 → 1.12.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/{anti-bot-DR56Y63V.js → anti-bot-GTTYNEFB.js} +1 -1
- package/dist/{browser-WQZ3D6AE.js → browser-3HQKDZQ6.js} +4 -2
- package/dist/{browser-T3V3JWVH.js → browser-ILXTEWH4.js} +1 -1
- package/dist/{browser-HBL72GPZ.js → browser-XPDMY2NX.js} +3 -3
- package/dist/{cdp-driver-J3YQ4LJV.js → cdp-driver-ESH3PDE6.js} +2 -1
- package/dist/cdp-driver-N2E2ZNSC.js +4644 -0
- package/dist/cdp-driver-YIKR4BUX.js +49 -0
- package/dist/chunk-3FWLW7FS.js +106 -0
- package/dist/chunk-A6LPGFAL.js +437 -0
- package/dist/{chunk-RRBXV7KE.js → chunk-DKM2JOZL.js} +633 -63
- package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
- package/dist/{chunk-5UGR6MUK.js → chunk-HBHOKZPN.js} +51 -16
- package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
- package/dist/{chunk-INTQPBYF.js → chunk-JKVUFP3G.js} +6 -2
- package/dist/chunk-KJTABK3Z.js +1255 -0
- package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
- package/dist/{chunk-4452SPFI.js → chunk-NODRQGOK.js} +51 -16
- package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
- package/dist/{chunk-NITFVWWS.js → chunk-OVW2UEWN.js} +981 -407
- package/dist/{chunk-WSCP7QCJ.js → chunk-R6IJ6PIV.js} +14 -11
- package/dist/{chunk-YMUSHPU4.js → chunk-SH6OTPXN.js} +46 -16
- package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
- package/dist/{cdp-driver-2T4P4ZE2.js → chunk-Z6CUR3VG.js} +1542 -134
- package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
- package/dist/cli.js +1136 -584
- package/dist/{daemon-client-GKEPT4NY.js → daemon-client-7D6EZNOE.js} +46 -16
- package/dist/{daemon-client-O6BYVRXV.js → daemon-client-HEGAXWGK.js} +1 -1
- package/dist/daemon-main.js +1031 -522
- package/dist/{human-interaction-5AO42MBA.js → human-interaction-4YR2N6R6.js} +2 -2
- package/dist/{human-interaction-C5OEGXGO.js → human-interaction-Y5IDH6LD.js} +1 -1
- package/dist/{human-interaction-ISXZTAYY.js → human-interaction-ZUTR5AC2.js} +2 -2
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1147 -593
- package/dist/{proxy-C6CK3UH5.js → proxy-LUR4U5YF.js} +2 -1
- package/dist/{recovery-J2ISVGUL.js → recovery-FTZGV6VY.js} +2 -2
- package/dist/{recovery-ZJVVHP7N.js → recovery-L5GLDX4O.js} +1 -1
- package/dist/{recovery-EF33LKRJ.js → recovery-Z3RDZENA.js} +2 -2
- package/dist/{session-recorder-H3KEYU26.js → session-recorder-3BEVWHOK.js} +1 -1
- package/dist/{session-recorder-QRZMKFVL.js → session-recorder-SLDBENVF.js} +1 -1
- package/dist/{session-replayer-LJUC4TI7.js → session-replayer-K4A6OI4M.js} +90 -2
- package/package.json +1 -1
- package/dist/chunk-BAHSRZIX.js +0 -2191
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// src/cdp-interceptor/proxy.ts
|
|
2
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
3
|
-
|
|
4
1
|
// src/cdp-interceptor/rules/shared.ts
|
|
5
2
|
var PLAYWRIGHT_INTERNAL_MARKERS = [
|
|
6
3
|
"__commonJS",
|
|
@@ -46,22 +43,22 @@ function extractAllStrings(rawArgs) {
|
|
|
46
43
|
// src/cdp-interceptor/rules/dom-mutation.ts
|
|
47
44
|
var DOM_PATTERNS = [
|
|
48
45
|
// ── P0: Value/Checked (bypasses React onChange) ────────────
|
|
49
|
-
{ pattern: /\.value\s*=\s*(?!["'\s]*$)/, name: ".value =", severity: "danger", action: "
|
|
50
|
-
{ pattern: /\.checked\s*=\s*(?:true|false)/, name: ".checked =", severity: "danger", action: "
|
|
51
|
-
{ pattern: /\.indeterminate\s*=\s*true/, name: ".indeterminate =", severity: "warn", action: "
|
|
52
|
-
{ pattern: /\.valueAsDate\s*=/, name: ".valueAsDate =", severity: "warn", action: "
|
|
53
|
-
{ pattern: /\.valueAsNumber\s*=/, name: ".valueAsNumber =", severity: "warn", action: "
|
|
46
|
+
{ pattern: /\.value\s*=\s*(?!["'\s]*$)/, name: ".value =", severity: "danger", action: "pass", errorCode: -32001, suggestion: "Use page.fill(selector, value) which dispatches proper input/change events." },
|
|
47
|
+
{ pattern: /\.checked\s*=\s*(?:true|false)/, name: ".checked =", severity: "danger", action: "pass", errorCode: -32001, suggestion: "Use page.check(selector) or page.uncheck(selector)." },
|
|
48
|
+
{ pattern: /\.indeterminate\s*=\s*true/, name: ".indeterminate =", severity: "warn", action: "pass", errorCode: -32021, suggestion: "No human can set indeterminate state \u2014 remove this call." },
|
|
49
|
+
{ pattern: /\.valueAsDate\s*=/, name: ".valueAsDate =", severity: "warn", action: "pass", errorCode: -32022, suggestion: "Use page.fill() with formatted date string instead." },
|
|
50
|
+
{ pattern: /\.valueAsNumber\s*=/, name: ".valueAsNumber =", severity: "warn", action: "pass", errorCode: -32022, suggestion: "Use page.fill() with numeric string instead." },
|
|
54
51
|
// ── P1: Select/Option (bypasses React onChange on select) ─
|
|
55
|
-
{ pattern: /\.selectedIndex\s*=\s*\d+/, name: ".selectedIndex =", severity: "danger", action: "
|
|
56
|
-
{ pattern: /(?:options|children)\s*\[[^\]]*\]\s*\.\s*selected\s*=\s*(?:true|false)/, name: "options[N].selected =", severity: "danger", action: "
|
|
57
|
-
{ pattern: /\.value\s*=\s*["'][^"']*["']\s*[;)]?\s*$/, name: "selectElement.value =", severity: "warn", action: "
|
|
52
|
+
{ pattern: /\.selectedIndex\s*=\s*\d+/, name: ".selectedIndex =", severity: "danger", action: "pass", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
|
|
53
|
+
{ pattern: /(?:options|children)\s*\[[^\]]*\]\s*\.\s*selected\s*=\s*(?:true|false)/, name: "options[N].selected =", severity: "danger", action: "pass", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
|
|
54
|
+
{ pattern: /\.value\s*=\s*["'][^"']*["']\s*[;)]?\s*$/, name: "selectElement.value =", severity: "warn", action: "pass", errorCode: -32023, suggestion: "For select elements, use page.selectOption(selector, value)." },
|
|
58
55
|
// ── P2: Content properties (bypasses virtual DOM diffing) ──
|
|
59
56
|
{ pattern: /\.innerHTML\s*=/, name: ".innerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: ".innerHTML bypasses React/Vue diffing. Use component state or page.setContent()." },
|
|
60
57
|
{ pattern: /\.outerHTML\s*=/, name: ".outerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: "outerHTML replacement destroys React fiber tree. Use page.setContent()." },
|
|
61
|
-
{ pattern: /\.innerText\s*=/, name: ".innerText =", severity: "warn", action: "
|
|
62
|
-
{ pattern: /\.textContent\s*=/, name: ".textContent =", severity: "warn", action: "
|
|
63
|
-
{ pattern: /\.outerText\s*=/, name: ".outerText =", severity: "warn", action: "
|
|
64
|
-
{ pattern: /nodeValue\s*=/, name: "node.nodeValue =", severity: "info", action: "
|
|
58
|
+
{ pattern: /\.innerText\s*=/, name: ".innerText =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "Framework components should be updated via state, not innerText." },
|
|
59
|
+
{ pattern: /\.textContent\s*=/, name: ".textContent =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "textContent bypasses React/Vue diffing. Use component state instead." },
|
|
60
|
+
{ pattern: /\.outerText\s*=/, name: ".outerText =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "Non-standard property \u2014 use proper framework update methods." },
|
|
61
|
+
{ pattern: /nodeValue\s*=/, name: "node.nodeValue =", severity: "info", action: "pass", errorCode: -32025, suggestion: "Direct text node mutation. Use textContent instead if needed." },
|
|
65
62
|
// ── P3: Style properties ──────────────────────────────────
|
|
66
63
|
{ pattern: /\.style\s*=\s*["']/, name: ".style = (string override)", severity: "info", action: "pass", errorCode: -32008, suggestion: 'Setting style as a string overwrites CSSStyleDeclaration. Use element.style.prop = "value".' },
|
|
67
64
|
{ pattern: /\.style\.cssText\s*=/, name: ".style.cssText =", severity: "info", action: "pass", errorCode: -32008, suggestion: "style.cssText replacement destroys inline styles \u2014 use individual property set." },
|
|
@@ -109,7 +106,7 @@ var DOM_PATTERNS = [
|
|
|
109
106
|
{ pattern: /\.scrollBy\s*\(/, name: ".scrollBy()", severity: "info", action: "pass", errorCode: -32015, suggestion: "ScrollBy without user gesture." },
|
|
110
107
|
{ pattern: /\.scrollIntoView\s*\(/, name: ".scrollIntoView()", severity: "info", action: "pass", errorCode: -32015, suggestion: "scrollIntoView is a common bot pattern. Let Playwright handle scrolling." },
|
|
111
108
|
// ── P11: Shadow DOM ───────────────────────────────────────
|
|
112
|
-
{ pattern: /\.shadowRoot\s*=/, name: ".shadowRoot = (override)", severity: "info", action: "
|
|
109
|
+
{ pattern: /\.shadowRoot\s*=/, name: ".shadowRoot = (override)", severity: "info", action: "pass", errorCode: -32036, suggestion: "ShadowRoot is read-only \u2014 this set attempt is detectable." },
|
|
113
110
|
// ── P12: Force reflow / layout thrashing ──────────────────
|
|
114
111
|
{ pattern: /\.offsetHeight\b(?!\s*===?\s*)/, name: ".offsetHeight read (forced reflow)", severity: "warn", action: "pass", errorCode: -32016, suggestion: "Reading offsetHeight triggers forced reflow \u2014 anti-crawlers detect this as layout probing." },
|
|
115
112
|
{ pattern: /\.offsetWidth\b(?!\s*===?\s*)/, name: ".offsetWidth read (forced reflow)", severity: "warn", action: "pass", errorCode: -32016, suggestion: "Reading offsetWidth triggers forced reflow \u2014 detectable." },
|
|
@@ -222,7 +219,7 @@ function analyzeTrajectory(samples) {
|
|
|
222
219
|
const stopY = samples[samples.length - 1].y;
|
|
223
220
|
return {
|
|
224
221
|
ruleId: "mouse-trajectory",
|
|
225
|
-
action: "
|
|
222
|
+
action: "pass",
|
|
226
223
|
severity: "danger",
|
|
227
224
|
reason: `Suspicious mouse trajectory: ${issues.join("; ")}`,
|
|
228
225
|
suggestion: `This mouse movement appears automated (straight line A\u2192B, no natural variation).
|
|
@@ -364,7 +361,7 @@ function analyzeKeyTiming(samples) {
|
|
|
364
361
|
if (allIdentical) {
|
|
365
362
|
return {
|
|
366
363
|
ruleId: "input-keystroke",
|
|
367
|
-
action: "
|
|
364
|
+
action: "pass",
|
|
368
365
|
severity: "danger",
|
|
369
366
|
reason: `All ${intervals.length} keystroke intervals are exactly ${intervals[0]}ms \u2014 impossible for human typing.`,
|
|
370
367
|
suggestion: `Use page.fill(selector, text) instead of page.type() with delay.
|
|
@@ -376,7 +373,7 @@ Or add random variation: page.type(selector, text, {delay: 50 + Math.random() *
|
|
|
376
373
|
if (cv < 0.08) {
|
|
377
374
|
return {
|
|
378
375
|
ruleId: "input-keystroke",
|
|
379
|
-
action: "
|
|
376
|
+
action: "pass",
|
|
380
377
|
severity: "warn",
|
|
381
378
|
reason: `Unnatural keystroke timing (CV=${cv.toFixed(3)}). Human typing has CV > 0.2 on average.`,
|
|
382
379
|
suggestion: `Add random variation to your typing delay: page.type(selector, text, {delay: 50 + Math.random() * 80}).`,
|
|
@@ -472,7 +469,7 @@ function makeDecision(p, context) {
|
|
|
472
469
|
const suggestion = p.suggestionOverride ?? `Detected: "${p.name}" \u2014 an automation tool marker that anti-crawler systems immediately flag. Remove this pattern from your code.`;
|
|
473
470
|
return {
|
|
474
471
|
ruleId: "automation-signals",
|
|
475
|
-
action: "
|
|
472
|
+
action: "pass",
|
|
476
473
|
severity: p.severity,
|
|
477
474
|
reason: `Automation marker detected: "${p.name}". Context: "${context}..."`,
|
|
478
475
|
suggestion,
|
|
@@ -547,7 +544,7 @@ var fingerprintingRule = {
|
|
|
547
544
|
if (p.pattern.test(userCode)) {
|
|
548
545
|
return {
|
|
549
546
|
ruleId: "fingerprinting",
|
|
550
|
-
action: "
|
|
547
|
+
action: "pass",
|
|
551
548
|
severity: p.severity,
|
|
552
549
|
reason: `Browser fingerprinting API accessed: "${p.name}". Anti-crawler systems use this to identify your browser.`,
|
|
553
550
|
suggestion: p.suggestion,
|
|
@@ -564,7 +561,7 @@ var fingerprintingRule = {
|
|
|
564
561
|
var EVENT_PATTERNS = [
|
|
565
562
|
// ── Direct method calls (all isTrusted=false) ─────────
|
|
566
563
|
{ pattern: /\.click\s*\(\s*\)/, name: "el.click()", severity: "danger", errorCode: -32070, suggestion: "el.click() fires isTrusted=false events. Use page.click(selector) which uses Input.dispatchMouseEvent (isTrusted=true)." },
|
|
567
|
-
{ pattern: /\.focus\s*\(\s*\)/, name: "el.focus()", severity: "
|
|
564
|
+
{ pattern: /\.focus\s*\(\s*\)/, name: "el.focus()", severity: "warn", errorCode: -32071, suggestion: "el.focus() without user interaction is detectable. Use page.click(selector) which naturally focuses." },
|
|
568
565
|
{ pattern: /\.blur\s*\(\s*\)/, name: "el.blur()", severity: "danger", errorCode: -32071, suggestion: "el.blur() without user interaction. Avoid in automation scripts." },
|
|
569
566
|
{ pattern: /\.submit\s*\(\s*\)/, name: "el.submit()", severity: "danger", errorCode: -32072, suggestion: `form.submit() bypasses onSubmit handlers. Click the submit button: page.click('button[type="submit"]').` },
|
|
570
567
|
{ pattern: /\.reset\s*\(\s*\)/, name: "el.reset()", severity: "danger", errorCode: -32072, suggestion: "form.reset() without user action. Let the user clear fields manually." },
|
|
@@ -579,7 +576,7 @@ var EVENT_PATTERNS = [
|
|
|
579
576
|
{ pattern: /\.showPicker\s*\(\s*\)/, name: "HTMLInputElement.showPicker()", severity: "info", errorCode: -32076, suggestion: "Date/color picker shown without click \u2014 detectable." },
|
|
580
577
|
{ pattern: /\.reportValidity\s*\(\s*\)/, name: "el.reportValidity()", severity: "info", errorCode: -32076, suggestion: "Validity reporting without form submission attempt." },
|
|
581
578
|
// ── dispatchEvent with synthetic events (isTrusted=false) ──
|
|
582
|
-
{ pattern: /dispatchEvent\s*\(\s*new\s+(?:Event|CustomEvent)\s*\(/, name: "dispatchEvent(new Event/CustomEvent)", severity: "
|
|
579
|
+
{ pattern: /dispatchEvent\s*\(\s*new\s+(?:Event|CustomEvent)\s*\(/, name: "dispatchEvent(new Event/CustomEvent)", severity: "warn", errorCode: -32077, suggestion: "Synthetic events have isTrusted=false. Use Input.dispatch* CDP methods for trusted events." },
|
|
583
580
|
{ pattern: /dispatchEvent\s*\(\s*new\s+MouseEvent\s*\(/, name: "dispatchEvent(new MouseEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic mouse events (isTrusted=false). Use Input.dispatchMouseEvent CDP method." },
|
|
584
581
|
{ pattern: /dispatchEvent\s*\(\s*new\s+KeyboardEvent\s*\(/, name: "dispatchEvent(new KeyboardEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic keyboard events (isTrusted=false). Use Input.dispatchKeyEvent CDP method." },
|
|
585
582
|
{ pattern: /dispatchEvent\s*\(\s*new\s+FocusEvent\s*\(/, name: "dispatchEvent(new FocusEvent)", severity: "warn", errorCode: -32078, suggestion: "Synthetic focus events bypass user interaction." },
|
|
@@ -608,7 +605,9 @@ var eventSimulationRule = {
|
|
|
608
605
|
if (p.pattern.test(userCode)) {
|
|
609
606
|
return {
|
|
610
607
|
ruleId: "event-simulation",
|
|
611
|
-
|
|
608
|
+
// danger 级硬拦(el.click 等 100% 暴露的模式);
|
|
609
|
+
// warning 级放行+提示(dispatchEvent 在部分合法场景如 DataTransfer paste 使用)
|
|
610
|
+
action: p.severity === "danger" ? "block" : "pass",
|
|
612
611
|
severity: p.severity,
|
|
613
612
|
reason: `Event simulation detected: "${p.name}". Synthetic events have isTrusted=false and are 100% detectable.`,
|
|
614
613
|
suggestion: p.suggestion,
|
|
@@ -667,7 +666,7 @@ var emulationOverrideRule = {
|
|
|
667
666
|
if (ctx.method !== p.method) continue;
|
|
668
667
|
return {
|
|
669
668
|
ruleId: "emulation-override",
|
|
670
|
-
action: "
|
|
669
|
+
action: "pass",
|
|
671
670
|
severity: p.severity,
|
|
672
671
|
reason: `CDP emulation/override detected: "${p.name}". This creates detectable inconsistencies between the JS environment and real browser state.`,
|
|
673
672
|
suggestion: p.suggestion,
|
|
@@ -697,7 +696,7 @@ var networkAnomalyRule = {
|
|
|
697
696
|
if (!headerStr.includes("sec-ch-ua")) {
|
|
698
697
|
return {
|
|
699
698
|
ruleId: "network-anomaly",
|
|
700
|
-
action: "
|
|
699
|
+
action: "pass",
|
|
701
700
|
severity: "warn",
|
|
702
701
|
reason: "Network.setExtraHTTPHeaders called without Sec-CH-UA client hints \u2014 browser normally sends these.",
|
|
703
702
|
suggestion: "Modern browsers send Sec-CH-UA headers automatically. Adding custom headers without them creates detectable inconsistency.",
|
|
@@ -711,7 +710,7 @@ var networkAnomalyRule = {
|
|
|
711
710
|
case "Network.clearBrowserCache": {
|
|
712
711
|
return {
|
|
713
712
|
ruleId: "network-anomaly",
|
|
714
|
-
action: "
|
|
713
|
+
action: "pass",
|
|
715
714
|
severity: "warn",
|
|
716
715
|
reason: "Network.clearBrowserCache called \u2014 cache clearing mid-session is unnatural for real users.",
|
|
717
716
|
suggestion: "Avoid cache clearing during sessions. Start with a fresh profile if needed.",
|
|
@@ -722,7 +721,7 @@ var networkAnomalyRule = {
|
|
|
722
721
|
case "Network.clearBrowserCookies": {
|
|
723
722
|
return {
|
|
724
723
|
ruleId: "network-anomaly",
|
|
725
|
-
action: "
|
|
724
|
+
action: "pass",
|
|
726
725
|
severity: "warn",
|
|
727
726
|
reason: "Network.clearBrowserCookies called \u2014 wiping cookies mid-session is a scraper optimization.",
|
|
728
727
|
suggestion: "Cookies should only be cleared via normal browser flow (expiration, user action).",
|
|
@@ -733,7 +732,7 @@ var networkAnomalyRule = {
|
|
|
733
732
|
case "Network.setBlockedURLs": {
|
|
734
733
|
return {
|
|
735
734
|
ruleId: "network-anomaly",
|
|
736
|
-
action: "
|
|
735
|
+
action: "pass",
|
|
737
736
|
severity: "warn",
|
|
738
737
|
reason: "Network.setBlockedURLs blocks resource loading \u2014 this changes the page behavior and is detectable.",
|
|
739
738
|
suggestion: "Blocking images/fonts/etc creates measurable differences in performance and page rendering.",
|
|
@@ -755,7 +754,7 @@ var networkAnomalyRule = {
|
|
|
755
754
|
case "Fetch.enable": {
|
|
756
755
|
return {
|
|
757
756
|
ruleId: "network-anomaly",
|
|
758
|
-
action: "
|
|
757
|
+
action: "pass",
|
|
759
758
|
severity: "warn",
|
|
760
759
|
reason: "Fetch.enable intercepts all network requests \u2014 a man-in-the-middle approach used by scrapers.",
|
|
761
760
|
suggestion: "Do not use Fetch domain for network interception if avoiding detection.",
|
|
@@ -795,7 +794,7 @@ var pageLifecycleRule = {
|
|
|
795
794
|
if (interval < 100) {
|
|
796
795
|
return {
|
|
797
796
|
ruleId: "page-lifecycle",
|
|
798
|
-
action: "
|
|
797
|
+
action: "pass",
|
|
799
798
|
severity: "warn",
|
|
800
799
|
reason: `Multiple Page.navigate calls within ${interval}ms of each other \u2014 unnatural rapid navigation.`,
|
|
801
800
|
suggestion: "Add proper waits between navigations: wait for page load before navigating again.",
|
|
@@ -812,7 +811,7 @@ var pageLifecycleRule = {
|
|
|
812
811
|
if (state.lastNavTime > 0 && now - state.lastNavTime < 500) {
|
|
813
812
|
return {
|
|
814
813
|
ruleId: "page-lifecycle",
|
|
815
|
-
action: "
|
|
814
|
+
action: "pass",
|
|
816
815
|
severity: "danger",
|
|
817
816
|
reason: "Page.captureScreenshot called within 500ms of navigation \u2014 content extraction pattern.",
|
|
818
817
|
suggestion: "Wait for the page to fully render before taking screenshots: wait for load/networkidle.",
|
|
@@ -823,7 +822,7 @@ var pageLifecycleRule = {
|
|
|
823
822
|
if (state.screenshots > 3 && state.navigations.length < 2) {
|
|
824
823
|
return {
|
|
825
824
|
ruleId: "page-lifecycle",
|
|
826
|
-
action: "
|
|
825
|
+
action: "pass",
|
|
827
826
|
severity: "warn",
|
|
828
827
|
reason: "Multiple screenshots on a single page without navigation \u2014 suspicious extraction behavior.",
|
|
829
828
|
suggestion: "Consider if all screenshots are necessary.",
|
|
@@ -836,7 +835,7 @@ var pageLifecycleRule = {
|
|
|
836
835
|
case "Page.printToPDF": {
|
|
837
836
|
return {
|
|
838
837
|
ruleId: "page-lifecycle",
|
|
839
|
-
action: "
|
|
838
|
+
action: "pass",
|
|
840
839
|
severity: "danger",
|
|
841
840
|
reason: "Page.printToPDF called \u2014 this is a telltale scraper pattern that gives away automation intent.",
|
|
842
841
|
suggestion: "Avoid PDF generation. If you must, add significant delays and user-like interaction first.",
|
|
@@ -848,7 +847,7 @@ var pageLifecycleRule = {
|
|
|
848
847
|
if (state.lastNavTime > 0 && now - state.lastNavTime < 1e3) {
|
|
849
848
|
return {
|
|
850
849
|
ruleId: "page-lifecycle",
|
|
851
|
-
action: "
|
|
850
|
+
action: "pass",
|
|
852
851
|
severity: "warn",
|
|
853
852
|
reason: "Page.reload called immediately after navigate \u2014 unnatural fast-reload pattern.",
|
|
854
853
|
suggestion: "Introduce delays between navigation and reload to simulate human behavior.",
|
|
@@ -862,7 +861,7 @@ var pageLifecycleRule = {
|
|
|
862
861
|
if (state.navigations.length < 2) {
|
|
863
862
|
return {
|
|
864
863
|
ruleId: "page-lifecycle",
|
|
865
|
-
action: "
|
|
864
|
+
action: "pass",
|
|
866
865
|
severity: "info",
|
|
867
866
|
reason: "Page.close called after minimal interaction \u2014 zombie pages common in automation.",
|
|
868
867
|
suggestion: "Ensure meaningful interaction before closing pages.",
|
|
@@ -878,7 +877,7 @@ var pageLifecycleRule = {
|
|
|
878
877
|
if (state.evaluateCount > 50 && state.navigations.length === 0) {
|
|
879
878
|
return {
|
|
880
879
|
ruleId: "page-lifecycle",
|
|
881
|
-
action: "
|
|
880
|
+
action: "pass",
|
|
882
881
|
severity: "info",
|
|
883
882
|
reason: "50+ evaluate calls without any Page.navigate \u2014 data extraction without real browsing.",
|
|
884
883
|
suggestion: "Navigate to a real page first. Evaluate on about:blank is suspicious.",
|
|
@@ -940,432 +939,6 @@ function createRuleEngine(customRules) {
|
|
|
940
939
|
};
|
|
941
940
|
}
|
|
942
941
|
|
|
943
|
-
// src/cdp-interceptor/logger.ts
|
|
944
|
-
function createLogger(config) {
|
|
945
|
-
const buffer = [];
|
|
946
|
-
const MAX_BUFFER = 2e3;
|
|
947
|
-
return {
|
|
948
|
-
info(message, meta) {
|
|
949
|
-
if (!config.enableLogging) return;
|
|
950
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
951
|
-
if (meta) {
|
|
952
|
-
console.log(`[CDPInterceptor ${ts}] ${message}`, JSON.stringify(meta));
|
|
953
|
-
} else {
|
|
954
|
-
console.log(`[CDPInterceptor ${ts}] ${message}`);
|
|
955
|
-
}
|
|
956
|
-
},
|
|
957
|
-
log(method, direction, sessionId, payload, decision) {
|
|
958
|
-
const entry = {
|
|
959
|
-
timestamp: Date.now(),
|
|
960
|
-
direction,
|
|
961
|
-
sessionId,
|
|
962
|
-
method,
|
|
963
|
-
payload: sanitizePayload(payload),
|
|
964
|
-
decision: decision ?? void 0
|
|
965
|
-
};
|
|
966
|
-
if (config.enableLogging) {
|
|
967
|
-
buffer.push(entry);
|
|
968
|
-
if (buffer.length > MAX_BUFFER) buffer.shift();
|
|
969
|
-
const tag = decision ? decision.action === "block" ? "\u{1F6AB}BLOCK" : decision.action === "transform" ? "\u{1F504}XFMR" : "\u2705" : " ";
|
|
970
|
-
const reason = decision ? ` [${decision.severity}] ${decision.reason}` : "";
|
|
971
|
-
console.log(`[CDP] ${tag} ${direction} ${method}${reason}`);
|
|
972
|
-
}
|
|
973
|
-
return entry;
|
|
974
|
-
},
|
|
975
|
-
getRecent(count) {
|
|
976
|
-
return buffer.slice(-count);
|
|
977
|
-
},
|
|
978
|
-
flush() {
|
|
979
|
-
buffer.length = 0;
|
|
980
|
-
}
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
function sanitizePayload(payload) {
|
|
984
|
-
if (typeof payload !== "object" || payload === null) return { raw: String(payload) };
|
|
985
|
-
const obj = payload;
|
|
986
|
-
const cleaned = {};
|
|
987
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
988
|
-
if (key === "data" && typeof value === "string" && value.length > 200) {
|
|
989
|
-
cleaned[key] = `<binary: ${value.length} chars>`;
|
|
990
|
-
} else if (key === "expression" && typeof value === "string" && value.length > 500) {
|
|
991
|
-
cleaned[key] = value.substring(0, 500) + "...";
|
|
992
|
-
} else {
|
|
993
|
-
cleaned[key] = value;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return cleaned;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// src/cdp-interceptor/advisor.ts
|
|
1000
|
-
function formatBlockMessage(decision, method) {
|
|
1001
|
-
const adv = advise(decision, method);
|
|
1002
|
-
const lines = [
|
|
1003
|
-
`[CDP-FIREWALL-BLOCK] rule=${decision.ruleId}`,
|
|
1004
|
-
`method=${method}`,
|
|
1005
|
-
`reason=${decision.reason}`,
|
|
1006
|
-
`suggestion=${decision.suggestion ?? adv.detail}`
|
|
1007
|
-
];
|
|
1008
|
-
if (adv.codeExample) {
|
|
1009
|
-
lines.push(`code-example=`);
|
|
1010
|
-
lines.push(adv.codeExample);
|
|
1011
|
-
}
|
|
1012
|
-
return lines.join("\n");
|
|
1013
|
-
}
|
|
1014
|
-
function advise(decision, originalMethod) {
|
|
1015
|
-
const baseAdvice = getBaseAdvice(decision, originalMethod);
|
|
1016
|
-
return {
|
|
1017
|
-
ruleId: decision.ruleId,
|
|
1018
|
-
title: baseAdvice.title,
|
|
1019
|
-
detail: decision.suggestion ?? baseAdvice.detail,
|
|
1020
|
-
codeExample: baseAdvice.codeExample
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
function getBaseAdvice(decision, method) {
|
|
1024
|
-
switch (decision.ruleId) {
|
|
1025
|
-
case "dom-mutation":
|
|
1026
|
-
return {
|
|
1027
|
-
title: "Direct DOM property mutation blocked",
|
|
1028
|
-
detail: `Your ${method} call tried to set a DOM property directly. In React/Vue/Angular, this bypasses the framework's virtual DOM completely meaning onChange/onInput never fires. The website CAN detect this mismatch as automation.`,
|
|
1029
|
-
codeExample: [
|
|
1030
|
-
"# \u274C BLOCKED \u2014 what you tried to do:",
|
|
1031
|
-
`page.evaluate(\`el.value = 'hello'\`) # triggers isTrusted=false`,
|
|
1032
|
-
"",
|
|
1033
|
-
"# \u2705 USE INSTEAD \u2014 proper CDP input dispatch:",
|
|
1034
|
-
"page.fill('#selector', 'hello') # Playwright: dispatches input+change events",
|
|
1035
|
-
"page.type('#selector', 'hello', {delay}) # Playwright: real keystrokes",
|
|
1036
|
-
"page.locator('#selector').fill('hello') # Playwright: recommended API"
|
|
1037
|
-
].join("\n")
|
|
1038
|
-
};
|
|
1039
|
-
case "mouse-trajectory":
|
|
1040
|
-
return {
|
|
1041
|
-
title: "Unnatural mouse trajectory blocked",
|
|
1042
|
-
detail: `Your ${method} sequence formed a perfectly linear path. Human hands have micro-tremors (1-3px variation at any point along the arc), acceleration curves, and never draw straight lines between distant points.`,
|
|
1043
|
-
codeExample: [
|
|
1044
|
-
"# \u274C BLOCKED \u2014 linear interpolation:",
|
|
1045
|
-
"for i in range(20):",
|
|
1046
|
-
" page.mouse.move(x0 + (x1-x0)*(i/20), y0 + (y1-y0)*(i/20))",
|
|
1047
|
-
"",
|
|
1048
|
-
"# \u2705 USE INSTEAD \u2014 Bezier curves with overshoot:",
|
|
1049
|
-
'# Use "ghost-cursor" or similar library',
|
|
1050
|
-
"from ghost_cursor import path_to",
|
|
1051
|
-
"path_to(page, (x1, y1))"
|
|
1052
|
-
].join("\n")
|
|
1053
|
-
};
|
|
1054
|
-
case "input-keystroke":
|
|
1055
|
-
if (method === "Input.insertText") {
|
|
1056
|
-
return {
|
|
1057
|
-
title: "Input.insertText detected (logged, not blocked)",
|
|
1058
|
-
detail: "Input.insertText bypasses native keyDown\u2192keyPress\u2192input\u2192keyUp events. Playwright uses this internally for page.fill(). Logged for observability."
|
|
1059
|
-
};
|
|
1060
|
-
}
|
|
1061
|
-
return {
|
|
1062
|
-
title: "Unnatural keystroke timing blocked",
|
|
1063
|
-
detail: `Your ${method} calls have unnaturally constant timing (e.g., exact 50ms intervals). Human typing always has variation (CV > 0.2).`,
|
|
1064
|
-
codeExample: [
|
|
1065
|
-
"# \u274C BLOCKED \u2014 exact constant delay:",
|
|
1066
|
-
"page.type('#input', 'hello', {delay: 50}) # every keystroke exactly 50ms apart",
|
|
1067
|
-
"",
|
|
1068
|
-
"# \u2705 USE INSTEAD \u2014 variable delay (human-like):",
|
|
1069
|
-
"page.fill('#input', 'hello') # recommended, dispatches events properly",
|
|
1070
|
-
"# OR: type with randomized delay",
|
|
1071
|
-
"page.type('#input', 'hello', {delay: 50 + Math.floor(Math.random() * 80)})"
|
|
1072
|
-
].join("\n")
|
|
1073
|
-
};
|
|
1074
|
-
case "automation-signals":
|
|
1075
|
-
return {
|
|
1076
|
-
title: "Browser automation marker detected",
|
|
1077
|
-
detail: `Your ${method} call accessed a property/marker that anti-crawler systems check to detect automation. These markers (navigator.webdriver, window.__playwright, etc.) are the #1 detection vector.`,
|
|
1078
|
-
codeExample: [
|
|
1079
|
-
"# \u274C BLOCKED \u2014 don't check for automation markers:",
|
|
1080
|
-
"navigator.webdriver # NEVER check this",
|
|
1081
|
-
"window.__playwright # NEVER check this",
|
|
1082
|
-
"chrome.runtime # NEVER check this",
|
|
1083
|
-
"",
|
|
1084
|
-
"# \u2705 Just go about your business normally.",
|
|
1085
|
-
"# Anti-detection is handled by the CDP firewall automatically."
|
|
1086
|
-
].join("\n")
|
|
1087
|
-
};
|
|
1088
|
-
case "fingerprinting":
|
|
1089
|
-
return {
|
|
1090
|
-
title: "Browser fingerprinting API access blocked",
|
|
1091
|
-
detail: `Your ${method} call accessed a known fingerprinting API. These APIs (canvas.toDataURL, WebGL getParameter, AudioContext, etc.) are used by anti-crawler systems to build a unique device fingerprint.`,
|
|
1092
|
-
codeExample: [
|
|
1093
|
-
"# \u274C BLOCKED \u2014 fingerprinting vector:",
|
|
1094
|
-
"canvas.toDataURL() # returns unique browser hash",
|
|
1095
|
-
'gl.getParameter(gl.VENDOR) # returns "SwiftShader" in headless',
|
|
1096
|
-
"screen.availWidth - screen.availHeight # no OS chrome in headless",
|
|
1097
|
-
"",
|
|
1098
|
-
"# \u2705 Avoid accessing these APIs. They are only used for fingerprinting."
|
|
1099
|
-
].join("\n")
|
|
1100
|
-
};
|
|
1101
|
-
case "event-simulation":
|
|
1102
|
-
return {
|
|
1103
|
-
title: "Synthetic event simulation blocked",
|
|
1104
|
-
detail: `Your ${method} call simulated user interaction via el.click() or dispatchEvent(new Event(...)). These produce isTrusted=false events, which are 100% detectable by any anti-crawler that checks isTrusted on critical events.`,
|
|
1105
|
-
codeExample: [
|
|
1106
|
-
"# \u274C BLOCKED \u2014 synthetic events (isTrusted=false):",
|
|
1107
|
-
"el.click() # isTrusted=false",
|
|
1108
|
-
'el.dispatchEvent(new Event("click")) # isTrusted=false',
|
|
1109
|
-
"el.focus() # isTrusted=false",
|
|
1110
|
-
"",
|
|
1111
|
-
"# \u2705 USE INSTEAD \u2014 CDP-level input dispatch (isTrusted=true):",
|
|
1112
|
-
"page.click(selector) # uses Input.dispatchMouseEvent",
|
|
1113
|
-
"page.fill(selector, value) # dispatches real input events"
|
|
1114
|
-
].join("\n")
|
|
1115
|
-
};
|
|
1116
|
-
case "emulation-override":
|
|
1117
|
-
return {
|
|
1118
|
-
title: "CDP emulation override blocked",
|
|
1119
|
-
detail: `Your ${method} call overrides browser behavior in a way that creates detectable inconsistencies. Anti-crawler systems cross-check multiple sources (e.g., navigator.userAgent vs HTTP User-Agent header) to catch these mismatches.`,
|
|
1120
|
-
codeExample: [
|
|
1121
|
-
"# \u274C BLOCKED \u2014 detectable emulation override:",
|
|
1122
|
-
"Emulation.setUserAgentOverride(...) # JS vs HTTP header mismatch",
|
|
1123
|
-
"Emulation.setGeolocationOverride(...) # IP geo vs overridden geo mismatch",
|
|
1124
|
-
"Emulation.setDeviceMetricsOverride(...) # matchMedia vs actual viewport",
|
|
1125
|
-
"",
|
|
1126
|
-
"# \u2705 These are handled automatically by the CDP firewall.",
|
|
1127
|
-
"# Do NOT call them manually."
|
|
1128
|
-
].join("\n")
|
|
1129
|
-
};
|
|
1130
|
-
case "network-anomaly":
|
|
1131
|
-
return {
|
|
1132
|
-
title: "Network anomaly detected",
|
|
1133
|
-
detail: `Your ${method} call triggered a network pattern that is characteristic of scrapers: blocking URLs, clearing caches, or intercepting requests.`,
|
|
1134
|
-
codeExample: [
|
|
1135
|
-
"# \u274C BLOCKED \u2014 scraper optimization:",
|
|
1136
|
-
"Network.clearBrowserCache() # natural users never do this",
|
|
1137
|
-
'Network.setBlockedURLs(["*fonts*"]) # blocking resources is detectable',
|
|
1138
|
-
"Fetch.enable() # MITM-style interception",
|
|
1139
|
-
"",
|
|
1140
|
-
"# \u2705 Let the browser manage its own cache and network normally."
|
|
1141
|
-
].join("\n")
|
|
1142
|
-
};
|
|
1143
|
-
case "page-lifecycle":
|
|
1144
|
-
return {
|
|
1145
|
-
title: "Suspicious page lifecycle pattern blocked",
|
|
1146
|
-
detail: `Your ${method} call reveals an unnatural page interaction sequence: navigating too fast, taking screenshots before the page renders, or generating PDFs (a telltale scraper giveaway).`,
|
|
1147
|
-
codeExample: [
|
|
1148
|
-
"# \u274C BLOCKED \u2014 unnatural lifecycle:",
|
|
1149
|
-
"page.goto(url); page.pdf() # PDF = scraper giveaway",
|
|
1150
|
-
"page.goto(url); page.screenshot() <500ms # screenshot before render",
|
|
1151
|
-
"page.goto(url) 3x in <100ms # rapid navigation barrage",
|
|
1152
|
-
"",
|
|
1153
|
-
"# \u2705 Add proper waits between actions:",
|
|
1154
|
-
'page.goto(url, {waitUntil: "networkidle"})',
|
|
1155
|
-
'page.waitForSelector("body")',
|
|
1156
|
-
"page.screenshot() # after rendering"
|
|
1157
|
-
].join("\n")
|
|
1158
|
-
};
|
|
1159
|
-
default:
|
|
1160
|
-
return {
|
|
1161
|
-
title: decision.reason,
|
|
1162
|
-
detail: decision.suggestion ?? `The CDP call "${method}" was blocked by rule "${decision.ruleId}".`
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// src/cdp-interceptor/proxy.ts
|
|
1168
|
-
function makeCompoundId(cdpSessionId, rawSessionId) {
|
|
1169
|
-
return `${cdpSessionId ?? "nil"}::${rawSessionId ?? "nil"}`;
|
|
1170
|
-
}
|
|
1171
|
-
var CDPInterceptorProxy = class {
|
|
1172
|
-
wss = null;
|
|
1173
|
-
engine;
|
|
1174
|
-
config;
|
|
1175
|
-
logger;
|
|
1176
|
-
started = false;
|
|
1177
|
-
stats = {
|
|
1178
|
-
totalMessages: 0,
|
|
1179
|
-
blockedMessages: 0,
|
|
1180
|
-
transformedMessages: 0,
|
|
1181
|
-
passedMessages: 0,
|
|
1182
|
-
byRule: {}
|
|
1183
|
-
};
|
|
1184
|
-
constructor(config) {
|
|
1185
|
-
this.config = config;
|
|
1186
|
-
this.engine = createRuleEngine(config.rules);
|
|
1187
|
-
this.logger = createLogger({
|
|
1188
|
-
enableLogging: config.enableLogging ?? true,
|
|
1189
|
-
logDir: config.logDir
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
/** The port the proxy is listening on (only valid after start()) */
|
|
1193
|
-
get port() {
|
|
1194
|
-
const addr = this.wss?.address();
|
|
1195
|
-
if (addr && typeof addr === "object") return addr.port;
|
|
1196
|
-
return 0;
|
|
1197
|
-
}
|
|
1198
|
-
/** Start the proxy server */
|
|
1199
|
-
async start() {
|
|
1200
|
-
if (this.started) return this.port;
|
|
1201
|
-
return new Promise((resolve, reject) => {
|
|
1202
|
-
this.wss = new WebSocketServer({ port: this.config.listenPort ?? 0 }, () => {
|
|
1203
|
-
const port = this.port;
|
|
1204
|
-
this.engine.start();
|
|
1205
|
-
this.started = true;
|
|
1206
|
-
this.logger.info("CDP interceptor proxy started", { port, endpoint: this.config.cdpEndpoint });
|
|
1207
|
-
resolve(port);
|
|
1208
|
-
});
|
|
1209
|
-
this.wss.on("error", reject);
|
|
1210
|
-
this.wss.on("connection", (clientWs, _req) => {
|
|
1211
|
-
this.handleConnection(clientWs);
|
|
1212
|
-
});
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
/** Stop the proxy server */
|
|
1216
|
-
async stop() {
|
|
1217
|
-
this.engine.stop();
|
|
1218
|
-
this.logger.flush();
|
|
1219
|
-
this.started = false;
|
|
1220
|
-
return new Promise((resolve) => {
|
|
1221
|
-
if (!this.wss) return resolve();
|
|
1222
|
-
this.wss.close(() => resolve());
|
|
1223
|
-
this.wss = null;
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
/** Get accumulated statistics */
|
|
1227
|
-
getStats() {
|
|
1228
|
-
return { ...this.stats };
|
|
1229
|
-
}
|
|
1230
|
-
/** Get recent log entries (for inspection) */
|
|
1231
|
-
getRecentLogs(count = 50) {
|
|
1232
|
-
return this.logger.getRecent(count);
|
|
1233
|
-
}
|
|
1234
|
-
// ── Connection handling ──────────────────────────────────────
|
|
1235
|
-
handleConnection(clientWs) {
|
|
1236
|
-
let browserWs = null;
|
|
1237
|
-
let isAlive = true;
|
|
1238
|
-
const pendingMessages = [];
|
|
1239
|
-
browserWs = new WebSocket(this.config.cdpEndpoint);
|
|
1240
|
-
clientWs.on("message", (raw) => {
|
|
1241
|
-
if (browserWs && browserWs.readyState === WebSocket.OPEN) {
|
|
1242
|
-
this.handleClientMessage(clientWs, browserWs, raw);
|
|
1243
|
-
} else {
|
|
1244
|
-
pendingMessages.push(raw);
|
|
1245
|
-
}
|
|
1246
|
-
});
|
|
1247
|
-
browserWs.on("open", () => {
|
|
1248
|
-
for (const buf of pendingMessages) {
|
|
1249
|
-
this.handleClientMessage(clientWs, browserWs, buf);
|
|
1250
|
-
}
|
|
1251
|
-
pendingMessages.length = 0;
|
|
1252
|
-
});
|
|
1253
|
-
browserWs.on("error", (err) => {
|
|
1254
|
-
this.logger.info("Browser WebSocket error", { error: String(err) });
|
|
1255
|
-
});
|
|
1256
|
-
browserWs.on("close", (code, reason) => {
|
|
1257
|
-
if (isAlive && clientWs.readyState === WebSocket.OPEN) {
|
|
1258
|
-
this.logger.info("Browser WS closed, closing client", { code, reason: String(reason) });
|
|
1259
|
-
clientWs.close();
|
|
1260
|
-
}
|
|
1261
|
-
});
|
|
1262
|
-
browserWs.on("message", (raw) => {
|
|
1263
|
-
this.handleBrowserMessage(clientWs, browserWs, raw);
|
|
1264
|
-
});
|
|
1265
|
-
const cleanup = () => {
|
|
1266
|
-
isAlive = false;
|
|
1267
|
-
if (browserWs && browserWs.readyState === WebSocket.OPEN) {
|
|
1268
|
-
browserWs.close();
|
|
1269
|
-
}
|
|
1270
|
-
};
|
|
1271
|
-
clientWs.on("close", cleanup);
|
|
1272
|
-
clientWs.on("error", cleanup);
|
|
1273
|
-
browserWs.on("close", () => {
|
|
1274
|
-
if (isAlive && clientWs.readyState === WebSocket.OPEN) {
|
|
1275
|
-
clientWs.close();
|
|
1276
|
-
}
|
|
1277
|
-
});
|
|
1278
|
-
browserWs.on("error", cleanup);
|
|
1279
|
-
}
|
|
1280
|
-
// ── Message processing ───────────────────────────────────────
|
|
1281
|
-
handleClientMessage(clientWs, browserWs, raw) {
|
|
1282
|
-
const msg = this.parseMessage(raw);
|
|
1283
|
-
if (!msg) return;
|
|
1284
|
-
this.stats.totalMessages++;
|
|
1285
|
-
if (!("method" in msg)) {
|
|
1286
|
-
browserWs.send(raw.toString());
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
const request = msg;
|
|
1290
|
-
const ctx = {
|
|
1291
|
-
method: request.method,
|
|
1292
|
-
params: request.params ?? {},
|
|
1293
|
-
sessionId: makeCompoundId("_cdpSession" in browserWs ? browserWs._cdpSession : void 0, request.sessionId),
|
|
1294
|
-
direction: "client\u2192browser"
|
|
1295
|
-
};
|
|
1296
|
-
const decision = this.engine.evaluate(ctx);
|
|
1297
|
-
this.logger.log(ctx.method, "client\u2192browser", ctx.sessionId, { method: ctx.method, params: ctx.params }, decision);
|
|
1298
|
-
if (decision) {
|
|
1299
|
-
this.recordDecision(decision);
|
|
1300
|
-
}
|
|
1301
|
-
if (decision?.action === "block") {
|
|
1302
|
-
const blockMsg = formatBlockMessage(decision, ctx.method);
|
|
1303
|
-
const errorResponse = {
|
|
1304
|
-
id: request.id,
|
|
1305
|
-
error: {
|
|
1306
|
-
code: decision.errorCode ?? -32e3,
|
|
1307
|
-
message: blockMsg
|
|
1308
|
-
},
|
|
1309
|
-
sessionId: request.sessionId
|
|
1310
|
-
};
|
|
1311
|
-
this.stats.blockedMessages++;
|
|
1312
|
-
console.error(`
|
|
1313
|
-
${blockMsg}
|
|
1314
|
-
`);
|
|
1315
|
-
clientWs.send(JSON.stringify(errorResponse));
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
if (decision?.action === "transform" && decision.transformedParams) {
|
|
1319
|
-
const transformed = { ...request, params: decision.transformedParams };
|
|
1320
|
-
this.stats.transformedMessages++;
|
|
1321
|
-
browserWs.send(JSON.stringify(transformed));
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
this.stats.passedMessages++;
|
|
1325
|
-
browserWs.send(raw.toString());
|
|
1326
|
-
}
|
|
1327
|
-
handleBrowserMessage(clientWs, _browserWs, raw) {
|
|
1328
|
-
const msg = this.parseMessage(raw);
|
|
1329
|
-
if (!msg) {
|
|
1330
|
-
clientWs.send(raw.toString());
|
|
1331
|
-
return;
|
|
1332
|
-
}
|
|
1333
|
-
if ("method" in msg) {
|
|
1334
|
-
const event = msg;
|
|
1335
|
-
const ctx = {
|
|
1336
|
-
method: event.method,
|
|
1337
|
-
params: event.params ?? {},
|
|
1338
|
-
sessionId: event.sessionId ?? "browser",
|
|
1339
|
-
direction: "browser\u2192client"
|
|
1340
|
-
};
|
|
1341
|
-
const decision = this.engine.evaluate(ctx);
|
|
1342
|
-
if (decision?.action === "block") {
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
clientWs.send(raw.toString());
|
|
1347
|
-
}
|
|
1348
|
-
// ── Utilities ────────────────────────────────────────────────
|
|
1349
|
-
parseMessage(raw) {
|
|
1350
|
-
try {
|
|
1351
|
-
return JSON.parse(raw.toString());
|
|
1352
|
-
} catch {
|
|
1353
|
-
return null;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
recordDecision(decision) {
|
|
1357
|
-
if (!this.stats.byRule[decision.ruleId]) {
|
|
1358
|
-
this.stats.byRule[decision.ruleId] = { matched: 0, blocked: 0, transformed: 0 };
|
|
1359
|
-
}
|
|
1360
|
-
this.stats.byRule[decision.ruleId].matched++;
|
|
1361
|
-
if (decision.action === "block") {
|
|
1362
|
-
this.stats.byRule[decision.ruleId].blocked++;
|
|
1363
|
-
} else if (decision.action === "transform") {
|
|
1364
|
-
this.stats.byRule[decision.ruleId].transformed++;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
};
|
|
1368
|
-
|
|
1369
942
|
export {
|
|
1370
943
|
domMutationRule,
|
|
1371
944
|
mouseTrajectoryRule,
|
|
@@ -1376,7 +949,5 @@ export {
|
|
|
1376
949
|
emulationOverrideRule,
|
|
1377
950
|
networkAnomalyRule,
|
|
1378
951
|
pageLifecycleRule,
|
|
1379
|
-
createRuleEngine
|
|
1380
|
-
advise,
|
|
1381
|
-
CDPInterceptorProxy
|
|
952
|
+
createRuleEngine
|
|
1382
953
|
};
|