База данных
Основная идея
База описывает весь путь: кто владеет сайтом, какие файлы получены, что проверено и какое исправление подготовлено. Пользователь владеет командой, команда — проектами, а другие пользователи получают доступ через членство в команде. Биллинг команды закреплён за её 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; сервер проверяет их на каждом запросе.
| Действие | Owner | Member | Viewer |
|---|---|---|---|
| Видеть команду, участников, проекты, отчёты, находки, историю проектов | да | да | да |
| Создать или переименовать проект | да | да | — |
| Проверить подключение, снять снимок, запустить проверку | да | да | — |
| Отметить решение по находке | да | да | — |
| Читать файлы копии, исправлять и восстанавливать копию | да | да | — |
| Ввести, заменить или отозвать пароль подключения | да | — | — |
| Архивировать проект, переименовать команду | да | — | — |
| Приглашать, менять роли, исключать участников | да | — | — |
| Выдавать и отзывать 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 проверяем и повторяем после сбоя.
- Устаревшие хеши запрещают применение плана.
- Исходный снимок нельзя удалять во время операции или срока восстановления.
- Удаление проекта не должно безусловно уничтожать историю и необходимые копии.