d105842bf2
New independent service (port 8094) with headless Chromium: - Phase A: What loads BEFORE any consent interaction - Phase B: What loads AFTER rejecting consent (CRITICAL if tracking persists) - Phase C: What loads AFTER accepting (check against cookie policy) - 10 CMP-specific selectors (Didomi, OneTrust, Cookiebot, Usercentrics, etc.) - Generic fallback via button text matching - 18 tracking service patterns for script classification Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
"""
|
|
Consent Tester Service — Playwright-based 3-phase cookie consent test.
|
|
|
|
Tests what scripts/cookies load BEFORE consent, AFTER rejection, and AFTER acceptance.
|
|
Runs as independent microservice on port 8094.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
|
|
from services.consent_scanner import run_consent_test, ConsentTestResult
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = FastAPI(title="BreakPilot Consent Tester", version="1.0.0")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
class ScanRequest(BaseModel):
|
|
url: str
|
|
timeout_per_phase: int = 10 # seconds to wait after page load
|
|
|
|
|
|
class ScanResponse(BaseModel):
|
|
url: str
|
|
banner_detected: bool
|
|
banner_provider: str
|
|
phases: dict
|
|
summary: dict
|
|
scanned_at: str
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "healthy", "service": "consent-tester"}
|
|
|
|
|
|
@app.post("/scan", response_model=ScanResponse)
|
|
async def scan_consent(req: ScanRequest):
|
|
"""Run 3-phase consent test on a URL."""
|
|
logger.info("Starting consent test for %s", req.url)
|
|
result = await run_consent_test(req.url, req.timeout_per_phase)
|
|
|
|
return ScanResponse(
|
|
url=req.url,
|
|
banner_detected=result.banner_detected,
|
|
banner_provider=result.banner_provider,
|
|
phases={
|
|
"before_consent": {
|
|
"scripts": result.before_scripts,
|
|
"cookies": result.before_cookies,
|
|
"tracking_services": result.before_tracking,
|
|
"violations": [v.__dict__ for v in result.before_violations],
|
|
},
|
|
"after_reject": {
|
|
"scripts": result.reject_scripts,
|
|
"cookies": result.reject_cookies,
|
|
"new_tracking": result.reject_new_tracking,
|
|
"violations": [v.__dict__ for v in result.reject_violations],
|
|
},
|
|
"after_accept": {
|
|
"scripts": result.accept_scripts,
|
|
"cookies": result.accept_cookies,
|
|
"new_tracking": result.accept_new_tracking,
|
|
"undocumented": result.accept_undocumented,
|
|
},
|
|
},
|
|
summary={
|
|
"critical": sum(1 for v in result.reject_violations if v.severity == "CRITICAL"),
|
|
"high": len(result.before_violations),
|
|
"undocumented": len(result.accept_undocumented),
|
|
"total_violations": len(result.before_violations) + len(result.reject_violations),
|
|
},
|
|
scanned_at=datetime.now(timezone.utc).isoformat(),
|
|
)
|