🏗 Фаза 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,153 @@
|
||||
"""
|
||||
Точка входа для 152-ФЗ Scanner.
|
||||
Поддерживает запуск через CLI (один URL или CSV) и через API-сервер.
|
||||
"""
|
||||
import asyncio
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from scanner.engine import ScannerEngine
|
||||
from scanner.models import SiteScan
|
||||
from reports.generator import PDFReportGenerator
|
||||
|
||||
|
||||
async def scan_single(url: str, checks: list[str] | None = None) -> SiteScan:
|
||||
"""Просканировать один URL и вывести результат."""
|
||||
engine = ScannerEngine()
|
||||
result = await engine.scan_url(url, checks)
|
||||
|
||||
# Выводим результаты в терминал
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🔍 Сканирование: {url}")
|
||||
print(f"{'='*60}")
|
||||
print(f"📊 Балл: {result.score}/100")
|
||||
print(f"{'✅ Соответствует' if result.is_compliant else '❌ НЕ соответствует'} 152-ФЗ")
|
||||
print(f"📝 {result.summary}")
|
||||
print(f"\n{'─'*40}")
|
||||
|
||||
for check in result.checks:
|
||||
emoji = {
|
||||
"pass": "✅",
|
||||
"fail": "❌",
|
||||
"skip": "⏭",
|
||||
"error": "⚠️",
|
||||
}.get(check.status.value, " ")
|
||||
print(f"{emoji} {check.title}: {check.message}")
|
||||
|
||||
print(f"{'='*60}\n")
|
||||
return result
|
||||
|
||||
|
||||
def scan_csv(filepath: str):
|
||||
"""Парксить CSV-файл с URL и запустить сканирование."""
|
||||
csv_path = Path(filepath)
|
||||
if not csv_path.exists():
|
||||
print(f"❌ Файл не найден: {filepath}")
|
||||
sys.exit(1)
|
||||
|
||||
urls = []
|
||||
with open(csv_path, "r", encoding="utf-8") as f:
|
||||
reader = csv.reader(f)
|
||||
for row in reader:
|
||||
if row:
|
||||
url = row[0].strip()
|
||||
if url and url.startswith(("http://", "https://")):
|
||||
urls.append(url)
|
||||
|
||||
if not urls:
|
||||
print("❌ В CSV не найдено валидных URL")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"📋 Загружено {len(urls)} URL из {csv_path}")
|
||||
|
||||
async def batch_scan():
|
||||
engine = ScannerEngine()
|
||||
results = await engine.scan_urls_batch(urls)
|
||||
|
||||
output = []
|
||||
for r in results:
|
||||
output.append({
|
||||
"url": r.url,
|
||||
"score": r.score,
|
||||
"is_compliant": r.is_compliant,
|
||||
"summary": r.summary,
|
||||
"checks": [c.model_dump() for c in r.checks],
|
||||
})
|
||||
|
||||
# Сохраняем JSON
|
||||
output_dir = Path(__file__).parent / "data" / "output"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
json_path = output_dir / "results.json"
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(output, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"\n📄 Результаты сохранены в: {json_path}")
|
||||
|
||||
# Генерируем PDF для каждого сайта
|
||||
generator = PDFReportGenerator()
|
||||
reports_dir = output_dir / "reports"
|
||||
|
||||
for result in output:
|
||||
try:
|
||||
pdf_path = generator.generate(result, str(reports_dir))
|
||||
print(f" 📄 PDF: {pdf_path}")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Ошибка генерации PDF для {result['url']}: {e}")
|
||||
|
||||
# Сводка
|
||||
compliant = sum(1 for r in output if r["is_compliant"])
|
||||
non_compliant = len(output) - compliant
|
||||
print(f"\n📊 Сводка:")
|
||||
print(f" Всего сайтов: {len(output)}")
|
||||
print(f" ✅ Соответствуют: {compliant}")
|
||||
print(f" ❌ НЕ соответствуют: {non_compliant}")
|
||||
|
||||
# Показываем топ нарушений
|
||||
print(f"\n🔥 Топ нарушений:")
|
||||
check_failures = {}
|
||||
for r in output:
|
||||
for check in r["checks"]:
|
||||
if check["status"] == "fail":
|
||||
name = check["title"]
|
||||
check_failures[name] = check_failures.get(name, 0) + 1
|
||||
|
||||
for name, count in sorted(check_failures.items(), key=lambda x: -x[1]):
|
||||
print(f" ❌ {name}: {count} сайтов")
|
||||
|
||||
asyncio.run(batch_scan())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="152-ФЗ Scanner")
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
# Подкоманда: single
|
||||
single_parser = subparsers.add_parser("scan", help="Сканировать один URL")
|
||||
single_parser.add_argument("url", help="URL сайта для сканирования")
|
||||
single_parser.add_argument("--checks", nargs="+", help="Какие проверки запустить")
|
||||
|
||||
# Подкоманда: csv
|
||||
csv_parser = subparsers.add_parser("csv", help="Сканировать CSV с URL")
|
||||
csv_parser.add_argument("filepath", help="Путь к CSV-файлу")
|
||||
|
||||
# Подкоманда: server
|
||||
server_parser = subparsers.add_parser("server", help="Запустить API-сервер")
|
||||
server_parser.add_argument("--port", type=int, default=8000, help="Порт (по умолчанию 8000)")
|
||||
server_parser.add_argument("--host", default="0.0.0.0", help="Хост (по умолчанию 0.0.0.0)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "scan":
|
||||
asyncio.run(scan_single(args.url, args.checks))
|
||||
elif args.command == "csv":
|
||||
scan_csv(args.filepath)
|
||||
elif args.command == "server":
|
||||
import uvicorn
|
||||
uvicorn.run("api.server:app", host=args.host, port=args.port, reload=True)
|
||||
else:
|
||||
parser.print_help()
|
||||
Reference in new issue
Block a user