{ "index": 167, "slug": "editorial-2023-05-mechanism-static-analysis", "title": "Статический анализ: где заканчивается правило и начинается решение", "excerpt": "Линтер и анализатор быстро находят совпадения в коде, но не выносят вердикт о работе приложения. Разбираем модель, воспроизводимую проверку и границы применимости.", "contentHtml": "
В pull request статический анализатор показывает несколько ошибок. Один разработчик предлагает сразу заблокировать слияние, другой — отключить правило целиком. Оба решения могут быть ошибочными. Отчёт сообщает, где и по какому правилу найдено совпадение, но не доказывает, что ветка исполняется, вход пришёл из недоверенной зоны или дефект попадёт в релиз.
\nЦена неверного действия практическая. Глобальное отключение убирает сигнал и для будущего кода, а безусловная блокировка превращает шум в очередь ручных исключений. Рабочая модель разделяет четыре слоя: правило формулирует гипотезу, анализатор фиксирует наблюдение, код и конфигурация дают контекст, а команда принимает решение с понятным scope. Ниже этот порядок можно повторить на обычном JavaScript-проекте.
\nСтатический анализ проверяет программу по исходному коду, его синтаксическому дереву, типам, зависимостям или правилам потока данных, не исполняя весь сценарий приложения как единое целое. Линтер обычно проверяет стиль и локальные конструкции. Проверка типов ищет несогласованные операции с типами. Security-анализатор может искать опасные источники, преобразования и точки использования данных.
\nРазные инструменты отвечают на разные вопросы. Линтер может уверенно сказать, что переменная объявлена, но не используется. Он не может по одной такой записи определить, доступен ли сервис в production. Проверка типов может найти несовместимость интерфейсов, но не подтверждает, что сервер вернул ожидаемое поле. Анализатор потока данных может показать возможный путь к опасному вызову, но должен учитывать конфигурацию, права и достижимость.
\n| Слой | Пример | Что подтверждает | Чего не подтверждает |
|---|---|---|---|
| Правило | no-unused-vars или security pattern | Какую гипотезу проверяет инструмент | Что гипотеза истинна во всех путях выполнения |
| Наблюдение | Файл, строка, сообщение, уровень | Где инструмент увидел совпадение | Что код достигнут в нужном окружении |
| Контекст | Commit, entry point, конфигурация, граница доверия | На какой ревизии и в каких условиях читать сигнал | Полноту runtime-карты без дополнительных проверок |
| Решение | Исправить, уточнить правило, оставить исключение | Какой ограниченный шаг принят и кто за него отвечает | Гарантию отсутствия других дефектов |
Эта граница защищает от подмены понятий. Статический анализ — ранний и дешёвый слой обратной связи, а не замена тестам, ручному review, проверке разрешений и наблюдению за работающей системой.
\nНа вход инструмент получает выбранную ревизию, набор файлов, конфигурацию и правила. Сначала он разбирает файлы или строит представление программы. Затем применяет правила и создаёт результаты. Результат обычно содержит идентификатор правила, сообщение, уровень и координату в файле. Формат отчёта может быть человекочитаемым или машинным.
\nИзменение любого входа меняет смысл результата. Тот же файл на другом commit может иметь иную строку. Новый parser может иначе понять синтаксис. Конфигурация может исключить generated-файлы или, наоборот, включить тестовые фикстуры. Поэтому в CI рядом с отчётом фиксируют commit, версию инструмента и конфигурацию. Без этих трёх значений повторная проверка превращается в догадку.
\nВозьмём ESLint как понятный пример локального статического анализа. Команда выполняется в проекте, где ESLint уже указан в package.json и зависимости установлены по lock-файлу. Это ограничение принципиально: запуск произвольной версии через сетевой загрузчик не даёт той же воспроизводимости.
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. Команда не должна считать нулевой результат доказательством корректности бизнес-сценария.
Для честного сравнения запусков сохраняйте не только число ошибок, но и commit, версию Node.js, версию ESLint, конфигурацию и список исключений. Иначе уменьшение количества результатов может означать не исправление, а изменение области проверки.
\nSARIF 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 и контекста — напоминание о том, что эти сведения нельзя восстановить из одной строки.
При загрузке SARIF в платформу нужно отдельно проверить адреса файлов. Например, GitHub сопоставляет относительный URI с файлом репозитория и учитывает partialFingerprints для повторных результатов. Несколько наборов результатов для одного commit требуют уникальной категории. Это правила конкретной платформы, а не универсальное свойство самого анализа.
| Симптом | Вероятная причина | Проверка | Ограниченное действие |
|---|---|---|---|
| Результат указывает на строку, которой уже нет | Отчёт относится к другому commit | Сверить commit отчёта и текущую ревизию, затем повторить запуск | Не переносить finding на новую строку вручную |
| Одинаковое правило срабатывает в тестах и production-коде | Scope правила шире предполагаемой границы | Сравнить назначение файлов и путь от входа до вызова | Уточнить pattern или разделить конфигурацию, не выключать правило глобально |
| После обновления инструмента результатов стало меньше | Изменились parser, правила или область файлов | Сравнить версии, конфигурацию и diff отчётов | Назвать причину изменения до принятия нового baseline |
| Найден возможный опасный вызов | Анализатор не знает проверку или права | Проследить источник, преобразования, allowlist, entry point и права | Исправить код либо оставить сигнал до подтверждения контекста |
| Команда просит добавить исключение | Конкретный участок признан допустимым или правило шумит | Определить точный объект, владельца, причину и срок пересмотра | Сделать узкое исключение с rollback, а не менять весь проект |
Термин «ложное срабатывание» тоже требует доказательства. Им можно назвать результат после проверки, которая показала, что условие правила не соответствует реальному риску в этом конкретном участке. Пока источник данных или путь выполнения неизвестны, точнее использовать статус «контекст не собран».
\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 остаётся полезной, но её нужно сверить с документацией платформы.
Результат можно передать в review, когда другой инженер может повторить проверку на той же ревизии и ответить на четыре вопроса: какое правило сработало, где именно, в каком контексте и почему выбрано это действие. Для исправления должны быть видны тест или повторный запуск. Для изменения правила — новая версия гипотезы и её scope. Для исключения — точный объект, причина, владелец, срок пересмотра и способ отката.
\nЕсли хотя бы одно звено неизвестно, итогом становится не «безопасно» и не «сломано», а конкретный незакрытый вопрос: какой файл, вход, настройка или владелец ещё не проверены. Такая модель сохраняет полезные сигналы, уменьшает случайные блокировки и не обещает статическому инструменту того, чего он не измерял.
\n