Читать 6 мин

Как расследовать аудит API, когда записи агента противоречат друг другу

Как расследовать аудит API при спорных действиях агента: сопоставить идентификаторы запросов, время, результаты и пропущенные события провайдера без поспешных выводов.

Как расследовать аудит API, когда записи агента противоречат друг другу

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

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

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

Сохраните записи до того, как кто-то обновит панель

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

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

После сбора вычислите хеш каждого файла. Если операционная система предоставляет стандартную утилиту SHA-256, достаточно команды оболочки:

$ shasum -a 256 provider-events.json agent-activity.json
81b5777b8416320fe26cb8a8dddb6a9e736fab4f5e7aa5812bf6afeffc5f4e82  provider-events.json
a1e98c01992b51104fbc8c5fcbaa78e65db31f1edb3e546f4c14d0e6d3673ba  agent-activity.json

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

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

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

Идентификатор запроса важнее временной метки

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

В простом случае всё выглядит так. В записи действия указано, что агент вызвал POST /v1/invoices; в заголовках ответа есть x-request-id: req_72M...; экспорт провайдера содержит req_72M...; созданный счёт имеет идентификатор inv_4P.... Теперь у вас есть связь между намерением, доставкой, обработкой у провайдера и сохранённым состоянием.

Более сложные случаи встречаются чаще. Провайдер может назначить идентификатор запроса только после разбора запроса. При сбое TLS у запроса не будет идентификатора провайдера, поскольку он не достиг приложения. Шлюз может создать один идентификатор, а нижестоящий сервис, другой. Асинхронный API может вернуть идентификатор задания, а затем записать запрошенный объект через несколько минут. Фиксируйте, какая граница выдала каждый идентификатор, вместо того чтобы сводить их в один столбец request_id.

Используйте таблицу сопоставления, в которой неопределённость видна сразу:

ПолеЛокальная запись действияЗапись провайдераЗатронутая система
Идентификатор корреляции клиентаrun-18-call-42run-18-call-42отсутствует
Идентификатор запроса провайдераreq_72M... в ответеreq_72M...отсутствует
Метод и путьPOST /v1/invoicesPOST /v1/invoicesсчёт создан
Результат504 timeout202 acceptedзадание job_91... выполнено
Время события10:04:03.219Z10:04:03Z10:04:11.802Z

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

Если провайдер поддерживает ключ идемпотентности для операций записи, используйте его. В черновике IETF Idempotency-Key хорошо описана практическая цель: клиент может повторить небезопасную операцию HTTP и не создать тот же эффект дважды. Поведение зависит от провайдера, поэтому изучите его правила хранения и сопоставления. Не предполагайте, что достаточно совпадения конечной точки.

Для API, которые принимают пользовательские заголовки, создавайте идентификатор корреляции до вызова и передавайте его в документированном заголовке, например X-Client-Request-ID. Сохраняйте его вместе с локальным событием. Никогда не помещайте в этот идентификатор секреты, промпты, пользовательские данные или токены. Безопасное значение не должно иметь смысла за пределами дела, например case-2025-041-run7-call18.

Время может опровергнуть версию, но редко доказывает её

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

RFC 3339 задаёт распространённый формат интернет-времени и рекомендует форму UTC с заглавной Z в конце, например 2025-03-08T10:04:03.219Z. Сохраняйте исходную строку даже после разбора. Разница между 10:04:03Z и 10:04:03.219Z важна, когда один источник округляет время до секунд, а другой сообщает миллисекунды.

Для каждого значимого события создайте четыре поля времени:

  • исходную временную метку в точности как в экспорте
  • нормализованную временную метку UTC
  • тип события, например отправлено, принято, выполнено или записано
  • владельца часов, например локальный Mac, периферийный узел провайдера, рабочий процесс провайдера или база данных

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

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

Будьте внимательны со временем поступления лога. Многие системы показывают и event_time, и created_at. Первое описывает момент события по часам системы, которая его отправила. Второе может означать момент получения или индексации агрегатором. Позднее поступление не означает позднее выполнение. Если событие появилось после начала инцидента, проверьте оба поля, прежде чем строить версию.

Полезная проверка порядка должна отвечать лишь на один вопрос: возможна ли предложенная версия? Событие провайдера в 10:04:03 и локальная отправка в 10:04:03.219 могут согласоваться, если часы различаются или провайдер округляет время вниз. Заявленное завершение в 10:02 при том, что провайдер сообщает о принятии задания в 10:04, невозможно, если только вы не смешали два события или неправильно поняли значение поля.

Разделяйте попытку, доставку, принятие и завершение

Команды часто сводят четыре разных состояния к одному слову «вызвал». Именно это упрощение чаще всего вызывает споры о логах.

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

Спецификация HTTP Semantics, RFC 9110, поясняет, что код состояния описывает ответ сервера, а не всю историю взаимодействия глазами клиента. 202 Accepted прямо означает, что обработка принята, но ещё не завершена. 204 No Content означает, что сервер успешно обработал запрос, но сам по себе не объясняет все последующие эффекты. При сетевом тайм-ауте HTTP-ответ может отсутствовать, хотя сервер всё равно обработал запрос.

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

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

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

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

Для отсутствующих событий нужно ограниченное объяснение

Перестаньте передавать агентам секреты
Агенты подключаются через sp mcp, а Sallyport сам выполняет действия HTTP и SSH.

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

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

Полезно помнить конкретный пример. Агент отправляет POST /exports и получает тайм-аут соединения. Команда ищет в логах провайдера локальный идентификатор клиента и ничего не находит. Затем она повторяет запрос и получает два уведомления о завершении экспорта.

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

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

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

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

Расследуйте всё по одной записи аудита
Журналы Sessions и Activity строятся на основе одного защищённого от записи зашифрованного журнала аудита.

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

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

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

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

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

Запись шлюза полезна только тогда, когда она фиксирует границу

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

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

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

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

Формулируйте вывод как утверждения со свидетельствами и ограничениями

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

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

Используйте формулировки, соответствующие уровню уверенности. «Журнал действий показывает, что процесс X запросил POST /v1/invoices в это время». «В экспорте провайдера есть запрос с тем же идентификатором провайдера». «Счёт существует, и его атрибуты совпадают с записанным ответом». Это проверяемые утверждения. Фраза «агент точно создал счёт» оправдана только тогда, когда сопоставление и граница доступа к учётным данным действительно это подтверждают.

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

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

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

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

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

Доказывает ли отсутствие записи у провайдера, что агент никогда не отправлял запрос?

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

Как связать действие AI-агента с запросом к API-провайдеру?

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

Какой формат времени использовать при расследовании инцидента API?

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

Может ли вызов API выполниться, если агент сообщает о тайм-ауте?

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

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

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

Что делать, если неудачный запрос агента мог создать ресурс?

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

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

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

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

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

Почему в логах API-провайдеров бывают пропуски?

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

Sallyport

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

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