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:
1 parent
7fb417dad5
commit
b9db935389
24 files changed
+1725
-286
No files matched your search
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
Проверка контактных данных оператора: ИНН, ОГРН, DPO.
|
||||
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 OperatorInfoCheck(BaseCheck):
|
||||
"""Проверяет наличие реквизитов оператора и DPO на сайте."""
|
||||
|
||||
name = "operator_info"
|
||||
title = "Контакты оператора и DPO"
|
||||
description = "Проверяет ИНН/ОГРН оператора и контакты ответственного за ПД"
|
||||
risk_on_fail = RiskLevel.HIGH
|
||||
default_weight = 15
|
||||
|
||||
# Regex для российских реквизитов
|
||||
INN_PATTERN = r"ИНН\s*:?\s*(\d{10}|\d{12})"
|
||||
OGRN_PATTERN = r"ОГРН(ИП)?\s*:?\s*(\d{13}|\d{15})"
|
||||
DPO_PATTERNS = [
|
||||
r"ответственн[ыо][ейм]\s*за\s*(обработку\s*)?(персональных?\s*)?данных?",
|
||||
r"(data\s*protection\s*officer|dpo)",
|
||||
r"лиц[ое]\s*ответственн[ао][ея]\s*за\s*пд",
|
||||
]
|
||||
CONTACT_PATTERN = r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)"
|
||||
|
||||
async def check(self, page: Page, url: str) -> CheckResult:
|
||||
try:
|
||||
# 1. Главная страница
|
||||
page_text = await page.evaluate("() => document.body.innerText")
|
||||
|
||||
# 2. Ищем ссылку на политику конфиденциальности
|
||||
policy_text = ""
|
||||
try:
|
||||
links = await page.query_selector_all("a[href]")
|
||||
policy_patterns = [
|
||||
r"политик[аые]\s*конфиденциальн", r"privacy\s*policy",
|
||||
r"персональные\s*данные", r"обработк[ае]\s*данных",
|
||||
]
|
||||
policy_url = None
|
||||
for link in links:
|
||||
href = await link.get_attribute("href") or ""
|
||||
text = (await link.inner_text()).strip()
|
||||
combined = (text + " " + href).lower()
|
||||
if any(re.search(p, combined, re.UNICODE | re.IGNORECASE) for p in policy_patterns):
|
||||
from urllib.parse import urljoin
|
||||
policy_url = urljoin(url, href)
|
||||
break
|
||||
|
||||
if policy_url:
|
||||
import aiohttp
|
||||
from bs4 import BeautifulSoup
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
policy_url,
|
||||
headers={"User-Agent": "Mozilla/5.0"},
|
||||
timeout=aiohttp.ClientTimeout(total=8),
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
html = await resp.text()
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
for tag in soup(["script", "style", "nav", "footer", "header"]):
|
||||
tag.decompose()
|
||||
policy_text = soup.get_text(separator=" ").strip()
|
||||
except Exception:
|
||||
policy_text = "" # Ошибка загрузки политики — работаем с главной
|
||||
|
||||
# 3. Объединяем текст главной + политики
|
||||
combined_text = page_text + "\n" + policy_text
|
||||
|
||||
# 4. Ищем реквизиты
|
||||
inn_match = re.search(self.INN_PATTERN, combined_text, re.IGNORECASE)
|
||||
inn = inn_match.group(1) if inn_match else None
|
||||
|
||||
ogrn_match = re.search(self.OGRN_PATTERN, combined_text, re.IGNORECASE)
|
||||
ogrn = ogrn_match.group(2) if ogrn_match else None
|
||||
|
||||
dpo_found = any(
|
||||
re.search(pattern, combined_text, re.IGNORECASE)
|
||||
for pattern in self.DPO_PATTERNS
|
||||
)
|
||||
|
||||
contacts = re.findall(self.CONTACT_PATTERN, combined_text)
|
||||
contacts = [c for c in contacts if not any(
|
||||
skip in c.lower() for skip in ("example", "test", "noreply", "no-reply")
|
||||
)][:3]
|
||||
|
||||
# 5. Результат
|
||||
has_inn = inn is not None
|
||||
has_ogrn = ogrn is not None
|
||||
has_dpo = dpo_found
|
||||
has_contact = len(contacts) > 0
|
||||
present = sum([has_inn, has_ogrn, has_dpo, has_contact])
|
||||
|
||||
details = {
|
||||
"inn": inn, "ogrn": ogrn,
|
||||
"dpo_found": dpo_found, "contacts": contacts,
|
||||
"policy_checked": bool(policy_text),
|
||||
}
|
||||
|
||||
if present >= 3:
|
||||
return self._pass_result(
|
||||
message=f"Реквизиты оператора: ИНН={'есть' if has_inn else 'нет'}, "
|
||||
f"ОГРН={'есть' if has_ogrn else 'нет'}, "
|
||||
f"DPO={'есть' if has_dpo else 'нет'}",
|
||||
details=details,
|
||||
)
|
||||
|
||||
if present >= 1:
|
||||
missing = []
|
||||
if not has_inn: missing.append("ИНН")
|
||||
if not has_ogrn: missing.append("ОГРН")
|
||||
if not has_dpo: missing.append("контакты DPO")
|
||||
return self._fail_result(
|
||||
message=f"Реквизиты неполные: отсутствуют {', '.join(missing)}",
|
||||
risk_level=RiskLevel.HIGH,
|
||||
details=details,
|
||||
)
|
||||
|
||||
return self._fail_result(
|
||||
message="Реквизиты оператора не найдены. Отсутствуют ИНН, ОГРН, DPO и контакты.",
|
||||
risk_level=RiskLevel.CRITICAL,
|
||||
details=details,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self._error_result(f"Ошибка проверки реквизитов: {str(e)}")
|
||||
Reference in new issue
Block a user