8 lines
22 KiB
JSON
8 lines
22 KiB
JSON
{
|
||
"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>"
|
||
}
|