v2.0: исправлены критические баги, добавлены 4 новые проверки, 4-фазный cookie-аудит, retry-логика, YAML-словари, LLM-анализ, тесты

Исправлено:
- engine: _calculate_score() при ошибке загрузки (score не 100 для мёртвых сайтов)
- privacy_link: замена page.goto() на aiohttp (гонка данных)
- form_checkbox: 3-уровневый поиск label + dark-pattern detection
- server_location: синхронный urllib → aiohttp
- api/server: перепутанные request/response модели в batch
- main.py: csv --checks флаг
- engine: фазовая модель (cookie_audit отдельно, остальные параллельно)
- engine: retry-логика (3 попытки, exponential backoff)
- PDF: emoji заменены на текст (WeasyPrint)

Добавлено:
- consent_doc: проверка отдельного согласия на обработку ПД
- operator_info: проверка ИНН/ОГРН/DPO (читает главную + политику)
- cookie_audit: 4-фазный аудит (load→detect→reject→compare)
- llm_policy: опциональный LLM-анализ 16 разделов (--deep)
- external_scripts: YAML-словари trackers.yaml (critical/warning/russian)
- logging (output.py), .gitignore, pyproject.toml
- smoke-тесты (16/16)
This commit is contained in:
neo committed 2026-06-10 00:49:07 +05:00
1 parent 7fb417dad5
commit b9db935389
24 files changed
+1725 -286

No files matched your search

+61 -72
View File
@@ -1,110 +1,99 @@
"""
Проверка внешних скриптов, собирающих данные пользователей.
Google Analytics, Facebook Pixel, Яндекс.Метрика и другие —
отправляют данные на сервера за пределами РФ.
Загружает словари трекеров из dictionaries/trackers.yaml.
Разделяет на категории: CRITICAL (запрещены), WARNING (рискованны), RUSSIAN (допустимы).
"""
from __future__ import annotations
import re
from pathlib import Path
import yaml
from playwright.async_api import Page
from scanner.checks.base import BaseCheck
from scanner.models import CheckResult, RiskLevel
# Путь к словарю трекеров
TRACKERS_YAML = Path(__file__).parent.parent.parent / "dictionaries" / "trackers.yaml"
class ExternalScriptsCheck(BaseCheck):
"""Проверяет наличие внешних скриптов, передающих данные за пределы РФ."""
name = "external_scripts"
title = "Внешние скрипты аналитики"
description = "Ищет скрипты Google Analytics, Facebook Pixel и другие иностранные сервисы"
description = "Ищет скрипты Google Analytics, Meta Pixel и другие иностранные сервисы"
risk_on_fail = RiskLevel.HIGH
default_weight = 15
EXTERNAL_PATTERNS = [
r"google-analytics\.com",
r"googletagmanager\.com",
r"googlesyndication\.com",
r"googleadservices\.com",
r"googleapis\.com",
r"google\.com/recaptcha",
r"facebook\.com/tr",
r"fbcdn\.net",
r"connect\.facebook\.net",
r"mc\.yandex\.ru",
r"monitoring\.yandex\.ru",
r"top\.mail\.ru",
r"clarity\.microsoft\.com",
r"hotjar\.com",
r"mixpanel\.com",
r"segment\.com",
r"amplitude\.com",
r"zendesk\.com",
r"intercom\.io",
r"chatra\.com",
r"tilda\.cdn",
r"wix\.com",
r"wordpress\.com",
r"shopify\.com",
]
def _load_trackers(self) -> dict:
"""Загрузить словарь трекеров из YAML."""
if not TRACKERS_YAML.exists():
# Fallback: базовый набор
return {
"critical": [{"domain": "google-analytics.com"}, {"domain": "facebook.com/tr"}],
"warning": [{"domain": "fonts.googleapis.com"}, {"domain": "google.com/recaptcha"}],
"russian": [{"domain": "mc.yandex.ru"}, {"domain": "vk.com/rtrg"}],
}
with open(TRACKERS_YAML, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
async def check(self, page: Page, url: str) -> CheckResult:
try:
html = await page.evaluate("() => document.documentElement.outerHTML")
trackers = self._load_trackers()
found_scripts = []
for pattern in self.EXTERNAL_PATTERNS:
matches = re.findall(pattern, html, re.IGNORECASE)
if matches:
found_scripts.append({
"pattern": pattern,
"matches": len(matches),
})
found = {"critical": [], "warning": [], "russian": []}
if not found_scripts:
for category in ("critical", "warning", "russian"):
for entry in trackers.get(category, []):
domain = entry["domain"]
if re.search(domain, html, re.IGNORECASE):
found[category].append(entry)
if not any(found.values()):
return self._pass_result(
message="Внешние скрипты аналитики не найдены.",
)
critical_services = [
"google-analytics",
"googletagmanager",
"facebook",
"clarity",
"segment",
"amplitude",
]
# Формируем сообщение
parts = []
if found["critical"]:
domains = [e["domain"] for e in found["critical"]]
parts.append(f"ЗАПРЕЩЁННЫЕ ({len(found['critical'])}): {', '.join(domains)}")
if found["warning"]:
domains = [e["domain"] for e in found["warning"]]
parts.append(f"РИСКОВАННЫЕ ({len(found['warning'])}): {', '.join(domains)}")
if found["russian"]:
domains = [e["domain"] for e in found["russian"]]
parts.append(f"Российские ({len(found['russian'])}): {', '.join(domains)}")
has_critical = False
for script in found_scripts:
if any(re.search(c, script["pattern"]) for c in critical_services):
has_critical = True
break
message = "Найдены внешние скрипты: " + " | ".join(parts)
if has_critical:
return self._fail_result(
message=(
f"Найдены внешние скрипты передачи данных: "
f"{', '.join(s['pattern'] for s in found_scripts)}. "
f"Они передают персональные данные за пределы РФ."
),
details={
"scripts_found": found_scripts,
"has_critical": True,
},
# Только российские → PASS с уведомлением
if not found["critical"] and not found["warning"]:
return self._pass_result(
message=f"Найдены только российские сервисы: {message}",
details={"found": found},
)
# Есть критические → FAIL
if found["critical"]:
actions = [f"{e['domain']}: {e.get('action', 'Удалить')}" for e in found["critical"]]
message += f"\nРекомендации: {'; '.join(actions)}"
return self._fail_result(
message=message,
risk_level=RiskLevel.CRITICAL,
details={"found": found},
)
# Только warning → FAIL с HIGH
return self._fail_result(
message=(
f"Найдены внешние скрипты: "
f"{', '.join(s['pattern'] for s in found_scripts)}. "
f"Требуется согласие пользователя."
),
details={
"scripts_found": found_scripts,
"has_critical": False,
},
message=message,
risk_level=RiskLevel.HIGH,
details={"found": found},
)
except Exception as e:
return self._error_result(message=f"Ошибка: {str(e)}")
return self._error_result(message=f"Ошибка при проверке внешних скриптов: {str(e)}")