docs: apply fleet-template (16-artifact scaffold)
Adds missing standard artifacts: - README.md (if missing) - AGENTS.md (AI agent contract) - PLAN.md (current sprint) - STATUS.md (where we are) - DEVELOPMENT.md (dev workflow) - DEPLOYMENT.md (deploy procedure) - TESTING.md (test strategy) - DECISIONS.md (ADR index + templates) - .github/CODEOWNERS - .github/workflows/ci.yml Preserves all existing artifacts. Refs: RugMunchMedia/fleet-template
This commit is contained in:
commit
47ba268131
310 changed files with 38429 additions and 0 deletions
199
behavioral_biometrics.py
Normal file
199
behavioral_biometrics.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
"""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."""
|
||||
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue