DarkRiDDeR12 мин

Шумное правило статанализа: не выключать до классификации сигнала

БезопасностьРазработка

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

Цена глобального выключения видна не в момент удаления правила. Исчезает сигнал для опасной категории — построения команды из недоверенного значения, — и следующий похожий участок больше не попадёт в очередь review. Обратная ошибка тоже дорога: если каждое совпадение объявлять уязвимостью, инженер тратит время на ложную аварийность, а сообщения перестают читать. Рабочий ответ находится между этими крайностями: сначала короткая классификация, затем явное решение с владельцем и сроком пересмотра.

Совпадение формы не является выводом о коде

Опасная категория в этой заметке узкая: правило видит передачу значения из условно недоверенного источника в функцию, которая строит команду. Такая форма заслуживает внимания, но сама строка не говорит, откуда реально пришло значение, выполняется ли ветка, есть ли проверка до вызова и попадает ли код в нужный артефакт. Поэтому нельзя называть SARIF result «подтверждённой уязвимостью» только по ruleId и номеру строки.

SARIF 2.1.0 — стандартный формат обмена результатами статического анализа. Он может хранить описание tool, rule, result и location. Формат упрощает передачу сигнала между инструментами, но не добавляет контекст, которого анализатор не собрал. В частности, uri и startLine указывают на позицию, а не на реальное исполнение. В статье дальше SARIF нужен как аккуратный контейнер фактов: каждое поле получает отдельный смысл и отдельную границу.

Первичная классификация одного сигнала
СлойЧто можно записатьЧего не следует писатьСледующая проверка
Правилоid, revision, категория, default level«правило доказало риск»прочитать intent и diff правила
ResultruleId, message, fingerprint, level«найден CVE»сверить result с точной версией правила
LocationURI и строка как указатель«строка точно исполняется»проверить ревизию исходника и entry point
Контекстasset, boundary, owner, scope релиза«контекст очевиден из SARIF»собрать запись у владельца компонента
Решениеkeep, tune или ограниченное suppress«выключить для всех»зафиксировать reason, reviewer и rollback

Учебный fixture: формируем только synthetic SARIF

Ниже запускается код этого пакета. Он создаёт один объект SARIF 2.1.0 в памяти с именем правила demo.untrusted-command-construction, URI src/demo-command.js и строкой 14. Эти значения помечены synthetic. Функция не читает файл с таким именем, не вызывает Semgrep, не выполняет runShell и не подключается к CI. Поэтому PASS — это проверка контракта учебного объекта, а не отчёт об анализе настоящего репозитория.

node web/scripts/upgrade-2023-05.mjs --verify-fixture

# Все URI, строка, fingerprint, правило и context ниже synthetic.
# Команда создаёт deterministic object только в памяти: не читает проект,
# не запускает Semgrep/другой scanner, не открывает сеть и не выводит CVE.
# PASS проверяет контракт SARIF 2.1.0, границы context и решения в учебном плане.
# PASS не доказывает finding, покрытие, достижимость, exploitability или безопасность.

Внутри buildSyntheticSarifLog() result связывается с ruleId, ruleIndex, message, partial fingerprint и location. Рядом прямо записаны границы: sourceRead: not-performed-by-fixture, scannerExecution: not-performed-by-fixture, reachability: not-assessed-by-fixture. Это важнее красивого JSON: потребитель не должен по умолчанию превращать случайный result в утверждение о запуске кода. Fixture ещё и отвергает другой kind входа, чтобы реальный scanner output нельзя было незаметно подложить под учебную модель.

Воронка классификации synthetic сигнала: rule и SARIF result проходят через проверку location и project context; на выходе остаются keep, tune или ограниченное suppress, а глобальное выключение исключено.
Схема показывает порядок разбора одного synthetic результата. Она не отражает реальные findings, coverage, количество срабатываний, данные проекта или эффективность правила.

Контекст нужно собирать отдельно

