Files
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

146 lines
7.0 KiB
Python

"""
Проверка наличия чекбокса согласия на обработку персональных данных.
Согласно 152-ФЗ, обработка данных допустима только при наличии согласия субъекта.
Формы на сайте (обратная связь, регистрация, подписка и т.д.)
должны содержать явное согласие пользователя.
"""
from __future__ import annotations
import re
from playwright.async_api import Page
from scanner.checks.base import BaseCheck
from scanner.models import CheckResult, RiskLevel
class FormCheckboxCheck(BaseCheck):
"""Проверяет наличие чекбокса согласия в формах на сайте."""
name = "form_checkbox"
title = "Чекбокс согласия в формах"
description = "Проверяет, есть ли чекбокс согласия на обработку ПД в формах, не предзаполнен ли он"
risk_on_fail = RiskLevel.CRITICAL
default_weight = 20
CONSENT_PATTERNS = [
r"согласие\s*на\s*обработку\s*данных",
r"согласие\s*на\s*обработку\s*персональных?\s*данных?",
r"я\s*согласен\s*на\s*",
r"agree\s*to\s*the\s*",
r"agree\s*to\s*privacy",
r"я\s*принимаю\s*условия",
r"processing\s*of\s*personal\s*data",
r"обработк[аею]\s*персональных?\s*данных?",
r"политик[уе]?\s*конфиденциальност[иы]",
]
async def check(self, page: Page, url: str) -> CheckResult:
try:
forms = await page.query_selector_all("form")
if not forms:
return self._skip_result(message="Формы на странице не найдены.")
total_forms = 0
forms_with_consent = 0
dark_patterns = [] # Предзаполненные чекбоксы
forms_without_consent = []
for form in forms:
total_forms += 1
checkboxes = await form.query_selector_all('input[type="checkbox"]')
if not checkboxes:
forms_without_consent.append({"reason": "нет чекбоксов"})
continue
form_has_consent = False
form_has_dark_pattern = False
for cb in checkboxes:
# Правильная детекция текста чекбокса:
# 1. Ищем связанный <label for="checkbox_id">
# 2. Если нет — родительский <label>
# 3. Если нет — соседние текстовые узлы
checkbox_id = await cb.get_attribute("id")
label_text = ""
if checkbox_id:
# Метод 1: label[for="id"]
label_el = await page.query_selector(f'label[for="{checkbox_id}"]')
if label_el:
label_text = (await label_el.inner_text()).strip()
if not label_text:
# Метод 2: родительский label (чекбокс внутри label)
parent_label = await cb.evaluate("""
(el) => {
let parent = el.parentElement;
while (parent) {
if (parent.tagName === 'LABEL') return parent.innerText;
parent = parent.parentElement;
}
return '';
}
""")
label_text = parent_label.strip() if parent_label else ""
if not label_text:
# Метод 3: aria-label / aria-labelledby
aria_labelledby = await cb.get_attribute("aria-labelledby")
if aria_labelledby:
ref_el = await page.query_selector(f'#{aria_labelledby}')
if ref_el:
label_text = (await ref_el.inner_text()).strip()
# Проверяем dark pattern: предзаполненный чекбокс
is_checked = await cb.is_checked()
if is_checked:
form_has_dark_pattern = True
# Проверяем текст на паттерны согласия
combined = label_text.lower()
if any(re.search(p, combined, re.UNICODE | re.IGNORECASE) for p in self.CONSENT_PATTERNS):
form_has_consent = True
if form_has_consent:
forms_with_consent += 1
if form_has_dark_pattern:
dark_patterns.append({"form_index": total_forms, "issue": "предзаполненный чекбокс согласия"})
else:
forms_without_consent.append({"reason": "нет текста согласия"})
# Формируем результат
if total_forms == forms_with_consent and not dark_patterns:
return self._pass_result(
message=f"Все формы ({forms_with_consent}) содержат чекбокс согласия, dark patterns не найдены.",
details={"forms_count": forms_with_consent, "dark_patterns": 0},
)
# Собираем сообщение
issues = []
if forms_with_consent < total_forms:
missing = total_forms - forms_with_consent
issues.append(f"{missing} форм без чекбокса согласия")
if dark_patterns:
issues.append(f"{len(dark_patterns)} предзаполненных чекбоксов (dark pattern)")
msg = f"Нарушения в формах: {', '.join(issues)}. "
if forms_with_consent == 0:
msg += "Это прямое нарушение 152-ФЗ."
# При dark patterns — повышаем риск
risk = RiskLevel.CRITICAL if dark_patterns else self.risk_on_fail
return self._fail_result(
message=msg,
risk_level=risk,
details={
"total_forms": total_forms,
"with_consent": forms_with_consent,
"without_consent": forms_without_consent,
"dark_patterns": dark_patterns,
},
)
except Exception as e:
return self._error_result(message=f"Ошибка при проверке чекбоксов: {str(e)}")