import React, { useState, useEffect } from 'react'; import { ChevronDown, ChevronRight, AlertTriangle, CheckCircle2, Shield, List, Ban, BarChart3, Loader2, RefreshCw, Plus, Trash2 } from 'lucide-react'; import { backendApi } from '../../services/apiClient'; type BlockId = 'A' | 'B' | 'C' | 'D' | 'E' | 'F'; const BLOCKS: Array<{ id: BlockId; title: string; steps: Array<{ id: string; action: string; responsible: string; result: string }> }> = [ { id: 'A', title: 'Организация (152-ФЗ)', steps: [ { id: 'A1', action: 'Назначить ответственного за обработку ПДн (приказ руководителя, круг обязанностей)', responsible: 'Руководство', result: 'Приказ, ФИО ответственного' }, { id: 'A2', action: 'Разработать политику обработки персональных данных (цели, основания, состав ПДн, сроки хранения, права субъектов, меры защиты)', responsible: 'Ответственный + юрист/внешний консультант', result: 'Документ «Политика обработки ПДн»' }, { id: 'A3', action: 'Опубликовать политику (сайт, личный кабинет, приложение)', responsible: 'Ответственный / админ', result: 'Ссылка на политику доступна пользователям' }, { id: 'A4', action: 'Подготовить формы согласия на обработку ПДн (для сотрудников, жильцов, контактов в заявках; при спецкатегориях — отдельное согласие)', responsible: 'Ответственный + юрист', result: 'Формы согласий, чекбоксы в формах сбора данных' }, { id: 'A5', action: 'Внедрить сбор согласий при регистрации/добавлении сотрудников и контактов', responsible: 'Разработка', result: 'Согласие фиксируется при вводе ПДн' }, { id: 'A6', action: 'Подать уведомление в РКН о обработке ПДн (до начала обработки; цели, категории данных, меры защиты)', responsible: 'Ответственный', result: 'Подтверждение приёма уведомления РКН' }, { id: 'A7', action: 'Принять локальные акты: положение об обработке и защите ПДн, регламент доступа к БД и файлам, инструкция для администраторов', responsible: 'Ответственный', result: 'Приказы/положения в организации' }, ]}, { id: 'B', title: 'HTTPS и секреты (защита при передаче)', steps: [ { id: 'B1', action: 'Определить домен и поддомены; проверить доступ к DNS для DNS-01', responsible: 'Инфраструктура', result: 'Список доменов, доступ к DNS' }, { id: 'B2', action: 'Выпустить wildcard-сертификат (Let\'s Encrypt, DNS-01) или отдельные сертификаты; сохранить в volume/каталог для прокси', responsible: 'Инфраструктура', result: 'Сертификаты на диске/volume' }, { id: 'B3', action: 'Добавить reverse proxy (Traefik / Caddy / Nginx) в docker-compose: 443 и 80, редирект 80→443, проксирование на приложение по HTTP', responsible: 'Разработка / DevOps', result: 'Работающий прокси в Docker' }, { id: 'B4', action: 'Настроить HSTS (Strict-Transport-Security) в прокси', responsible: 'Разработка / DevOps', result: 'HSTS включён' }, { id: 'B5', action: 'В проде задать VITE_API_BASE_URL с https; собирать фронт при деплое с этим значением', responsible: 'Разработка / DevOps', result: 'Все запросы к API идут по HTTPS' }, { id: 'B6', action: 'Убрать дефолтный JWT_SECRET из кода: при отсутствии process.env.JWT_SECRET не запускать сервер; хранить секрет в env/секретах', responsible: 'Разработка', result: 'JWT_SECRET только из env' }, { id: 'B7', action: 'Настроить автообновление сертификатов и перезагрузку прокси после обновления', responsible: 'Инфраструктура', result: 'Сертификаты продлеваются автоматически' }, ]}, { id: 'C', title: 'Хранение и доступ', steps: [ { id: 'C1', action: 'Проверить, что пароли нигде не логируются; при создании/обновлении пользователя всегда bcrypt-хеш', responsible: 'Разработка', result: 'Аудит логов и кода auth' }, { id: 'C2', action: 'Ограничить доступ ОС к папке backend/uploads (только процесс приложения); при необходимости шифрование тома', responsible: 'Инфраструктура', result: 'Права на uploads проверены' }, { id: 'C3', action: 'Зафиксировать в регламенте доступ ролей к полям с ПДн; при необходимости не отдавать в API чувствительные поля', responsible: 'Ответственный + разработка', result: 'Регламент, при необходимости доработка API' }, { id: 'C4', action: 'Логирование доступа к критичным операциям (изменение сотрудника, экспорт, смена пароля) — без паролей и паспортных данных', responsible: 'Разработка', result: 'Аудит-логи на критические действия' }, { id: 'C5', action: 'Зафиксировать в политике ПДн сроки хранения; при необходимости удаление/обезличивание по истечении срока', responsible: 'Ответственный + разработка', result: 'Сроки в документе, при необходимости автоматизация' }, { id: 'C6', action: '(Опционально) Убрать хранение пароля Doma AI в localStorage: токен или proxy авторизации через бэкенд', responsible: 'Разработка', result: 'Пароль не в localStorage' }, { id: 'C7', action: '(Опционально) Шифрование чувствительных полей в БД (паспорт, СНИЛС, реквизиты); ключ вне БД', responsible: 'Разработка', result: 'Принято решение и при необходимости реализовано' }, ]}, { id: 'D', title: 'Реагирование и обучение', steps: [ { id: 'D1', action: 'Разработать порядок реагирования на инциденты (обнаружение, фиксация, уведомление РКН и субъектов ПДн, устранение причин)', responsible: 'Ответственный + юрист', result: 'Документ «Порядок реагирования на утечки ПДн»' }, { id: 'D2', action: 'Провести инструктаж сотрудников по работе с ПДн (запрет передачи паролей вне регламента, только HTTPS)', responsible: 'Ответственный', result: 'Журнал инструктажей / запись о проведении' }, ]}, { id: 'E', title: 'Защита от взлома', steps: [ { id: 'E1', action: 'Rate limit на /api/auth/login (блокировка или задержка после N неудачных попыток с одного IP/логина)', responsible: 'Разработка', result: 'Защита от брутфорса входа' }, { id: 'E2', action: 'Валидация ввода; параметризованные запросы к БД; защита от XSS (CSP) и CSRF (токены при необходимости)', responsible: 'Разработка', result: 'Снижение риска SQL-инъекций, XSS, CSRF' }, { id: 'E3', action: 'Защитные заголовки HTTP: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, при необходимости CSP', responsible: 'Разработка / DevOps', result: 'Защита от XSS и clickjacking' }, { id: 'E4', action: 'Регулярная проверка уязвимостей (npm audit, Snyk) и обновление backend/frontend-пакетов; исправление критичных CVE', responsible: 'Разработка', result: 'Нет известных критичных уязвимостей' }, { id: 'E5', action: 'Firewall (только 80/443 снаружи, SSH по ключу или с ограничением IP); секреты только в env/vault', responsible: 'Инфраструктура', result: 'Сокращение поверхности атаки' }, { id: 'E6', action: 'Логирование неудачных входов и подозрительных запросов; при возможности алерты при аномалиях', responsible: 'Разработка / DevOps', result: 'Быстрое выявление попыток взлома' }, ]}, { id: 'F', title: 'Защита от вирусов и вредоносного ПО', steps: [ { id: 'F1', action: 'Антивирус на рабочих местах и серверах: регулярное сканирование, обновление баз', responsible: 'Инфраструктура / ИТ', result: 'Снижение риска заражения' }, { id: 'F2', action: 'Проверка загружаемых файлов: ограничение типов и размера; при возможности сканирование при загрузке (ClamAV)', responsible: 'Разработка / DevOps', result: 'Загрузки не становятся каналом вирусов' }, { id: 'F3', action: 'Обновление ОС и ПО: патчи хоста и образов Docker; Node, PostgreSQL и прокси до поддерживаемых версий', responsible: 'Инфраструктура', result: 'Меньше уязвимостей в стеке' }, { id: 'F4', action: 'Не запускать непроверенные скрипты; зависимости только из доверенных реестров (npm); при необходимости проверка целостности', responsible: 'Разработка', result: 'Снижение риска supply-chain атак' }, { id: 'F5', action: 'Политика для сотрудников: не открывать подозрительные вложения, не ставить неподтверждённое ПО; ограничение прав учёток', responsible: 'Ответственный / ИТ', result: 'Меньше риска заноса вирусов' }, ]}, ]; type TabId = 'dashboard' | 'plan'; function formatDate(iso: string) { try { return new Date(iso).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch { return iso; } } export const SecuritySection: React.FC = () => { const [tab, setTab] = useState('dashboard'); const [expanded, setExpanded] = useState>(new Set(['A', 'B'])); // Дашборд: настройки, статистика, логи, чёрный список const [settings, setSettings] = useState<{ captchaEnabled: boolean; turnstileSiteKey: string | null; turnstileSecretKeySet: boolean } | null>(null); const [stats, setStats] = useState<{ failedLast24h: number; totalAttemptsLast24h: number; blockedCount: number } | null>(null); const [logs, setLogs] = useState<{ items: Array<{ id: number; ip: string; loginMasked: string; success: boolean; createdAt: string }>; total: number; page: number; limit: number } | null>(null); const [blacklist, setBlacklist] = useState>([]); const [loading, setLoading] = useState(false); const [logsPage, setLogsPage] = useState(1); const [logsSuccessFilter, setLogsSuccessFilter] = useState<'all' | true | false>('all'); const [blacklistForm, setBlacklistForm] = useState({ type: 'ip' as 'ip' | 'login', value: '', reason: '' }); const [blacklistSubmitting, setBlacklistSubmitting] = useState(false); const [error, setError] = useState(null); // Форма настройки капчи (синхронизируем site key из settings при загрузке) const [captchaSiteKey, setCaptchaSiteKey] = useState(''); const [captchaSecretKey, setCaptchaSecretKey] = useState(''); const [captchaSaving, setCaptchaSaving] = useState(false); const loadDashboard = async () => { setLoading(true); setError(null); try { const [settingsRes, statsRes, logsRes, blacklistRes] = await Promise.all([ backendApi.getSecuritySettings(), backendApi.getSecurityStats(), backendApi.getSecurityLogs({ page: logsPage, limit: 20, success: logsSuccessFilter === 'all' ? undefined : logsSuccessFilter }), backendApi.getSecurityBlacklist(), ]); setSettings(settingsRes); setCaptchaSiteKey(settingsRes.turnstileSiteKey || ''); setStats(statsRes); setLogs(logsRes); setBlacklist(blacklistRes); } catch (e: unknown) { setError((e as Error)?.message || 'Ошибка загрузки дашборда'); } finally { setLoading(false); } }; useEffect(() => { if (tab === 'dashboard') loadDashboard(); }, [tab, logsPage, logsSuccessFilter]); const toggle = (id: BlockId) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const handleAddBlacklist = async () => { const val = blacklistForm.value.trim(); if (!val) return; setBlacklistSubmitting(true); setError(null); try { await backendApi.addSecurityBlacklist({ type: blacklistForm.type, value: val, reason: blacklistForm.reason.trim() || undefined }); setBlacklistForm({ ...blacklistForm, value: '', reason: '' }); await loadDashboard(); } catch (e: unknown) { setError((e as Error)?.message || 'Ошибка добавления'); } finally { setBlacklistSubmitting(false); } }; const handleDeleteBlacklist = async (id: number) => { try { await backendApi.deleteSecurityBlacklist(id); setBlacklist((prev) => prev.filter((r) => r.id !== id)); } catch (e: unknown) { setError((e as Error)?.message || 'Ошибка удаления'); } }; const handleSaveCaptcha = async () => { setCaptchaSaving(true); setError(null); try { const res = await backendApi.saveSecuritySettings({ turnstileSiteKey: captchaSiteKey.trim() || undefined, turnstileSecretKey: captchaSecretKey.trim() || undefined, }); setSettings(res); setCaptchaSiteKey(res.turnstileSiteKey || ''); setCaptchaSecretKey(''); await loadDashboard(); } catch (e: unknown) { setError((e as Error)?.message || 'Ошибка сохранения'); } finally { setCaptchaSaving(false); } }; return (
{tab === 'dashboard' && (
{error && (
{error}
)}

Мониторинг и настройки

{loading && !settings ? (
) : ( <> {/* Настройка капчи Turnstile */}
Капча (Turnstile)
{settings?.captchaEnabled ? 'Включена' : 'Выключена'} Задайте ключи из Cloudflare Turnstile и сохраните — виджет появится на форме входа.
{/* Статистика */}
Неудачных входов за 24 ч
{stats?.failedLast24h ?? '—'}
Всего попыток за 24 ч
{stats?.totalAttemptsLast24h ?? '—'}
В чёрном списке
{stats?.blockedCount ?? '—'}
{/* Логи входа */}

Логи входа

{logs?.items?.length ? logs.items.map((row) => ( )) : ( )}
IP Логин (маска) Результат Время
{row.ip} {row.loginMasked || '—'} {row.success ? Успех : Ошибка} {formatDate(row.createdAt)}
Нет записей
{logs && logs.total > logs.limit && (
Всего: {logs.total}
Стр. {logsPage}
)}
{/* Чёрный список */}

Чёрный список (IP или логин)

setBlacklistForm({ ...blacklistForm, value: e.target.value })} placeholder={blacklistForm.type === 'ip' ? '192.168.1.1' : 'логин'} className="text-sm border border-slate-200 rounded-lg px-3 py-2 min-w-[140px]" /> setBlacklistForm({ ...blacklistForm, reason: e.target.value })} placeholder="Причина (необяз.)" className="text-sm border border-slate-200 rounded-lg px-3 py-2 min-w-[120px]" />
{blacklist.length ? blacklist.map((row) => ( )) : ( )}
Тип Значение Причина Добавлено
{row.type} {row.value} {row.reason || '—'} {formatDate(row.createdAt)}
Пусто
)}
)} {tab === 'plan' && ( <>

План защиты персональных данных (152-ФЗ), HTTPS, шифрование при передаче и хранении, защита от взлома и от вирусов. Выполняйте шаги по порядку; приоритеты указаны ниже.

Приоритеты

  • Критично: A1–A2, A6, B3–B7 (HTTPS + JWT_SECRET). Без этого высоки риски штрафов и компрометации.
  • Важно: A3–A5, A7, C1–C4, E1–E3, E5, F1, F3. Требования 152-ФЗ, аудит, базовая защита от взлома и вирусов.
  • Далее: C5–C7, D1–D2, E4, E6, F2, F4, F5.
{BLOCKS.map((block) => { const isOpen = expanded.has(block.id); return (
{isOpen && (
{block.steps.map((step) => ( ))}
Действие Ответственный Результат
{step.id} {step.action} {step.responsible} {step.result}
)}
); })}

Шифрование по закону

Передача: TLS (HTTPS) везде. Пароли: bcrypt (уже используется). Хранение в БД: разграничение доступа; при повышенных рисках — шифрование чувствительных полей. Биометрия не используется — ст. 19 572-ФЗ не применяется.

)}
); };