pryscraper/behavioral_biometrics.py
cryptorugmunch bb77eb5f35 chore(license): re-license to dual MIT (core) + BSL 1.1 (stealth)
Re-license Pry from full Proprietary to a dual-license model:

- Core engine, extraction, templates (80+), MCP server, x402 payment rail,
  CLI, SDK, browser extension, WordPress plugin, Shopify app, and
  llm_providers: MIT (see LICENSE)
- Anti-detection / stealth subset (15 files): BSL 1.1 with Change Date
  2029-01-01 (see LICENSE-BSL-STEALTH)

BSL files (anti-detection moat):
  ultimate_scraper.py, stealth_engine.py, stealth_scripts/*.js (6),
  camoufox_integration.py, tls_fingerprint.py, cookie_warmer.py,
  behavioral_biometrics.py, adaptive.py, browser_pool.py, network.py,
  captcha_solver.py, shadow_dom.py, lazy_load.py, signup_automator.py,
  auth_connector.py

This enables community contributions to the core engine (templates,
integrations, MCP tools) while protecting the anti-detection techniques
that constitute the actual competitive moat. BSL Additional Use Grant
permits free non-production use; production deployment requires a
commercial license from enterprise@rugmunch.io.

Changes:
- Replace proprietary LICENSE with MIT LICENSE + new LICENSE-BSL-STEALTH
- Add SPDX-License-Identifier headers to 300+ source files
- Add docs/adr/0002-dual-licensing.md (ADR documenting the decision)
- Update README.md: new License section with BSL Additional Use Grant
- Update LICENSING_PRICING_STRATEGY.md: Section 3 (PryScraper) for dual license
- Update AGENTS.md: license line in header + new rule 8 (PRs touching BSL rejected)
- Update pyproject.toml: license = "MIT AND BSL-1.1" + classifiers + license-files
- Update DECISIONS.md index with ADR-0002
- Update STATUS.md (2026-07-03) and PLAN.md sprint goals

Refs: ADR-0002
2026-07-02 19:49:21 +02:00

206 lines
7.8 KiB
Python

"""Pry — Behavioral Biometrics v2.
Real human behavior simulation: hesitation, scroll-back, mouse drift, reading time.
Modern anti-bot systems detect 'too perfect' behavior. This module makes
behavior more realistic by adding human imperfections."""
# SPDX-License-Identifier: BSL-1.1
# Copyright (c) 2026 Rug Munch Media LLC
#
# Part of Pry — Stealth / Anti-Detection Module
# Licensed under Business Source License 1.1 — see LICENSE-BSL-STEALTH.
# Change Date: 2029-01-01 (converts to MIT).
import logging
import math
import random
from typing import Any
logger = logging.getLogger(__name__)
class HumanBehaviorSimulator:
"""Generate realistic human behavior patterns."""
def __init__(self) -> None:
self._page_focus_time = 0
def mouse_path(
self,
start: tuple[float, float],
end: tuple[float, float],
steps: int | None = None,
) -> list[dict[str, float]]:
"""Generate a human-like mouse path between two points.
Uses bezier curve with random control points to create natural
curved paths, with speed variation (fast in middle, slow at endpoints).
"""
if steps is None:
distance = math.sqrt((end[0] - start[0]) ** 2 + (end[1] - start[1]) ** 2)
steps = max(10, min(50, int(distance / 20)))
# Random control points for bezier curve
ctrl1 = (
start[0] + (end[0] - start[0]) * random.uniform(0.2, 0.4) + random.uniform(-50, 50),
start[1] + (end[1] - start[1]) * random.uniform(0.2, 0.4) + random.uniform(-50, 50),
)
ctrl2 = (
start[0] + (end[0] - start[0]) * random.uniform(0.6, 0.8) + random.uniform(-30, 30),
start[1] + (end[1] - start[1]) * random.uniform(0.6, 0.8) + random.uniform(-30, 30),
)
path: list[dict[str, float]] = []
for i in range(steps + 1):
t = i / steps
# Cubic bezier
x = (
(1 - t) ** 3 * start[0]
+ 3 * (1 - t) ** 2 * t * ctrl1[0]
+ 3 * (1 - t) * t ** 2 * ctrl2[0]
+ t ** 3 * end[0]
)
y = (
(1 - t) ** 3 * start[1]
+ 3 * (1 - t) ** 2 * t * ctrl1[1]
+ 3 * (1 - t) * t ** 2 * ctrl2[1]
+ t ** 3 * end[1]
)
# Speed: slow at start/end, fast in middle
speed_mod = math.sin(t * math.pi) * 0.5 + 0.5 # 0 at endpoints, 1 in middle
# Add tiny jitter
x += random.uniform(-2, 2)
y += random.uniform(-2, 2)
path.append(
{
"x": round(x, 1),
"y": round(y, 1),
"t": round(t, 3),
"speed": round(speed_mod, 3),
}
)
return path
def reading_pause(self, content_length: int) -> float:
"""How long a human would pause to read content of this length.
Based on average reading speed of 250 words/minute."""
words = content_length / 5 # Rough estimate
seconds = (words / 250) * 60
# Add variance: 60-130% of average (some skim, some read carefully)
variance = random.uniform(0.6, 1.3)
# Add micro-pauses every ~20 words
micro_pauses = max(0, words // 20) * random.uniform(0.5, 2.0)
return round(seconds * variance + micro_pauses, 2)
def scroll_pattern(
self, page_height: int, viewport_height: int = 800
) -> list[dict[str, Any]]:
"""Generate realistic scroll pattern for a page.
Humans don't scroll linearly — they scroll, pause, scroll back, etc.
"""
patterns: list[dict[str, Any]] = []
current_y = 0
# Initial scroll: fast down to see the page
current_y = min(page_height, viewport_height * 0.5)
patterns.append(
{
"y": current_y,
"speed": "fast",
"pause_after": random.uniform(0.5, 1.5),
}
)
while current_y < page_height - viewport_height:
# Decide: continue down, or scroll back up
if random.random() < 0.15 and current_y > viewport_height:
# Scroll back up a bit
current_y = max(0, current_y - random.randint(100, 400))
patterns.append(
{
"y": current_y,
"speed": "slow",
"pause_after": random.uniform(1.0, 3.0),
"action": "scroll_back",
}
)
else:
# Scroll down a bit
scroll_amount = random.randint(200, 600)
current_y = min(page_height, current_y + scroll_amount)
# Pause longer on certain content (images, headings)
pause = (
random.uniform(1.0, 4.0)
if random.random() < 0.2
else random.uniform(0.2, 1.0)
)
patterns.append(
{
"y": current_y,
"speed": "normal",
"pause_after": pause,
}
)
# Final scroll to bottom
patterns.append(
{"y": page_height, "speed": "fast", "pause_after": 0.5}
)
return patterns
def typing_pattern(self, text: str) -> list[dict[str, Any]]:
"""Generate realistic typing timings.
Humans have variable typing speed: faster on common words,
slower on rare words, occasional pauses to think.
"""
timings: list[dict[str, Any]] = []
common_words = {
"the", "a", "an", "is", "are", "was", "and", "or", "but",
"in", "on", "at", "to", "for", "of", "with",
}
words = text.split(" ")
for i, word in enumerate(words):
if word.lower().strip(".,!?") in common_words:
delay = random.uniform(0.05, 0.15) # Fast for common words
else:
delay = random.uniform(0.1, 0.3) # Slower for less common
# Occasional "thinking" pause
if random.random() < 0.05:
delay += random.uniform(0.5, 2.0)
# Space between words: faster
if i < len(words) - 1:
delay += random.uniform(0.05, 0.12)
timings.append({"char": word, "delay_ms": round(delay * 1000)})
return timings
def click_decision_delay(self) -> float:
"""How long a human takes to decide to click something they see.
Range: 200ms (impulsive) to 2000ms (cautious)."""
# Most clicks are fast (200-500ms)
r = random.random()
if r < 0.4:
return random.uniform(0.2, 0.5) # Impulsive
if r < 0.9:
return random.uniform(0.5, 1.2) # Normal
return random.uniform(1.2, 2.5) # Cautious (rare)
def form_filling_sequence(self, field_count: int) -> list[dict[str, Any]]:
"""Generate realistic form filling sequence with field-switch delays."""
sequence: list[dict[str, Any]] = []
for i in range(field_count):
# Type field
sequence.append(
{
"action": "type",
"field_index": i,
"duration_ms": random.randint(500, 3000),
}
)
# Tab to next field (or submit on last)
if i < field_count - 1:
sequence.append({"action": "tab", "pause_ms": random.randint(200, 800)})
sequence.append({"action": "review", "pause_ms": random.randint(500, 2000)})
sequence.append({"action": "submit", "duration_ms": random.randint(300, 1000)})
return sequence
behavior = HumanBehaviorSimulator()