🏗 Фаза 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,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