🏗 Фаза 1-4: Архитектура, ядро, проверки, API, генератор отчетов

- scanner/engine.py — Playwright-движок сканирования
- scanner/models.py — Pydantic модели
- scanner/checks/ — 5 проверок (cookie, privacy, forms, server, external)
- api/server.py — FastAPI REST API
- reports/generator.py — PDF через WeasyPrint + Jinja2
- main.py — CLI + сервер

Проверки:
✅ cookie_banner — поиск cookie-баннера
✅ privacy_link — проверка ссылки на политику
✅ form_checkbox — проверка чекбоксов согласия
✅ server_location — геолокация сервера (RФ или нет)
✅ external_scripts — внешние скрипты аналитики
This commit is contained in:
neo committed 2026-06-09 21:10:21 +05:00
commit de18090304
20 files changed
+1665

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
"""
Проверка наличия ссылки на Политику конфиденциальности.
Согласно 152-ФЗ, оператор обязан опубликовать документ,
определяющий его политику в отношении обработки персональных данных.
"""
from __future__ import annotations
import re
from urllib.parse import urljoin
from playwright.async_api import Page
from scanner.checks.base import BaseCheck
from scanner.models import CheckResult, RiskLevel
class PrivacyLinkCheck(BaseCheck):
"""Проверяет наличие и доступность ссылки на Политику конфиденциальности."""
name = "privacy_link"
title = "Ссылка на Политику конфиденциальности"
description = "Проверяет, есть ли на сайте ссылка на политику конфиденциальности"
risk_on_fail = RiskLevel.HIGH
default_weight = 15
PRIVACY_PATTERNS = [
r"privacy",
r"политик[аые]",
r"конфиденциальн[аыо]",
r"персональные\s*данные",
r"personal\s*data",
r"data\s*policy",
r"обработка\s*данных",
r"user\s*privacy",
]
async def check(self, page: Page, url: str) -> CheckResult:
try:
# Получаем все ссылки на странице
links = await page.query_selector_all("a[href]")
link_texts = []
for link in links:
href = await link.get_attribute("href") or ""
text = (await link.inner_text()).strip()
if text:
link_texts.append({"text": text, "href": href})
# Ищем ссылки на политику
found_links = []
for lt in link_texts:
combined = (lt["text"] + " " + lt["href"]).lower()
if any(re.search(p, combined, re.UNICODE) for p in self.PRIVACY_PATTERNS):
full_url = urljoin(url, lt["href"])
found_links.append({"text": lt["text"], "url": full_url})
if not found_links:
return self._fail_result(
message="Ссылка на Политику конфиденциальности не найдена.",
details={"total_links": len(link_texts), "page_text_sample": link_texts[:3]},
)
# Проверяем, что ссылка ведет на рабочую страницу
policy_url = found_links[0]["url"]
try:
await page.goto(policy_url, timeout=5000)
policy_text = await page.evaluate("() => document.body.innerText")
await page.goto(url, timeout=5000) # Возвращаемся на главную
if len(policy_text) < 50:
return self._fail_result(
message=f"Страница политики ({policy_url}) слишком короткая ({len(policy_text)} символов).",
details={"policy_url": policy_url, "char_count": len(policy_text)},
)
return self._pass_result(
message=f"Найдена ссылка на Политику конфиденциальности ({policy_url})",
details={
"policy_url": policy_url,
"link_text": found_links[0]["text"],
"char_count": len(policy_text),
},
)
except Exception as e:
return self._fail_result(
message=f"Страница политики не доступна: {str(e)}",
details={"policy_url": policy_url},
)
except Exception as e:
return self._error_result(message=f"Ошибка при проверке ссылки на политику: {str(e)}")