Перейти к содержимому

База данных ​

Основная идея

База описывает весь путь: кто владеет сайтом, какие файлы получены, что проверено и какое исправление подготовлено. Пользователь владеет командой, команда — проектами, а другие пользователи получают доступ через членство в команде. Биллинг команды закреплён за её owner; один проект соответствует одному сайту, а проверка — конкретному снимку. Архивы, полные отчёты и файлы изменений храним в S3, а связи, статусы и доступные для поиска данные — в PostgreSQL.

Модель создана частично. Пользователи, команды, участники, приглашения, ключи команды, проекты и ключи идемпотентности уже описаны схемой PostgreSQL с миграциями. Остальное ниже — логические поля и связи; их таблицы появятся с соответствующими функциями.

Сущности ​

СущностьНазначениеОсновные поля
userУчётная запись владельца или участника командid, имя, email, идентификатор входа, хеш пароля, время подтверждения email, статус active/blocked
teamВладелец проектов и область доступаid, ownerId, название, статус
team_memberЧленство пользователя в командеteamId, userId, роль, статус, время вступления
team_api_keyДоступ конкретной интеграции к одной командеid, teamId, createdByUserId, название, префикс, хеш секрета, scopes, expiresAt, revokedAt, lastUsedAt
projectОдин защищаемый сайт командыid, teamId, название, URL, статус
connectionДоступ к файлам сайтаid, projectId, протокол, хост, порт, логин, корень, ссылка на секрет, статус
snapshotНеизменяемый снимок файловid, projectId, источник, ключи архива и манифеста в S3, хеш, покрытие, время сбора
scanОдин запуск проверки снимкаid, snapshotId, вид проверки, версии инструментов/правил, статус, сводка, ключ отчёта
findingНаходка конкретной проверкиid, scanId, правило, путь/позиция, серьёзность, свидетельства, fingerprint, состояние разбора
remediationПлан исправления и его результатid, projectId, sourceSnapshotId, версия плана, diff, исходные хеши, статус, resultSnapshotId

Время создания и обновления добавляем там, где оно применимо.

Связи ​

text
user ── владеет ── team ── владеет ── project
  └─ team_member ──┘                    ├─ connection
                                        ├─ snapshot
                                        │   └─ scan
                                        │       └─ finding
                                        └─ remediation
                                            ├─ sourceSnapshotId
                                            ├─ remediation_findings
                                            └─ resultSnapshotId → snapshot → scan

Один пользователь может владеть несколькими командами и участвовать в других. У команды несколько проектов и API-ключей; каждый ключ относится только к одной команде. У проекта несколько подключений и снимков. Каждое исправление связано с конкретным исходным снимком своего проекта.

User → team → project ​

Один project — один сайт. Отдельная сущность site не нужна; для идентификатора проекта в новой модели используем projectId.

У команды ровно один owner, указанный в team.ownerId. Проект принадлежит команде через project.teamId, а не конкретному участнику. Поэтому уход сотрудника не меняет владельца сайта и не удаляет его историю.

При первом входе создаём личную команду, где пользователь — owner: даже одиночный пользователь проходит тот же путь и позже может пригласить коллег.

Вход и сессия ​

Вход в Securify — по идентификатору входа (в 0.1.0 это email) и паролю. При регистрации приходит письмо со ссылкой подтверждения на 24 часа; на открывшейся странице нужно ввести пароль. Так адрес подтверждает только тот, кто знает пароль, и никто не может заранее занять чужой email. До подтверждения войти нельзя. После подтверждения создаётся личная команда.

Пароль — от 12 символов, без требований к составу; слишком распространённые пароли не принимаются. В базе хранится только его хеш (Argon2id). При ошибке сервис отвечает «неверный логин или пароль», не уточняя причину. После 10 неудачных попыток за 15 минут вход по этому идентификатору временно закрыт на 15 минут, частые попытки с одного адреса тоже ограничены. Забытый пароль сбрасывается по ссылке из письма (30 минут); после сброса или смены пароля остальные сессии закрываются, а на почту приходит уведомление. Регистрация и сброс отвечают одинаково для любых адресов.

