Files

8 lines
22 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 167,
"slug": "editorial-2023-05-mechanism-static-analysis",
"title": "Статический анализ: где заканчивается правило и начинается решение",
"excerpt": "Линтер и анализатор быстро находят совпадения в коде, но не выносят вердикт о работе приложения. Разбираем модель, воспроизводимую проверку и границы применимости.",
"contentHtml": "<p>В pull request статический анализатор показывает несколько ошибок. Один разработчик предлагает сразу заблокировать слияние, другой — отключить правило целиком. Оба решения могут быть ошибочными. Отчёт сообщает, где и по какому правилу найдено совпадение, но не доказывает, что ветка исполняется, вход пришёл из недоверенной зоны или дефект попадёт в релиз.</p>\n<p>Цена неверного действия практическая. Глобальное отключение убирает сигнал и для будущего кода, а безусловная блокировка превращает шум в очередь ручных исключений. Рабочая модель разделяет четыре слоя: правило формулирует гипотезу, анализатор фиксирует наблюдение, код и конфигурация дают контекст, а команда принимает решение с понятным scope. Ниже этот порядок можно повторить на обычном JavaScript-проекте.</p>\n<h2>Что называют статическим анализом</h2>\n<p>Статический анализ проверяет программу по исходному коду, его синтаксическому дереву, типам, зависимостям или правилам потока данных, не исполняя весь сценарий приложения как единое целое. Линтер обычно проверяет стиль и локальные конструкции. Проверка типов ищет несогласованные операции с типами. Security-анализатор может искать опасные источники, преобразования и точки использования данных.</p>\n<p>Разные инструменты отвечают на разные вопросы. Линтер может уверенно сказать, что переменная объявлена, но не используется. Он не может по одной такой записи определить, доступен ли сервис в production. Проверка типов может найти несовместимость интерфейсов, но не подтверждает, что сервер вернул ожидаемое поле. Анализатор потока данных может показать возможный путь к опасному вызову, но должен учитывать конфигурацию, права и достижимость.</p>\n<table><caption>Четыре слоя одного результата</caption><thead><tr><th scope='col'>Слой</th><th scope='col'>Пример</th><th scope='col'>Что подтверждает</th><th scope='col'>Чего не подтверждает</th></tr></thead><tbody><tr><td>Правило</td><td><code>no-unused-vars</code> или security pattern</td><td>Какую гипотезу проверяет инструмент</td><td>Что гипотеза истинна во всех путях выполнения</td></tr><tr><td>Наблюдение</td><td>Файл, строка, сообщение, уровень</td><td>Где инструмент увидел совпадение</td><td>Что код достигнут в нужном окружении</td></tr><tr><td>Контекст</td><td>Commit, entry point, конфигурация, граница доверия</td><td>На какой ревизии и в каких условиях читать сигнал</td><td>Полноту runtime-карты без дополнительных проверок</td></tr><tr><td>Решение</td><td>Исправить, уточнить правило, оставить исключение</td><td>Какой ограниченный шаг принят и кто за него отвечает</td><td>Гарантию отсутствия других дефектов</td></tr></tbody></table>\n<p>Эта граница защищает от подмены понятий. Статический анализ — ранний и дешёвый слой обратной связи, а не замена тестам, ручному review, проверке разрешений и наблюдению за работающей системой.</p>\n<h2>Механизм: от исходника до отчёта</h2>\n<p>На вход инструмент получает выбранную ревизию, набор файлов, конфигурацию и правила. Сначала он разбирает файлы или строит представление программы. Затем применяет правила и создаёт результаты. Результат обычно содержит идентификатор правила, сообщение, уровень и координату в файле. Формат отчёта может быть человекочитаемым или машинным.</p>\n<p>Изменение любого входа меняет смысл результата. Тот же файл на другом commit может иметь иную строку. Новый parser может иначе понять синтаксис. Конфигурация может исключить generated-файлы или, наоборот, включить тестовые фикстуры. Поэтому в CI рядом с отчётом фиксируют commit, версию инструмента и конфигурацию. Без этих трёх значений повторная проверка превращается в догадку.</p>\n<figure><img src='/assets/editorial/2023/static-analysis-2023-rule-context.svg' alt='Схема статического анализа: правило задаёт гипотезу, SARIF хранит структуру и результат, а проектный контекст добавляет границу доверия, владельца и область изменения перед решением' loading='lazy' /><figcaption>Формат отчёта переносит наблюдение между инструментами, но проектный контекст всё равно должен быть проверен отдельно.</figcaption></figure>\n<h2>Воспроизводимый запуск на JavaScript</h2>\n<p>Возьмём ESLint как понятный пример локального статического анализа. Команда выполняется в проекте, где ESLint уже указан в <code>package.json</code> и зависимости установлены по lock-файлу. Это ограничение принципиально: запуск произвольной версии через сетевой загрузчик не даёт той же воспроизводимости.</p>\n<pre><code>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</code></pre>\n<p>Здесь код выхода и JSON-отчёт имеют разные роли. Ненулевой <code>status</code> показывает, что политика ESLint нашла ошибки или предупреждения. Файл <code>reports/eslint.json</code> сохраняет детали для разбора. Последняя команда суммирует счётчики; если в проекте нет <code>jq</code>, можно прочитать JSON тем же Node.js. Команда не должна считать нулевой результат доказательством корректности бизнес-сценария.</p>\n<p>Для честного сравнения запусков сохраняйте не только число ошибок, но и commit, версию Node.js, версию ESLint, конфигурацию и список исключений. Иначе уменьшение количества результатов может означать не исправление, а изменение области проверки.</p>\n<h2>SARIF: переносимый отчёт, а не вердикт</h2>\n<p>SARIF 2.1.0 — стандартный формат обмена результатами статического анализа. Он описывает log, запускающий инструмент, правила, результаты и locations. Благодаря этому один producer может передать отчёт в другой просмотрщик или платформу. Но SARIF не запускает программу и не добавляет в результат сведения о бизнес-риске, владельце компонента или реальной достижимости.</p>\n<pre><code>{\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}</code></pre>\n<p>Фрагмент показывает структуру JavaScript-объекта для чтения, а не готовый файл, который следует отправить без проверки схемы: настоящий SARIF использует JSON с двойными кавычками. Поле <code>ruleId</code> связывает результат с правилом, <code>level</code> задаёт уровень сообщения, а <code>location</code> помогает открыть место в конкретной ревизии. Отсутствие в примере commit и контекста — напоминание о том, что эти сведения нельзя восстановить из одной строки.</p>\n<p>При загрузке SARIF в платформу нужно отдельно проверить адреса файлов. Например, GitHub сопоставляет относительный URI с файлом репозитория и учитывает <code>partialFingerprints</code> для повторных результатов. Несколько наборов результатов для одного commit требуют уникальной категории. Это правила конкретной платформы, а не универсальное свойство самого анализа.</p>\n<h2>Как отличить полезный сигнал от неверного решения</h2>\n<div class='table-scroll'><table><caption>Диагностика по схеме «симптом — причина — проверка — действие»</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Вероятная причина</th><th scope='col'>Проверка</th><th scope='col'>Ограниченное действие</th></tr></thead><tbody><tr><td>Результат указывает на строку, которой уже нет</td><td>Отчёт относится к другому commit</td><td>Сверить commit отчёта и текущую ревизию, затем повторить запуск</td><td>Не переносить finding на новую строку вручную</td></tr><tr><td>Одинаковое правило срабатывает в тестах и production-коде</td><td>Scope правила шире предполагаемой границы</td><td>Сравнить назначение файлов и путь от входа до вызова</td><td>Уточнить pattern или разделить конфигурацию, не выключать правило глобально</td></tr><tr><td>После обновления инструмента результатов стало меньше</td><td>Изменились parser, правила или область файлов</td><td>Сравнить версии, конфигурацию и diff отчётов</td><td>Назвать причину изменения до принятия нового baseline</td></tr><tr><td>Найден возможный опасный вызов</td><td>Анализатор не знает проверку или права</td><td>Проследить источник, преобразования, allowlist, entry point и права</td><td>Исправить код либо оставить сигнал до подтверждения контекста</td></tr><tr><td>Команда просит добавить исключение</td><td>Конкретный участок признан допустимым или правило шумит</td><td>Определить точный объект, владельца, причину и срок пересмотра</td><td>Сделать узкое исключение с rollback, а не менять весь проект</td></tr></tbody></table></div>\n<p>Термин «ложное срабатывание» тоже требует доказательства. Им можно назвать результат после проверки, которая показала, что условие правила не соответствует реальному риску в этом конкретном участке. Пока источник данных или путь выполнения неизвестны, точнее использовать статус «контекст не собран».</p>\n<h2>Порядок проверки одного результата</h2>\n<ol><li><strong>Зафиксируйте входы.</strong> Сохраните commit, версию инструмента, конфигурацию, идентификатор правила, файл, строку и полный текст сообщения.</li><li><strong>Прочитайте правило.</strong> Определите, какую конструкцию оно ищет, какие файлы входят в scope и какие исключения уже предусмотрены.</li><li><strong>Откройте ту же ревизию.</strong> Проверьте, что URI и строка относятся к тому же исходнику, который анализировал CI.</li><li><strong>Восстановите путь.</strong> Для security-сигнала найдите источник значения, преобразования, проверки, sink и entry point. Для lint-сигнала проверьте локальную конструкцию и намерение кода.</li><li><strong>Добавьте контекст.</strong> Запишите компонент, границу доверия, конфигурацию, владельца и область изменения. Неизвестное поле оставьте явно неизвестным.</li><li><strong>Выберите действие.</strong> Исправьте дефект, уточните правило для будущих результатов или примените узкое исключение только к доказанно допустимому месту.</li><li><strong>Проверьте отрицательный путь.</strong> Убедитесь, что другой commit, соседний файл и тот же источник данных не исчезли из проверки из-за широкого исключения.</li><li><strong>Сохраните повторяемый результат.</strong> Оставьте команду запуска, expected outcome и условие отката. Следующий инженер должен получить тот же вопрос и увидеть, что изменилось.</li></ol>\n<h2>Где проходит граница применимости</h2>\n<p>Статический анализ не видит автоматически весь runtime. Динамические импорты, feature flags, generated-код, переменные окружения, ответы внешних сервисов и права пользователя могут изменить путь выполнения. Если инструмент не строит нужную модель данных, его результат может быть слишком узким или слишком широким.</p>\n<p>Линтер не заменяет тесты. Типы не заменяют проверку протокола на реальном ответе. Security-правило не является доказательством exploitability и не измеряет риск для бизнеса. Даже корректный SARIF-файл доказывает только соответствие формату и наличие записанных результатов.</p>\n<p>Примеры с ESLint, <code>jq</code> и SARIF требуют установленного Node.js, lock-файла и настройки проекта. Имена <code>src/export.js</code>, <code>demo.no-command</code> и сообщение в JSON учебные. Команды не запускаются в статье и не дают результатов конкретного репозитория. Для другой CI-платформы изменятся путь к отчёту, способ загрузки и требования к permissions; модель фиксации commit, конфигурации и scope остаётся полезной, но её нужно сверить с документацией платформы.</p>\n<h2>Критерий готовности</h2>\n<p>Результат можно передать в review, когда другой инженер может повторить проверку на той же ревизии и ответить на четыре вопроса: какое правило сработало, где именно, в каком контексте и почему выбрано это действие. Для исправления должны быть видны тест или повторный запуск. Для изменения правила — новая версия гипотезы и её scope. Для исключения — точный объект, причина, владелец, срок пересмотра и способ отката.</p>\n<p>Если хотя бы одно звено неизвестно, итогом становится не «безопасно» и не «сломано», а конкретный незакрытый вопрос: какой файл, вход, настройка или владелец ещё не проверены. Такая модель сохраняет полезные сигналы, уменьшает случайные блокировки и не обещает статическому инструменту того, чего он не измерял.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://eslint.org/docs/latest/use/core-concepts/' target='_blank' rel='noopener noreferrer'>ESLint: Core Concepts</a> и <a href='https://eslint.org/docs/latest/use/command-line-interface' target='_blank' rel='noopener noreferrer'>Command Line Interface Reference</a> — назначение правил и параметры запуска, включая формат и файл вывода.</li><li><a href='https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os.html' target='_blank' rel='noopener noreferrer'>OASIS: SARIF Version 2.1.0 Plus Errata 01</a> — официальный стандарт формата результатов статического анализа.</li><li><a href='https://docs.github.com/en/code-security/how-tos/find-and-fix-code-vulnerabilities/integrate-with-existing-tools/upload-sarif-file' target='_blank' rel='noopener noreferrer'>GitHub Docs: Uploading a SARIF file</a> — пример загрузки отчётов, категории и partial fingerprints в конкретной платформе.</li><li><a href='https://csrc.nist.gov/pubs/sp/800/218/final' target='_blank' rel='noopener noreferrer'>NIST SP 800-218 SSDF Version 1.1</a> — официальная рамка безопасной разработки, где автоматические проверки рассматриваются как часть процесса, а не как единственное доказательство.</li></ul>"
}