Читать 7 мин

Журналы аудита агентов: записи сессий и записи вызовов

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

Журналы аудита агентов: записи сессий и записи вызовов

Журналы аудита агентов дают сбой, когда пытаются ответить на два разных вопроса одной записью. Во время инцидента нужно установить, кто имел полномочия действовать, и точно выяснить, какое действие достигло внешнего мира. Запись запуска не заменяет запись вызова, а список запросов не объясняет, почему этому процессу разрешили действовать.

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

Запись запуска показывает, кто имел полномочия

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

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

В записи сессии нужны и события жизненного цикла. Одобрение это событие. Отзыв это событие. Завершение процесса это событие. Блокировка хранилища учётных данных, если она останавливает работу, тоже заслуживает связи с остальными событиями. Без этих границ расследующий не сможет понять, произошёл ли вызов в одобренный период или уже после предполагаемой остановки.

Не превращайте запись сессии в дневник каждой мысли модели, строки терминала и правки файла. Так вы получите массив конфиденциальных материалов и всё равно пропустите важную границу: процесс агента получил полномочия просить шлюз выполнять внешние действия. Чётко фиксируйте свидетельства, подтверждающие именно эту границу.

Упрощённая запись сессии может выглядеть так:

{
  "type": "session.authorized",
  "session_id": "ses_7f4c2",
  "observed_at": "2025-04-18T14:03:11Z",
  "process": {
    "pid": 8124,
    "signing_authority": "Example Development Team",
    "parent_pid": 8090
  },
  "decision": "approved",
  "approved_by": "local_operator"
}

Для полезности записи не обязательно знать имя сотрудника. На общем компьютере local_operator может быть честным уровнем атрибуции. Если притвориться, что известно больше, получится уверенная выдумка. Если ваша среда может связать одобрение с аутентифицированным человеком, запишите эту связь и способ её установления.

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

Запись вызова показывает, что затронуло внешний мир

Запись вызова должна описывать одну попытку операции и её результат. Это свидетельство нужно, когда спрашивают: «Отправил ли агент этот запрос, куда именно и с каким результатом?»

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

Для HTTP-действия сохраняйте идентификатор назначения, HTTP-метод, путь или контролируемое представление пути, ссылку на учётные данные вместо их значения, время запроса, время завершения, результат, ID сессии и ID вызова. Для SSH сохраняйте предполагаемую идентичность хоста, команду или её безопасное представление, результат соединения, код выхода на удалённой стороне, если он доступен, ID сессии и ID вызова.

Тщательно решайте, какие данные запроса хранить. Привычка записывать все заголовки и тела запросов однажды приведёт к инциденту. Там часто скрываются заголовки авторизации, cookie, подписанные URL, токены доступа, личные данные клиентов и пароли. Хорошая запись сохраняет смысл операции и удаляет секретные материалы. Например, POST /v1/users/123/disable может быть достаточен для восстановления административного изменения, а копирование всего JSON-тела раскроет гораздо больше, чем нужно для расследования.

Используйте идентификатор назначения, а не только строку исходного URL. https://api.example.test и https://api.example.test:443 могут указывать на одно место, тогда как похожий хост может отличаться одним символом. Для SSH записывайте идентификатор хоста, использованный при проверке, если система может его получить. Одного имени хоста недостаточно, чтобы установить, достигло ли соединение ожидаемой машины.

Эта пара записей показывает разделение:

{
  "type": "call.completed",
  "call_id": "call_b91d",
  "session_id": "ses_7f4c2",
  "observed_at": "2025-04-18T14:09:27Z",
  "channel": "http",
  "operation": "POST",
  "target": "api.example.test/v1/deployments/42/cancel",
  "credential_ref": "deployment-service",
  "authorization": "session_approved",
  "outcome": "completed",
  "response_status": 202
}

ID сессии показывает, чьи полномочия использовал вызов. Назначение и результат показывают, что произошло. Если объединить их в одно расплывчатое событие вроде agent performed task, ни на один из вопросов вы не ответите хорошо.

Одобрение и выполнение это разные факты

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

Оператор может одобрить новый процесс агента, а затем отойти от компьютера. Процесс может не выполнить ни одного вызова, потому что задача завершилась локально. Он может отправить один неудачный API-запрос или пятьдесят успешных. Запись одобрения во всех случаях останется одинаковой. Только отдельные записи вызовов показывают результат.

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