Минимальная context record для этого типа сигнала состоит из пяти полей: asset, entry point, trust boundary, owner и release scope. Asset отвечает, какая часть системы обсуждается. Entry point называет предполагаемый вход в сценарий. Trust boundary отмечает, откуда значение стало недоверенным для данной проверки. Owner выбирает человека или роль, которая может подтвердить устройство компонента. Release scope связывает обсуждение с конкретным изменением, а не со всем продуктом.

Каждое поле можно оставить неизвестным; тогда это надо назвать явно. Если не найден entry point, статус — не «ложное срабатывание», а «контекст неполный». Если неизвестна trust boundary, нельзя писать, что значение безопасно. Если rule сработал в generated fixture, это ещё не ответ про поставленный пакет. Такой стиль записи сдерживает обе ошибки: он не повышает сигнал до security verdict и не скрывает вопрос под словом noise.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. В pull request повторяется один ruleId, а в комментариях есть только «мешает» или «ложный плюс». Зафиксируйте ruleId, revision, result fingerprint, URI и строку, не добавляя вывод о риске.
  2. Причина. Совпадение синтаксической формы, контекст вызова и политика правила обсуждаются одним предложением. Поэтому reviewer не понимает, что именно предлагается изменить.
  3. Проверка. Сверьте rule intent и версию анализатора, затем заполните asset, entry point, trust boundary, owner и scope. Для реального проекта этот шаг делает владелец компонента, а не SARIF parser.
  4. Действие. Если контекст неполный, оставьте result видимым и создайте короткую задачу на сбор данных. Если контекст собран, выберите keep, точечный tune с новой revision либо scoped suppress с валидными reviewBy и expiresOn.
  5. Проверка контракта. Выполните node web/scripts/upgrade-2023-05.mjs --verify-fixture. PASS обязан сохранить статусы not-assessed и not-determined; если fixture сделал verdict, модель испорчена.
  6. Rollback. Любое учебное решение хранит прежнее состояние visible-in-synthetic-plan. В настоящем проекте rollback — отдельный diff конфигурации и повторный review, а не надежда на историю CI.

Почему не начинать с исключения

Исключение полезно, когда уже известен его объект. Для result это может быть точный fingerprint, конкретный rule revision, причина, owner и дата, когда решение нужно пересмотреть. Даты должны быть календарными, а reviewBy не может наступать после expiresOn: иначе исключение успеет исчезнуть раньше обязательной проверки. Без этих полей suppress — это не документированная граница, а удаление наблюдаемости. Глобальное выключение ещё шире: оно меняет судьбу будущих результатов, которых reviewer пока не видел. Поэтому fixture вообще не принимает action disable-globally; он не даёт объявить широкое действие безопасным только потому, что оно удобно.

Точечный tune отличается от suppression. Tune меняет саму гипотезу правила: например, добавляет условие о явной boundary marker или уточняет pattern. Значит, в решении нужны revision правила и описание diff. Suppression сохраняет rule, но временно ограничивает обработку одного идентифицируемого result. Эти варианты нельзя подменять: если проблема в неточном pattern, вечное исключение прячет долг правила; если проблема в одном переходном месте, новый rule revision может неоправданно сузить будущие сигналы.

Ограничения модели и следующий шаг

Fixture не знает язык настоящего проекта, конфигурацию анализатора, baseline, generated code, feature flags, права доступа, историю результата, правило подавления, CI и production runtime. Он не проверяет SARIF JSON Schema внешним валидатором и не доказывает, что учебный YAML-фрагмент ниже соответствует конкретному запуску Semgrep. Использованная версия Semgrep 1.20.0 опубликована 28 апреля 2023 года; исходник этой версии объявляет опцию --sarif, но это не аттестация ваших настроек или результата.

Следующий шаг — взять один реальный ruleId без копирования чувствительных фрагментов кода и заполнить пять полей context record. Затем записать одно из трёх решений: оставить правило, подготовить rule diff или создать ограниченное исключение. Если доказательств пока нет, решение должно звучать короче: «контекст не собран, правило остаётся видимым, владелец и дата проверки назначены». Такая запись не выдаёт опыт за метрику и не заставляет команду верить шумному сообщению на слово.

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