- 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 — внешние скрипты аналитики
119 lines
5.7 KiB
Python
119 lines
5.7 KiB
Python
"""
|
|
Pydantic модели для 152-ФЗ Scanner.
|
|
Описывают структуры данных: результаты проверок, отчеты, сканирование.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Any, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class CheckStatus(str, Enum):
|
|
PASS = "pass" # ✅ Проверка пройдена
|
|
FAIL = "fail" # ❌ Проверка не пройдена
|
|
SKIP = "skip" # ⚠️ Проверка пропущена
|
|
ERROR = "error" # 🔴 Ошибка при проверке
|
|
|
|
|
|
class RiskLevel(str, Enum):
|
|
LOW = "low" # 🟢 Низкий риск
|
|
MEDIUM = "medium" # 🟡 Средний риск
|
|
HIGH = "high" # 🔴 Высокий риск
|
|
CRITICAL = "critical" # 🚫 Критический риск
|
|
|
|
|
|
class CheckResult(BaseModel):
|
|
"""Результат одной проверки."""
|
|
name: str # Имя проверки (например, "cookie_banner")
|
|
title: str # Название для отображения (например, "Cookie-баннер")
|
|
status: CheckStatus # Статус проверки
|
|
risk_level: RiskLevel = RiskLevel.LOW # Уровень риска при провале
|
|
message: str = "" # Описание результата
|
|
details: dict[str, Any] = {} # Детали для отладки/отчета
|
|
|
|
|
|
class SiteScan(BaseModel):
|
|
"""Результат сканирования одного сайта."""
|
|
url: str # URL сайта
|
|
scanned_at: datetime # Время сканирования
|
|
checks: list[CheckResult] = [] # Результаты всех проверок
|
|
score: int = 100 # Общий балл (0-100)
|
|
risks: list[CheckResult] = [] # Проваленные проверки (только fail)
|
|
critical_risks: list[CheckResult] = [] # Критические провалы
|
|
is_compliant: bool = True # Соответствует ли 152-ФЗ
|
|
summary: str = "" # Краткое резюме
|
|
|
|
def _calculate_score(self):
|
|
"""Вычисляет итоговый балл и статус соответствия."""
|
|
risk_weights = {
|
|
RiskLevel.LOW: 5,
|
|
RiskLevel.MEDIUM: 10,
|
|
RiskLevel.HIGH: 20,
|
|
RiskLevel.CRITICAL: 30,
|
|
}
|
|
|
|
score = 100
|
|
risks = []
|
|
critical_risks = []
|
|
|
|
for check in self.checks:
|
|
if check.status == CheckStatus.FAIL:
|
|
penalty = risk_weights.get(check.risk_level, 5)
|
|
score -= penalty
|
|
risks.append(check)
|
|
if check.risk_level == RiskLevel.CRITICAL:
|
|
critical_risks.append(check)
|
|
|
|
self.score = max(0, score)
|
|
self.risks = risks
|
|
self.critical_risks = critical_risks
|
|
self.is_compliant = self.score >= 70
|
|
|
|
# Формируем краткое резюме
|
|
fail_count = sum(1 for c in self.checks if c.status == CheckStatus.FAIL)
|
|
error_count = sum(1 for c in self.checks if c.status == CheckStatus.ERROR)
|
|
skip_count = sum(1 for c in self.checks if c.status == CheckStatus.SKIP)
|
|
|
|
if self.is_compliant:
|
|
emoji = "🟢" if self.score >= 90 else "🟡"
|
|
self.summary = f"{emoji} Сайт частично соответствует 152-ФЗ (балл: {self.score}/100). Найдено нарушений: {fail_count}."
|
|
else:
|
|
emoji = "🔴" if self.critical_risks else "🟠"
|
|
self.summary = f"{emoji} Сайт НЕ соответствует 152-ФЗ (балл: {self.score}/100). Критических нарушений: {len(self.critical_risks)}. Требуется доработка."
|
|
|
|
return self
|
|
|
|
|
|
class ScanRequest(BaseModel):
|
|
"""Запрос на сканирование (через API)."""
|
|
url: str = Field(..., description="URL сайта для сканирования")
|
|
checks: Optional[list[str]] = None # Какие проверки запустить (все если None)
|
|
|
|
|
|
class CSVScanRequest(BaseModel):
|
|
"""Запрос на сканирование CSV-файла."""
|
|
urls: list[str] = Field(..., description="Список URL для сканирования")
|
|
|
|
|
|
class ScanResponse(BaseModel):
|
|
"""Ответ API — результат сканирования."""
|
|
scan_id: str # Уникальный ID сканирования
|
|
status: str # pending / completed / error
|
|
url: Optional[str] = None # URL (если одно)
|
|
urls_count: Optional[int] = None # Количество URL (если CSV)
|
|
results: Optional[list[SiteScan]] = None # Результаты
|
|
error: Optional[str] = None # Ошибка (если есть)
|
|
created_at: datetime # Время создания запроса
|
|
|
|
|
|
class PDFReport(BaseModel):
|
|
"""Данные для генерации PDF-отчета."""
|
|
site_scan: SiteScan # Результаты сканирования
|
|
generated_at: datetime # Время генерации
|
|
company_name: Optional[str] = None # Название компании (если известно)
|
|
contact_email: Optional[str] = None # Email для связи
|
|
scanner_version: str = "1.0.0" # Версия сканера
|