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

sa-tests-db полезен ровно потому, что это внешний набор для проверки анализаторов. Он нужен не для красивой демонстрации, а для грубой, неудобной вещи: взять инструмент и посмотреть, как он проходит квалификационный критерий на чужом корпусе, а не на примерах, которые вы сами себе подготовили.

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

Поэтому sa-tests-db для меня не «ещё один набор тестов». Это способ проверить, где инструмент реально умеет искать ошибки, а где пока только создаёт впечатление.

На прогоне от 23 марта 2026 года по этому набору квалификационный критерий прошли шесть языков из корпуса: C, C#, Go, Java, JavaScript и Python. Но сам по себе этот список не так интересен. Интереснее, что именно пришлось поменять, чтобы до него дойти.

Почему поиск по шаблонам упирается в потолок

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

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

Из-за этого возникают две типичные ошибки.

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

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

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

Что меняет анализ потоков данных

Чтобы пройти дальше, пришлось перейти от «строк вокруг вызова» к реальным потокам данных.

Смысл тут простой. Анализатор должен уметь отвечать не на вопрос «стоит ли рядом sprintf», а на вопрос «могут ли данные из getenv, recv, fgets или другого источника действительно дойти до SPI_execute, system, popen и других опасных точек».

Это уже другой уровень. Вместо текстового соседства появляется реальная связь между определением значения и его использованием.

На практике это сразу даёт две вещи:

  • становится меньше ложных срабатываний на случайных соседствах;

  • начинают находиться длинные цепочки, которые раньше выпадали.

То есть анализ перестаёт угадывать по виду кода и начинает проверять маршрут данных.

Почему без межпроцедурного анализа картина всё равно неполная

Даже обычный поток данных внутри одной функции не решает проблему целиком.

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

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

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

Почему одного потока данных тоже мало

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

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

Если анализ этого не учитывает, он снова начинает шуметь. Формально путь есть, practically код защищён условием.

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

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

Зачем нужна чувствительность к полям

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

Отсюда много мусора. Безобидное поле помечается так же, как опасное, просто потому что они лежат рядом.

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

Почему в итоге остался гибридный подход

Шаблонный поиск я в итоге не выбросил. Он всё ещё полезен как быстрый первый проход.

Но сам по себе он слишком грубый, поэтому рабочая схема получилась гибридной:

  1. Быстрый проход находит подозрительные места.

  2. Простые безопасные случаи отсекаются сразу.

  3. Для оставшегося анализа проверяется реальный путь данных.

  4. Если путь подтверждён, находка поднимается выше.

  5. Если не подтверждён, приоритет снижается.

Так удобнее работать. Сначала смотреть то, где путь действительно существует, а не тратить время на всё подряд.

Что здесь показывает sa-tests-db

Именно на этом месте внешний набор становится полезен по-настоящему.

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

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

В этом и есть практический смысл sa-tests-db. Он не доказывает, что инструмент стал идеальным. Он показывает намного более полезную вещь: где инструмент уже работает достаточно устойчиво, а где ещё врёт.

Итог

Если коротко, набор sa-tests-db нужен как внешняя проверка на честность.

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

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

Иначе анализатор видит не программу, а её поверхность.

Источники: