Files
152-fz-scanner/scanner/checks/operator_info.py
T
neo b9db935389 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)
2026-06-10 00:49:07 +05:00

136 lines
6.1 KiB
Python

"""
Проверка контактных данных оператора: ИНН, ОГРН, 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)}")