Читать 6 мин

Обновляйте формат аудита, сохраняя старые доказательства

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

Обновляйте формат аудита, сохраняя старые доказательства

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

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

Сначала сохраняйте байты, потом думайте об удобстве

Авторитетный артефакт это исходная последовательность байтов записи плюс контекст, необходимый для ее проверки. Распарсенная строка в текущей базе данных это рабочая копия. Объект JSON, показанный в панели, это представление. Ни то ни другое не заменяет байты доказательств, участвовавшие в подписи, хеш-цепочке или аутентифицированном конверте.

Это различие кажется излишней дотошностью, пока обновление не перепишет поле. Допустим, версия 1 хранила SSH-цель как строку, введенную пользователем:

{"schema_version":1,"event":"ssh.execute","target":"[email protected]:22","command":"uptime"}

Версия 2 хочет разделить поля, чтобы фильтровать по хосту и порту:

{"schema_version":2,"event":"ssh.execute","user":"build","host":"prod.example","port":22,"command":"uptime"}

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

Разделяйте три вещи:

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

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

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

Присваивайте каждой записи явную версию

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

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

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

Надежный конверт может выглядеть так:

{
  "schema_version": 3,
  "record_id": "01J8X7K5W3H0Q9M6P2R4A1C8ZD",
  "recorded_at": "2026-07-22T14:08:31.482Z",
  "kind": "http.request.completed",
  "previous_digest": "sha256:4a4d...",
  "payload": {
    "method": "POST",
    "authority": "api.example.test",
    "status": 201
  }
}

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

Также разделяйте версию схемы записи и версию смысла события. Первая отвечает на вопрос: «Как разобрать и проверить эти байты?» Вторая отвечает на вопрос: «Что означало это событие в момент создания?»

Например, замена actor со свободного отображаемого имени на стабильный идентификатор процесса может сохранить структуру JSON, но изменить утверждение. Это не просто версия схемы 4. Это смысловое изменение, и в спецификации доказательств нужно объяснить, с какого момента начинается новое значение. То же относится к изменению единиц измерения поля, переходу от местного времени к UTC или замене статуса наблюдаемого ответа на решение политики.

Фиксируйте способ проверки, а не только поля

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

Для каждой версии запишите:

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

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

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

Та же проблема возникает с бинарными форматами. RFC 8949 определяет требования к детерминированному кодированию CBOR и отмечает, что прежнее соглашение о порядке требует явно названного режима совместимости. Верификатор не может безопасно предполагать, что каждый исторический создатель записи одинаково понимал слово «канонический».

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

Вместо этого держите выбор версии рядом с границей работы с байтами:

read envelope bytes
  -> identify protected schema_version
  -> select verifier V1, V2, or V3
  -> validate that version's grammar
  -> reproduce that version's authenticated bytes
  -> verify digest, signature, and chain link
  -> decode a display model only after verification

Представление для отображения намеренно создается последним. Рендерер может быть удобным. Верификатор не имеет права додумывать.

Неизвестные версии должны завершаться отказом

Если верификатор встречает неподдерживаемую версию, он должен вернуть явный результат unsupported_version. Нельзя считать неизвестные поля несущественными, принимать последнюю структуру или запускать универсальный запасной декодер.

Инженеры часто сопротивляются этому, потому что хотят совместимость вперед. Она уместна для приложения, которое читает необязательные поля представления. Для проверки доказательств она опасна: поле, кажущееся необязательным, позже может определять аутентифицированную интерпретацию.

Используйте форму результата, которая отделяет сбой доказательств от ограничений инструмента:

{
  "record_id": "01J8X7K5W3H0Q9M6P2R4A1C8ZD",
  "schema_version": 4,
  "status": "unsupported_version",
  "verified": false,
  "supported_versions": [1, 2, 3],
  "reason": "Verifier 2.7.0 has no verification recipe for schema version 4"
}

Такой результат сообщает точный факт: инструмент не установил подлинность. Он не обвиняет запись в подделке и не делает вид, что запись корректна. Различайте invalid, incomplete, unsupported_version и verified и в выводе команд, и в API.

Хеш-цепочка добавляет еще одно требование совместимости. Нельзя делать выводы об истории цепочки по одному итоговому дайджесту. Верификатору нужны правила для конкретной версии: правила первой записи, порядка записей, кодирования дайджеста родителя и формата контрольной точки. Здесь полезно ориентироваться на Certificate Transparency: RFC 9162 определяет доказательства согласованности, показывающие, что старое дерево является тем же префиксом более нового дерева, вместо требования доверять новому сообщенному корневому хешу.

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

Никогда не соединяйте две истории, записывая старый дайджест в комментарий или поле отображения. Соединение должно входить в защищенные данные.

Миграции должны создавать производные данные, а не заменять источник

Проверьте исходный экспорт
Используйте sp audit verify, чтобы проверить цепочку аудита по нетронутому экспорту доказательств.

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

Для каждой перенесенной записи или пакета сохраняйте:

{
  "source_digest": "sha256:4a4d...",
  "source_schema_version": 1,
  "migration_id": "audit-v1-to-v2",
  "migration_build": "2.7.0+e31c9f4",
  "output_digest": "sha256:77c8...",
  "migrated_at": "2026-07-22T14:12:09Z"
}

Время migrated_at относится к производному объекту, а не к исходному событию. Не перезаписывайте recorded_at и не показывайте созданную запись v2 так, будто ее выдала старая система. Такая ошибка уже принесла внутренним расследованиям больше путаницы, чем любой очевидный сбой анализатора.

Некоторые миграции нельзя выполнить без потерь. В записи v1 может быть одна строка target, тогда как v2 требует структурированный URI. Если разбор не удался или результат неоднозначен, сохраните исходную строку и явно запишите статус миграции. Не придумывайте структурированное значение только потому, что оно нужно вашему новому индексу.

Например:

{
  "source_digest": "sha256:4a4d...",
  "migration_status": "partial",
  "derived": {
    "target_raw": "[email protected]:22",
    "host": "prod.example",
    "port": 22
  },
  "unresolved": ["user"]
}

Такой результат может выглядеть менее аккуратно, чем полностью заполненная строка. Зато он честнее. Будущий расследователь увидит и то, что говорила старая запись, и то, что предположила миграция.

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

Постоянный набор тестовых файлов выявляет тихие поломки

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

В наборе должно быть больше, чем несколько удачных записей. Храните побайтно точные тестовые файлы для таких случаев:

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

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

Используйте манифест, фиксирующий дайджесты тестовых файлов и ожидаемое поведение верификатора:

fixture: v1/0007-http-request.json
sha256: 4a4d5f0c...
expect:
  status: verified
  schema_version: 1
  chain_position: 7

fixture: v1/0007-http-request-tampered.json
sha256: 91af2a7d...
expect:
  status: invalid
  error_code: payload_digest_mismatch

Затем проверяйте совместимость в нескольких направлениях.

  1. Самый старый сохраненный верификатор по-прежнему должен проверять свой исходный набор.
  2. Текущий верификатор должен проверять каждый сохраненный исторический набор.
  3. Кандидатный модуль записи должен создавать записи, которые текущий верификатор принимает под новой версией.
  4. Кандидатная миграция должна сохранять заявленный дайджест источника и создавать ожидаемый производный объект.
  5. Каждый верификатор должен отклонять тестовые файлы, созданные для неподдерживаемых будущих версий.

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

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

Проверяйте границу обновления так, как это сделал бы расследователь

Отзывайте запуски, сохраняйте историю
Отзывайте запуск агента из журнала Sessions, сохраняя историю записанных действий.

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

Представьте цепочку, в которой v1 записывает одобрение сессии агента, несколько HTTP-вызовов и отзыв сессии. Версия 2 добавляет более подробное поле результата запроса и новую кодировку контрольной суммы. Тест должен начать с исходной записи v1, добавить корректные записи v1, создать задокументированную пограничную контрольную точку, добавить записи v2 и экспортировать весь набор.

В ожидаемом отчете о проверке переход должен быть виден ясно:

$ audit verify evidence-bundle
verified v1 records: 18
verified v1 terminal digest: sha256:6c12...e98a
verified v1-to-v2 continuity checkpoint
verified v2 records: 6
chain status: verified

Теперь запустите сценарии сбоев, которые возникают при обновлениях в продакшене:

  • удалите последнюю запись v1, оставив записи v2;
  • измените дайджест v1 в контрольной точке;
  • используйте запись v2 с меткой версии v1;
  • экспортируйте только сегмент v2 и запросите вердикт для всей истории;
  • запустите верификатор до v2 на смешанном наборе.

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

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

Делайте верификатор достаточно маленьким, чтобы он пережил приложение

Доказательства рядом с действиями агента
Записывайте действия HTTP и SSH, пока Sallyport, а не агент, хранит и использует учетные данные.

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

Разделите обязанности:

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

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

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

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

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

Заранее определите политику вывода формата из эксплуатации

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

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

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

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

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

Нужно ли при изменении схемы мигрировать записи аудита на месте?

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

Чем отличаются версия схемы и семантическая версия события?

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

Где хранить версию схемы аудита?

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

Может ли новый верификатор аудита читать записи, созданные до обновления?

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

Безопасно ли переписывать старые журналы аудита в новый формат JSON?

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

Решают ли правила канонического JSON проблемы совместимости журналов аудита?

Канонический JSON не дает безобидным на вид различиям сериализатора, например пробелам или порядку свойств, менять подписанные байты. Но он не исправляет неясные значения полей, дубликаты полей, потерю точности или не задокументированное изменение входных данных для хеширования.

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

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

Делает ли хеш-цепочка миграцию схемы безопасной сама по себе?

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

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

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

Как проверить экспорт аудита Sallyport после обновления?

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

Sallyport

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

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