🏗 Фаза 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,118 @@
|
||||
"""
|
||||
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" # Версия сканера
|
||||
Reference in new issue
Block a user