Как построить собственный harness для поиска уязвимостей в коде на Linux/VPS
Современные кодовые базы становятся всё более распределёнными и polyglot‑ориентированными. Чтобы эффективно выявлять уязвимости в таких средах, нужна система, которая не привязана к конкретной языковой парсере или отдельной модели ИИ, а может работать как interchangeable компонент. В этой статье мы опишем, как собрать такой harness на обычном Linux‑сервере или VPS, используя Docker, SQLite и простые скрипты оркестрации.
Основные идеи harness‑подхода
Цель harness‑а — обеспечить непрерывный, масштабируемый процесс поиска уязвимостей, при котором:
- Модели ИИ рассматриваются как Stateless вычислительные движки; всё состояние хранится вне их контекста.
- Каждый этап пайплайна (разведка, охота, валидация) выполняется отдельным агентом, получающим задачу из очереди.
- Результаты сохраняются в базе данных, что позволяет возобновлять работу после сбоя и избегать дублирования.
- Для снижения ложных срабатываний используется adversarial review: одна модель генерирует гипотезы, другая — пытается их опровергнуть.
Такой подход делает систему устойчивой к изменениям в базовых моделях: вы можете подменять их без изменения самой оркестрации.
Архитектура пайплайна
Harness состоит из двух основных подсистем:
- VDH (Vulnerability Discovery Harness) — отвечает за первичное обнаружение потенциальных проблем в коде.
- VVS (Vulnerability Validation System) — получает Findings из VDH и проводит deduplication, оценку и подготовку к исправлению.
Каждая подсистема реализована как набор этапов, связанных через очередь задач. Этапы работают в режиме producer‑consumer, записывая промежуточные результаты в одну SQLite‑базу, ключевую по кортежу (run_id, repo, stage). Это обеспечивает:
- Persistency — при падении контейнера достаточно перезапустить orchestrator, и работа продолжится с того же места.
- Возможность параллельного выполнения без потери контекста, так как каждый агент получает только свою порцию данных.
- Лёгкую отладку: можно посмотреть, какие задачи были обработаны на каждом этапе.
- Recon — агент строит угрозовую модель конкретного репозитория, выделяя классы атак (инъекции, повреждение памяти, race conditions и т.д.) и, при необходимости, генерирует repo‑специфичные правила.
- Hunt — несколько агентов параллельно пытаются «сломать» код, используя sandbox‑окружение. Они компилируют фрагменты, запускают их и фиксируют аварийные ситуации.
- Gapfill / Feedback / Trace — генерируют новые задачи на основе промежуточных результатов (например, выявляют недостающие зависимости или переходят к связанным репозиториям).
- Dedup — на этом этапе агрегируются похожие находки, чтобы уменьшить шум перед передачей в VVS.
- Validate — отдельный агент, которому запрещено создавать собственные Findings, пытается опровергнуть каждую гипотезу, используя только детерминированные проверки (существование файлов, корректность патча и теста).
- Deduplication — детерминированный код строит инвертированные индексы по touched files, функциям и редким токенам, получая короткий список кандидатов. Только затем LLM анализирует этот список, решая, можно ли объединить несколько находок в одну.
- Judgment — другая модель оценивает достоверность каждой находки, проверяя наличие рабочего PoC и предлагаемого патча.
- Fixing — после подтверждения формируется задача на исправление, которая может быть передана в систему управления задачами или в CI/CD.
Этап обнаружения (VDH)
VDH реализует многократный цикл, в котором:
Важно ограничивать размер контекста каждой модели: мы держим её ниже 25 % от общего окна, иначе модель начинает «забывать» ранее найденные проблемы и генерировать ложные срабатывания.
Этап валидации (VVS)
Все Findings, прошедшие VDH, попадают в общую очередь VVS, где они проходят:
Разделение моделей между VDH и VVS гарантирует, что каждая находка оценивается независимым набором весов весов, что существенно снижает вероятность системных ошибок.
Управление состоянием и persistence
Все этапы записывают свои результаты в одну SQLite‑таблицу:
CREATE TABLE IF NOT EXISTS task_results (
run_id TEXT NOT NULL,
repo TEXT NOT NULL,
stage TEXT NOT NULL,
payload BLOB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (run_id, repo, stage)
);
Оркестратор просто читает из этой таблицы, какие задачи ещё не выполнены, и подаёт их в соответствующие агенты. При ошибке API или падении контейнера достаточно перезапустить процесс — уже выполненные этапы не будут повторяться.
Динамическое моделирование угроз и микро‑форки
На этапе Recon агент может самостоятельно создавать новые классы атак, специфичные для исследуемого кода. Это позволяет адаптироваться к нестандартным уязвимостям, например, к timing side‑channels в криптографических библиотеках.
Если Hunter обнаруживает интересный путь, выходящий за текущий scope, он использует механизм Sibling Forking: создаёт копию самого себя с точным структурным seed‑ом, чтобы исследование продолжилось без потери контекста основного анализа.
Когда агенту нужен внешний ресурс (например, специальная сборочная среда или VM), он записывает запрос в общий Wishlist. Система периодически проверяет этот список и, после предоставления зависимости человеком, автоматически перезапускает связанную задачу.
Трассировка зависимостей между репозиториями
Для выявления сквозных уязвимостей harness строит общий символ‑индекс и граф зависимостей всех репозиториев в fleet. Tracer‑агент проверяет, может ли вредоносный input достичь уязвимого места через цепочку вызовов. Если да, он порождает новые hunt‑задачи уже в потребительском репозитории.
Этот этап требует:
- Доступ к исходному коду всех репозиториев (через монтированные тома или git‑clones внутри контейнеров).
- Скрипт, который собирает export‑symbols (например, через nm, readelf или язык‑специфичные инструменты) и сохраняет их в той же SQLite‑базе.
Борьба с ложными срабатываниями
Чтобы исключить «vacuous findings» (например, «если пользователь может писать в БД, он может писать в БД»), harness требует от Hunter:
- Явно сформулировать threat model: кто атакующий, какой границы нарушается.
- Предоставить рабочий PoC в виде теста, который выполняется против неизменённого кодобазы.
- Предложить патч, проходящий базовую проверку (применяется к чистому кодобазу и не ломает сборку).
- Пройти детерминированную валидацию, которая подтверждает existence of cited files и корректность patch‑а и теста.
Если любой из пунктов не выполнен, находка помечается как непригодная и не попадает в очередь VVS.
Практические советы по развертыванию на VPS
1. Окружение — используйте свежий дистрибутив Ubuntu 22.04 или Debian 12. Установите Docker и docker‑compose.
2. Создание сети — изолируйте harness в отдельной Docker‑сети, чтобы ограничить доступ к внешним сервисам.
3. Запуск sandbox‑агента — если ваш Hunter работает внутри Docker, добавьте при запуске:
docker run --rm \
--security-opt seccomp=unconfined \
--security-opt apparmor=unconfined \
-v /path/to/repo:/src:ro \
my-hunter-image
Эти опции необходимы, иначе вызов unshare внутри контейнера будет заблокирован ядром.
4. База данных — разместите SQLite‑файл в томе, монтируемом в контейнеры, чтобы все этапы видели одно и то же состояние.
5. Оркестрация — простейший оркестратор можно написать на Python: цикл, который выбирает из базы задачи со статусом «pending», запускает соответствующий образ Docker с нужными переменными окружения и обновляет запись после завершения.
6. Мониторинг — экспортируйте метрики количества выполненных задач, среднее время этапа и количество уникальных Findings в Prometheus, используя готовый экспортер или простой HTTP‑эндпоинт.
7. Резервное копирование — периодически делайте snapshot томов с кодом и базой данных; это позволит быстро восстановить работу после сбоя узла.
Заключение
Построив модель‑независимый harness, вы получаете систему, которая:
- Не привязана к конкретной версии или поставщику LLM.
- Масштабируется на сотни репозиториев благодаря внешнему хранению состояния и графу зависимостей.
- Снижает ложные срабатывания за счёт adversarial review и обязательного PoC.
- Легко развёртывается на обычном VPS с Docker и минимальным набором скриптов.
Такой подход позволяет сосредоточиться на улучшении промптов и логики охоты, а не на постоянной переделке инфраструктуры при смене базовой модели. Если вы уже экспериментируете с одиночными сканерами на одном репозитории, следующим логичным шагом будет вынести каждый этап в отдельный контейнер, добавить persistence‑слой и получить готовый к промышленному использованию harness для непрерывного поиска уязвимостей.