После входа выдаём серверную сессию: случайный токен в cookie HttpOnly, Secure, SameSite=Lax, в базе — только его хеш. Сессия заканчивается после 72 часов бездействия или через 14 дней. С другого устройства вы входите тем же идентификатором и паролем, у каждого устройства своя сессия. Выдача ключа, передача владения, закрытие команды и ввод пароля хостинга требуют повторного ввода пароля, если вход был больше 15 минут назад. Выход, «выйти везде» и блокировка пользователя отзывают сессии; роли проверяются на каждом запросе.

Идентификатор входа — то, что пользователь вводит в поле логина; он уникален. В 0.1.0 это email, указанный при регистрации. Поле email при этом остаётся адресом для писем: подтверждения, сброса пароля и уведомлений. Отдельный идентификатор позволит позже входить, например, по номеру телефона. Вход через внешние сервисы (Google, GitHub) будет храниться отдельной записью у пользователя.

В 0.1.0 нет входа через сторонние сервисы (OAuth) и двухфакторного входа.

Членство и роли ​

team_member связывает пользователей и команды. Пара (teamId, userId) уникальна. Owner также имеет активное членство; владельца определяем по team.ownerId, не дублируя независимо редактируемую роль owner в другой таблице.

Права в MVP; сервер проверяет их на каждом запросе.

ДействиеOwnerMemberViewer
Видеть команду, участников, проекты, отчёты, находки, историю проектовдадада
Создать или переименовать проектдада—
Проверить подключение, снять снимок, запустить проверкудада—
Отметить решение по находкедада—
Читать файлы копии, исправлять и восстанавливать копиюдада—
Ввести, заменить или отозвать пароль подключенияда——
Архивировать проект, переименовать командуда——
Приглашать, менять роли, исключать участниковда——
Выдавать и отзывать API-ключи, видеть их событияда——
Передать владение, закрыть команду, тариф и оплатада——
Выйти из командыпосле передачидада

Пароль хостинга даёт доступ к production сайта, поэтому его вводит только owner. Публиковать изменения на хостинг в 0.1.0 не может никто.

В team_member.role для обычного членства достаточно member и viewer; полномочия owner следуют из владения командой, а при передаче владения прежний owner остаётся member. Member не меняет тариф, платёжные реквизиты или владельца. Его действия могут расходовать баланс команды только в пределах заданного owner бюджета; покупки сверх него недоступны.

Для первой версии участники имеют соответствующую роли область доступа ко всем проектам команды. Настройки доступа к отдельному проекту добавим отдельно. Права просмотра отчёта не означают право раскрыть пароли или любые исходники.

Приглашения и отзыв ​

Добавление участника требует приглашения и его принятия. Служебная таблица team_invitation: команда, email, роль, хеш одноразового токена, срок 7 дней и статус; у одного адреса в команде не больше одного ожидающего приглашения. Приглашение не даёт доступа до принятия; нужно проверить соответствие подтверждённого email приглашённому адресу.

После исключения участника отзываем командные разрешения и связанные MCP-доступы. Следующие обращения проверяют актуальное членство. Долгие задачи повторно проверяют права перед чувствительными действиями; выполненные действия и их авторство остаются в истории.

Передача владения ​

Owner не может выйти или быть удалён из команды без передачи владения либо отдельной процедуры закрытия команды. Передаём владение активному member или viewer с его подтверждением в течение 7 дней (team_ownership_transfer). Одной транзакцией меняем ownerId, оставляем прежнего owner участником, отзываем ключи команды и записываем событие; команда не остаётся без владельца.

API-ключи команды ​

Ключ принадлежит одной команде, а createdByUserId фиксирует автора выдачи. У команды несколько именованных ключей — отдельно для каждого агента или интеграции. Универсального ключа ко всем командам пользователя в MVP нет.

Owner создаёт ключ, выбирает срок и разрешения (scopes). Срок обязателен: по умолчанию 30 дней, максимум 90. По умолчанию доступны team:read, projects:read и reports:read; остальные права включаются явно и не превышают права member.

РазрешениеДействия
team:readМетаданные команды без платёжных данных и секретов
projects:read, projects:writeПросмотр или создание и настройка проектов команды
reports:readСтатусы, сводки, находки и история операций проектов
findings:writeРешение агента по находке с обоснованием; копию не меняет
scans:runПолучение снимка через настроенное подключение и проверка в пределах лимитов
workspace:readПоиск и чтение разрешённых файлов копии
remediations:writeПлан и изменение копии, восстановление копии; повторный scan требует scans:run

Ключ не позволяет менять owner, участников, биллинг, пароли подключений, создавать новые ключи, удалять команду или публиковать код на хостинг. Создание оплачиваемого проекта требует заранее согласованного лимита owner; без него агент может подготовить черновик, но не увеличить подписку.

Секрет генерируем случайным (256 бит), показываем один раз; в БД храним только хеш, идентификатор и префикс. Пользователь сохраняет ключ в защищённых настройках интеграции, вне чата, skill, URL и репозитория. Запрос передаёт его в заголовке авторизации по HTTPS. Для замены выдаём новый ключ и отзываем старый.

При каждом запросе проверяем ключ, срок, отзыв, активность команды, разрешение и принадлежность ресурса. teamId в аргументах не меняет команду ключа. Отзыв действует и на долгие MCP-сессии; фоновые задачи повторно проверяют доступ перед чувствительными действиями. Уже выполненное не отменяется; безопасное завершение и очистка остаются обязанностью сервиса.

В MVP ключи выдаёт только owner. При передаче команды отзываем прежние ключи; при блокировке или удалении автора они также перестают действовать. В журнале фиксируем команду, ключ, автора выдачи, операцию и результат, но не секрет. Это действие интеграции, а не личный запрос owner. Лимиты расходов и нагрузки действуют независимо от способа доступа.

Совместимость авторизации проверяем на выбранном MCP-клиенте. Встроенный агент использует ограниченный серверный контекст сессии; ключ вручную для нашего чата создавать не нужно. Skills не хранят секреты и не выдают полномочия.

OWASP: HTTPS, права и отзыв ключей. Ключ сам по себе не заменяет проверки операций и подтверждения чувствительных действий.

Биллинг у owner, расходы у команды ​

Подписка и расходы относятся к команде. Плательщик и управляющий оплатой — owner. Будущая billing_account связывает команду с плательщиком; инициатора каждой операции учитываем отдельно. Автоматический биллинг не входит в 0.1.0.

При передаче команды согласуем оплату новым владельцем. Не переносим чужую карту и не меняем уже выставленные счета; передачу завершаем после согласования будущей оплаты, если у команды есть действующая подписка. Цена, баланс и открытые вопросы →

Проверка доступа ​

Личность определяет серверная авторизация, а не переданный агентом userId. teamId выбирает контекст запроса, но сам по себе не даёт доступ: для пользовательской сессии сервер проверяет активное членство и роль, для интеграции — действующий ключ команды и его разрешения. В обоих случаях проверяем принадлежность проекта команде. Все его подключения, снимки, находки и исправления остаются в этой области: дочерние записи хранят teamId рядом с projectId, а составной внешний ключ не позволяет связать ресурс с проектом другой команды. Запрос к чужому ресурсу получает ответ «не найдено» без раскрытия данных. Перенос проектов между командами пока не входит в MVP.

Connection: доступ отдельно от проекта ​

У одного проекта могут быть подключения для чтения и последующей публикации. Сохраняем назначение, статус проверки, время отзыва и идентичность сервера для SFTP/FTPS. Подключение принадлежит одному проекту.

Пароль или ключ вводится вне чата и хранится зашифрованным с отдельно управляемым ключом либо во внешнем vault. MCP возвращает метаданные и connectionId, но не секрет. Контейнер анализа не получает пароль хостинга. Отзыв подключения не удаляет уже полученные снимки и историю.

Snapshot: что именно мы получили ​

Снимок сохраняет полученные байты, манифест и ограничения покрытия. После завершения его содержимое неизменно. Статус сбора может меняться до завершения, но новые файлы после этого образуют новый снимок.

Источник — подключение к хостингу либо результат remediation. У производного снимка указываем родительский снимок и исправление. Это позволяет отличить скачанное состояние сайта от изменённой у нас копии.

Хеши подтверждают целостность полученных файлов, но не атомарность снимка работающего сайта и не отсутствие заражения. Полный бекап с БД — отдельная задача.

Scan: чем и когда проверили ​

Каждый scan связан с одним снимком. В нём фиксируем конфигурацию, версии инструментов и правил, времена начала/окончания, ошибки и покрытие. В 0.1.0 один scan включает проверку структуры архива, распаковку и файловые проверки; результаты шагов сохраняются внутри отчёта. Повторный анализ результирующего снимка создаёт отдельный scan.

