{ "index": 167, "slug": "editorial-2023-05-mechanism-static-analysis", "title": "Статический анализ: где заканчивается правило и начинается решение", "excerpt": "Линтер и анализатор быстро находят совпадения в коде, но не выносят вердикт о работе приложения. Разбираем модель, воспроизводимую проверку и границы применимости.", "contentHtml": "

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

\n

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

\n

Что называют статическим анализом

\n

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

\n

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

\n
Четыре слоя одного результата
СлойПримерЧто подтверждаетЧего не подтверждает
Правилоno-unused-vars или security patternКакую гипотезу проверяет инструментЧто гипотеза истинна во всех путях выполнения
НаблюдениеФайл, строка, сообщение, уровеньГде инструмент увидел совпадениеЧто код достигнут в нужном окружении
КонтекстCommit, entry point, конфигурация, граница доверияНа какой ревизии и в каких условиях читать сигналПолноту runtime-карты без дополнительных проверок
РешениеИсправить, уточнить правило, оставить исключениеКакой ограниченный шаг принят и кто за него отвечаетГарантию отсутствия других дефектов
\n

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

\n

Механизм: от исходника до отчёта

\n

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

\n

Изменение любого входа меняет смысл результата. Тот же файл на другом commit может иметь иную строку. Новый parser может иначе понять синтаксис. Конфигурация может исключить generated-файлы или, наоборот, включить тестовые фикстуры. Поэтому в CI рядом с отчётом фиксируют commit, версию инструмента и конфигурацию. Без этих трёх значений повторная проверка превращается в догадку.

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

Воспроизводимый запуск на JavaScript

\n

Возьмём ESLint как понятный пример локального статического анализа. Команда выполняется в проекте, где ESLint уже указан в package.json и зависимости установлены по lock-файлу. Это ограничение принципиально: запуск произвольной версии через сетевой загрузчик не даёт той же воспроизводимости.

\n
git rev-parse HEAD\nnode --version\nnpm ci\nmkdir -p reports\n\nset +e\nnpx eslint . --format json --output-file reports/eslint.json\nstatus=$?\nset -e\n\ntest -s reports/eslint.json\nprintf 'eslint_exit=%s' $status\njq '[.[].errorCount, .[].warningCount] | add' reports/eslint.json
\n

Здесь код выхода и JSON-отчёт имеют разные роли. Ненулевой status показывает, что политика ESLint нашла ошибки или предупреждения. Файл reports/eslint.json сохраняет детали для разбора. Последняя команда суммирует счётчики; если в проекте нет jq, можно прочитать JSON тем же Node.js. Команда не должна считать нулевой результат доказательством корректности бизнес-сценария.

\n

Для честного сравнения запусков сохраняйте не только число ошибок, но и commit, версию Node.js, версию ESLint, конфигурацию и список исключений. Иначе уменьшение количества результатов может означать не исправление, а изменение области проверки.

\n

SARIF: переносимый отчёт, а не вердикт

\n

SARIF 2.1.0 — стандартный формат обмена результатами статического анализа. Он описывает log, запускающий инструмент, правила, результаты и locations. Благодаря этому один producer может передать отчёт в другой просмотрщик или платформу. Но SARIF не запускает программу и не добавляет в результат сведения о бизнес-риске, владельце компонента или реальной достижимости.

\n
{\n  'version': '2.1.0',\n  'runs': [{\n    'tool': { 'driver': { 'name': 'demo-linter', 'rules': [{ 'id': 'demo.no-command' }] } },\n    'results': [{\n      'ruleId': 'demo.no-command',\n      'level': 'warning',\n      'message': { 'text': 'Проверить передачу значения в командный вызов' },\n      'locations': [{\n        'physicalLocation': {\n          'artifactLocation': { 'uri': 'src/export.js' },\n          'region': { 'startLine': 14 }\n        }\n      }]\n    }]\n  }]\n}
\n

Фрагмент показывает структуру JavaScript-объекта для чтения, а не готовый файл, который следует отправить без проверки схемы: настоящий SARIF использует JSON с двойными кавычками. Поле ruleId связывает результат с правилом, level задаёт уровень сообщения, а location помогает открыть место в конкретной ревизии. Отсутствие в примере commit и контекста — напоминание о том, что эти сведения нельзя восстановить из одной строки.

\n

При загрузке SARIF в платформу нужно отдельно проверить адреса файлов. Например, GitHub сопоставляет относительный URI с файлом репозитория и учитывает partialFingerprints для повторных результатов. Несколько наборов результатов для одного commit требуют уникальной категории. Это правила конкретной платформы, а не универсальное свойство самого анализа.

\n

Как отличить полезный сигнал от неверного решения

\n
Диагностика по схеме «симптом — причина — проверка — действие»
СимптомВероятная причинаПроверкаОграниченное действие
Результат указывает на строку, которой уже нетОтчёт относится к другому commitСверить commit отчёта и текущую ревизию, затем повторить запускНе переносить finding на новую строку вручную
Одинаковое правило срабатывает в тестах и production-кодеScope правила шире предполагаемой границыСравнить назначение файлов и путь от входа до вызоваУточнить pattern или разделить конфигурацию, не выключать правило глобально
После обновления инструмента результатов стало меньшеИзменились parser, правила или область файловСравнить версии, конфигурацию и diff отчётовНазвать причину изменения до принятия нового baseline
Найден возможный опасный вызовАнализатор не знает проверку или праваПроследить источник, преобразования, allowlist, entry point и праваИсправить код либо оставить сигнал до подтверждения контекста
Команда просит добавить исключениеКонкретный участок признан допустимым или правило шумитОпределить точный объект, владельца, причину и срок пересмотраСделать узкое исключение с rollback, а не менять весь проект
\n

Термин «ложное срабатывание» тоже требует доказательства. Им можно назвать результат после проверки, которая показала, что условие правила не соответствует реальному риску в этом конкретном участке. Пока источник данных или путь выполнения неизвестны, точнее использовать статус «контекст не собран».

\n

Порядок проверки одного результата

\n
  1. Зафиксируйте входы. Сохраните commit, версию инструмента, конфигурацию, идентификатор правила, файл, строку и полный текст сообщения.
  2. Прочитайте правило. Определите, какую конструкцию оно ищет, какие файлы входят в scope и какие исключения уже предусмотрены.
  3. Откройте ту же ревизию. Проверьте, что URI и строка относятся к тому же исходнику, который анализировал CI.
  4. Восстановите путь. Для security-сигнала найдите источник значения, преобразования, проверки, sink и entry point. Для lint-сигнала проверьте локальную конструкцию и намерение кода.
  5. Добавьте контекст. Запишите компонент, границу доверия, конфигурацию, владельца и область изменения. Неизвестное поле оставьте явно неизвестным.
  6. Выберите действие. Исправьте дефект, уточните правило для будущих результатов или примените узкое исключение только к доказанно допустимому месту.
  7. Проверьте отрицательный путь. Убедитесь, что другой commit, соседний файл и тот же источник данных не исчезли из проверки из-за широкого исключения.
  8. Сохраните повторяемый результат. Оставьте команду запуска, expected outcome и условие отката. Следующий инженер должен получить тот же вопрос и увидеть, что изменилось.
\n

Где проходит граница применимости

\n

Статический анализ не видит автоматически весь runtime. Динамические импорты, feature flags, generated-код, переменные окружения, ответы внешних сервисов и права пользователя могут изменить путь выполнения. Если инструмент не строит нужную модель данных, его результат может быть слишком узким или слишком широким.

\n

Линтер не заменяет тесты. Типы не заменяют проверку протокола на реальном ответе. Security-правило не является доказательством exploitability и не измеряет риск для бизнеса. Даже корректный SARIF-файл доказывает только соответствие формату и наличие записанных результатов.

\n

Примеры с ESLint, jq и SARIF требуют установленного Node.js, lock-файла и настройки проекта. Имена src/export.js, demo.no-command и сообщение в JSON учебные. Команды не запускаются в статье и не дают результатов конкретного репозитория. Для другой CI-платформы изменятся путь к отчёту, способ загрузки и требования к permissions; модель фиксации commit, конфигурации и scope остаётся полезной, но её нужно сверить с документацией платформы.

\n

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

\n

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

\n

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

\n

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

\n" }