diff --git a/editorial/agent-rewrites/143.json b/editorial/agent-rewrites/143.json index 2706472..8e1dfaf 100644 --- a/editorial/agent-rewrites/143.json +++ b/editorial/agent-rewrites/143.json @@ -3,5 +3,5 @@ "slug": "editorial-2024-01-mechanism-legacy-modernization", "title": "Модернизация legacy без ложной совместимости: как проверять поведение", "excerpt": "Одинаковый HTTP-ответ не доказывает совместимость старой и новой реализации. Разбираем контракт, побочные эффекты, повтор запроса и минимальную проверку перед переключением маршрута.", - "contentHtml": "

После замены старого обработчика команда получает знакомый симптом: новый endpoint отвечает тем же статусом и похожим JSON, но клиент ломается на повторном запросе. В одном случае validation-ошибка превращается в 500. В другом preview начинает публиковать событие. В третьем повтор той же операции создаёт вторую запись. Цена ошибки — не только откат релиза. Команда уже меняет данные, отправляет уведомления или теряет доверие к ответу, а по snapshot это не видно.

\n

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

\n

Тезис: переносить нужно договор, а не форму ответа

\n

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

\n

Parity здесь означает не побайтное равенство. Она означает совпадение заранее выбранных инвариантов. Для read-only preview важны статус, обязательные поля и отсутствие публикации. Для записи важны idempotency key, порядок эффекта и состояние после повтора. Для платежа добавляются денежная точность, авторизация и reconciliation. Один общий список проверок не подходит всем операциям.

\n

Механизм совместимости

\n

Разделите контракт на четыре слоя. Transport описывает метод, путь, статус и значимые заголовки. Payload описывает типы, обязательные поля, значение null и отсутствие поля. Effect описывает запись, публикацию, очистку cache и запрет повторного действия. Time описывает, в каком состоянии читаются данные и что означает «тот же запрос»: тот же input, business key или idempotency key.

\n

OpenAPI помогает зафиксировать первый и часть второго слоя. Он делает видимыми paths, operations, схемы и ответы. Но одинаковая схема не говорит, записал ли обработчик событие, когда он прочитал баланс и что произойдёт при повторе. Поэтому schema — это граница формы, а не сертификат поведенческой совместимости.

\n
type CompatibilityCase = {\n  name: 'valid' | 'invalid' | 'repeat';\n  precondition: string;\n  request: unknown;\n  expected: {\n    statusCategory: string;\n    fields: string[];\n    effect: 'none' | 'one' | 'same-key-no-duplicate';\n  };\n  evidence: string;\n};\n\nconst repeatCase: CompatibilityCase = {\n  name: 'repeat',\n  precondition: 'same idempotency key, known initial state',\n  request: { amount: 1000, idempotencyKey: 'case-42' },\n  expected: {\n    statusCategory: 'success-or-replayed-success',\n    fields: ['operationId', 'status'],\n    effect: 'same-key-no-duplicate'\n  },\n  evidence: 'response plus effect log in the test environment'\n};
\n

Это учебный TypeScript-пример. Он показывает форму записи, но не вызывает endpoint и не доказывает, что повтор безопасен. Значение поля evidence должно ссылаться на реально доступный журнал, тестовую базу или другой наблюдаемый источник. Если источник ещё не подключён, результат нельзя помечать как parity passed.

\n
Диагностика совместимости одного legacy-шва
СимптомПричинаПроверкаДействие
Одинаковый JSON, но повтор создаёт записьСравнили payload и не проверили effectПовторить запрос с тем же ключом и проверить журнал эффектаДобавить idempotency rule или оставить операцию на legacy
Невалидный ввод получил 500Новая реализация потеряла категорию ошибкиСопоставить status category и поля ошибки для invalid caseСохранить внешний error contract либо версионировать API
Результаты расходятся только утромРазличается время чтения состояния или часовой поясПовторить case на фиксированном состоянии и записать timestampЯвно определить time boundary и источник времени
Новый ответ содержит дополнительное полеСовместимое расширение принято без проверки consumerПроверить парсеры старых клиентов и правило unknown fieldsОставить поле optional или подготовить migration path
Команда не знает, какое отличие важноУ контракта нет владельца и инвариантовНазначить owner и классифицировать каждое расхождениеОстановить расширение шва до решения владельца
\n
\"Матрица
Матрица помогает разделить свойства операции. Она не содержит ответы реальных сервисов и не заменяет parity-проверку в доступном контуре.
\n

Как читать различия

\n

Сначала присвойте различию класс. Cosmetic — изменение, от которого не зависит consumer: например, пробел в сообщении или порядок незначимых ключей. Compatible extension — дополнительное optional-поле, которое старый клиент по договору игнорирует. Behavioral mismatch — другой статус, обязательное поле, значение, момент чтения или эффект. Unknown — различие обнаружено, но его значение ещё не установлено.

\n

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

\n

Три минимальных case

\n

Начните с трёх случаев, но не принимайте их за полное покрытие. Valid показывает основной результат и обязательные поля. Invalid проверяет категорию отказа и отсутствие недопустимого эффекта. Repeat проверяет одинаковый ключ или вход и ожидаемое действие. Для каждого случая запишите precondition, request, expected result, место наблюдения и owner.

\n

Если операция зависит от внешнего provider, курса, очереди или времени, это часть case. Зафиксируйте состояние, которое можно воспроизвести, или пометьте свойство неизвестным. Snapshot ответа подходит для payload. Для effect он недостаточен: два пути могут вернуть одинаковый JSON, но только один отправить сообщение. Здесь нужен журнал, счётчик, тестовая запись или иной источник, который действительно видит эффект.

\n

Порядок проверки перед переключением

\n
  1. Выберите одну операцию и назовите потребителя. Не начинайте с «переписать модуль».
  2. Запишите transport, payload, effect и time. Отдельно отметьте свойства, которые не входят в обещание.
  3. Назначьте владельца контракта. Он решает, что сохранять, что версионировать и что считать неизвестным.
  4. Подготовьте valid, invalid и repeat case с фиксированными precondition и expected result.
  5. Проверьте старый путь и сохраните evidence в одном сопоставимом формате. Не сравнивайте результаты из разных состояний.
  6. Запустите новый путь на тех же case. Для effect используйте источник, который видит запись или публикацию, а не только response body.
  7. Классифицируйте каждое отличие. Behavioral mismatch блокирует расширение маршрута; compatible extension требует проверки consumer.
  8. Определите обратное действие. Возврат маршрута к legacy не означает восстановление уже изменённых данных.
  9. Переключайте только выбранный шов. Остальной трафик остаётся на известном пути до отдельного решения.
\n

Отрицательный путь: когда модернизацию нужно остановить

\n

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

\n

Не пытайтесь закрыть неизвестность большим snapshot или процентом «покрытия parity». Одно число смешивает критичный платёж и косметическую подпись. Полезнее список незакрытых классов: time-dependent, effectful, external-provider, access-denied. Для каждого выберите действие: проверить следующим, оставить на legacy или изменить контракт с версией.

\n

Ограничения и критерий готовности

\n

OpenAPI описывает интерфейс, но не бизнес-смысл. Три case задают стартовую границу, но не покрывают всю систему. Учебный код выше не читает legacy, не запускает трафик, не сравнивает базу и не измеряет производительность. Поэтому статья не утверждает сохранение поведения какой-либо production-системы. Реальная готовность требует evidence из конкретного тестового или staging-контура.

\n

Шов готов к ограниченному переключению, когда owner назван, четыре слоя контракта заполнены, valid/invalid/repeat воспроизводимы, каждое обязательное различие классифицировано, effect наблюдаем, а возврат маршрута проверен отдельно от восстановления данных. Критический unknown должен отсутствовать или иметь явно принятое решение оставить операцию на legacy. Только тогда команда может объяснить, что именно она перенесла и какую цену изменения согласовала.

\n

Проверяемые источники

" + "contentHtml": "

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

\n

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

\n

Главный вопрос: что именно нужно сохранить

\n

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

\n

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

\n

Четыре слоя поведенческого контракта

\n

Разложите шов на четыре слоя. Transport — метод, путь, статус и значимые заголовки. Payload — типы, обязательность полей, разница между отсутствующим и null, формат ошибки. Effect — запись в базе, публикация сообщения, очистка кеша или запрет повторного действия. Time — состояние, в котором прочитаны данные, часовой пояс, срок действия и смысл слова «повтор».

\n

OpenAPI фиксирует интерфейс HTTP и описывает входные и выходные схемы. Это полезная граница: инструменты видят paths, operations, responses и типы данных. Но схема не сообщает, записал ли обработчик событие, из какого snapshot прочитал баланс или будет ли второй POST создавать ресурс. Наличие валидной схемы — доказательство формы, а не поведенческой совместимости.

\n
\"Матрица
У каждого слоя свой способ наблюдения: заголовки и статус, тело ответа, журнал эффекта и фиксированное состояние. Иллюстрация не содержит ответов реального сервиса и не заменяет проверку в тестовом контуре.
\n

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

\n
Как интерпретировать различие между legacy и новой реализацией
РазличиеПочему оно возниклоЧто проверитьРешение
Порядок ключей в JSONИзменился сериализаторЗависит ли потребитель от порядка объектаНормализовать сравнение; не считать порядок значимым без явного контракта
Добавилось необязательное полеНовая схема расшириласьИгнорирует ли старый клиент неизвестные поля по договоруОставить поле optional и задокументировать правило либо версионировать ответ
Поменялись статус или код ошибкиДругой валидатор или обработчик исключенийКакие ветки клиента зависят от 4xx/5xx и error codeСохранить error contract или объявить несовместимое изменение
Повтор создал вторую записьОтвет сравнили, эффект — нетОдинаковый ли ключ намерения и есть ли дедупликация на сервереДобавить прикладную идемпотентность либо не переводить операцию
Расходится только значение утромРазное состояние или часовой поясФиксированные данные, timestamp и источник времениОпределить time boundary и повторить кейс на одном состоянии
\n

Минимальный набор кейсов

\n

Для первой проверки достаточно трёх кейсов, но это не полное тестовое покрытие. Valid показывает основной результат и обязательные поля. Invalid проверяет отказ, его категорию и отсутствие запрещённого эффекта. Repeat отправляет тот же intent повторно и проверяет состояние после первой попытки. В каждом кейсе должны быть precondition, запрос, ожидаемый ответ, ожидаемый эффект, место наблюдения и владелец.

\n

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

\n

Воспроизводимое сравнение двух путей

\n

Ниже — стендовый shell-пример. Он не предполагает конкретный фреймворк: укажите адреса старого и нового маршрута в первых строках. Для работы нужны только curl и jq. Пример сравнивает статус и тело, а для эффекта оставляет отдельный запрос к журналу аудита. Если у сервиса нет такого наблюдаемого источника, эффект остаётся неизвестным.

\n
set -eu\n\nOLD_URL=\"http://localhost:8080/legacy/orders\"\nNEW_URL=\"http://localhost:8081/orders\"\nAUDIT_URL=\"http://localhost:8081/audit\"\nCASE_ID=\"compat-42\"\nTMP_DIR=\"$(mktemp -d)\"\ntrap 'rm -rf \"$TMP_DIR\"' EXIT\n\nrequest() {\n  url=\"$1\"\n  name=\"$2\"\n  curl -sS -D \"$TMP_DIR/$name.headers\" \\\n    -o \"$TMP_DIR/$name.json\" \\\n    -w '%{http_code}' \\\n    -H 'content-type: application/json' \\\n    -H \"Idempotency-Key: $CASE_ID\" \\\n    --data '{\"orderId\":\"demo-42\",\"amount\":1000,\"currency\":\"RUB\"}' \\\n    \"$url\"\n}\n\nold_status=\"$(request \"$OLD_URL\" old)\"\nnew_status=\"$(request \"$NEW_URL\" new)\"\nprintf 'status: old=%s new=%s\\n' \"$old_status\" \"$new_status\"\njq -S . \"$TMP_DIR/old.json\" > \"$TMP_DIR/old.sorted.json\"\njq -S . \"$TMP_DIR/new.json\" > \"$TMP_DIR/new.sorted.json\"\ndiff -u \"$TMP_DIR/old.sorted.json\" \"$TMP_DIR/new.sorted.json\" || true\n\n# Это отдельная проверка эффекта, если в стенде есть audit endpoint.\ncurl -sS \"$AUDIT_URL?case=$CASE_ID\" | jq '{records,events}'
\n

Здесь нормализация через jq -S убирает только шум порядка ключей. Она не скрывает отсутствующее поле, другое значение или лишнюю запись. diff возвращает ненулевой код при различии, поэтому в учебном примере добавлено || true, чтобы вывести результат и продолжить ручную классификацию. В CI лучше убрать его и завершать job с ошибкой после формирования артефакта сравнения.

\n

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

\n

Что наблюдать кроме response body

\n

Снимок ответа удобен для payload, но слеп к эффекту. Для записи в базу нужен запрос к тестовой базе или endpoint аудита. Для сообщения — счётчик публикаций и идентификатор события. Для кеша — наблюдаемый hit/miss или версия записи. Для внешнего провайдера — его тестовый журнал и correlation id. У каждого доказательства должны быть одинаковые precondition и временное окно для старого и нового пути.

\n

Разделяйте request id и idempotency key. Первый помогает найти конкретную попытку в логах. Второй описывает намерение, общее для повторных попыток. У одного intent может быть несколько request id. Если эти значения смешать, расследование покажет два запроса и не ответит на вопрос, была ли операция одна.

\n

Постепенное переключение и rollback

\n

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

\n

Canary-сравнение не отменяет абсолютных ограничений. Даже если ошибка canary похожа на control, обе группы могут совместно использовать базу, очередь или внешний провайдер. Поэтому задайте отдельные стоп-условия: неожиданный статус, повторная запись, лишняя публикация, нарушение авторизации или рост задержки выше согласованного порога. Rollback маршрута возвращает запросы на legacy, но не откатывает данные и уже доставленные события. Для них нужен самостоятельный план сверки.

\n
  1. Выберите одну операцию, потребителя и владельца контракта.
  2. Заполните четыре слоя: transport, payload, effect и time.
  3. Зафиксируйте valid, invalid и repeat с одинаковыми начальными данными.
  4. Снимите ответы старого пути и сохраните заголовки, тело, timestamp и request id.
  5. Повторите те же кейсы на новом пути; для repeat используйте тот же ключ намерения.
  6. Проверьте запись, публикацию, кеш и внешние вызовы через реальные наблюдаемые источники.
  7. Классифицируйте каждое отличие как косметическое, расширение, поведенческое или неизвестное.
  8. До расширения canary определите стоп-условие, владельца решения и судьбу данных после rollback.
\n

Когда миграцию нужно остановить

\n

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

\n

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

\n

Ограничения применимости

\n

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

\n

Shell-пример учебный: он не запускается против конкретной production-системы, не знает её схему аудита и не утверждает, что какой-либо реальный endpoint идемпотентен. URL, формат ключа, окно хранения, правила ошибок и допустимые различия нужно взять из договора именно вашего сервиса. Пока критичный unknown не проверен и не принят владельцем, расширять маршрут нельзя.

\n

Итог: критерий готовности

\n

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

\n

Проверяемые источники

" } diff --git a/editorial/agent-rewrites/144.json b/editorial/agent-rewrites/144.json index 8f7702f..9cd310d 100644 --- a/editorial/agent-rewrites/144.json +++ b/editorial/agent-rewrites/144.json @@ -3,5 +3,5 @@ "slug": "editorial-2024-01-practice-legacy-modernization", "title": "Модернизация legacy-системы: как выбрать безопасный шов", "excerpt": "Полное переписывание редко начинается с доказанной границы. Разбираем, как выделить одну операцию, описать её поведение, подключить новый путь через адаптер и не спутать возврат маршрута с восстановлением данных.", - "contentHtml": "

Симптом заметен по backlog: задача называется «переписать расчёт заказа», но не содержит одного входа и одного результата. Внутри старого модуля смешаны HTTP-обработчик, скидки, запись статуса, письмо и вызовы соседних систем. Команда создаёт новый сервис, а через несколько недель не знает, какая часть поведения уже перенесена. На переключении обнаруживаются редкие правила и побочные эффекты. Цена ошибки — задержка релиза, двойная запись, потерянное письмо или откат, который меняет маршрут, но не возвращает данные.

\n

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

\n

Что именно нужно выделить

\n

Папка, класс или новый микросервис сами по себе не образуют границу. Граница появляется там, где можно задать проверяемый контракт. Для HTTP это может быть один endpoint. Для очереди — один тип сообщения и ключ повторной обработки. Для интерфейса — одно действие пользователя и один command.

\n

Опишите шов пятью строками: consumer, input, response, effect и owner. Consumer показывает, кто вызывает операцию. Input фиксирует обязательные поля, форматы и повтор. Response описывает статус и значимые поля. Effect перечисляет запись, сообщение, инвалидацию кеша или отсутствие изменения. Owner принимает решение при расхождении. Неизвестный эффект помечайте как unknown. Не заменяйте его предположением о parity.

\n

Широкая формулировка вроде «вся корзина» скрывает слишком много решений. Сузьте её до preview или confirm. Preview обычно легче сделать без изменения состояния. Confirm имеет побочные эффекты и требует отдельного контракта повторов, ошибок и идемпотентности. Эти операции могут использовать общие данные, но не должны попадать в один первый rollout только потому, что так выглядит удобнее.

\n

Учебный пример: quote и confirm

\n

Ниже — ограниченный учебный пример. Он не описывает конкретную production-систему и не доказывает совместимость с настоящим legacy-кодом. Пусть старый модуль рассчитывает цену заказа по запросу POST /quote. Клиент ожидает сумму, срок действия предложения и код ошибки. При POST /confirm модуль резервирует товар и отправляет событие в очередь. Начнём только с /quote: у него нет записи заказа, а результат можно сравнить до переключения.

\n
type QuoteInput = {\n  productId: string\n  quantity: number\n  customerTier?: 'base' | 'plus'\n}\n\ntype QuoteResult =\n  | { status: 'ok'; total: number; expiresAt: string }\n  | { status: 'rejected'; code: 'INVALID_QUANTITY' | 'NOT_AVAILABLE' }\n\nfunction routeQuote(input: QuoteInput, mode: 'legacy' | 'candidate') {\n  if (mode === 'candidate') return modernQuoteAdapter(input)\n  return legacyQuote(input)\n}
\n

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

\n

Для /quote сравните не все байты ответа, а заранее названные инварианты: статус, итоговую сумму, срок действия и код отказа. Проверьте округление, отсутствие товара, нулевое и отрицательное количество, повторный запрос и тайм-аут зависимости. Учебные данные должны быть явно ограничены. Они показывают, как составить проверку; они не заменяют ответы старого модуля, историю инцидентов и реальные ограничения среды.

\n

Симптом → причина → проверка → действие

\n
СимптомПричинаПроверкаДействие
«Перенесём весь расчёт»Единицей работы стала архитектура, а не операцияНазвать один consumer, input, response и effectСузить шов до quote или отдельного варианта результата
Зелёный тест локальноТест не знает скрытые вызовы и правила legacyСверить valid, invalid и repeat cases с источником поведенияОставить тест учебным и собрать отдельное evidence
Новый сервис копирует схемуСхема не фиксирует порядок эффектов и повторПроверить статус, идемпотентность, ошибки и время ответаДобавить compatibility record или уменьшить scope
Нет владельца маршрутаНекому принять решение при расхожденииНазначить owner решения и owner состоянияНе включать новый путь до явного решения
Rollback означает «вернуться назад»Маршрут смешан с уже изменёнными даннымиРазделить route return, provider effect и data recoveryОписать только обратимое действие; остальное считать отдельной работой
\n
\"Схема
Шов удерживает решение о маршруте на границе операции. Иллюстрация учебная: она не показывает реальный трафик, базу или подтверждённую совместимость.
\n

Почему нужен маршрутизатор

\n

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

\n

Постепенное вытеснение не означает автоматическую безопасность. AWS описывает strangler fig как способ постепенно заменять отдельную функциональность работающего монолита. Это снижает размер изменения, но не проверяет бизнес-смысл полей и не отменяет работу с состоянием. Для каждой операции всё равно нужны owner, наблюдаемый сигнал, окно проверки и правило остановки.

\n

Порядок действий

\n
  1. Зафиксируйте наблюдаемый симптом и цену ошибки. Укажите, что может раздвоиться: ответ, запись, сообщение или внешний вызов.
  2. Выберите одну операцию с узкой границей. Запишите consumer, input, response, effect и owner. Если effect неизвестен, оставьте его unknown.
  3. Составьте таблицу valid, invalid и repeat cases. Для каждого случая укажите источник поведения: код, тест, лог, трассу или ручное подтверждение.
  4. Оставьте legacy основным путём и создайте адаптер нового пути. Адаптер не меняет побочные эффекты, которые не входят в контракт.
  5. Проверьте новый путь на ограниченных данных. Сравните только объявленные инварианты и отдельно отмечайте расхождение, которое требует сужения шва.
  6. Опишите ручное решение о включении. Назовите сигнал, период наблюдения, условие остановки и человека, который принимает решение.
  7. Подготовьте возврат маршрута к известной legacy-конфигурации. Отдельно запишите, что делать с данными и внешними эффектами, которые уже нельзя отменить.
  8. Расширяйте границу только после разбора расхождений. Если новое поведение требует общего состояния, сначала оформите этот state boundary отдельным швом.
\n

Отрицательный путь важнее красивого ответа

\n

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

\n

У операций с состоянием есть дополнительное ограничение. Возврат маршрута не удаляет созданную запись, не отменяет платёж и не отзывает сообщение у внешнего провайдера. Для такого эффекта нужны идентификатор операции, владелец сверки и отдельное правило компенсации. Если компенсация не доказана, не называйте rollout обратимым. Выберите read-only или preview-шов либо оставьте эффект у legacy.

\n

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

\n

Проверяемый критерий готовности

\n

Первый шов готов к ограниченному рассмотрению, если другой инженер может без устного контекста показать: один вход, один ожидаемый результат, список значимых эффектов, владельца решения, набор valid/invalid/repeat cases, источник каждого факта и известный legacy-маршрут. Для маршрута есть проверяемый возврат. Для необратимых данных отдельно названо, что возврат не покрывает.

\n

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

\n

Проверяемые источники

" + "contentHtml": "

Симптом обычно появляется ещё до изменения кода: в backlog стоит задача «переписать расчёт заказа», но не названы один вход и один результат. Старый модуль одновременно принимает HTTP-запрос, применяет скидку, пишет статус, отправляет письмо и вызывает соседнюю систему. Команда создаёт новый сервис, а через несколько недель уже не может сказать, какая часть поведения перенесена. Цена ошибки — двойная запись, потерянное уведомление или откат, который меняет маршрут, но не возвращает данные.

\n

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

\n

Начните с операции, а не с папки

\n

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

\n

Перед проектированием запишите пять полей. Consumer показывает, кто вызывает операцию. Input фиксирует обязательные поля, форматы и правила повтора. Response описывает статус и значимые значения. Effect перечисляет запись, публикацию, отправку письма, очистку кеша или отсутствие изменения. Owner принимает решение при расхождении. Если эффект пока неизвестен, так и напишите: unknown — это открытый риск, а не разрешение считать операцию безопасной.

\n
Минимальная карточка шва перед первым переключением
ПолеПримерПроверка
ОперацияPOST /quoteПонятно, какой запрос входит в волну
ConsumerСервис корзиныНазван конкретный вызывающий код
Responsetotal, expiresAt, код отказаВыделены поля, влияющие на потребителя
EffectТолько чтениеНет записи, публикации или внешнего вызова
OwnerВладелец расчётаЕсть человек или команда для решения stop/continue
Returnlegacy-onlyСледующий запрос можно вернуть на известный путь
\n

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

\n

Зафиксируйте контракт до адаптера

\n

Совместимость — это не «новый endpoint вернул похожий JSON». Разделите её на четыре слоя. Transport — метод, путь, статус и значимые заголовки. Payload — типы, обязательность, null и отсутствие поля. Effect — запись, публикация, изменение кеша и поведение при повторе. Time — момент чтения состояния и смысл слова «повтор»: тот же набор полей, business key или idempotency key.

\n

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

\n

Ниже — учебный контракт для read-only расчёта. Он не описывает конкретную production-систему и не доказывает parity. В настоящем проекте названия полей, округление, коды ошибок и срок действия должны быть взяты из действующего обработчика, тестов или зафиксированного ответа legacy.

\n
type QuoteInput = {\n  productId: string\n  quantity: number\n  customerTier?: 'base' | 'plus'\n}\n\ntype QuoteResult =\n  | { status: 'ok'; total: number; expiresAt: string }\n  | { status: 'rejected'; code: 'INVALID_QUANTITY' | 'NOT_AVAILABLE' }\n\nfunction routeQuote(input: QuoteInput, mode: 'legacy' | 'candidate') {\n  return mode === 'candidate'\n    ? modernQuoteAdapter(input)\n    : legacyQuote(input)\n}
\n

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

\n

Сравнивайте control и candidate

\n

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

\n

Команда ниже воспроизводит сравнение на тестовом контуре, если ваш маршрутизатор документированно поддерживает заголовок X-Route. Это условный интерфейс учебного адаптера, его нельзя отправлять в произвольный production endpoint. Значения BASE_URL и ITEM_ID задайте сами, а поля для нормализации согласуйте с владельцем контракта.

\n
set -eu\n\ntest -n $BASE_URL || { echo 'set BASE_URL' >&2; exit 2; }\ntest -n $ITEM_ID || { echo 'set ITEM_ID' >&2; exit 2; }\n\nfor route in legacy candidate; do\n  curl --silent --show-error --fail \\\\\n    -H 'X-Route: '$route \\\\\n    '$BASE_URL/catalog/items/'$ITEM_ID > $route.json\ndone\n\njq -S 'del(.requestId, .generatedAt)' legacy.json > legacy.normalized.json\njq -S 'del(.requestId, .generatedAt)' candidate.json > candidate.normalized.json\ndiff -u legacy.normalized.json candidate.normalized.json
\n

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

\n
Симптом, проверка и решение до расширения шва
СимптомВероятная причинаПроверкаДействие
Оба пути вернули 200Сравнили транспорт, а не смыслПроверить поля, ошибки, время и эффектДобавить contract cases
Отказ стал 500Потеряна категория ошибкиПовторить invalid case на одном состоянииСохранить внешний контракт или версионировать его
Повтор создаёт вторую записьНе определено правило повтораПроверить record id и журнал эффектаДобавить idempotency key или оставить запись в legacy
Candidate медленнее только вечеромРазные нагрузка или состояние кешаСопоставить окно, population и backendПовторить сравнение в сопоставимых условиях
После возврата появились дублиRollback маршрута не отменил записьСверить состояние и внешние вызовыОстановить write-path и назначить reconciliation owner
\n
Схема безопасного шва: клиент отправляет запрос в маршрутизатор, legacy остаётся контрольным путём, а ограниченный candidate подключается через адаптер; запись и внешние эффекты вынесены за границу чтения
Первый шов оставляет решение о маршруте на границе одной операции. Схема учебная: она не показывает реальный трафик, базу данных или подтверждённую совместимость.
\n

Маршрутизатор снижает размер изменения, но не риск сам по себе

\n

Маршрутизатор нужен в конкретном месте: он принимает решение, оставить запрос на legacy или передать его candidate. Для остальных операций действует прежнее правило. Если флаг разбросан по внутренним вызовам, один запрос может частично пройти по старому и частично по новому пути, и сравнение потеряет смысл. Логи должны различать как минимум маршрут, версию адаптера, корреляционный идентификатор и результат.

\n

AWS описывает для strangler fig три фазы: transform — создать новую функциональность параллельно со старой, coexist — временно держать обе реализации и направлять трафик через прокси, eliminate — вывести старую часть после переноса. Это модель постепенного вытеснения, а не доказательство того, что конкретная миграция безопасна. Прокси может стать единой точкой отказа или узким местом, поэтому его latency и ошибки измеряют отдельно.

\n

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

\n

Не называйте rollback восстановлением

\n

Для read-only операции возврат обычно означает смену правила маршрутизатора: следующие запросы идут в legacy. Но общий кеш, мигрированный справочник или sticky session могут связать два пути. До возврата проверьте, понимает ли старая реализация актуальное состояние и не изменял ли candidate его косвенно.

\n

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

\n

Минимум для записи — идентификатор операции, журнал переходов, ключ идемпотентности или доказанное отсутствие повторной доставки, владелец сверки и процедура расхождения. Если этих данных нет, первый шов лучше сузить до чтения, добавить preview или оставить запись в legacy. Аварийный delete-скрипт без карты зависимостей не является планом восстановления.

\n

Порядок работы

\n
  1. Опишите симптом и цену ошибки: что может раздвоиться — ответ, запись, сообщение или внешний вызов.
  2. Выберите одну операцию с понятными входами и ограниченным числом потребителей.
  3. Заполните consumer, input, response, effect, owner и известный legacy-маршрут.
  4. Соберите valid, invalid и repeat cases. Для каждого укажите состояние, ожидаемый результат и источник факта.
  5. Поставьте адаптер в pass-through и проверьте, что старый путь не изменился.
  6. Запустите candidate на тех же данных, сравните объявленные инварианты и отдельно проверьте эффект.
  7. Задайте малую population, контроль, окно, сигналы, пороги и ручное решение stop/continue.
  8. При расхождении остановите расширение. Классифицируйте причину: контракт, время, зависимость, права или побочный эффект.
  9. Проверьте возврат следующих запросов на legacy. Судьбу уже изменённых данных разберите отдельной процедурой.
  10. Расширяйте шов только после закрытия критичных неизвестных. Новую state boundary оформите отдельной операцией.
\n

Границы применимости

\n

Strangler-подход требует, чтобы внешний вызов можно было перехватить и маршрутизировать. AWS отдельно отмечает, что он не подходит маленьким системам с низкой сложностью, а прокси может стать bottleneck. Если запрос нельзя разделить по операции или нет способа быстро вернуть legacy, сначала нужна другая форма изменения — например, внутренний совместимый слой без переключения трафика.

\n

Canary и сравнение ответов не дают статистической гарантии. Малая population может не содержать редкую роль, synthetic data не показывает реальные состояния, а общий кеш нарушает независимость control и candidate. Для денег, прав, персональных данных и внешних транзакций read-only схема из примера недостаточна: нужны threat model, аудит доступа, идемпотентность и сверка с владельцем данных.

\n

Учебный TypeScript и shell выше не запускают ваш legacy, не измеряют его p95 и не доказывают результат в production. Они задают воспроизводимую форму проверки. Факты о конкретной системе берите из кода, тестов, логов и трасс выбранного контура; неизвестное оставляйте неизвестным, пока его не проверит владелец.

\n

Критерий готовности

\n

Шов готов к ограниченному рассмотрению, когда другой инженер без устного контекста показывает один вход, один ожидаемый результат, список эффектов, owner решения, valid/invalid/repeat cases, источник каждого ожидания и проверенный legacy-маршрут. Для нового пути видны отдельные сигналы. Для возврата есть действие, а для необратимых данных отдельно описано, чего возврат не покрывает.

\n

Если неизвестен владелец эффекта, нельзя восстановить исходное состояние или consumer скрыт, это не повод увеличивать процент трафика. Сузьте операцию, соберите доказательство или оставьте её на legacy. Безопасность первой волны означает не «новый сервис уже написан», а «расхождение можно обнаружить, остановить и объяснить».

\n

Проверяемые источники

" } diff --git a/editorial/agent-rewrites/145.json b/editorial/agent-rewrites/145.json index 91183bd..5db7929 100644 --- a/editorial/agent-rewrites/145.json +++ b/editorial/agent-rewrites/145.json @@ -2,6 +2,6 @@ "index": 145, "slug": "editorial-2023-12-field-security-audit", "title": "Аудит веб-проекта: как превратить список рисков в безопасный порядок исправлений", - "excerpt": "Высокий score не говорит, что менять первым. Разбираем security-triage через evidence, границы системы, владельца, обратимый шаг и проверяемый критерий результата.", - "contentHtml": "

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

\n

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

\n

Тезис: порядок исправлений строит evidence

\n

Разделите карточку риска на пять частей: evidence, exposure, owner, reversibility и decision. Evidence связывает утверждение с источником. Exposure показывает путь от внешнего входа к данным или действию. Owner подтверждает границу и принимает изменение. Reversibility описывает возврат. Decision объясняет, почему пункт идёт сейчас.

\n

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

\n

Как score вводит в заблуждение

\n

CVSS описывает характеристики уязвимости и помогает сравнивать техническую тяжесть. Но score не видит карту продукта. Он не знает, какой путь критичен для бизнеса, согласован ли тест, принадлежит ли endpoint вашей команде и что произойдёт после изменения. Поэтому высокий Base score может ждать уточнения, а менее громкий пункт с неизвестной публичной границей — получить первый безопасный gate.

\n

Порядок должен быть объясним одним предложением: «Сначала подтверждаем владельца и ожидаемый отказ на публичной границе доступа, затем согласуем контракт identity-провайдера, после этого меняем выдачу файла с готовым rollback». Если такую фразу нельзя составить, список смешивает проверку фактов, согласование scope и разработку.

\n
СимптомПричинаПроверкаДействие
Самый высокий score всегда первыйSeverity подменяет контекстЗапросить claim, источник и ограничениеОбосновать порядок evidence и exposure
Есть домен, но нет границыАктив и third-party путь смешаныНарисовать один пользовательский маршрутУбрать неподтверждённый участок
«Добавим проверку доступа» без тестаРешение опередило критерийНазвать ожидаемый allow и denyНаписать validation до кода
Rollback означает «откатить»Не названы версия и ответственныйПроверить обратное действиеНазначить owner и stop condition
security.txt считают разрешениемDisclosure смешали с authorizationПроверить объект, метод и времяБез согласования ограничиться инвентаризацией
\n
\"Схема
Маршрут показывает порядок вопросов. Он не оценивает реальный риск, не находит уязвимость и не запускает remediation.
\n

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

\n

Карточка не обязана быть большой. Поле claim описывает факт или вопрос, а не решение. source указывает журнал, запрос, конфигурацию, владельца или документ. status различает hypothesis, observed и confirmed. В limit записывают то, чего проверка не показывает.

\n
const card = {\n  id: 'SEC-042',\n  claim: 'роль reader получает чужой заказ',\n  scope: 'orders-api / GET /orders/:id',\n  owner: 'orders-team',\n  status: 'hypothesis',\n  source: 'reproduction-2026-08-02-01',\n  expected: { allow: 'свой заказ', deny: 'чужой заказ: 403' },\n  limit: 'тестовая среда; production не проверялся',\n  next: 'согласовать сценарий с владельцем API',\n  rollback: 'изменений в системе нет'\n};\n\nif (card.status === 'hypothesis') {\n  console.log('Не называть карточку подтверждённой уязвимостью');\n}
\n

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

\n

Проверяемый маршрут от гипотезы к изменению

\n
  1. Опишите симптом. Запишите сценарий, время и наблюдаемый результат. Не называйте сигнал критической уязвимостью заранее.
  2. Назовите границу. Укажите приложение, endpoint, роль, данные и переход к внешней зависимости. Домен не является картой продукта.
  3. Проверьте разрешение. Зафиксируйте объект, среду, окно времени, допустимый метод, запретные действия, контакт и условие остановки. Документ disclosure не заменяет эту запись.
  4. Разделите allow и deny. Для авторизации назовите разрешённый результат и отказ. Для файла опишите приём, серверное имя, хранение и проверку доступа при выдаче.
  5. Назначьте владельца. Он подтверждает контракт и принимает решение.
  6. Выберите обратимый gate. Сначала добавьте чтение, тест, флаг или review контракта. Не меняйте необратимую схему, пока не закрыты evidence и rollback.
  7. Сформулируйте критерий. Назовите наблюдаемый результат, источник результата и момент проверки.
  8. Обновите карточку. Измените status, source, limit и rationale. Если evidence не подтвердился, верните hypothesis или закройте false positive с объяснением.
\n

Отрицательный путь важнее красивого отчёта

\n

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

\n

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

\n

Ограничения метода

\n

Матрица triage не заменяет penetration test, threat model, code review или incident response. Она не вычисляет business impact и не обещает срок исправления. OWASP ASVS задаёт проверяемые требования, но не знает архитектуру проекта. CVSS помогает описать тяжесть, но не выбирает владельца и не создаёт разрешение. RFC 9116 описывает канал раскрытия, а не право тестировать домен.

\n

Источники могут обновляться. Поэтому в отчёте фиксируйте версию стандарта и идентификатор требования. Не пишите «проверено по OWASP» без названия документа, версии, scope и метода. Не выдавайте учебный объект, синтетическую запись или локальный тест за production evidence.

\n

Критерий готовности

\n

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

\n

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

\n

Проверяемые источники

" + "excerpt": "Длинный отчёт аудита не говорит, что исправлять первым. Разбираем учебный кейс через scope, актив, доказательство, влияние и обратимую проверку — с таблицей приоритетов и безопасными командами.", + "contentHtml": "

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

\n

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

\n

Сначала отделяем наблюдение от риска

\n

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

\n

Полезная запись выглядит как проверяемое утверждение: «роль viewer при запросе к документу другого владельца получает HTTP 200 и тело документа». К ней прикладываются дата и окружение проверки, обезличенный идентификатор, ожидаемый результат и фактический результат. Секреты, персональные данные и полный ответ сессии в отчёт не попадают.

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

Граница проверки важнее скорости сканера

\n

До активного запроса зафиксируйте scope: домены, окружения, пути, тестовые учётные записи, разрешённые часы и типы нагрузки. Отдельно укажите исключения: платёжные операции, реальные персональные данные, сторонние callback-адреса и любые действия, меняющие состояние. У команды должны быть контакт владельца и способ остановить проверку. Разрешение на тестирование staging не означает разрешение сканировать production или соседний домен.

\n

OWASP WSTG разделяет пассивное изучение и активные тесты, а среди активных категорий отдельно называет авторизацию, сессии, валидацию ввода, бизнес-логику и API. Это полезная карта покрытия, но не обещание, что один прогон обнаружит все дефекты. NIST SP 800-115 также описывает планирование, проведение и анализ тестов и прямо ограничивает документ обзором методов, а не полной программой безопасности.

\n

Для каждой находки добавьте владельца действия. Владелец не обязательно тот, кто нашёл проблему: endpoint может принадлежать команде API, политика cookies — платформе, а решение о временном ограничении доступа — владельцу продукта. Если owner не установлен, строка должна оставаться в очереди уточнения, а не маскироваться высоким баллом.

\n

Строим приоритет из контекста, а не из одного числа

\n

Для первого прохода достаточно пяти полей: ценность актива, достижимость входа, требуемые права, подтверждённость воздействия и обратимость временной меры. Я использую шкалы от 0 до 3 не как стандарт и не как точный расчёт денежного ущерба, а как прозрачное правило очереди. Итоговый балл помогает упорядочить ручную работу; он не заменяет обсуждение владельца и проверку доказательства.

\n
Фактор0–123
Ценность активаТестовые или публичные данныеВнутренние данные или обычный аккаунтПлатёжные, персональные или административные данные
ДостижимостьТолько локально или за несколькими барьерамиДоступно авторизованному пользователюДоступно из публичной точки входа
ПраваНужна привилегированная рольНужна обычная учётная записьДостаточно гостевого запроса
Доказательство воздействияТолько версия, баннер или эвристикаАномальный ответ, но без подтверждения ущербаПовторяемое нарушение инварианта или доступ к тестовым данным
ОбратимостьБезопасный read-only тестНужна изолированная копия или согласованный rollbackИзменяет данные, требует остановки и отдельного разрешения
\n

Сумма не должна скрывать стоп-факторы. Подтверждённый гостевой доступ к персональным данным помещают в срочную очередь даже при низкой уверенности в масштабе. И наоборот, рекомендация обновить библиотеку без версии, затронутого пути и подтверждённой экспозиции сначала требует инвентаризации. Для внешнего сигнала можно добавить наличие CVE в каталоге CISA Known Exploited Vulnerabilities: CISA предлагает использовать этот каталог как вход в собственную модель управления уязвимостями, а не как универсальную замену контексту актива.

\n

Учебный разбор трёх находок

\n

Представим сервис документов на тестовом окружении. Сканер нашёл три проблемы.

\n
  1. Доступ к чужому документу. Пользователь с ролью viewer меняет числовой documentId и получает тело документа другого тестового пользователя. Актив — содержимое документа, вход — публичный API после входа, доказательство — два независимых тестовых аккаунта и повторяемый HTTP 200. Приоритет высокий: сначала ограничить endpoint или отключить спорную операцию, затем исправить проверку владельца и оставить regression-тест.
  2. Отсутствует Content-Security-Policy. Заголовок не найден на HTML-странице. Это полезная защитная мера, но отсутствие CSP не доказывает XSS. Сначала нужно определить, есть ли исполняемые inline-скрипты, доверенные источники и реальный сценарий внедрения. Без такого контекста находка идёт в усиление контроля, а не обгоняет подтверждённую ошибку авторизации.
  3. Устаревшая зависимость. Файл блокировки содержит версию с публичным advisory. Приоритет зависит от того, загружается ли уязвимый код в серверный или клиентский путь, доступен ли затронутый endpoint и есть ли эксплуатация именно этой версии. Исправление начинают с проверки дерева зависимостей и совместимого обновления, а не с безусловной замены пакета в production.
\n

Эти примеры показывают разницу между категорией и решением. OWASP Top 10:2021 удобен для общего языка, но его категория не сообщает владельцу, какой запрос выполнить и какой результат считать исправлением. Для технических требований лучше зафиксировать версию ASVS: идентификаторы требований могут меняться между версиями, поэтому в отчёте рядом с номером нужен тег версии.

\n

Безопасная последовательность воспроизведения

\n

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

\n
export APP_URL='https://staging.example.test'\nexport TEST_TOKEN='replace-with-short-lived-viewer-token'\n\n# 1. Проверяем ожидаемый публичный ответ, не меняя состояние.\ncurl -sS -D /tmp/audit-public.headers -o /tmp/audit-public.body \\\n  --max-time 5 -w 'public status=%{http_code}\\n' \\\n  \"$APP_URL/api/documents/42\"\n\n# 2. Повторяем тот же GET от имени viewer.\ncurl -sS -D /tmp/audit-viewer.headers -o /tmp/audit-viewer.body \\\n  --max-time 5 -H \"Authorization: Bearer $TEST_TOKEN\" \\\n  -w 'viewer status=%{http_code}\\n' \\\n  \"$APP_URL/api/documents/42\"\n\n# 3. Сверяем только статус и безопасный признак тела.\ndiff -u /tmp/audit-public.headers /tmp/audit-viewer.headers || true\nsha256sum /tmp/audit-public.body /tmp/audit-viewer.body
\n

Команды дают сигнал, но не доказывают именно IDOR (доступ к объекту по изменяемому идентификатору), пока 42 не принадлежит другой тестовой учётной записи и тело не содержит её документ. Для доказательства добавьте в fixture два заранее созданных объекта с известными владельцами, проверьте ожидаемый 403 или безопасный 404 и убедитесь, что ответ не раскрывает содержимое. Если endpoint использует cookie, CSRF-токен или дополнительный заголовок, включите их только в тестовом контуре и опишите контракт отдельно.

\n

Исправление закрывает инвариант

\n

Хорошая задача на исправление формулируется не как «починить безопасность», а как инвариант. Например: «viewer может читать только документы, перечисленные в его области доступа; запрос к чужому документу возвращает 403 без тела документа; администратор сохраняет разрешённый доступ». Такой контракт связывает код, тест и наблюдение после релиза.

\n

Для ошибки авторизации проверка должна жить рядом с серверным обработчиком, а не только в скрытии кнопки на клиенте. Для заголовка проверьте все HTML-входы, CDN и кэш: один ответ origin с CSP не доказывает, что тот же заголовок дошёл до браузера. Для зависимости зафиксируйте версию lockfile, тесты совместимости и путь отката. OWASP ASVS полезен как список проверяемых технических требований, но не сообщает, какие бизнес-данные критичны именно в вашем проекте.

\n

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

\n

Ограничения применимости

\n

Эта схема рассчитана на веб-приложение и API, где команда может создать тестовые аккаунты, читать журналы запросов и согласовать безопасные GET-проверки. Она не заменяет threat modeling, анализ исходного кода, проверку облачной инфраструктуры, оценку поставщика, юридическое решение о раскрытии или полноценный penetration test. Сканер не видит бизнес-правила, а ручной тест не доказывает отсутствие дефектов в непроверенных ролях и путях.

\n

Не переносите баллы из таблицы между проектами как SLA: шкалы, критичность данных и допустимое время реакции задаёт владелец системы. Не запускайте fuzzing, нагрузку, попытки обхода MFA и тесты удаления на чужом или production-окружении без отдельного письменного scope. Если доказательство требует реальных персональных или платёжных данных, остановите воспроизведение и согласуйте обезличенный fixture.

\n

Критерий готовности очереди

\n

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

\n
  1. Зафиксировать scope, исключения, роли и контакт для остановки теста.
  2. Удалить дубликаты и отделить эвристику от воспроизводимого нарушения.
  3. Для подтверждённых находок записать инвариант, владельца и обратимую первую меру.
  4. Проверить сначала публичные входы к ценным активам и нарушения границ доступа.
  5. Повторить исходный тест после исправления и добавить его в автоматическую проверку.
  6. Пересмотреть остаточные риски и явно оставить в очереди то, что не удалось проверить.
\n

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

\n

Проверяемые источники

" } diff --git a/editorial/agent-rewrites/146.json b/editorial/agent-rewrites/146.json index 81cb676..a5fbc2e 100644 --- a/editorial/agent-rewrites/146.json +++ b/editorial/agent-rewrites/146.json @@ -1,7 +1,7 @@ { "index": 146, "slug": "editorial-2023-12-mechanism-security-audit", - "title": "Аудит веб-проекта: как отличить evidence от гипотезы и разрешения", - "excerpt": "Отчёт об аудите полезен только тогда, когда каждое утверждение связано со scope, источником, разрешённым методом и проверяемым следующим шагом. Разбираем эту границу на учебном примере.", - "contentHtml": "

После аудита в документе появляется строка: «найдена уязвимость на границе авторизации». Но рядом нет точного актива, версии среды, описания наблюдения и подтверждения, что проверка была разрешена. Через день команда уже спорит не о факте, а о формулировке. Одни требуют срочного исправления, другие не могут повторить проверку. Цена ошибки — неверный приоритет и риск изменить рабочий путь без понимания причины. Если вывод окажется ложным, команда потратит время на защиту несуществующей проблемы. Если он окажется верным, слабая запись задержит исправление.

\n

Тезис. Аудит строится из четырёх раздельных объектов: scope называет участок системы, authorization ограничивает допустимые действия, evidence связывает утверждение с наблюдением, а decision назначает владельца и следующий шаг. Ни один объект не заменяет другой. Публичный URL не описывает всю систему. Ссылка на стандарт не даёт право на тест. Скриншот не доказывает воспроизводимость. Score не превращается сам в план исправления.

\n

Сначала зафиксируйте наблюдаемую границу

\n

Начните с одного пользовательского сценария: вход, смена адреса, оплата или загрузка документа. Опишите путь от действия пользователя до изменения состояния. Для каждого перехода запишите asset ID, границу, владельца, класс данных и вопрос проверки. Формулировка «проверить API» слишком широкая. Формулировка «подтвердить, кто принимает решение о доступе между браузером и API» уже задаёт предмет.

\n

Карта активов не утверждает, что защита работает или не работает. Она показывает, где команда ожидает контракт и кто может его объяснить. Это важное отрицательное свойство карты. Если для identity provider нет владельца, запись не должна превращаться в «низкий риск». Её статус — пробел в evidence и запрос на подтверждение.

\n
Четыре слоя записи аудита
СлойВопросМинимальная записьЧего она не доказывает
ScopeКакой участок обсуждаем?asset ID, boundary, ownerЧто участок уже проверен
AuthorizationЧто разрешено делать?метод, среда, окно, stop conditionЧто проверка дала положительный результат
EvidenceЧто именно наблюдалось?источник, время, версия, ограничениеЧто вывод переносится на всю систему
DecisionКто и что делает дальше?owner, reversible step, criterionЧто исправление уже выполнено
\n
\"Матрица
Схема показывает форму связи между карточками. Это не карта реальной сети, не результат сканирования и не подтверждение безопасности продукта.
\n

Механизм: от наблюдения к проверяемому выводу

\n

Evidence начинается с узкого утверждения. Например: «в согласованной тестовой среде запрос без нужной роли получил ответ 200 на маршруте X». Такая запись ещё не объясняет причину и не говорит, что production уязвим. Она фиксирует наблюдение, условия и границу вывода. Чтобы перейти от наблюдения к finding, нужен повторяемый метод, сопоставимый результат и право выполнить именно это действие.

\n

У карточки evidence должны быть простые поля. scopeId связывает материал с активом. observation описывает факт, а не интерпретацию. source указывает лог, запрос, тестовый отчёт или подтверждение владельца. status различает гипотезу, наблюдение и внешне подтверждённый результат. limit показывает, чего материал не покрывает. nextAction задаёт обратимый шаг. Если одного поля нет, вывод нужно сузить.

\n

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

\n
const card = {\n  scopeId: 'synthetic-asset-web-api',\n  observation: 'synthetic-role-check-needs-owner-confirmation',\n  source: 'synthetic-record-only',\n  status: 'synthetic-not-externally-verified',\n  limit: 'no-real-system-no-production-claim',\n  nextAction: 'synthetic-owner-review-before-change'\n};\n\nconst accepted =\n  card.status === 'synthetic-not-externally-verified' &&\n  card.limit.includes('no-real-system');\n\n// accepted === true означает только корректную форму учебной записи.\n// status = 'confirmed' здесь должно быть отклонено.
\n

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

\n

Разрешение не следует из доступности ресурса

\n

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

\n

Файл security.txt помогает найти канал раскрытия, но не расширяет scope и не создаёт подразумеваемое разрешение на тест. Так же работают публичная документация, ссылка на программу поиска ошибок и доступность административной формы: это сведения о контакте или интерфейсе, а не согласование конкретного действия. Запишите их как источники контекста, не как поле authorization.

\n

Симптом → причина → проверка → действие

\n
Диагностическая таблица для первой проверки
СимптомПричинаПроверкаДействие
В отчёте есть finding, но нет asset IDДомен выдали за scopeПостроить путь сценария и назвать boundaryПеревести вывод в hypothesis до заполнения карты
Есть скриншот, но нет метода и версииМатериал отделили от условий наблюденияПроверить источник, время, среду и повторДобавить limit или снять статус подтверждения
Ссылка на security.txt записана как permissionКанал связи смешали с authorizationНайти отдельную запись о владельце и допустимом методеОстановить активную проверку до согласования
Высокий score требует «срочно чинить»Оценку тяжести приняли за decisionПроверить exposure, owner, обратимость и критерийНазначить triage и выбрать обратимый шаг
Интеграция не имеет владельцаThird-party boundary не вошла в картуЗапросить owner и контракт обмена даннымиЗафиксировать evidence gap, не объявлять zero risk
\n

Порядок действий

\n
  1. Выберите один ценный пользовательский сценарий и назовите его начало и конец.
  2. Составьте карту активов: ID, граница, владелец, класс данных и вопрос проверки.
  3. Разделите инвентаризацию и authorization. До активного действия запишите среду, окно, метод, запреты и stop condition.
  4. Для каждого утверждения создайте evidence card с наблюдением, источником, версией, статусом и ограничением.
  5. Проверьте отрицательные ветки: что происходит без владельца, без разрешения, без источника и при попытке расширить вывод.
  6. Переведите пробелы в hypothesis или evidence gap. Не называйте их finding только потому, что формулировка звучит уверенно.
  7. Назначьте обратимый следующий шаг и критерий закрытия. После проверки сохраните ссылку на результат и пересмотрите scope, если граница изменилась.
\n

Ограничения и путь возврата

\n

Такая модель не обнаруживает уязвимости сама. Она не заменяет ручное тестирование, автоматические проверки, threat modeling, анализ кода или договор с владельцем внешней системы. Стандарт помогает выбрать язык и метод, но не создаёт evidence. Карта не покрывает автоматически скрытые сервисы. Один успешный сценарий не доказывает безопасность остальных ролей и состояний.

\n

Если новое наблюдение опровергло карточку, не стирайте историю. Верните статус в hypothesis или evidence-gap, сохраните причину пересмотра, уберите вывод из списка подтверждённых findings и назначьте владельца следующего запроса. Это и есть rollback документа. Для реального проекта отдельно определите хранение, доступ и удаление материалов; учебный пример этого не решает.

\n

Критерий готовности проверяем. Для выбранного сценария существует versioned scope record. Каждая граница имеет владельца. Каждое активное действие связано с отдельным authorization record. Каждое утверждение связано с источником, наблюдаемым фактом, версией и limit. Отрицательные ветки останавливают неподтверждённый вывод. Следующий шаг имеет owner, reversible action и criterion. Если хотя бы одного поля нет, аудит не завершён: результатом остаётся конкретный evidence gap, а не общий статус «проверено».

\n

Проверяемые источники

\n" + "title": "Аудит веб-проекта: как превратить наблюдение в проверяемый вывод", + "excerpt": "Практический аудит начинается не со сканера, а с границ системы, разрешённых действий и цепочки доказательств. Разбираем, как отделить факт от гипотезы и выбрать безопасный следующий шаг.", + "contentHtml": "

После аудита в документе появляется строка: «найдена уязвимость на границе авторизации». Но рядом нет точного актива, версии среды, описания наблюдения и подтверждения, что проверка была разрешена. Через день команда уже спорит не о факте, а о формулировке. Одни требуют срочного исправления, другие не могут повторить проверку. Цена ошибки — неверный приоритет и риск изменить рабочий путь без понимания причины. Если вывод окажется ложным, команда потратит время на защиту несуществующей проблемы. Если он окажется верным, слабая запись задержит исправление.

\n

Тезис. Аудит строится из четырёх раздельных объектов: scope называет участок системы, authorization ограничивает допустимые действия, evidence связывает утверждение с наблюдением, а decision назначает владельца и следующий шаг. Ни один объект не заменяет другой. Публичный URL не описывает всю систему. Ссылка на стандарт не даёт право на тест. Скриншот не доказывает воспроизводимость. Score не превращается сам в план исправления.

\n

Сначала зафиксируйте наблюдаемую границу

\n

Начните с одного пользовательского сценария: вход, смена адреса, оплата или загрузка документа. Опишите путь от действия пользователя до изменения состояния. Для каждого перехода запишите asset ID, границу, владельца, класс данных и вопрос проверки. Формулировка «проверить API» слишком широкая. Формулировка «подтвердить, кто принимает решение о доступе между браузером и API» уже задаёт предмет.

\n

Карта активов не утверждает, что защита работает или не работает. Она показывает, где команда ожидает контракт и кто может его объяснить. Это важное отрицательное свойство карты. Если для identity provider нет владельца, запись не должна превращаться в «низкий риск». Её статус — пробел в evidence и запрос на подтверждение.

\n
Четыре слоя записи аудита
СлойВопросМинимальная записьЧего она не доказывает
ScopeКакой участок обсуждаем?asset ID, boundary, ownerЧто участок уже проверен
AuthorizationЧто разрешено делать?метод, среда, окно, stop conditionЧто проверка дала положительный результат
EvidenceЧто именно наблюдалось?источник, время, версия, ограничениеЧто вывод переносится на всю систему
DecisionКто и что делает дальше?owner, reversible step, criterionЧто исправление уже выполнено
\n
\"Матрица
Схема показывает форму связи между карточками. Это не карта реальной сети, не результат сканирования и не подтверждение безопасности продукта.
\n

Как наблюдение становится проверяемым выводом

\n

Evidence начинается с узкого утверждения. Например: «в согласованной тестовой среде запрос без нужной роли получил ответ 200 на маршруте X». Такая запись ещё не объясняет причину и не говорит, что production уязвим. Она фиксирует наблюдение, условия и границу вывода. Чтобы перейти от наблюдения к finding, нужен повторяемый метод, сопоставимый результат и право выполнить именно это действие.

\n

У карточки evidence должны быть простые поля. scopeId связывает материал с активом. observation описывает факт, а не интерпретацию. source указывает лог, запрос, тестовый отчёт или подтверждение владельца. status различает гипотезу, наблюдение и внешне подтверждённый результат. limit показывает, чего материал не покрывает. nextAction задаёт обратимый шаг. Если одного поля нет, вывод нужно сузить.

\n

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

\n
const card = {\n  scopeId: 'synthetic-asset-web-api',\n  observation: 'synthetic-role-check-needs-owner-confirmation',\n  source: 'synthetic-record-only',\n  status: 'synthetic-not-externally-verified',\n  limit: 'no-real-system-no-production-claim',\n  nextAction: 'synthetic-owner-review-before-change'\n};\n\nconst accepted =\n  card.status === 'synthetic-not-externally-verified' &&\n  card.limit.includes('no-real-system');\n\n// accepted === true означает только корректную форму учебной записи.\n// status = 'confirmed' здесь должно быть отклонено.
\n

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

\n

Разрешение не следует из доступности ресурса

\n

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

\n

Файл security.txt помогает найти канал раскрытия, но не расширяет scope и не создаёт подразумеваемое разрешение на тест. Так же работают публичная документация, ссылка на программу поиска ошибок и доступность административной формы: это сведения о контакте или интерфейсе, а не согласование конкретного действия. Запишите их как источники контекста, не как поле authorization.

\n

Для безопасного read-only шага можно запросить стандартный файл раскрытия у своего или явно разрешённого домена:

TARGET='https://example.org'\ncurl --fail --silent --show-error --location \\\n  --max-time 10 \"$TARGET/.well-known/security.txt\" \\\n  | sed -n '1,40p'

Команда делает только GET и печатает первые 40 строк. Код 404 означает, что файл не найден по этому адресу или сервер вернул иной ответ; это не доказательство уязвимости и не повод расширять проверку. Наличие security.txt помогает найти контакт, но не превращается в разрешение на сканирование.

Симптом → причина → проверка → действие

\n
Диагностическая таблица для первой проверки
СимптомПричинаПроверкаДействие
В отчёте есть finding, но нет asset IDДомен выдали за scopeПостроить путь сценария и назвать boundaryПеревести вывод в hypothesis до заполнения карты
Есть скриншот, но нет метода и версииМатериал отделили от условий наблюденияПроверить источник, время, среду и повторДобавить limit или снять статус подтверждения
Ссылка на security.txt записана как permissionКанал связи смешали с authorizationНайти отдельную запись о владельце и допустимом методеОстановить активную проверку до согласования
Высокий score требует «срочно чинить»Оценку тяжести приняли за decisionПроверить exposure, owner, обратимость и критерийНазначить triage и выбрать обратимый шаг
Интеграция не имеет владельцаThird-party boundary не вошла в картуЗапросить owner и контракт обмена даннымиЗафиксировать evidence gap, не объявлять zero risk
\n

Порядок действий

\n
  1. Выберите один ценный пользовательский сценарий и назовите его начало и конец.
  2. Составьте карту активов: ID, граница, владелец, класс данных и вопрос проверки.
  3. Разделите инвентаризацию и authorization. До активного действия запишите среду, окно, метод, запреты и stop condition.
  4. Для каждого утверждения создайте evidence card с наблюдением, источником, версией, статусом и ограничением.
  5. Проверьте отрицательные ветки: что происходит без владельца, без разрешения, без источника и при попытке расширить вывод.
  6. Переведите пробелы в hypothesis или evidence gap. Не называйте их finding только потому, что формулировка звучит уверенно.
  7. Назначьте обратимый следующий шаг и критерий закрытия. После проверки сохраните ссылку на результат и пересмотрите scope, если граница изменилась.
\n

Ограничения и путь возврата

\n

Такая модель не обнаруживает уязвимости сама. Она не заменяет ручное тестирование, автоматические проверки, threat modeling, анализ кода или договор с владельцем внешней системы. Стандарт помогает выбрать язык и метод, но не создаёт evidence. Карта не покрывает автоматически скрытые сервисы. Один успешный сценарий не доказывает безопасность остальных ролей и состояний.

\n

Если новое наблюдение опровергло карточку, не стирайте историю. Верните статус в hypothesis или evidence-gap, сохраните причину пересмотра, уберите вывод из списка подтверждённых findings и назначьте владельца следующего запроса. Это и есть rollback документа. Для реального проекта отдельно определите хранение, доступ и удаление материалов; учебный пример этого не решает.

\n

Критерий готовности проверяем. Для выбранного сценария существует versioned scope record. Каждая граница имеет владельца. Каждое активное действие связано с отдельным authorization record. Каждое утверждение связано с источником, наблюдаемым фактом, версией и limit. Отрицательные ветки останавливают неподтверждённый вывод. Следующий шаг имеет owner, reversible action и criterion. Если хотя бы одного поля нет, аудит не завершён: результатом остаётся конкретный evidence gap, а не общий статус «проверено».

\n

Проверяемые источники

\n" } diff --git a/editorial/agent-rewrites/147.json b/editorial/agent-rewrites/147.json index a7cfb2c..c0a3984 100644 --- a/editorial/agent-rewrites/147.json +++ b/editorial/agent-rewrites/147.json @@ -1,7 +1,7 @@ { "index": 147, "slug": "editorial-2023-12-practice-security-audit", - "title": "Аудит веб-проекта начинается с границ: как не проверять чужую систему", - "excerpt": "Как зафиксировать активы, владельцев и разрешённые действия до проверки веб-проекта. С примером scope-карты, диагностической таблицей и критерием готовности.", - "contentHtml": "

В задаче написано: «провести аудит веб-проекта». У команды есть домен, несколько учётных записей и длинный список проверок. Через день один инженер проверяет форму входа, другой смотрит CDN, а внешний identity provider и хранилище файлов никто не включил в разговор. Команда получает аккуратный отчёт, но не знает, какую часть системы он покрывает.

\n

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

\n

Тезис. Практический аудит веб-проекта — это управляемая цепочка: пользовательский путь → активы → границы → разрешение → проверяемое утверждение. Если один элемент неизвестен, результатом становится не «низкий риск», а зафиксированный пробел. Такой порядок сокращает область случайного воздействия и делает следующий шаг воспроизводимым.

\n

Карта активов не равна списку URL

\n

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

\n

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

\n
Минимальная карточка актива
ПолеУчебный примерЧто уточняетЧего не доказывает
Идентификаторweb-api-profileСвязывает карту и результат проверкиЧто такой актив существует в production
ГраницаБраузер → APIПоказывает переход ответственностиЧто переход защищён
ВладелецКоманда профиляДаёт адрес для уточнения контрактаЧто владелец разрешил любой тест
ДанныеИдентификатор пользователяПомогает оценить последствия ошибкиЧто поле действительно хранится именно здесь
ВопросРоль проверяется до чтенияЗадаёт проверяемое утверждениеЧто нарушение уже найдено
\n

Карта должна описывать переходы, а не только узлы. Для загрузки файла отдельно отметьте браузер, API, хранилище и выдачу. Один и тот же файл проходит разные решения: кто принимает байты, кто назначает серверное имя, кто определяет право чтения и кто выдаёт ответ. Если на карте есть только endpoint загрузки, доступ к уже сохранённому файлу выпадает из scope.

\n
\"Карта
Иллюстрация показывает учебную карту активов и границ. Она не содержит адресов реальной сети и не является разрешением на тестирование.
\n

Граница аудита состоит из двух решений

\n

Первое решение отвечает на вопрос «какой объект обсуждаем». Это scope: домен, приложение, API, хранилище, среда и пользовательский путь. Второе отвечает на вопрос «что разрешено делать». Это authorization: допустимый метод, время, учётная запись, нагрузка, запретные действия, контакт для остановки и владелец результата.

\n

Публичность объекта не создаёт разрешение. Доступная из браузера форма не разрешает перебор параметров. Ссылка на документацию не разрешает отправлять нагрузку. Файл security.txt задаёт канал для сообщений о проблемах, но не превращает любой запрос в согласованный тест. Если владелец и допустимое действие не названы, остановитесь на инвентаризации и чтении документации.

\n

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

\n

Механизм: от актива к проверяемому утверждению

\n

Хороший вопрос аудита связывает действие, субъект и ресурс. Например: «пользователь с ролью reader не может получить профиль другого пользователя по изменённому идентификатору». Вопрос содержит субъект, объект и ожидаемый отказ. Его можно проверить в тестовой среде с двумя учебными аккаунтами. Он не требует сразу проверять все endpoints и роли.

\n

Код ниже только показывает форму записи. Имена, значения и адреса вымышлены. Пример не обращается к сети и не сообщает о состоянии какого-либо проекта. Функция блокирует проверку, если владелец не подтвердил границу или метод.

\n
const auditBoundary = {\n  asset: 'web-api-profile',\n  environment: 'staging',\n  owner: 'profile-team',\n  dataClass: 'user-profile',\n  method: 'two-account-read-check',\n  permission: 'approved-by-owner',\n  stopContact: 'on-call-profile',\n};\n\nfunction assertReady(boundary) {\n  const required = ['asset', 'environment', 'owner', 'method',\n    'permission', 'stopContact'];\n\n  for (const field of required) {\n    if (!boundary[field]) {\n      throw new Error(`audit boundary is incomplete: ${field}`);\n    }\n  }\n\n  if (boundary.permission !== 'approved-by-owner') {\n    throw new Error('active testing is not authorized');\n  }\n}\n\nassertReady(auditBoundary);
\n

В реальном проекте проверка должна также учитывать срок действия согласования, область аккаунтов, допустимую частоту запросов и способ удаления тестовых данных. Строка approved-by-owner не заменяет документ. Она показывает, что без явного разрешения код не должен переходить к активному действию.

\n

Симптом → причина → проверка → действие

\n
Диагностика незрелого scope
СимптомПричинаПроверкаДействие
В задаче указан только основной доменТочку входа приняли за системуПройти один пользовательский путь до хранилища и внешних сервисовДобавить активы и владельцев каждого перехода
Сканер нашёл десятки предупрежденийНет приоритета и вопроса проверкиДля каждого сигнала назвать актив, данные и воспроизводимый запросОтделить подтверждённый симптом от непроверенной гипотезы
Проверяющий не знает, где остановитьсяНе записаны лимит, окно и контактПопросить владельца подтвердить метод и стоп-условиеНе начинать активную проверку до согласования
Форма входа проверена, а файл выдан без обсужденияКарту строили по экрану, а не по даннымНайти путь файла от приёма до ответаДобавить границу выдачи и отдельный вопрос о праве чтения
Отчёт говорит «уязвимость найдена»Наблюдение смешали с выводом о причинеПовторить запрос, сохранить вход, ответ и версию средыНазвать факт, гипотезу и следующий безопасный тест отдельно
\n

Отрицательный путь важнее красивого отчёта

\n

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

\n

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

\n

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

\n

Порядок действий

\n
  1. Выберите один пользовательский путь с понятной ценой ошибки: вход, изменение профиля, платёж или файл.
  2. Нарисуйте путь от браузера до конечного эффекта. Добавьте API, identity provider, очередь, хранилище и внешний сервис, если они участвуют.
  3. Для каждого узла укажите идентификатор, границу ответственности, владельца, класс данных и вопрос проверки.
  4. Отдельно запишите разрешённую среду, аккаунты, метод, период, лимит запросов, запретные действия и контакт остановки.
  5. Сформулируйте один положительный и один отрицательный пример. Положительный показывает штатный доступ, отрицательный — ожидаемый отказ.
  6. Проверьте, что метод отвечает вопросу и не расширяет scope. Если нужно изменить ресурс, отправить нагрузку или выйти на внешний сервис, получите отдельное согласование.
  7. Сохраните вход, ответ, время, версию среды и идентификатор владельца. Не прикладывайте секреты и персональные данные, если они не нужны для доказательства.
  8. Разделите результат на факт, гипотезу и действие. Назначьте владельца исправления только после воспроизведения наблюдаемого симптома.
  9. Повторите карту перед следующим классом проверки. Новый актив или новая граница требуют нового вопроса и проверки разрешения.
\n

Ограничения метода

\n

Карта активов не заменяет threat model, код-ревью, тесты или внешний penetration test. Она решает более узкую задачу: не потерять границы и не начать действие без понятного контракта. Полная карта невозможна, если архитектура меняется быстрее, чем её документация. В этом случае помечайте неизвестное поле и назначайте владельца, а не заполняйте его догадкой.

\n

Проверка в staging не доказывает поведение production. Два учебных аккаунта не покрывают все роли. Ответ 403 не доказывает отсутствие утечки через кеш, экспорт или другой endpoint. Автоматический сканер полезен для поиска кандидатов, но его предупреждение требует проверки входа, ответа, контекста и влияния.

\n

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

\n

Проверяемый критерий готовности

\n

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

\n

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

\n

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

\n

Проверяемые источники

\n" + "title": "Аудит веб-проекта: scope, разрешение и воспроизводимая проверка", + "excerpt": "Как очертить путь данных, разделить scope и разрешение и подготовить одну безопасную проверку до активного тестирования. С картой границ, локальным fail-closed примером и диагностической таблицей.", + "contentHtml": "

Проблема аудита веб-проекта обычно появляется ещё до первого запроса. В задаче есть домен и слово «проверить», но не указано, входит ли в работу identity provider, CDN, файловое хранилище и тестовая база. Один инженер начинает с формы входа, другой — с заголовков прокси. В конце получается длинный отчёт без ответа на главный вопрос: какую систему и с каким разрешением действительно проверили.

\n

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

\n

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

\n

Сначала карта пути, потом список URL

\n

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

\n

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

\n
Минимальная карточка участка аудита
ПолеУчебное значениеЧто проверяемГраница вывода
Активprofile-apiAPI участвует в выбранном путиНе доказывает, что API доступно из production
ПереходБраузер → APIГде запрос меняет владельца решенияНе доказывает наличие защиты на переходе
ВладелецКоманда профиляКому подтвердить контракт и остановкуНе означает разрешение на любой метод
ДанныеИдентификатор пользователяКакие последствия у ошибочного чтенияНе доказывает место хранения поля
Утверждениеreader не читает чужой профильСубъект, ресурс и ожидаемый отказНе является найденной уязвимостью
\n
\"Учебная
Карта показывает не сеть, а способ разговора об ответственности. В ней нет адресов, секретов и разрешения на активное тестирование.
\n

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

\n

Scope и разрешение — разные записи

\n

Scope отвечает на вопрос «что обсуждаем»: домен, приложение, среда, endpoint, хранилище, учётные записи и путь пользователя. Authorization отвечает на вопрос «что можно делать»: метод, период, лимит запросов, тестовые данные, запретные действия, контакт для остановки и допустимый результат.

\n

Публичность не равна разрешению. Доступная форма не разрешает менять чужой идентификатор. Документация API не разрешает отправлять нагрузку. Запись security.txt задаёт канал для сообщений о проблемах; RFC 9116 отдельно предупреждает, что наличие или отсутствие файла не следует трактовать как разрешение или запрет тестирования.

\n

Эти записи полезно разделять даже внутри одной задачи. Scope может быть согласован для production, а активные запросы — только для staging. Владелец может разрешить чтение с двумя учебными аккаунтами, но не изменение данных. Наблюдение может быть воспроизводимым, а причина — ещё гипотезой. Одна строка «аудит разрешён» стирает эти различия.

\n
Что должно быть подтверждено до действия
СлойМинимальный вопросЕсли ответа нет
ОбъектКакой домен, среда и путь входят в работу?Остановиться на инвентаризации
ВладелецКто отвечает за актив и принимает результат?Назначить контакт до теста
ДействиеКакой метод разрешён: чтение, запись или только анализ?Не расширять метод по ходу
ОграничениеКакие аккаунты, данные, лимит и стоп-условие действуют?Сформулировать явное «не проверяем»
\n

Превращаем тему в проверяемое утверждение

\n

Фраза «проверить авторизацию» не задаёт воспроизводимость. Утверждение «учебный пользователь с ролью reader получает отказ при чтении профиля второго учебного пользователя по изменённому идентификатору» уже содержит субъект, ресурс, действие и ожидаемый результат. Его можно проверить одним сценарием и не выдавать этот сценарий за покрытие всех ролей.

\n

Ниже — самодостаточный пример на Node.js. Он не открывает URL, не сканирует сеть и не имитирует разрешение. Запустите его на Node.js 18 или новее: сохраните блок как audit-boundary.mjs и выполните командой node audit-boundary.mjs. В рабочей системе значения владельца, среды и согласования должны прийти из вашего процесса, а не из этого примера.

\n
const boundary = {\n  asset: 'profile-api',\n  environment: 'staging',\n  owner: 'profile-team',\n  subject: 'reader-test-account',\n  resource: 'second-test-profile',\n  method: 'read-only-two-account-check',\n  permission: 'approved-by-owner',\n  rateLimit: '1 request',\n  stopContact: 'profile-on-call',\n};\n\nconst required = [\n  'asset', 'environment', 'owner', 'subject', 'resource',\n  'method', 'permission', 'rateLimit', 'stopContact',\n];\n\nfor (const field of required) {\n  if (!boundary[field]) {\n    throw new Error('incomplete audit boundary: ' + field);\n  }\n}\n\nif (boundary.permission !== 'approved-by-owner') {\n  throw new Error('active testing is not authorized');\n}\n\nif (boundary.method !== 'read-only-two-account-check') {\n  throw new Error('method is outside the example scope');\n}\n\nconsole.log('PASS: boundary is ready for one read-only check');
\n

Ожидаемый вывод — одна строка PASS. Если удалить permission или заменить метод на запись, скрипт завершится ошибкой до обращения к системе. Это полезный fail-closed барьер, но не контроль доступа: он не видит письмо владельца, не проверяет права в API и не доказывает, что сервер вернёт 403.

\n

Разделяем наблюдение, гипотезу и вывод

\n

У каждого результата должна быть короткая цепочка: симптом → причина → проверка → действие. Симптом — то, что можно повторить. Причина — объяснение, которое ещё нужно подтвердить. Проверка — минимальный эксперимент в разрешённой среде. Действие — изменение или следующий безопасный шаг, а не автоматическая реакция на слово «критический».

\n
Диагностика незрелого аудита
СимптомРабочая гипотезаПроверкаДействие
В задаче указан только доменТочку входа приняли за системуПройти путь до identity provider, API и храненияДобавить узлы, переходы и владельцев
Сканер дал много предупрежденийНет вопроса и приоритетаДля сигнала записать вход, ответ, актив и версию средыОтделить кандидат от подтверждённого факта
Проверяющий не знает, где остановитьсяНе заданы лимит и стоп-контактСверить метод с разрешением владельцаНе начинать активное действие
После входа доступен файлПроверили экран, но не выдачуПроверить право чтения отдельным учебным аккаунтомДобавить выдачу в карту и вопрос
Один запрос вернул 403Отказ принимают за полное покрытиеПовторить с указанным субъектом и ресурсомОписать границу результата, не обещать «всё закрыто»
\n

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

\n

Отрицательный путь — часть результата

\n

Хорошая карта описывает, когда инженер обязан остановиться. Актив найден, но владелец неизвестен — записываем пробел и не отправляем запрос. Разрешение относится к staging — production исключаем. Разрешён один запрос чтения — не добавляем перебор идентификаторов и не проверяем запись. Это не отказ от аудита, а контроль радиуса действия.

\n

Технический отказ тоже нужно называть точно. Ответ 403 на один запрос подтверждает отказ для конкретного субъекта, ресурса, метода и контекста. Он не доказывает, что тот же объект не выдаётся через экспорт, кеш, другой endpoint или другой слой прокси. Если эти ветки не проверялись, пишите «не проверено», а не «отсутствует».

\n

OWASP WSTG v4.2 предлагает сначала собрать сведения и карту архитектуры, а затем связывать её с тестами. Это помогает не потерять reverse proxy, сервер приложения и внешнюю службу идентификации. Но методика не заменяет согласование доступа: она описывает подход к тестированию, а не выдаёт право тестировать конкретный домен.

\n

Порядок работы, который можно повторить

\n
  1. Выберите один пользовательский путь и запишите его конечный эффект.
  2. Нарисуйте переходы: браузер, прокси, API, identity provider, очередь, хранилище и выдача — только если они участвуют в пути.
  3. Для каждого участка укажите актив, владельца, класс данных, вход, выход и проверяемое утверждение.
  4. Отдельно зафиксируйте среду, учебные аккаунты, метод, лимит, окно времени, запретные действия и контакт остановки.
  5. Сформулируйте положительный и отрицательный сценарии. Первый показывает разрешённый доступ, второй — ожидаемый отказ.
  6. Запустите сначала проверку контракта локально, как в примере. Только после этого выполняйте согласованный запрос.
  7. Сохраните минимальный evidence: условия, запрос, ответ, время, версию среды и границу результата.
  8. Разделите запись на факт, гипотезу, ограничение и следующее действие. Владельца исправления назначайте после подтверждения симптома.
  9. Перед следующим активом или новым методом повторите проверку scope и разрешения.
\n

Ограничения применимости

\n

Карта активов решает узкую задачу: не потерять границы и не начать активное действие без понятного контракта. Она не заменяет threat model, анализ исходного кода, тестирование инфраструктуры, privacy review, внешний penetration test или план реагирования.

\n

Staging не доказывает поведение production. Два учебных аккаунта не покрывают все роли и tenant-связи. Локальный fail-closed скрипт не контролирует сервер. Сканер помогает находить кандидатов, но предупреждение не является finding до проверки входа, ответа, воспроизводимости и влияния.

\n

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

\n

Критерий готовности к первой проверке

\n

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

\n

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

\n

Проверяемые источники

\n" } diff --git a/editorial/agent-rewrites/148.json b/editorial/agent-rewrites/148.json index 13cb5d4..08cb93c 100644 --- a/editorial/agent-rewrites/148.json +++ b/editorial/agent-rewrites/148.json @@ -1 +1,7 @@ -{ "index": 148, "slug": "editorial-2023-11-field-postmortem", "title": "Полевой разбор сбоя: как отделить факт от догадки и довести проверку до действия", "excerpt": "Практический маршрут для разбора сбоя: восстановить доступные факты, проверить решение в его контексте и выбрать одну обратимую защиту с явным критерием готовности.", "contentHtml": "

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

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

Сначала восстановите наблюдаемое

Начните с симптома, а не с объяснения. Запишите, что увидел пользователь или оператор: запросы стали получать ошибку, очередь перестала уменьшаться, запись появилась дважды, откат не изменил состояние. Добавьте время, область воздействия и способ обнаружения. Если значение неизвестно, напишите «не установлено». Такая строка полезнее числа, которое никто не может подтвердить.

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

Отделяйте время события от времени знания. Сбой мог начаться в 10:02, а команда увидела его в 10:11. Решение в 10:12 нужно оценивать по сигналам, доступным в 10:12. Поздняя трасса или найденный после инцидента фрагмент конфигурации объясняют контекст расследования, но не меняют исходный набор данных.

Механизм: три разных записи

Разбор становится проверяемым, если в нём не смешиваются факты, решения и будущие проверки. Факт описывает наблюдаемое событие и ссылается на источник. Решение описывает действие и перечисляет факты, которые были доступны перед ним. Эксперимент проверяет гипотезу после события и имеет ограниченный масштаб, критерий остановки и возврат.

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

Такой порядок не отменяет технический анализ. Он не запрещает говорить о root cause. Он требует пометить причинную связь как гипотезу, пока её не поддерживают данные. Иногда один инцидент имеет несколько contributing causes: дефект, слабый сигнал и неясный runbook. Сведение всего к одной причине убирает условия, при которых защита не сработала.

Учебный пример: решение при неполном сигнале

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

type Fact = {\n  id: string\n  observedAt: string\n  source: string\n  statement: string\n}\n\ntype Decision = {\n  action: 'pause-change' | 'rollback' | 'continue-observing'\n  basedOn: string[]\n  decidedAt: string\n}\n\ntype Experiment = {\n  hypothesis: string\n  scope: string\n  criterion: string\n  rollback: string\n}

В этом фрагменте структура важнее названий полей. basedOn может ссылаться только на факты, известные до decidedAt. Эксперимент не должен обещать «исключить все повторы». Его критерий должен отвечать на вопрос: что именно проверяем, когда остановимся и какое действие выполним при отрицательном результате.

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

\"Цикл
Учебный цикл связывает факт, решение, неизвестность и ограниченную проверку. Иллюстрация не показывает реальный incident workflow, трафик или подтверждённый production-эффект.

Симптом → причина → проверка → действие

Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
В разборе есть имя, но нет защитыПерсональная оценка заменила описание условия отказаПопросить назвать сигнал, границу управления и отказавшую защитуПереписать вывод как проверяемое системное условие
Хронология противоречит логамПозднее знание смешали с исходным контекстомСверить время события, время знания и источник каждой строкиРазделить timeline и историю расследования
Все версии выглядят правдоподобноГипотезы записали как фактыУ каждого утверждения найти evidence reference или пометить unknownОставить competing hypotheses и назначить различающую проверку
После разбора появился длинный backlogКоманда обещает исправить всё сразуДля каждой задачи проверить scope, criterion и rollbackВыбрать один риск и один обратимый эксперимент
Тест зелёный, а повтор не обнаруживаетсяПроверяли форму, но не сигнал и отрицательный путьСымитировать отказ, тайм-аут, повтор и отсутствие данныхДобавить наблюдаемый сигнал или признать, что тест ограничен формой

Проверьте решение в его моменте

Выберите одно действие из хронологии. Запишите, что было известно до него, какие варианты были доступны и чем они отличались по риску. Не спрашивайте сначала «почему инженер так сделал». Спросите: какой сигнал он видел, какие права имел, какой срок был у решения, какой путь возврата существовал. Ответ может выявить плохой выбор. Но он также может показать, что нужный dashboard отсутствовал, runbook был неоднозначен, а безопасный откат требовал доступа, которого у смены не было.

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

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

Сделайте одну профилактическую проверку

После хронологии легко составить десять улучшений: новый alert, обязательный review, запрет ручных изменений, переработка сервиса. Длинный список создаёт иллюзию движения. Выберите один риск, который можно проверить за короткий цикл. Укажите гипотезу, scope, owner роли, criterion, дату review и rollback. Если результат нельзя увидеть без новых предположений, эксперимент слишком широк.

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

Техническая профилактика должна менять механизм. Если проблема связана с отсутствующим сигналом, добавьте измерение и проверьте его на отрицательном сценарии. OpenTelemetry разделяет telemetry signals на traces, metrics и logs; это удобная рамка для выбора наблюдаемого свидетельства, но сам факт наличия сигнала не доказывает, что он покрывает нужный вопрос. Сначала сформулируйте вопрос, затем выберите сигнал.

Порядок действий

  1. Опишите симптом, время, область воздействия и цену повторения. Не добавляйте причину в эту строку.
  2. Соберите факты с timestamp и ссылкой на источник. Разделите время события и время, когда команда узнала о нём.
  3. Выберите одно решение. Привяжите его только к фактам, доступным до решения, и перечислите реальные альтернативы.
  4. Вынесите поздние объяснения в hypotheses. Для каждой укажите подтверждающий или опровергающий источник.
  5. Отметьте unknown явно. Назначьте вопрос, владельца роли и безопасный способ проверки. Не заполняйте пробел именем человека.
  6. Проверьте отрицательный путь: отказ, тайм-аут, повтор, неполный ответ и неуспешный rollback.
  7. Выберите один обратимый эксперимент. Назовите scope, criterion, owner, дату review и точку остановки.
  8. Проведите проверку независимым читателем или автоматическим тестом формы. Запишите, что именно проверка не покрывает.
  9. Передайте результат в рабочий процесс с владельцем и сроком. Закройте задачу только по критерию, а не по факту обсуждения.

Ограничения

Разбор не восстанавливает удалённые логи и не превращает неполный сигнал в доказательство. При малом retention часть причин останется неизвестной. При sampling трасса может не содержать нужный запрос. При ручной хронологии порядок сообщений может быть неточным. Эти ограничения нужно показывать рядом с выводом.

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

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

Проверяемый критерий готовности

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

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

Проверяемые источники

"} +{ + "index": 148, + "slug": "editorial-2023-11-field-postmortem", + "title": "Postmortem без поиска виноватого: от неполного сигнала к проверяемому действию", + "excerpt": "Как отделить факт от поздней гипотезы, восстановить контекст решения и выбрать одну защиту с измеримым критерием. Внутри — безопасный пример и границы применимости.", + "contentHtml": "

После сбоя отчёт часто начинается с фамилии и заканчивается формулой «усилить контроль». Такой текст выглядит решительным, но не отвечает на два рабочих вопроса: что команда действительно знала в момент решения и какое изменение обнаружит повтор. Цена неточного postmortem — следующая смена повторяет риск, а спор о виноватом вытесняет проверку механизма.

\n

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

\n

Начните с наблюдаемого симптома

\n

Первая строка должна описывать эффект, а не причину. «В 10:02 доля ответов 5xx выросла с 0,4% до 8,1% на маршруте /checkout» проверяема. «Инженер не уследил за релизом» — это оценка и к тому же не говорит, какой сигнал отсутствовал. Для симптома зафиксируйте период, затронутый маршрут или ресурс, способ обнаружения и влияние на пользователя.

\n

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

\n

Разделяйте время события и время знания. Сбой мог начаться в 10:02, alert прийти в 10:06, а полная трасса появиться в 10:20. Решение в 10:07 оценивают по данным, доступным в 10:07. Поздно найденная причина полезна для расследования, но не должна незаметно подменять контекст дежурного.

\n

Разведите факт, решение и гипотезу

\n

У этих записей разные роли. Факт отвечает на вопрос «что наблюдалось и откуда это известно». Решение фиксирует действие, момент и набор доступных данных. Гипотеза объясняет возможную связь и должна иметь проверку, которая способна её ослабить или опровергнуть. Если всё записать в одном абзаце, читатель перестаёт видеть, где заканчивается evidence и начинается интерпретация.

\n
Минимальная карта разбора: от симптома к следующему действию
ЗаписьПример формулировкиКак проверитьЧего она не доказывает
ФактВ 10:06 alert показал рост 5xx на одном маршрутеОткрыть метрику с тем же окном и фильтромПричину роста
РешениеВ 10:07 остановили дальнейший rolloutСверить журнал изменения и доступные сигналыЧто остановка была единственным верным выбором
ГипотезаНовая конфигурация меняет тайм-аут upstreamСравнить конфигурации и повторить запрос в изолированном контуреЧто гипотеза объясняет весь impact
ДействиеДобавить проверку перед применением опасного спискаПрогнать пустой, частичный и повторный входЧто защита устраняет все классы отказов
\n

Слово «unknown» здесь обозначает состояние данных, а не провал автора. Запишите неизвестный вопрос, допустимый источник и безопасный способ проверки. Если лог удалён, так и напишите: «причина не установлена из-за retention 24 часа». Это честнее, чем выбирать наиболее правдоподобную версию и выдавать её за факт.

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

Оцените решение в его моменте

\n

Выберите одно решение из хронологии и восстановите его контекст. Запишите время, сигнал, доступные права, ограничения по времени и варианты возврата. Такой разбор может выявить ошибку, но нередко обнаруживает системный пробел: dashboard не показывал разбивку по версии, безопасный rollback требовал другой роли, а runbook не описывал тайм-аут.

\n

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

\n

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

\n

Безопасный пример: не обрабатывать пустой набор

\n

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

\n
node <<'NODE'\nfunction buildPlan(targetIds) {\n  if (!Array.isArray(targetIds) || targetIds.length === 0) {\n    throw new Error('refuse empty target set');\n  }\n\n  return targetIds.map((id) => ({ id, action: 'delete' }));\n}\n\nfor (const [name, ids] of [['normal', ['a', 'b']], ['empty', []]]) {\n  try {\n    console.log(name, buildPlan(ids));\n  } catch (error) {\n    console.log(name, error.message);\n  }\n}\nNODE
\n

Запустите этот блок в Node.js без дополнительных пакетов. Ожидаемый вывод — план для normal и empty refuse empty target set для пустого входа. Проверяется только граница функции: пустой набор не превращается в широкую операцию. В настоящем сервисе дополнительно нужны авторизация, лимит размера, проверка существования объектов, журналирование решения и транзакционные правила.

\n

В postmortem такой пример связывают с фактами аккуратно. Факт: вход после повторного чтения оказался пустым. Гипотеза: библиотека или API различает «фильтр не передан» и «фильтр пуст». Проверка: контрактный тест на оба значения плюс журнал фактического набора перед побочным эффектом. Действие: отклонять пустой набор и показывать причину оператору. Локальный тест не доказывает отсутствие проблемы в базе или безопасность всего endpoint.

\n

Превратите вывод в измеримое действие

\n

Фраза «улучшить мониторинг» не имеет конца. Действие должно называть изменение, владельца роли, приоритет, критерий готовности и способ возврата. «Добавить метрику» тоже недостаточно: укажите имя измерения, допустимую задержку и ветку, в которой alert должен сработать. Один узкий action item лучше десятка обещаний без проверки.

\n
Как сделать последующее действие проверяемым
Слабая записьУточнённая записьКритерий
Усилить валидациюОтклонять пустой список до вызова операцииТест на пустой вход завершается отказом и не вызывает побочный эффект
Улучшить alertПоказывать долю 5xx по маршруту и версии за пятиминутное окноТестовый отказ виден в метрике не позднее заданного порога
Обновить runbookДобавить решение для timeout, владельца и команды возвратаНовый дежурный выбирает действие без устного пояснения
\n

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

\n

Выберите сигнал, который отвечает на вопрос

\n

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

\n

OpenTelemetry перечисляет traces, metrics и logs как разные сигналы. Это полезная классификация, но наличие SDK не подтверждает полноту данных. Sampling может убрать нужную трассу, высокая кардинальность может сделать разрез по идентификатору дорогим, а лог без correlation id не свяжет событие с решением. В postmortem укажите эти ограничения рядом с выводом.

\n

Порядок действий после инцидента

\n
  1. Опишите симптом, окно времени, область воздействия и способ обнаружения.
  2. Соберите факты с timestamp и ссылкой на источник; отметьте задержки, sampling и пробелы retention.
  3. Отделите решение от результата: запишите, что было известно до действия и какие альтернативы существовали.
  4. Сформулируйте одну или несколько конкурирующих гипотез, не называя их доказанной причиной без проверки.
  5. Проверьте отрицательный путь: пустой или повторный вход, timeout, частичный ответ, потерянный сигнал и неудачный rollback.
  6. Выберите одно обратимое действие с владельцем роли, приоритетом, критерием и точкой возврата.
  7. Повторите проверку на том же классе отказа и запишите, что тест не покрывает.
  8. Передайте действие в рабочий трекер и закрывайте его только после наблюдаемого критерия.
\n

Ограничения применимости

\n

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

\n

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

\n

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

\n

Проверяемые источники

\n" +}