Успешное выполнение и результат безопасности — разные поля. Проверка может успешно завершиться с обнаруженным malware. Ошибка или неполное покрытие не означает «угроз нет».

Новый запуск с обновлёнными правилами создаёт новый scan. Повтор доставки одного запроса не должен создавать новый запуск: для этого сохраняем ключ идемпотентности, а попытки исполнения учитываем отдельно.

Finding: что обнаружили ​

Находка хранит утверждение инструмента и свидетельства из конкретного снимка. Не перезаписываем её исходные данные после анализа агентом. Решение «подтверждено», «ложное срабатывание» или «нужно исследование» сохраняем отдельно с обоснованием и автором.

fingerprint помогает сопоставлять находки разных проверок по правилу, расположению и контексту. Это подсказка для сравнения, а не универсальный идентификатор проблемы: код и позиции могут измениться.

Отсутствие находки в следующем отчёте не доказывает исправление, если изменились правила или нужные файлы не были проверены. Сохраняем связь с проверкой результата. Секретные значения не включаем в обычный текст свидетельств.

Remediation: что предложили и изменили ​

Одно исправление может закрывать несколько находок, а для одной находки могут существовать несколько попыток исправления. Поэтому используем связующую таблицу remediation_findings с парой remediationId / findingId. Связанные находки должны относиться к исходному снимку того же проекта.

В remediation сохраняем план, его версию и хеш, список затрагиваемых файлов, исходные хеши, ссылку на исходный снимок для восстановления, diff и ожидаемые проверки. В 0.1.0 снимок уже содержит оригиналы; отдельно копировать их не нужно. Изменение согласованного плана требует новой версии и нового разрешения там, где оно необходимо.

Для первой версии различаем подготовку, применение к копии, проверку, успех, ошибку и восстановление. Верификация ссылается на конкретный scan результирующего снимка и другие выполненные проверки. Восстановление фиксируется новым событием/версией состояния, а не стирает историю.

Применено к копии не означает опубликовано на хостинг. Публикация потребует отдельной операции, разрешения и результата; в первый путь MVP она не входит.

Что хранится в S3 и PostgreSQL ​

В S3В PostgreSQL
Архивы и манифесты снимковВладение, связи и статусы
Полные JSON-отчёты сканеровСводки и отдельные находки для поиска
Diff, исходные версии файлов, артефакты проверокПлан, история и ссылки на объекты
Сохранённое состояние рабочей копииПривязка к снимку и версии изменения

Сохраняем ключ и версию объекта, хеш и размер, а не постоянную публичную ссылку. Временную ссылку выдаём после проверки прав. Объект в S3 нельзя считать сохранённым только потому, что его ключ уже появился в базе.

ORM и миграции ​

Схема описывается в Prisma 8: модели лежат рядом со своими слайсами, миграции — планы в Git, которые применяются отдельным шагом перед запуском новой версии API. Проверки значений и частичные уникальные индексы заданы в самой схеме; отложенные внешние ключи и триггеры, которые схема не выражает, дописаны в план миграции. Схемой владеет только API; worker к базе напрямую не подключается.

Связь проекта с командой закреплена на уровне базы: изменить команду проекта нельзя, а записи, которые ссылаются на проект, ссылаются на пару «проект и его команда», так что подставить проект другой команды не получится. Владелец команды обязан иметь активное членство — база проверяет это при завершении транзакции.

Служебные записи и целостность ​

Помимо основных сущностей нужны ссылки подтверждения и сброса, попытки входа и сессии, приглашения, передачи владения, remediation_findings, задачи и попытки очереди, события, учёт артефактов и ключи идемпотентности. Контейнер и временная папка — средства исполнения, а не владельцы данных.

  • Один owner с активным членством; права проверяются при каждом запросе.
  • Все связи остаются внутри разрешённого проекта и команды, включая находки исправления.
  • Изменения БД согласуем транзакционно; запись S3 проверяем и повторяем после сбоя.
  • Устаревшие хеши запрещают применение плана.
  • Исходный снимок нельзя удалять во время операции или срока восстановления.
  • Удаление проекта не должно безусловно уничтожать историю и необходимые копии.

Вернуться к архитектуре →

Securify · Документация проекта · Прототип → MVP