AI-ревью безопасности: пилот на одном репозитории

Почти каждая команда, которая пробует AI-анализ безопасности, упирается в один и тот же вопрос: модель действительно нашла уязвимость или просто сгенерировала правдоподобное предупреждение? Ответ «вроде полезно» не годится ни для решения о внедрении, ни для бюджета. Нужен эксперимент с заранее заданными метриками, жёсткими границами и честной разметкой результатов.
В этой статье — протокол такого эксперимента. Один репозиторий, режим только чтения, фиксированный набор входных данных и четыре измерения: подтверждённые находки, ложные срабатывания, время человека на проверку и качество предложенных регрессионных тестов. Протокол не привязан к конкретному продукту: его можно применить к любому агенту или модели, которые умеют читать код и выдавать структурированный отчёт.
Важно: все числа, имена файлов и фрагменты отчётов ниже — иллюстрации формата, а не результаты реального прогона. Мы не публикуем выдуманных метрик; свои вы получите сами, пройдя шаги.
Что именно мы проверяем
Пилот отвечает на один узкий вопрос: окупается ли время инженера, потраченное на разбор AI-находок, на этом конкретном репозитории? Поэтому в рамки пилота не входят:
- автоматическое исправление кода и создание pull request;
- доступ агента к сети, секретам, CI/CD и продовым окружениям;
- сравнение «всех инструментов рынка» — одна конфигурация, один репозиторий;
- выводы о безопасности продукта в целом.
Такие ограничения не перестраховка, а условие измеримости: чем меньше переменных, тем проще понять, откуда взялась каждая находка.
Шаг 1. Выберите репозиторий и зафиксируйте снимок
Хороший кандидат — сервис среднего размера, который команда хорошо знает: есть люди, способные быстро подтвердить или опровергнуть находку. Плохие кандидаты — заброшенный монолит без владельцев и репозиторий с конфиденциальными данными в истории.
Зафиксируйте конкретный коммит, чтобы все прогоны шли по одному и тому же коду:
# Отдельная рабочая копия только для пилота
git clone --no-tags https://git.example.internal/team/service.git pilot-src
cd pilot-src
git checkout --detach <SHA-зафиксированного-коммита>
git rev-parse HEAD > ../pilot/snapshot.sha
# Проверка, что в рабочей копии нет незакоммиченных изменений
git status --porcelain | wc -l # ожидается 0
Адрес репозитория в примере условный — подставьте свой. Перед передачей кода агенту прогоните ваш штатный сканер секретов и удалите из копии всё, что не должно покидать периметр (файлы .env, дампы, ключи в фикстурах).
Шаг 2. Сделайте окружение по-настоящему read-only
«Агенту сказали ничего не менять» — это не ограничение. Ограничение — это права файловой системы и отсутствие сети. Минимальный вариант на контейнерах:
docker run --rm -it \
--network none \
--read-only \
--tmpfs /tmp:rw,size=256m \
--cap-drop ALL \
--security-opt no-new-privileges \
-v "$PWD/pilot-src:/src:ro" \
-v "$PWD/pilot/out:/out:rw" \
<образ-с-вашим-агентом>
/srcсмонтирован только на чтение — агент физически не может изменить код;- запись разрешена только в
/out, куда складываются отчёты; --network noneисключает утечку кода и загрузку зависимостей во время анализа.
Если ваш агент работает через внешний API модели, сеть полностью отключить нельзя. Тогда разрешите исходящий трафик только к одному эндпоинту через прокси с allowlist и заранее согласуйте с ответственными за безопасность, какой код можно отправлять внешнему провайдеру.
Проверьте ограничения до начала пилота, а не после:
# Внутри контейнера — обе команды должны завершиться ошибкой
touch /src/probe.txt
curl -sS --max-time 5 https://example.com
Шаг 3. Зафиксируйте задание и формат ответа
Свободный текст «вот что я нашёл» невозможно посчитать. Задайте агенту строгий формат: одна находка — одна запись. Пример схемы:
{
"id": "F-001",
"category": "injection | authz | secrets | crypto | deserialization | other",
"file": "path/to/file",
"lines": "120-134",
"claim": "Что именно уязвимо и почему",
"exploit_path": "Как внешний вход достигает этого места",
"severity_claimed": "low | medium | high | critical",
"regression_test": "Код теста, который падает на текущем коде и проходит после исправления"
}
Поле exploit_path — ключевое. Находка без пути от входа к уязвимому месту почти всегда оказывается теоретической. Требование описать путь заметно упрощает разметку.
Сохраните промпт, версию модели, параметры и конфигурацию агента рядом со снимком коммита. Без этого повторный прогон через месяц ничего не скажет.
pilot/
snapshot.sha
prompt.md
agent-config.json # модель, температура, лимиты, версия обвязки
out/
run-1/findings.jsonl
run-2/findings.jsonl
triage.csv
Сделайте как минимум два прогона на одном снимке. Расхождение между ними — самостоятельная метрика: если половина находок появляется только в одном из прогонов, на однократный запуск в CI полагаться нельзя.
Шаг 4. Разметка: правила до первой находки
Самая частая причина бесполезных пилотов — правила разметки придумывают по ходу, подстраиваясь под удобные результаты. Договоритесь о категориях заранее:
| Статус | Критерий |
|---|---|
| Подтверждена | Воспроизведена тестом или ручной проверкой; путь от внешнего входа реален |
| Подтверждена, но не эксплуатируема | Дефект есть, но вход контролирует только доверенная сторона; полезна как hardening |
| Ложное срабатывание | Утверждение неверно: санитизация есть, путь не существует, код недостижим |
| Дубликат | Та же первопричина, что у другой находки |
| Не определено | Нужен эксперт или окружение, которого нет в пилоте; не считается ни в плюс, ни в минус |
Разметку делают двое независимо, затем сверяют расхождения. Если ревьюеры часто не согласны, проблема в критериях, а не в агенте.
Для каждой записи фиксируйте время проверки — от открытия находки до принятия решения. Формат triage.csv:
id,run,status,reviewer,minutes,severity_confirmed,test_quality,notes
F-001,1,confirmed,rev-a,14,medium,2,"тест падает до фикса"
F-002,1,false_positive,rev-a,6,,0,"есть экранирование выше по стеку"
Строки выше — иллюстрация формата, а не реальные данные.
Шаг 5. Оценка регрессионных тестов
Предложенный тест ценен, только если он ловит именно заявленный дефект. Используйте простую шкалу от 0 до 3:
- 0 — теста нет, он не компилируется или проверяет что-то постороннее;
- 1 — запускается, но проходит на текущем (уязвимом) коде;
- 2 — падает на текущем коде из-за заявленного дефекта;
- 3 — как 2, плюс проходит после минимального исправления и не зависит от сети или порядка запуска.
Тесты запускайте в отдельной копии, а не в read-only снимке, и тоже без сети:
git worktree add ../pilot-tests "$(cat ../pilot/snapshot.sha)"
cd ../pilot-tests
# Положите тест из findings.jsonl в каталог тестов, затем:
docker run --rm --network none -v "$PWD:/w" -w /w <образ-для-тестов> \
<команда-запуска-одного-теста>
# Ожидаемо: тест ПАДАЕТ на исходном коде
# После ручного минимального исправления — тот же запуск, тест должен ПРОЙТИ
git -C ../pilot-tests diff --stat # убедитесь, что изменён только нужный файл
Никогда не запускайте сгенерированный код тестов на машине с доступом к секретам или внутренней сети: это такой же недоверенный код, как и любой другой вывод модели.
Шаг 6. Подсчёт метрик
Когда разметка закончена, посчитайте сводку. Пример для triage.csv с помощью стандартных утилит (предполагается, что в полях нет запятых внутри кавычек; для реальных данных лучше использовать скрипт на Python с модулем csv):
# Распределение по статусам
cut -d, -f3 triage.csv | tail -n +2 | sort | uniq -c
# Суммарное время разбора, минуты
awk -F, 'NR>1 {s+=$5} END {print s}' triage.csv
# Средняя оценка тестов среди подтверждённых
awk -F, 'NR>1 && $3=="confirmed" {s+=$7; n++} END {if (n) print s/n}' triage.csv
Из этих чисел складываются показатели, на которые стоит смотреть:
- Точность = подтверждённые / (подтверждённые + ложные). «Не определено» и дубликаты в знаменатель не входят, но считаются отдельно.
- Минуты на одну подтверждённую находку = всё время разбора / число подтверждённых. Это главная экономическая метрика: в неё входит время, потраченное на шум.
- Доля пригодных тестов = тесты с оценкой 2–3 / подтверждённые находки.
- Стабильность = находки, появившиеся в обоих прогонах / все уникальные находки.
- Новизна = подтверждённые находки, которых нет в выводе ваших существующих SAST-инструментов и в трекере задач.
Последняя метрика часто важнее точности: агент с высокой точностью, который находит только то, что уже ловит штатный сканер, ничего не добавляет.
Проверка результата
Пилот можно считать корректным, если выполняются все условия:
- в каталоге пилота есть
snapshot.sha, промпт, конфигурация агента и отчёты каждого прогона; git status --porcelainвpilot-srcпо-прежнему пуст, а проверки записи и сети из шага 2 завершались ошибкой;- у каждой находки есть статус, ревьюер и время; доля «не определено» не превышает заранее оговорённый порог;
- каждая находка со статусом «подтверждена» воспроизведена, а не просто «выглядит правдоподобно»;
- критерий решения сформулирован до подсчёта, например: «продолжаем, если на подтверждённую находку уходит не больше N минут и есть хотя бы одна новая находка уровня medium и выше». Значение N выбирает команда, исходя из своей стоимости часа ревью.
Типовые ошибки
- Считать находки, а не подтверждения. «Агент выдал 80 предупреждений» — это объём работы для людей, а не результат.
- Не учитывать время на ложные срабатывания. Пять минут на каждое из сорока опровержений — это больше трёх часов инженерного времени.
- Менять промпт между прогонами. После правки промпта это новый эксперимент со своим набором прогонов.
- Разметка одним человеком. Автор кода склонен отвергать находки, энтузиаст AI — принимать. Двое независимых ревьюеров снижают обе ошибки.
- Доверять severity от модели. Заявленная критичность — гипотеза; в метрики идёт только подтверждённая.
- Давать агенту права «на всякий случай». Запись, сеть или токены CI превращают пилот анализа в отдельный риск.
- Выбирать репозиторий с известными уязвимостями и подсказывать их в промпте. Так вы измерите способность модели подтверждать подсказки, а не находить новое.
Ограничения подхода
Один репозиторий — это выборка из одного элемента. Результат говорит о конкретной кодовой базе, языке и стиле, а не об инструменте вообще. Перенос на другие сервисы требует повторного пилота.
Протокол плохо измеряет пропуски: мы видим, что агент нашёл, но не знаем, что он не нашёл. Частично это компенсируется сравнением с уже известными проблемами из трекера и выводами SAST, но полной полноты (recall) так не получить.
Классы уязвимостей, требующие контекста за пределами репозитория — конфигурация инфраструктуры, бизнес-логика нескольких сервисов, поведение в рантайме, — в read-only анализе одного репозитория почти не видны. Низкий результат по ним — ограничение постановки, а не обязательно слабость модели.
Наконец, результаты зависят от версии модели и обвязки. Пилот, проведённый на одной версии, нужно повторять после обновления — благо при сохранённом снимке и конфигурации это дёшево.
Что дальше
Если метрики прошли критерий решения, следующий шаг — не «включить везде», а расширить пилот: второй репозиторий на другом языке и прогон только на диффах pull request вместо полного снимка. Если не прошли — сохраните каталог пилота: он станет базовой линией для сравнения со следующей версией модели или другой конфигурацией агента.
Больше практических протоколов — в разделе гайдов, определения терминов — в глоссарии.