NIST Special Publication 800-92, Guide to Computer Security Log Management, различает источник события, инфраструктуру журналирования и процесс анализа. Практический вывод для систем с агентами прост: собирайте событие на уровне, который знает соответствующий факт. Уровень авторизации знает, получил ли сессия полномочия. Шлюз действий знает, какую операцию он выполнил с защищёнными учётными данными. Брандмауэр знает о замеченном им трафике. У каждой записи свой объём доказательств.

Полезно указывать точное состояние авторизации в каждом вызове. session_approved означает, что у сессии было действующее одобрение. per_call_approved означает, что оператор одобрил именно это использование. denied_locked означает, что хранилище отклонило попытку, пока было заблокировано. denied_user означает, что её отклонил оператор. Это не декоративные метки. Они показывают расследующему, достигла ли операция исполнителя действий и остановило ли её вмешательство человека.

Не помечайте каждый успешный вызов как «одобренный». Это слово скрывает разницу между разрешением, выданным при старте сессии, и разрешением, выданным в момент использования. Когда расследующий спросит, одобрил ли кто-то удаление, запись должна дать ответ с первого прочтения.

Одобрение каждого вызова бывает полезно, но применяйте его как узкий контроль. Включайте его для учётных данных, каждое использование которых связано с большим риском, или для целей, где оператор должен видеть изменение состояния. Если требовать нажатия человека для обычного чтения и безобидных сборочных вызовов, люди привыкнут одобрять поток запросов. Формально одобрение останется, но информированного согласия в нём уже не будет.

Хронология сбоя показывает пробелы, которые скрывают сводки

Одна хронология инцидента наглядно показывает ценность двух ракурсов. Представим автономного агента для программирования, которому поручили удалить устаревшие тестовые среды. Оператор одобряет его процесс на время сессии. Агент находит старые учётные данные API развёртывания и просит шлюз выполнять вызовы с их помощью.

В 14:03 журнал сессий фиксирует одобренный процесс и назначает ему ses_7f4c2. В 14:07 журнал действий фиксирует запрос GET, который перечисляет развёртывания. В 14:09 он фиксирует показанный выше вызов отмены. В 14:10 вторая попытка отмены получает ответ 403. В 14:12 оператор отзывает сессию, заметив, что агент выбрал неправильную группу сред.

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

А теперь представим, что вы хранили только записи вызовов. Вы увидите два запроса на отмену. Но не сможете установить, какой локальный процесс их инициировал, одобрил ли его оператор, оставалось ли одобрение действительным в тот момент и быстро ли проверяющий отреагировал после отзыва.

Хронология должна сохранять порядок событий, не делая вид, будто время на часах абсолютно точное. Часы машин расходятся. Удалённые службы указывают собственные временные отметки. Записывайте время шлюза, когда он заметил запрос, и, если важно, отдельно сохраняйте время результата на удалённой стороне. Если в журнале аудита есть последовательность, используйте её для упорядочивания. Не объявляйте причинность только потому, что два события произошли в одну и ту же секунду.

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

Идентификаторы связи должны иметь строгого владельца

Проверяйте данные до анализа
Запускайте sp audit verify автономно для шифротекста, не открывая хранилище и не раскрывая секреты действий.

ID сессии работает только тогда, когда его назначает и контролирует шлюз. Не позволяйте агенту передавать идентификатор сессии и не принимайте его за свидетельство безопасности.

Агенты могут переносить произвольный текст между вызовами инструментов. Они могут повторно использовать устаревший ID после перезапуска, ошибиться в нём или намеренно заявить ID другой сессии, если интерфейс это позволяет. Шлюз должен выводить связь из аутентифицированного локального соединения или отношения между процессами, а затем сам добавлять ID сессии к каждому вызову.

С ID вызовов нужно поступать так же. Создавайте их на границе действий до того, как запрос покинет систему. Если HTTP-запрос повторяется, фиксируйте, является ли повтор новой попыткой, связанной с исходным вызовом, или частью одного вызова с несколькими транспортными попытками. Работать может любая модель. Смешивание моделей разрушает точность подсчёта во время сбоя.

Используйте небольшую и последовательную модель связи:

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

Не делайте каждый идентификатор понятным человеку во всей системе. Его задача надёжно связывать записи. Рядом можно хранить удобные для человека метки, но они меняются, пересекаются и провоцируют небрежное редактирование.

Для параллельных агентов корреляция предотвращает типичную ошибку. Инженер видит разрушительный запрос в 16:21, находит расшифровку терминала агента за 16:21 и предполагает, что это один и тот же случай. Тем временем другой процесс агента работал под той же учётной записью. ID сессии в записи действия устраняет догадки. Если стабильной связи нет, укажите это ограничение в отчёте об инциденте, а не заполняйте пробел уверенностью.

В журнале аудита должны быть видны и отказы, и использование

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

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

Отказ на уровне отдельного вызова требует такой же аккуратности. Если оператор отклоняет SSH-команду, запишите представление команды, цель, время решения и его результат. Тогда отсутствие кода выхода с удалённой стороны имеет смысл: исполнитель никогда не запускал команду. Это отличается от команды, которая запустилась и вернула ненулевой код.

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

Не поддавайтесь моде на огромный язык политик для каждого действия агента. Он привлекает обещанием полной автоматизации. На практике движок политик добавляет вторую программу, которую командам нужно проверять, тестировать, обновлять и объяснять во время инцидента. Если политики нужны для управления сетью или службами, применяйте их на соответствующем уровне. Нечитаемый набор правил не отменяет необходимости в понятных свидетельствах о сессиях и вызовах.

Запись должна различать такие результаты:

  • У агента никогда не было авторизованной сессии.
  • У сессии были полномочия, но хранилище было заблокировано.
  • Шлюз запросил решение для отдельного вызова, и оператор его отклонил.
  • Шлюз выполнил действие, а удалённая цель отклонила его или завершила с ошибкой.
  • Шлюз выполнил действие и получил успешный результат.

Для этих случаев нужны разные дальнейшие действия. Отклонённый запрос сессии может указывать на недоверенный процесс. Удалённый ответ 403 может говорить о проблеме с областью действия учётных данных. Успешный, но нежелательный вызов может потребовать проверки инструкции задачи, решения об одобрении сессии и разрешённого применения учётных данных цели.

Защита от подмены сохраняет запись после инцидента

Назначайте сессии на шлюзе
Встроенный MCP-шлюз связывает каждый процесс агента с сессией до того, как тот получит доступ к защищённым учётным данным.

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

Разница важна. Цепочка хешей может показать, что запись изменилась или исчезла из середины сохранённой цепочки. Она не доказывает, что система записала каждое событие, которое должна была записать. Она не спасает хост, уже скомпрометированный до создания события. Она не показывает, понял ли оператор карточку одобрения. Всё, что выходит за эти пределы, превращается в показную безопасность.

RFC 5848, Signed Syslog Messages, рассматривает близкую проблему: сообщения журнала могут потерять целостность и подтверждение происхождения, проходя через разные системы. Этот вывод применим и к локальному зашифрованному журналу, а не только к syslog. Защищайте журналы рядом с местом возникновения события, сохраняйте свидетельства порядка и проверяйте записи вместо того, чтобы просто доверять красивому интерфейсу.

Sallyport хранит журналы Sessions и Activity в одном защищённом от записи зашифрованном журнале аудита, связанном цепочкой хешей. Команда sp audit verify автономно проверяет цепочку по шифротексту без ключа хранилища. Это полезно, когда проверяющему нужно подтвердить целостность записей, не получая секреты, использованные для действий.

Запускайте проверку до фильтрации, экспорта или добавления пометок к записям для инцидента. Сначала сохраните исходные зашифрованные свидетельства. Затем сделайте рабочую копию для анализа. Если проверка не пройдена, зафиксируйте сбой и точный проверенный объект. Не продолжайте молча с очищенным экспортом: вопрос целостности уже стал частью инцидента.

Защита от подмены меняет и рабочую дисциплину. Если команда знает, что позднее редактирование будет заметно, журнал аудита перестают воспринимать как удобное место для переписывания истории после неудачного развёртывания. Это само по себе не предотвращает ошибки. Зато сохраняет свидетельства, необходимые для извлечения уроков.

Хранение требует границ, а не бесконтрольного сбора

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

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

Начните с вопросов, на которые команда должна ответить во время инцидента. Через какое время после запуска агента владелец службы может заметить нежелательное изменение? Как долго нужно отслеживать использование учётных данных после ухода сотрудника? Какие нормы или договоры устанавливают срок хранения? Эти ответы задают период. Они не оправдывают сбор необработанных подсказок, полных ответов или ненужных секретов.

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

