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
@@ -18,7 +18,7 @@ class FormCheckboxCheck(BaseCheck):
|
||||
|
||||
name = "form_checkbox"
|
||||
title = "Чекбокс согласия в формах"
|
||||
description = "Проверяет, есть ли чекбокс согласия на обработку ПД в формах"
|
||||
description = "Проверяет, есть ли чекбокс согласия на обработку ПД в формах, не предзаполнен ли он"
|
||||
risk_on_fail = RiskLevel.CRITICAL
|
||||
default_weight = 20
|
||||
|
||||
@@ -31,85 +31,113 @@ class FormCheckboxCheck(BaseCheck):
|
||||
r"я\s*принимаю\s*условия",
|
||||
r"processing\s*of\s*personal\s*data",
|
||||
r"обработк[аею]\s*персональных?\s*данных?",
|
||||
r"политик[ууе]?\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="Формы на странице не найдены.",
|
||||
)
|
||||
return self._skip_result(message="Формы на странице не найдены.")
|
||||
|
||||
forms_needing_consent = 0
|
||||
total_forms = 0
|
||||
forms_with_consent = 0
|
||||
dark_patterns = [] # Предзаполненные чекбоксы
|
||||
forms_without_consent = []
|
||||
|
||||
for form_idx, form in enumerate(forms):
|
||||
# Проверяем, есть ли чекбокс в форме
|
||||
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
|
||||
|
||||
has_consent = False
|
||||
checkbox_texts = []
|
||||
form_has_consent = False
|
||||
form_has_dark_pattern = False
|
||||
|
||||
for cb in checkboxes:
|
||||
cb_text = await cb.inner_text() if not await cb.get_attribute("value") else await cb.get_attribute("value")
|
||||
cb_label = await form.evaluate(
|
||||
f'(el) => {{\n'
|
||||
f' const cb = el.closest("label") || el.parentElement;\n'
|
||||
f' return cb ? cb.innerText : "";\n'
|
||||
f'}}', form
|
||||
)
|
||||
# Правильная детекция текста чекбокса:
|
||||
# 1. Ищем связанный <label for="checkbox_id">
|
||||
# 2. Если нет — родительский <label>
|
||||
# 3. Если нет — соседние текстовые узлы
|
||||
checkbox_id = await cb.get_attribute("id")
|
||||
label_text = ""
|
||||
|
||||
checkbox_texts.append(cb_text or cb_label or "без текста")
|
||||
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 ""
|
||||
|
||||
combined = f"{cb_text} {cb_label}".lower()
|
||||
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):
|
||||
has_consent = True
|
||||
form_has_consent = True
|
||||
|
||||
forms_needing_consent += 1
|
||||
if has_consent:
|
||||
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({
|
||||
"form_index": form_idx,
|
||||
"checkboxes": checkbox_texts[:3],
|
||||
})
|
||||
forms_without_consent.append({"reason": "нет текста согласия"})
|
||||
|
||||
if forms_with_consent == forms_needing_consent:
|
||||
# Формируем результат
|
||||
if total_forms == forms_with_consent and not dark_patterns:
|
||||
return self._pass_result(
|
||||
message=f"Все формы ({forms_with_consent}) содержат чекбокс согласия.",
|
||||
details={
|
||||
"forms_count": forms_with_consent,
|
||||
"checked_forms": forms_with_consent,
|
||||
},
|
||||
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:
|
||||
return self._fail_result(
|
||||
message=(
|
||||
f"Формы ({forms_needing_consent}) не содержат чекбокс согласия на обработку ПД. "
|
||||
f"Это прямое нарушение 152-ФЗ."
|
||||
),
|
||||
details={
|
||||
"total_forms": forms_needing_consent,
|
||||
"without_consent": forms_without_consent,
|
||||
"sample_texts": [f["checkboxes"][0] if f["checkboxes"] else "без чекбоксов" for f in forms_without_consent[:3]],
|
||||
},
|
||||
)
|
||||
msg += "Это прямое нарушение 152-ФЗ."
|
||||
|
||||
# При dark patterns — повышаем риск
|
||||
risk = RiskLevel.CRITICAL if dark_patterns else self.risk_on_fail
|
||||
|
||||
# Частичное покрытие
|
||||
return self._fail_result(
|
||||
message=(
|
||||
f"Только {forms_with_consent} из {forms_needing_consent} форм содержат чекбокс согласия."
|
||||
),
|
||||
message=msg,
|
||||
risk_level=risk,
|
||||
details={
|
||||
"total_forms": forms_needing_consent,
|
||||
"total_forms": total_forms,
|
||||
"with_consent": forms_with_consent,
|
||||
"without_consent_count": forms_needing_consent - forms_with_consent,
|
||||
"without_consent": forms_without_consent,
|
||||
"dark_patterns": dark_patterns,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user