feat: stealth hardening + test suite rewrite, bump to 0.1.4

- Remove --enable-automation via ignore_default_args (matches agent-browser
  Playwright patch — fixes connectionRTT, enables --fingerprint-* flags)
- Randomize fingerprint seed per launch (unique canvas/WebGL/audio per session)
- Change default GPU to RTX 3070 (higher market share = better blending)
- Rewrite stealth_test.py with JS evaluation for 6 detection sites
- Add --proxy flag to test script for Helper VPS routing
- All 6/6 tests passing (verified on VPS with v142 binary)
This commit is contained in:
CloakHQ
2026-02-23 07:58:50 +01:00
parent 2d7894ad68
commit c2eb0ef21a
6 changed files with 265 additions and 42 deletions
+4
View File
@@ -45,3 +45,7 @@ AGENTS.md
# Private docs (launch posts, strategy) # Private docs (launch posts, strategy)
docs/ docs/
# Release scripts
publish.sh
.env
+2 -2
View File
@@ -12,7 +12,7 @@ Usage:
""" """
from .browser import launch, launch_async, launch_context from .browser import launch, launch_async, launch_context
from .config import CHROMIUM_VERSION, DEFAULT_STEALTH_ARGS from .config import CHROMIUM_VERSION, get_default_stealth_args
from .download import binary_info, clear_cache, ensure_binary from .download import binary_info, clear_cache, ensure_binary
from ._version import __version__ from ._version import __version__
@@ -24,6 +24,6 @@ __all__ = [
"clear_cache", "clear_cache",
"binary_info", "binary_info",
"CHROMIUM_VERSION", "CHROMIUM_VERSION",
"DEFAULT_STEALTH_ARGS", "get_default_stealth_args",
"__version__", "__version__",
] ]
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.3" __version__ = "0.1.4"
+4 -2
View File
@@ -17,7 +17,7 @@ from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
from .config import DEFAULT_STEALTH_ARGS from .config import get_default_stealth_args
from .download import ensure_binary from .download import ensure_binary
logger = logging.getLogger("cloakbrowser") logger = logging.getLogger("cloakbrowser")
@@ -63,6 +63,7 @@ def launch(
executable_path=binary_path, executable_path=binary_path,
headless=headless, headless=headless,
args=chrome_args, args=chrome_args,
ignore_default_args=["--enable-automation"],
**_build_proxy_kwargs(proxy), **_build_proxy_kwargs(proxy),
**kwargs, **kwargs,
) )
@@ -123,6 +124,7 @@ async def launch_async(
executable_path=binary_path, executable_path=binary_path,
headless=headless, headless=headless,
args=chrome_args, args=chrome_args,
ignore_default_args=["--enable-automation"],
**_build_proxy_kwargs(proxy), **_build_proxy_kwargs(proxy),
**kwargs, **kwargs,
) )
@@ -209,7 +211,7 @@ def _build_args(stealth_args: bool, extra_args: list[str] | None) -> list[str]:
"""Combine stealth args with user-provided args.""" """Combine stealth args with user-provided args."""
result = [] result = []
if stealth_args: if stealth_args:
result.extend(DEFAULT_STEALTH_ARGS) result.extend(get_default_stealth_args())
if extra_args: if extra_args:
result.extend(extra_args) result.extend(extra_args)
return result return result
+8 -5
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import os import os
import platform import platform
import random
from pathlib import Path from pathlib import Path
from ._version import __version__ from ._version import __version__
@@ -17,16 +18,18 @@ CHROMIUM_VERSION = "142.0.7444.175"
# Default stealth arguments passed to the patched Chromium binary. # Default stealth arguments passed to the patched Chromium binary.
# These activate source-level fingerprint patches compiled into the binary. # These activate source-level fingerprint patches compiled into the binary.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
DEFAULT_STEALTH_ARGS: list[str] = [ def get_default_stealth_args() -> list[str]:
"""Build stealth args with a random fingerprint seed per launch."""
seed = random.randint(10000, 99999)
return [
"--no-sandbox", "--no-sandbox",
"--disable-blink-features=AutomationControlled", "--disable-blink-features=AutomationControlled",
# Fingerprint overrides (activate compiled C++ patches) f"--fingerprint={seed}",
"--fingerprint=98765",
"--fingerprint-platform=windows", "--fingerprint-platform=windows",
"--fingerprint-hardware-concurrency=8", "--fingerprint-hardware-concurrency=8",
"--fingerprint-gpu-vendor=NVIDIA Corporation", "--fingerprint-gpu-vendor=NVIDIA Corporation",
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 4070", "--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
] ]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Platform detection # Platform detection
+236 -22
View File
@@ -1,57 +1,271 @@
"""Run stealth tests against major bot detection services. """Run stealth tests against major bot detection services.
Tests cloakbrowser against multiple detection sites and reports results. Tests cloakbrowser against multiple detection sites, extracts pass/fail
verdicts via JS evaluation, and reports results with screenshots.
Usage:
python examples/stealth_test.py
python examples/stealth_test.py --headed # watch in real-time
python examples/stealth_test.py --no-screenshots
python examples/stealth_test.py --proxy http://10.50.96.5:8888
""" """
import json
import sys
import time
from cloakbrowser import launch from cloakbrowser import launch
HEADED = "--headed" in sys.argv
SCREENSHOTS = "--no-screenshots" not in sys.argv
PROXY = None
for i, arg in enumerate(sys.argv):
if arg == "--proxy" and i + 1 < len(sys.argv):
PROXY = sys.argv[i + 1]
def test_bot_sannysoft(page):
"""bot.sannysoft.com — classic bot detection checks."""
page.goto("https://bot.sannysoft.com", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(3000)
results = page.evaluate("""() => {
const rows = document.querySelectorAll('table tr');
const data = {};
rows.forEach(r => {
const cells = r.querySelectorAll('td');
if (cells.length >= 2) {
const key = cells[0].innerText.trim();
const val = cells[1].innerText.trim();
const cls = cells[1].className || '';
data[key] = {value: val, passed: !cls.includes('failed')};
}
});
return data;
}""")
failed = [k for k, v in results.items() if not v["passed"]]
total = len(results)
passed = total - len(failed)
return {"passed": passed, "total": total, "failed": failed}
def test_bot_incolumitas(page):
"""bot.incolumitas.com — comprehensive 30+ check bot detection."""
page.goto("https://bot.incolumitas.com", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(12000) # needs time to run all detection tests
# Site outputs JSON blocks in page text, not HTML tables
results = page.evaluate("""() => {
const text = document.body.innerText;
const okMatches = text.match(/"\\w+":\\s*"OK"/g) || [];
const failMatches = text.match(/"\\w+":\\s*"FAIL"/g) || [];
const failedTests = failMatches.map(m => m.match(/"(\\w+)"/)[1]);
return {
passed: okMatches.length,
failed: failMatches.length,
failedTests,
total: okMatches.length + failMatches.length
};
}""")
return results
def test_browserscan(page):
"""browserscan.net/bot-detection — WebDriver, UA, CDP, Navigator checks."""
page.goto("https://www.browserscan.net/bot-detection", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(5000)
results = page.evaluate("""() => {
const items = document.querySelectorAll('[class*="result"], [class*="item"], [class*="check"]');
let normal = 0, abnormal = 0;
const text = document.body.innerText;
// Count "Normal" vs "Abnormal" verdicts
const normalMatches = text.match(/Normal/g);
const abnormalMatches = text.match(/Abnormal/g);
return {
normal: normalMatches ? normalMatches.length : 0,
abnormal: abnormalMatches ? abnormalMatches.length : 0,
pageText: text.substring(0, 500)
};
}""")
return results
def test_deviceandbrowserinfo(page):
"""deviceandbrowserinfo.com/are_you_a_bot — fingerprint + behavioral detection."""
page.goto("https://deviceandbrowserinfo.com/are_you_a_bot", wait_until="domcontentloaded", timeout=30000)
page.wait_for_timeout(8000)
results = page.evaluate("""() => {
const text = document.body.innerText;
// Site outputs JSON with "isBot": false and detail checks
const botMatch = text.match(/"isBot":\\s*(true|false)/);
const isBot = botMatch ? botMatch[1] === 'true' : null;
const checks = {};
const patterns = [
'isBot', 'hasBotUserAgent', 'hasWebdriverTrue',
'isHeadlessChrome', 'isAutomatedWithCDP', 'hasSuspiciousWeakSignals',
'isPlaywright', 'hasInconsistentChromeObject'
];
patterns.forEach(p => {
const match = text.match(new RegExp('"' + p + '":\\s*(true|false)'));
if (match) checks[p] = match[1] === 'true';
});
return {isBot, checks};
}""")
return results
def test_fingerprintjs(page):
"""demo.fingerprint.com/web-scraping — industry-standard bot detection."""
page.goto("https://demo.fingerprint.com/web-scraping", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(5000)
# Click search to trigger bot detection — bots get blocked, humans see flights
try:
page.click("button:has-text('Search')", timeout=5000)
page.wait_for_timeout(5000)
except Exception:
pass
results = page.evaluate("""() => {
const text = document.body.innerText;
// Bots see error messages; humans see flight prices
const hasFlights = text.includes('Price per adult') || text.includes('$');
const isBlocked = text.includes('request was blocked') || text.includes('bot visit detected');
return {passed: hasFlights && !isBlocked, isBlocked, hasFlights};
}""")
return results
def test_recaptcha(page):
"""recaptcha-demo.appspot.com — Google's official reCAPTCHA v3 score."""
page.goto(
"https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php",
wait_until="networkidle",
timeout=30000,
)
# Page auto-submits via grecaptcha.execute() — wait for backend response
page.wait_for_timeout(8000)
results = page.evaluate("""() => {
const text = document.body.innerText;
// Score appears in JSON response block: "score": 0.9
const scoreMatch = text.match(/"score":\\s*(\\d+\\.\\d+)/);
return {
score: scoreMatch ? parseFloat(scoreMatch[1]) : null,
pageText: text.substring(0, 500)
};
}""")
return results
TESTS = [ TESTS = [
{
"name": "bot.sannysoft.com",
"url": "https://bot.sannysoft.com",
"runner": test_bot_sannysoft,
"verdict": lambda r: f"{r['passed']}/{r['total']} passed"
+ (f" (FAILED: {', '.join(r['failed'])})" if r["failed"] else " — ALL GREEN"),
"pass": lambda r: len(r["failed"]) == 0,
},
{ {
"name": "bot.incolumitas.com", "name": "bot.incolumitas.com",
"url": "https://bot.incolumitas.com", "url": "https://bot.incolumitas.com",
"check": "Bot detection analysis", "runner": test_bot_incolumitas,
"verdict": lambda r: f"{r['passed']}/{r['total']} passed"
+ (f" (FAILED: {', '.join(r.get('failedTests', []))})" if r.get("failed", 0) > 0 else " — ALL GREEN"),
"pass": lambda r: r.get("failed", 0) <= 1, # fpscanner.WEBDRIVER false positive expected (all builds)
}, },
{ {
"name": "BrowserScan", "name": "BrowserScan",
"url": "https://www.browserscan.net/bot-detection", "url": "https://www.browserscan.net/bot-detection",
"check": "Bot detection status", "runner": test_browserscan,
"verdict": lambda r: f"Normal: {r['normal']}, Abnormal: {r['abnormal']}",
"pass": lambda r: r.get("abnormal", 1) == 0,
}, },
{ {
"name": "deviceandbrowserinfo.com", "name": "deviceandbrowserinfo.com",
"url": "https://deviceandbrowserinfo.com/are_you_a_bot", "url": "https://deviceandbrowserinfo.com/are_you_a_bot",
"check": "isBot flag", "runner": test_deviceandbrowserinfo,
"verdict": lambda r: f"isBot: {r.get('isBot', 'unknown')}"
+ (f" checks: {json.dumps(r.get('checks', {}))}" if r.get("checks") else ""),
"pass": lambda r: not r.get("isBot", True),
}, },
{ {
"name": "FingerprintJS", "name": "FingerprintJS",
"url": "https://demo.fingerprint.com/web-scraping", "url": "https://demo.fingerprint.com/web-scraping",
"check": "Bot detection result", "runner": test_fingerprintjs,
"verdict": lambda r: "PASSED (flights shown)" if r.get("passed") else "BLOCKED" if r.get("isBlocked") else "NO FLIGHTS",
"pass": lambda r: r.get("passed", False),
},
{
"name": "reCAPTCHA v3 (Google)",
"url": "https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php",
"runner": test_recaptcha,
"verdict": lambda r: f"Score: {r.get('score', 'N/A')}",
"pass": lambda r: (r.get("score") or 0) >= 0.7,
}, },
] ]
browser = launch(headless=True)
page = browser.new_page()
print("=" * 60) def main():
print("CloakBrowser Stealth Test Suite") print("=" * 60)
print("=" * 60) print("CloakBrowser Stealth Test Suite")
print("=" * 60)
print(f"Mode: {'headed' if HEADED else 'headless'}")
print(f"Screenshots: {'on' if SCREENSHOTS else 'off'}")
print(f"Proxy: {PROXY or 'none'}")
print()
for test in TESTS: browser = launch(headless=not HEADED, proxy=PROXY)
print(f"\n--- {test['name']} ---") page = browser.new_page()
results_summary = []
for test in TESTS:
name = test["name"]
print(f"--- {name} ---")
print(f"URL: {test['url']}") print(f"URL: {test['url']}")
try:
page.goto(test["url"], wait_until="networkidle", timeout=30000)
page.wait_for_timeout(3000)
# Screenshot each test try:
filename = f"stealth_test_{test['name'].replace('.', '_').replace(' ', '_')}.png" result = test["runner"](page)
passed = test["pass"](result)
verdict = test["verdict"](result)
status = "PASS" if passed else "FAIL"
results_summary.append((name, status, verdict))
print(f"Result: [{status}] {verdict}")
if SCREENSHOTS:
filename = f"stealth_test_{name.replace('.', '_').replace(' ', '_').replace('/', '_')}.png"
page.screenshot(path=filename) page.screenshot(path=filename)
print(f"Screenshot: {filename}") print(f"Screenshot: {filename}")
print(f"Title: {page.title()}")
except Exception as e: except Exception as e:
results_summary.append((name, "ERROR", str(e)))
print(f"Error: {e}") print(f"Error: {e}")
browser.close() print()
print("\n" + "=" * 60) browser.close()
print("Tests complete. Check screenshots for results.")
print("=" * 60) # Summary table
print("=" * 60)
print("RESULTS SUMMARY")
print("=" * 60)
for name, status, verdict in results_summary:
icon = {"PASS": "+", "FAIL": "!", "ERROR": "x"}[status]
print(f" [{icon}] {name}: {verdict}")
passed_count = sum(1 for _, s, _ in results_summary if s == "PASS")
total = len(results_summary)
print(f"\n {passed_count}/{total} tests passed")
print("=" * 60)
return 0 if passed_count == total else 1
if __name__ == "__main__":
sys.exit(main())