🏗 Фаза 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:
commit
de18090304
20 files changed
+1665
No files matched your search
@@ -0,0 +1,25 @@
|
||||
from scanner.checks.base import BaseCheck
|
||||
from scanner.checks.cookie_banner import CookieBannerCheck
|
||||
from scanner.checks.privacy_link import PrivacyLinkCheck
|
||||
from scanner.checks.form_checkbox import FormCheckboxCheck
|
||||
from scanner.checks.server_location import ServerLocationCheck
|
||||
from scanner.checks.external_scripts import ExternalScriptsCheck
|
||||
|
||||
# Реестр всех проверок — новые проверки автоматически регистрируются здесь
|
||||
ALL_CHECKS: dict[str, type[BaseCheck]] = {
|
||||
"cookie_banner": CookieBannerCheck,
|
||||
"privacy_link": PrivacyLinkCheck,
|
||||
"form_checkbox": FormCheckboxCheck,
|
||||
"server_location": ServerLocationCheck,
|
||||
"external_scripts": ExternalScriptsCheck,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"BaseCheck",
|
||||
"CookieBannerCheck",
|
||||
"PrivacyLinkCheck",
|
||||
"FormCheckboxCheck",
|
||||
"ServerLocationCheck",
|
||||
"ExternalScriptsCheck",
|
||||
"ALL_CHECKS",
|
||||
]
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Базовый класс для всех проверок 152-ФЗ.
|
||||
Каждая проверка наследуется от этого класса и реализует метод check().
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from playwright.async_api import Page
|
||||
|
||||
from scanner.models import CheckResult, CheckStatus, RiskLevel
|
||||
|
||||
|
||||
class BaseCheck(ABC):
|
||||
"""Базовый класс для проверки соответствия 152-ФЗ."""
|
||||
|
||||
name: str = "base" # Уникальное имя (используется в реестре)
|
||||
title: str = "Base Check" # Отображаемое название
|
||||
description: str = "" # Описание что проверяет
|
||||
risk_on_fail: RiskLevel = RiskLevel.LOW # Уровень риска при провале
|
||||
default_weight: int = 10 # Вес (влияние на общий балл)
|
||||
|
||||
@abstractmethod
|
||||
async def check(self, page: Page, url: str) -> CheckResult:
|
||||
"""
|
||||
Выполнить проверку сайта.
|
||||
|
||||
Args:
|
||||
page: Playwright Page объект (сайт уже загружен)
|
||||
url: URL проверяемого сайта
|
||||
|
||||
Returns:
|
||||
CheckResult с результатом проверки
|
||||
"""
|
||||
pass
|
||||
|
||||
def _pass_result(self, message: str = "", details: dict = {}) -> CheckResult:
|
||||
"""Создать успешный результат."""
|
||||
return CheckResult(
|
||||
name=self.name,
|
||||
title=self.title,
|
||||
status=CheckStatus.PASS,
|
||||
risk_level=self.risk_on_fail,
|
||||
message=message or f"✓ {self.title} — проверено, нарушений не найдено",
|
||||
details=details,
|
||||
)
|
||||
|
||||
def _fail_result(self, message: str, details: dict = {}, risk_level: RiskLevel | None = None) -> CheckResult:
|
||||
"""Создать результат с провалом."""
|
||||
return CheckResult(
|
||||
name=self.name,
|
||||
title=self.title,
|
||||
status=CheckStatus.FAIL,
|
||||
risk_level=risk_level or self.risk_on_fail,
|
||||
message=f"✗ {message}",
|
||||
details=details,
|
||||
)
|
||||
|
||||
def _skip_result(self, message: str = "") -> CheckResult:
|
||||
"""Создать результат с пропуском."""
|
||||
return CheckResult(
|
||||
name=self.name,
|
||||
title=self.title,
|
||||
status=CheckStatus.SKIP,
|
||||
risk_level=RiskLevel.LOW,
|
||||
message=message or f"⏭ {self.title} — пропущено",
|
||||
)
|
||||
|
||||
def _error_result(self, message: str) -> CheckResult:
|
||||
"""Создать результат с ошибкой."""
|
||||
return CheckResult(
|
||||
name=self.name,
|
||||
title=self.title,
|
||||
status=CheckStatus.ERROR,
|
||||
risk_level=RiskLevel.HIGH,
|
||||
message=f"⚠ {message}",
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Проверка наличия cookie-баннера (уведомления о cookies).
|
||||
Согласно 152-ФЗ, сайт обязан получать согласие пользователя
|
||||
на обработку персональных данных, включая cookies.
|
||||
"""
|
||||
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 CookieBannerCheck(BaseCheck):
|
||||
"""Проверяет наличие cookie-баннера на сайте."""
|
||||
|
||||
name = "cookie_banner"
|
||||
title = "Cookie-баннер"
|
||||
description = "Проверяет, отображается ли на сайте уведомление о cookie"
|
||||
risk_on_fail = RiskLevel.HIGH
|
||||
default_weight = 15
|
||||
|
||||
# Паттерны для поиска cookie-баннера
|
||||
COOKIE_PATTERNS = [
|
||||
r"cookie",
|
||||
r"куки",
|
||||
r"уведомление\s*о\s*cookie",
|
||||
r"согласие\s*на\s*использование\s*cookie",
|
||||
r"we\s*use\s*cookies",
|
||||
r"privacy\s*policy",
|
||||
r"данные\s*пользователя",
|
||||
r"обработка\s*персональных\s*данных",
|
||||
r"personal\s*data",
|
||||
r"consent",
|
||||
r"согласие",
|
||||
]
|
||||
|
||||
async def check(self, page: Page, url: str) -> CheckResult:
|
||||
try:
|
||||
# Получаем весь текст страницы (включая динамически загруженный)
|
||||
page_text = await page.evaluate("() => document.body.innerText")
|
||||
page_html = await page.evaluate("() => document.documentElement.outerHTML")
|
||||
|
||||
# Ищем cookie-баннер по видимым элементам
|
||||
cookie_elements = await page.query_selector_all(
|
||||
'[id*="cookie"], [class*="cookie"], [id*="consent"], [class*="consent"], [id*="privacy"], [class*="privacy"], [id*="policy"], [class*="policy"]'
|
||||
)
|
||||
|
||||
found_banner = False
|
||||
banner_texts = []
|
||||
|
||||
for el in cookie_elements:
|
||||
text = await el.inner_text()
|
||||
if text:
|
||||
banner_texts.append(text[:200])
|
||||
|
||||
# Также ищем по тексту всей страницы
|
||||
has_keyword = any(
|
||||
re.search(pattern, page_text, re.IGNORECASE | re.UNICODE)
|
||||
for pattern in self.COOKIE_PATTERNS
|
||||
)
|
||||
|
||||
if banner_texts:
|
||||
found_banner = True
|
||||
return self._pass_result(
|
||||
message=f"Найден cookie-баннер ({len(banner_texts)} элементов)",
|
||||
details={
|
||||
"banner_count": len(banner_texts),
|
||||
"sample_text": banner_texts[0][:100],
|
||||
},
|
||||
)
|
||||
|
||||
if has_keyword:
|
||||
return self._pass_result(
|
||||
message="На странице найдены упоминания cookie/согласия",
|
||||
details={"keyword_found": True},
|
||||
)
|
||||
|
||||
return self._fail_result(
|
||||
message="Cookie-баннер не найден. Сайт собирает данные без явного согласия.",
|
||||
details={
|
||||
"page_text_sample": page_text[:200],
|
||||
"search_patterns": self.COOKIE_PATTERNS,
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self._error_result(message=f"Ошибка при проверке cookie-баннера: {str(e)}")
|
||||
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Проверка внешних скриптов, собирающих данные пользователей.
|
||||
Google Analytics, Facebook Pixel, Яндекс.Метрика и другие —
|
||||
отправляют данные на сервера за пределами РФ.
|
||||
"""
|
||||
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 ExternalScriptsCheck(BaseCheck):
|
||||
"""Проверяет наличие внешних скриптов, передающих данные за пределы РФ."""
|
||||
|
||||
name = "external_scripts"
|
||||
title = "Внешние скрипты аналитики"
|
||||
description = "Ищет скрипты Google Analytics, Facebook Pixel и другие иностранные сервисы"
|
||||
risk_on_fail = RiskLevel.HIGH
|
||||
default_weight = 15
|
||||
|
||||
EXTERNAL_PATTERNS = [
|
||||
r"google-analytics\.com",
|
||||
r"googletagmanager\.com",
|
||||
r"googlesyndication\.com",
|
||||
r"googleadservices\.com",
|
||||
r"googleapis\.com",
|
||||
r"google\.com/recaptcha",
|
||||
r"facebook\.com/tr",
|
||||
r"fbcdn\.net",
|
||||
r"connect\.facebook\.net",
|
||||
r"mc\.yandex\.ru",
|
||||
r"monitoring\.yandex\.ru",
|
||||
r"top\.mail\.ru",
|
||||
r"clarity\.microsoft\.com",
|
||||
r"hotjar\.com",
|
||||
r"mixpanel\.com",
|
||||
r"segment\.com",
|
||||
r"amplitude\.com",
|
||||
r"zendesk\.com",
|
||||
r"intercom\.io",
|
||||
r"chatra\.com",
|
||||
r"tilda\.cdn",
|
||||
r"wix\.com",
|
||||
r"wordpress\.com",
|
||||
r"shopify\.com",
|
||||
]
|
||||
|
||||
async def check(self, page: Page, url: str) -> CheckResult:
|
||||
try:
|
||||
html = await page.evaluate("() => document.documentElement.outerHTML")
|
||||
|
||||
found_scripts = []
|
||||
for pattern in self.EXTERNAL_PATTERNS:
|
||||
matches = re.findall(pattern, html, re.IGNORECASE)
|
||||
if matches:
|
||||
found_scripts.append({
|
||||
"pattern": pattern,
|
||||
"matches": len(matches),
|
||||
})
|
||||
|
||||
if not found_scripts:
|
||||
return self._pass_result(
|
||||
message="Внешние скрипты аналитики не найдены.",
|
||||
)
|
||||
|
||||
critical_services = [
|
||||
"google-analytics",
|
||||
"googletagmanager",
|
||||
"facebook",
|
||||
"clarity",
|
||||
"segment",
|
||||
"amplitude",
|
||||
]
|
||||
|
||||
has_critical = False
|
||||
for script in found_scripts:
|
||||
if any(re.search(c, script["pattern"]) for c in critical_services):
|
||||
has_critical = True
|
||||
break
|
||||
|
||||
if has_critical:
|
||||
return self._fail_result(
|
||||
message=(
|
||||
f"Найдены внешние скрипты передачи данных: "
|
||||
f"{', '.join(s['pattern'] for s in found_scripts)}. "
|
||||
f"Они передают персональные данные за пределы РФ."
|
||||
),
|
||||
details={
|
||||
"scripts_found": found_scripts,
|
||||
"has_critical": True,
|
||||
},
|
||||
)
|
||||
|
||||
return self._fail_result(
|
||||
message=(
|
||||
f"Найдены внешние скрипты: "
|
||||
f"{', '.join(s['pattern'] for s in found_scripts)}. "
|
||||
f"Требуется согласие пользователя."
|
||||
),
|
||||
details={
|
||||
"scripts_found": found_scripts,
|
||||
"has_critical": False,
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self._error_result(message=f"Ошибка: {str(e)}")
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Проверка наличия чекбокса согласия на обработку персональных данных.
|
||||
Согласно 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="Формы на странице не найдены.",
|
||||
)
|
||||
|
||||
forms_needing_consent = 0
|
||||
forms_with_consent = 0
|
||||
forms_without_consent = []
|
||||
|
||||
for form_idx, form in enumerate(forms):
|
||||
# Проверяем, есть ли чекбокс в форме
|
||||
checkboxes = await form.query_selector_all('input[type="checkbox"]')
|
||||
|
||||
has_consent = False
|
||||
checkbox_texts = []
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
checkbox_texts.append(cb_text or cb_label or "без текста")
|
||||
|
||||
combined = f"{cb_text} {cb_label}".lower()
|
||||
if any(re.search(p, combined, re.UNICODE | re.IGNORECASE) for p in self.CONSENT_PATTERNS):
|
||||
has_consent = True
|
||||
|
||||
forms_needing_consent += 1
|
||||
if has_consent:
|
||||
forms_with_consent += 1
|
||||
else:
|
||||
forms_without_consent.append({
|
||||
"form_index": form_idx,
|
||||
"checkboxes": checkbox_texts[:3],
|
||||
})
|
||||
|
||||
if forms_with_consent == forms_needing_consent:
|
||||
return self._pass_result(
|
||||
message=f"Все формы ({forms_with_consent}) содержат чекбокс согласия.",
|
||||
details={
|
||||
"forms_count": forms_with_consent,
|
||||
"checked_forms": forms_with_consent,
|
||||
},
|
||||
)
|
||||
|
||||
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] for f in forms_without_consent[:3]],
|
||||
},
|
||||
)
|
||||
|
||||
# Частичное покрытие
|
||||
return self._fail_result(
|
||||
message=(
|
||||
f"Только {forms_with_consent} из {forms_needing_consent} форм содержат чекбокс согласия."
|
||||
),
|
||||
details={
|
||||
"total_forms": forms_needing_consent,
|
||||
"with_consent": forms_with_consent,
|
||||
"without_consent_count": forms_needing_consent - forms_with_consent,
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self._error_result(message=f"Ошибка при проверке чекбоксов: {str(e)}")
|
||||
@@ -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)}")
|
||||
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
Проверка геолокации сервера.
|
||||
Согласно 152-ФЗ (ст. 18), сбор персональных данных граждан РФ
|
||||
должен производиться с использованием баз данных, находящихся на территории РФ.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from playwright.async_api import Page
|
||||
|
||||
from scanner.checks.base import BaseCheck
|
||||
from scanner.models import CheckResult, RiskLevel
|
||||
|
||||
|
||||
class ServerLocationCheck(BaseCheck):
|
||||
"""Проверяет, находится ли сервер сайта в России."""
|
||||
|
||||
name = "server_location"
|
||||
title = "Геолокация сервера"
|
||||
description = "Проверяет, находится ли сервер сайта на территории РФ"
|
||||
risk_on_fail = RiskLevel.CRITICAL
|
||||
default_weight = 25
|
||||
|
||||
# Популярные иностранные провайдеры (можно расширять)
|
||||
RUSSIAN_HOSTERS = [
|
||||
"yandex", "mail.ru", "vk", "selectel", "timeweb", "beget",
|
||||
"reg.ru", "nic.ru", "rtcdn", "astra", "sbercloud",
|
||||
"cloud4russia", "гостех", "ростелеком", "мтс", "билайн",
|
||||
"мегафон", "вымпелком", "ростелеком", "netology",
|
||||
]
|
||||
|
||||
async def check(self, page: Page, url: str) -> CheckResult:
|
||||
try:
|
||||
# Получаем IP-адрес сервера через JavaScript
|
||||
ip_info = await page.evaluate("""() => {
|
||||
return new Promise((resolve) => {
|
||||
// Пробуем определить IP через несколько методов
|
||||
const methods = [
|
||||
() => fetch('https://api.ipify.org?format=json')
|
||||
.then(r => r.json()).then(d => ({ip: d.ip, method: 'ipify'})),
|
||||
() => fetch('https://api.my-ip.io/v2/json')
|
||||
.then(r => r.json()).then(d => ({ip: d.ip, method: 'my-ip-io'})),
|
||||
];
|
||||
|
||||
// Пробуем каждый метод
|
||||
(async () => {
|
||||
for (const method of methods) {
|
||||
try {
|
||||
const result = await method();
|
||||
resolve(result);
|
||||
return;
|
||||
} catch (e) {}
|
||||
}
|
||||
resolve(null);
|
||||
})();
|
||||
});
|
||||
}""")
|
||||
|
||||
if not ip_info:
|
||||
return self._skip_result(
|
||||
message="Не удалось определить IP-адрес сервера.",
|
||||
)
|
||||
|
||||
ip = ip_info.get("ip", "")
|
||||
method = ip_info.get("method", "")
|
||||
|
||||
# Определяем страну по IP (используем публичный API)
|
||||
country = await self._get_country(ip)
|
||||
|
||||
if not country:
|
||||
return self._skip_result(
|
||||
message=f"Не удалось определить страну для IP {ip}.",
|
||||
)
|
||||
|
||||
# Проверяем, Россия ли это
|
||||
if country.lower() == "ru" or "Россия" in country:
|
||||
return self._pass_result(
|
||||
message=f"Сервер находится в России (IP: {ip}, Страна: {country})",
|
||||
details={
|
||||
"ip": ip,
|
||||
"country": country,
|
||||
"method": method,
|
||||
},
|
||||
)
|
||||
|
||||
# Сервер за рубежом — критическое нарушение
|
||||
return self._fail_result(
|
||||
message=(
|
||||
f"Сервер находится ЗА ПРЕДЕЛАМИ РОССИИ ({country}, IP: {ip}). "
|
||||
f"Это нарушение ст. 18 152-ФЗ: базы данных граждан РФ должны быть в РФ."
|
||||
),
|
||||
details={
|
||||
"ip": ip,
|
||||
"country": country,
|
||||
"method": method,
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self._error_result(message=f"Ошибка при проверке геолокации: {str(e)}")
|
||||
|
||||
async def _get_country(self, ip: str) -> str | None:
|
||||
"""Получает страну по IP через публичный API."""
|
||||
try:
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
# Пробуем несколько бесплатных API
|
||||
apis = [
|
||||
f"https://ipapi.co/{ip}/json/",
|
||||
f"https://ip-api.com/json/{ip}?fields=country",
|
||||
f"https://api.ipgeolocation.io/ipgeo?ip={ip}",
|
||||
]
|
||||
|
||||
for api_url in apis:
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
api_url,
|
||||
headers={"User-Agent": "Mozilla/5.0"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
|
||||
# ipapi.co формат
|
||||
if "country_name" in data:
|
||||
return data["country_name"]
|
||||
# ip-api.com формат
|
||||
if "country" in data:
|
||||
return data["country"]
|
||||
# ipgeolocation формат
|
||||
if "country_name" in data:
|
||||
return data["country_name"]
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
Reference in new issue
Block a user