fix: restore all missing consent-tester service modules

banner_detector.py, script_analyzer.py, category_tester.py, authenticated_scanner.py
were only on the feature branch — needed for consent-tester to start.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-05 00:14:26 +02:00
parent 3fade26d89
commit f3e44cf59f
4 changed files with 814 additions and 0 deletions
@@ -0,0 +1,230 @@
"""
Authenticated Scanner — tests post-login functionality.
Checks §312k BGB (cancellation), Art. 17 (deletion), Art. 20 (export),
Art. 7(3) (consent withdrawal), Art. 15 (data access).
Credentials are NEVER stored, logged, or transmitted beyond the browser context.
"""
import logging
from dataclasses import dataclass, field
from playwright.async_api import async_playwright, Page
logger = logging.getLogger(__name__)
USER_AGENT = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
@dataclass
class CheckResult:
found: bool = False
selector: str = ""
text: str = ""
clicks_needed: int = 0
screenshot: bytes = b""
@dataclass
class AuthTestResult:
authenticated: bool = False
login_error: str = ""
cancel_subscription: CheckResult = field(default_factory=CheckResult)
delete_account: CheckResult = field(default_factory=CheckResult)
export_data: CheckResult = field(default_factory=CheckResult)
consent_settings: CheckResult = field(default_factory=CheckResult)
profile_visible: CheckResult = field(default_factory=CheckResult)
# Search patterns for each check (DE + EN)
CANCEL_PATTERNS = [
"kündigen", "kuendigen", "vertrag beenden", "abo beenden",
"mitgliedschaft kündigen", "cancel subscription", "unsubscribe",
"cancel membership", "vertrag kündigen",
]
DELETE_PATTERNS = [
"konto löschen", "konto loeschen", "account löschen", "delete account",
"account deaktivieren", "profil löschen", "remove account",
]
EXPORT_PATTERNS = [
"daten exportieren", "daten herunterladen", "export data", "download data",
"meine daten", "datenauskunft", "data download", "daten anfordern",
]
CONSENT_PATTERNS = [
"einwilligung", "einstellungen", "datenschutz-einstellungen",
"consent", "privacy settings", "cookie-einstellungen",
"werbeeinstellungen", "marketing preferences",
]
PROFILE_PATTERNS = [
"profil", "mein konto", "kontodaten", "persönliche daten",
"profile", "my account", "account settings", "personal data",
]
async def run_authenticated_test(
url: str,
username: str,
password: str,
username_selector: str = "",
password_selector: str = "",
submit_selector: str = "",
) -> AuthTestResult:
"""Run authenticated area test. Credentials are destroyed after test."""
result = AuthTestResult()
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=["--no-sandbox", "--disable-dev-shm-usage"],
)
context = await browser.new_context(user_agent=USER_AGENT)
page = await context.new_page()
try:
# Step 1: Login
await page.goto(url, wait_until="networkidle", timeout=30000)
await page.wait_for_timeout(2000)
login_ok = await _try_login(
page, username, password,
username_selector, password_selector, submit_selector,
)
if not login_ok:
result.login_error = "Login fehlgeschlagen — Formular nicht gefunden oder Credentials falsch"
await context.close()
await browser.close()
return result
result.authenticated = True
await page.wait_for_timeout(3000)
# Step 2: Check cancellation (§312k BGB)
result.cancel_subscription = await _check_patterns(page, CANCEL_PATTERNS, "cancel")
logger.info("Cancel check: found=%s", result.cancel_subscription.found)
# Step 3: Check delete account (Art. 17 DSGVO)
result.delete_account = await _check_patterns(page, DELETE_PATTERNS, "delete")
# Step 4: Check data export (Art. 20 DSGVO)
result.export_data = await _check_patterns(page, EXPORT_PATTERNS, "export")
# Step 5: Check consent settings (Art. 7(3) DSGVO)
result.consent_settings = await _check_patterns(page, CONSENT_PATTERNS, "consent")
# Step 6: Check profile visibility (Art. 15 DSGVO)
result.profile_visible = await _check_patterns(page, PROFILE_PATTERNS, "profile")
except Exception as e:
logger.error("Authenticated test failed: %s", e)
result.login_error = str(e)
finally:
# CRITICAL: Destroy context — wipes all credentials, cookies, session
await context.close()
await browser.close()
return result
async def _try_login(
page: Page, username: str, password: str,
user_sel: str, pass_sel: str, submit_sel: str,
) -> bool:
"""Attempt to fill and submit login form."""
try:
# Auto-detect selectors if not provided
if not user_sel:
for sel in ['input[type="email"]', 'input[name="email"]', 'input[name="username"]',
'input[name="login"]', 'input[id="email"]', 'input[id="username"]']:
if await page.locator(sel).count() > 0:
user_sel = sel
break
if not pass_sel:
for sel in ['input[type="password"]', 'input[name="password"]', 'input[id="password"]']:
if await page.locator(sel).count() > 0:
pass_sel = sel
break
if not submit_sel:
for sel in ['button[type="submit"]', 'input[type="submit"]',
'button:has-text("Anmelden")', 'button:has-text("Login")',
'button:has-text("Sign in")', 'button:has-text("Einloggen")']:
if await page.locator(sel).count() > 0:
submit_sel = sel
break
if not user_sel or not pass_sel:
return False
await page.fill(user_sel, username)
await page.fill(pass_sel, password)
if submit_sel:
await page.click(submit_sel)
else:
await page.press(pass_sel, "Enter")
await page.wait_for_timeout(5000)
# Check if login succeeded (URL changed or login form disappeared)
still_on_login = await page.locator('input[type="password"]').count() > 0
return not still_on_login
except Exception as e:
logger.warning("Login attempt failed: %s", e)
return False
async def _check_patterns(page: Page, patterns: list[str], check_name: str) -> CheckResult:
"""Search current page and navigation for patterns."""
result = CheckResult()
# Check current page text
for pattern in patterns:
try:
locator = page.get_by_text(pattern, exact=False)
count = await locator.count()
if count > 0:
text = await locator.first.text_content()
result.found = True
result.text = (text or "").strip()[:100]
return result
except Exception:
continue
# Check links/buttons
for pattern in patterns:
try:
for sel in [f'a:has-text("{pattern}")', f'button:has-text("{pattern}")',
f'[href*="{pattern.replace(" ", "-")}"]']:
locator = page.locator(sel)
if await locator.count() > 0:
result.found = True
result.selector = sel
result.text = pattern
return result
except Exception:
continue
# Check navigation menus (common locations for account management)
for nav_sel in ['nav', '[role="navigation"]', '.sidebar', '.account-menu', '#account']:
try:
nav = page.locator(nav_sel)
if await nav.count() > 0:
nav_text = (await nav.first.text_content() or "").lower()
for pattern in patterns:
if pattern.lower() in nav_text:
result.found = True
result.text = f"In Navigation: {pattern}"
return result
except Exception:
continue
return result