Ограничьте доступ к проверке ролями, но не используйте контроль доступа как причину скрывать материалы от людей, отвечающих за инциденты. Владельцу службы может быть нужно знать, что вызов изменил его службу. Для этого ему не нужны bearer-токен или не относящееся к делу тело запроса клиента.

Если запись указывает на конфиденциальное содержимое, хранящееся в другом месте, сохраняйте контролируемую ссылку и процедуру получения. Например, можно оставить ID запроса, по которому служба назначения найдёт защищённое тело запроса согласно собственным правилам доступа. Так журнал действий останется полезным, а конфиденциальные бизнес-данные не будут дублироваться в каждой системе аудита.

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

Стройте представление об инциденте, двигаясь от вызова наружу

Во время активного расследования начните с подозрительного вызова и двигайтесь наружу. Обычно это самое конкретное свидетельство: цель, операция, время и результат. По его ID сессии найдите запись полномочий, затем изучите соседние вызовы этой сессии и событие её отзыва или завершения.

Используйте такую последовательность:

  1. Сохраните и проверьте исходные записи аудита до их редактирования или экспорта.
  2. Найдите запись вызова по назначению, ID вызова, операции или временному окну инцидента.
  3. Получите связанную запись сессии и подтвердите идентичность процесса, время одобрения и состояние жизненного цикла.
  4. Просмотрите все вызовы этой сессии вокруг события, включая отказы и повторы.
  5. Сравните хронологию действий с собственными записями службы назначения, а пробелы опишите, не пытаясь угадать их содержание.

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

Не меняйте сразу все учётные данные только потому, что это кажется решительным шагом. Если хранилище не давало агенту доступ к секретам, а запись показывает вызов шлюза к одной известной цели, массовая смена может создать ненужные перебои. Немедленно отзовите активные сессии, если продолжающееся злоупотребление возможно. Затем по записям вызовов решите, какие учётные данные, цели или области доступа требуют действий.

Два ракурса действительно создают больше записей. Но они устраняют самую дорогую фразу в отчёте об инциденте: «Мы не можем установить, действительно ли одобренный агент внёс это изменение». Показывайте полномочия на границе сессии, а результаты на границе вызова. Всё остальное оставляет команде задачу восстанавливать событие безопасности по разрозненным обрывкам.

Вопросы и ответы

Чем журнал сессий агента отличается от журнала действий?

Запись сессии описывает один процесс или запуск агента: кто его запустил, как он был идентифицирован, когда начался и завершился, а также одобрил или отозвал ли его оператор. Запись вызова описывает одну попытку внешнего действия, например HTTP-запрос или SSH-команду. Нужны обе записи, потому что даже доверенный запуск может выполнить небезопасный вызов.

Достаточно ли одобрения сессии для ИИ-агентов?

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

Какой журнал проверять первым после подозрительного действия агента?

Начните с отдельной записи вызова. Она показывает назначение, операцию, время, результат и сессию, из которой был отправлен запрос. Затем перейдите к записи сессии, чтобы установить, какой процесс имел полномочия и отозвал ли кто-то их до или после вызова.

Как связать записи сессий и вызовов?

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

Нужно ли включать отклонённые действия агента в журнал аудита?

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

Когда ИИ-агенту нужно требовать одобрение для каждого API-вызова?

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

Доказывает ли журнал аудита с цепочкой хешей, что ничего не пропущено?

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

Какие поля должны быть в журнале аудита ИИ-агента?

Храните контекст, достаточный для восстановления полномочий и действия: идентификатор процесса, ID сессии, тип операции, идентификатор назначения, временные отметки, решение об авторизации, результат и идентификатор связи. Не помещайте обычные записи аудита сырые API-ключи, закрытые SSH-ключи, токены доступа или конфиденциальные тела запросов.

Как проверять SSH-команды, которые запускают агенты для программирования?

SSH-команда должна создавать запись вызова с идентификатором целевого хоста, самой командой или контролируемым представлением, ID сессии, состоянием авторизации, временем выполнения, результатом выхода и безопасными для хранения сведениями об ошибке. В записи должно быть видно, что команду выполнил шлюз, но закрытый ключ не должен попадать к агенту.

Что делать после неожиданного внешнего вызова агента?

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

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov