Инженерия

Проектирование SOAR-плейбуков: реактивный поток данных, ветвление и аргументы в пользу dry-run

Большинство SOAR-плейбуков — хрупкие скрипты в красивой обёртке. Те, что выживают в продуктиве, имеют четыре общие черты: визуальный редактор с реактивным потоком данных, типизированный контракт узла, настоящее условное ветвление и режим dry-run, который прогоняет логику, не запуская побочные эффекты. Разбираем, как работает каждая из них и что меняется, когда они есть.

Автор Krasper Engineering 10 Июн 2026 8 мин чтения
SOAR Playbook Design — hero

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

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

Плейбук, не способный впитать эту вариативность, — по сути скрипт, а скрипт, который нельзя безопасно менять, становится риском в тот момент, когда начинает трогать продуктивные системы. Именно поэтому многие SOAR-проекты тихо теряют доверие команды после первого года.

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

Содержание

  1. Почему большинство плейбуков деградирует
  2. Реактивный поток данных: почему редактор важен
  3. Контракт узла: типизированные порты, явные режимы отказа
  4. Условное ветвление как примитив первого класса
  5. Разбор примера: реагирование на фишинг от начала до конца
  6. Механика dry-run: тестирование без радиуса поражения
  7. Что меняется, когда есть все четыре

1. Почему большинство плейбуков деградирует

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

Потом начинают приходить вариации.

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

Каждая вариация рождает исключение. Исключения накапливаются как навешанная условная логика, всё глубже вложенная и всё менее пригодная для ревью. Через полгода плейбук — это YAML на 2000 строк, который понимает один инженер и не хочет трогать. Команда начинает работать в обход.

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

2. Реактивный поток данных: почему редактор важен

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

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

┌──────────────┐       ┌──────────────────┐       ┌──────────────┐
│  Alert       │       │  Enrich asset    │       │  Severity    │
│  ingress     ├──────▶│                  ├──────▶│  classifier  │
│              │       │  in: ip          │       │              │
│  out: alert  │       │  out: asset      │       │  out: level  │
└──────────────┘       └──────────────────┘       └──────┬───────┘
                                                            │
                          ┌─────────────────────────────────┴───┐
                          │                                     │
                          ▼                                     ▼
                   ┌────────────┐                       ┌────────────┐
                   │  Contain   │                       │  Notify    │
                   │  (high)    │                       │  (medium)  │
                   └────────────┘                       └────────────┘
Реактивный граф узлов: типизированные порты, явные рёбра

Что даёт это представление и не даёт скрипт:

  • Граф и есть контракт. Ревьюер может его прочитать, а junior-инженер — изменить один узел, не разбираясь во всём целиком.
  • Побочные эффекты локализованы. Узел, трогающий продуктив, — отдельная опознаваемая фигура на полотне; узлы вокруг него — чистые преобразования.
  • Сравнение версий становится структурным. Изменение графа — это изменение конкретного набора узлов и рёбер, а не мешанина сдвинувшихся отступов в скрипте на тысячу строк, поэтому пул-реквесты по плейбукам действительно можно ревьюить.

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

3. Контракт узла: типизированные порты, явные режимы отказа

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

Первое: входы и выходы типизированы. У каждого порта объявлена схема, и рантайм отказывается соединить порт, отдающий IpAddress, с портом, ожидающим EmailMessage. Звучит очевидно, но половина отказов плейбуков, которые мы видели на аудитах, восходит к несовпадению типов в рантайме, которое редактор мог поймать на этапе проектирования.

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

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

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

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

4. Условное ветвление как примитив первого класса

Ранние SOAR-продукты относились к условиям как к второстепенной детали: узел с выражением «если» и выходом в один следующий узел. Для одной-двух веток это работает. На масштабе разваливается.

Полноценный примитив ветвления выглядит так:

  • Узел switch с N объявленными выходами, каждый охраняется типизированным предикатом над входами.
  • Рантайм вычисляет предикаты в объявленном порядке, направляет в первое совпадение и обрывает остальные.
  • Обязательный выход default ловит всё, что не подошло ни одному предикату. Без него редактор не сохранит плейбук.
  • Выходные порты switch несут суженные типы: после ветки «оповещение из облачного почтового шлюза» тип порта alert сужается до CloudMailAlert, и последующие узлы видят только те поля, на которые могут опираться.
┌────────────────────────────┐
 │  switch on alert.source    │
 │                            │
 │  case cloud_mail  ────────┼──▶ cloud-mail subgraph
 │  case onprem_mail ────────┼──▶ onprem subgraph
 │  case api_gateway ────────┼──▶ api subgraph
 │  default          ────────┼──▶ unknown-source handler
 └────────────────────────────┘
Узел switch с обязательным default и сужением типов на выходах

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

Ветка default необязательна не просто так. Необработанные случаи в продуктиве в три часа ночи не дают ничего. Либо плейбук решает, что делать, либо явно эскалирует человеку. Оба варианта приемлемы; молчаливое проваливание — нет.

5. Разбор примера: реагирование на фишинг от начала до конца

Фишинг — стандартный пример, потому что он есть у каждого SOC и поверхность вариаций у него большая. Скелет реального плейбука выглядит так:

[Alert ingress: mail-security signal]
        │
        ▼
[Parse headers + extract URLs/attachments]
        │
        ▼
[Enrich: sender reputation, domain age, URL reputation]
        │
        ▼
[switch: phishing_confidence]
   ├── high   → [Quarantine mail across all mailboxes]
   │              │
   │              ▼
   │           [Identify all recipients] ── for each ──▶
   │              ├── [User in privileged group?] ── yes ──▶ [Force password reset]
   │              │                                 └── no  ──▶ [Notify user via approved channel]
   │              ▼
   │           [Endpoint check: any URL clicked?] ── yes ──▶ [Isolate endpoint + open IR ticket]
   │
   ├── medium → [Quarantine for sender, flag for analyst review]
   │
   ├── low    → [Tag for trend analysis, no user impact]
   │
   └── default → [Hold for analyst, no automatic action]
Плейбук реагирования на фишинг: скелет

Несколько вещей стоит заметить в этой форме.

Шаг обогащения — один узел, а не три; параллельное распараллеливание платформа берёт на себя. Автор не пишет код конкурентности; он объявляет, что классификатору нужны sender_reputation, domain_age и url_reputation, а рантайм разрешает зависимости параллельно там, где может.

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

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

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

6. Механика dry-run: тестирование без радиуса поражения

Единственная функция, определяющая, будут ли инженеры реально дорабатывать плейбуки, — это dry-run.

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

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

text
┌────────────────────────────────────┐
│ Side-effect adapter                │
│                                    │
│ if mode == "dry_run":              │
│   record_intended_call(args)       │
│   return synthetic_success(args)   │
│                                    │
│ else:                              │
│   call_real_api(args)              │
└────────────────────────────────────┘
Адаптер побочного эффекта: точка входа с учётом режима

Выгоды накапливаются:

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

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

7. Что меняется, когда есть все четыре

Реактивный поток данных, типизированный контракт узла, полноценное ветвление и dry-run помогают и по отдельности, но вместе они меняют картину. Сдвиг проявляется в двух вещах, которые действительно можно измерить.

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

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

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

Заключение

SOAR-продукт живёт или умирает благодаря своей среде авторства, а не поставляемым в комплекте плейбукам: значение имеют редактор, контракт узла, примитивы ветвления и гарантия dry-run. Плейбуки — лишь то, как эта среда используется.

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

Следующий материал серии спускается на слой ниже, в сам рантайм: как сохраняются выполнения, как восстанавливаются частичные отказы и почему журнал выполнения — самый важный аудиторский артефакт, который производит платформа.

Дополнительное чтение

  • NIST SP 800-61r3: Computer Security Incident Handling Guide
  • MITRE D3FEND: фреймворк обнаружения, отказа и нарушения
  • Эрик Эванс: Domain-Driven Design, глава об интерфейсах, раскрывающих намерение

Готовы защитить
корпоративную инфраструктуру?

Запишитесь на технический брифинг. Без продаж, только архитекторы и ваша команда.