Исправлено: - 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)
100 lines
4.4 KiB
Python
100 lines
4.4 KiB
Python
"""
|
||
Проверка внешних скриптов, собирающих данные пользователей.
|
||
Загружает словари трекеров из 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, Meta Pixel и другие иностранные сервисы"
|
||
risk_on_fail = RiskLevel.HIGH
|
||
default_weight = 15
|
||
|
||
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 = {"critical": [], "warning": [], "russian": []}
|
||
|
||
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="Внешние скрипты аналитики не найдены.",
|
||
)
|
||
|
||
# Формируем сообщение
|
||
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)}")
|
||
|
||
message = "Найдены внешние скрипты: " + " | ".join(parts)
|
||
|
||
# Только российские → 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=message,
|
||
risk_level=RiskLevel.HIGH,
|
||
details={"found": found},
|
||
)
|
||
|
||
except Exception as e:
|
||
return self._error_result(message=f"Ошибка при проверке внешних скриптов: {str(e)}")
|