Правило статического анализа срабатывает на строках, которые команда считает безопасными в своём контексте. Через несколько повторов появляется просьба выключить правило: оно шумит, отвлекает review и мешает выпуску. Проблема не в самом раздражении. В одном действии смешиваются три разных вопроса: что именно совпало с правилом, какой контекст у этой строки и какое изменение конфигурации действительно нужно.
Цена глобального выключения видна не в момент удаления правила. Исчезает сигнал для опасной категории — построения команды из недоверенного значения, — и следующий похожий участок больше не попадёт в очередь review. Обратная ошибка тоже дорога: если каждое совпадение объявлять уязвимостью, инженер тратит время на ложную аварийность, а сообщения перестают читать. Рабочий ответ находится между этими крайностями: сначала короткая классификация, затем явное решение с владельцем и сроком пересмотра.
Совпадение формы не является выводом о коде
Опасная категория в этой заметке узкая: правило видит передачу значения из условно недоверенного источника в функцию, которая строит команду. Такая форма заслуживает внимания, но сама строка не говорит, откуда реально пришло значение, выполняется ли ветка, есть ли проверка до вызова и попадает ли код в нужный артефакт. Поэтому нельзя называть SARIF result «подтверждённой уязвимостью» только по ruleId и номеру строки.
SARIF 2.1.0 — стандартный формат обмена результатами статического анализа. Он может хранить описание tool, rule, result и location. Формат упрощает передачу сигнала между инструментами, но не добавляет контекст, которого анализатор не собрал. В частности, uri и startLine указывают на позицию, а не на реальное исполнение. В статье дальше SARIF нужен как аккуратный контейнер фактов: каждое поле получает отдельный смысл и отдельную границу.
| Слой | Что можно записать | Чего не следует писать | Следующая проверка |
|---|---|---|---|
| Правило | id, revision, категория, default level | «правило доказало риск» | прочитать intent и diff правила |
| Result | ruleId, message, fingerprint, level | «найден CVE» | сверить result с точной версией правила |
| Location | URI и строка как указатель | «строка точно исполняется» | проверить ревизию исходника и 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 нельзя было незаметно подложить под учебную модель.
Контекст нужно собирать отдельно
Минимальная 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.
Маршрут: симптом → причина → проверка → действие
- Симптом. В pull request повторяется один ruleId, а в комментариях есть только «мешает» или «ложный плюс». Зафиксируйте ruleId, revision, result fingerprint, URI и строку, не добавляя вывод о риске.
- Причина. Совпадение синтаксической формы, контекст вызова и политика правила обсуждаются одним предложением. Поэтому reviewer не понимает, что именно предлагается изменить.
- Проверка. Сверьте rule intent и версию анализатора, затем заполните asset, entry point, trust boundary, owner и scope. Для реального проекта этот шаг делает владелец компонента, а не SARIF parser.
- Действие. Если контекст неполный, оставьте result видимым и создайте короткую задачу на сбор данных. Если контекст собран, выберите keep, точечный tune с новой revision либо scoped suppress с валидными reviewBy и expiresOn.
- Проверка контракта. Выполните
node web/scripts/upgrade-2023-05.mjs --verify-fixture. PASS обязан сохранить статусыnot-assessedиnot-determined; если fixture сделал verdict, модель испорчена. - 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 или создать ограниченное исключение. Если доказательств пока нет, решение должно звучать короче: «контекст не собран, правило остаётся видимым, владелец и дата проверки назначены». Такая запись не выдаёт опыт за метрику и не заставляет команду верить шумному сообщению на слово.
Проверяемые источники
- OASIS: Static Analysis Results Interchange Format (SARIF) Version 2.1.0, OASIS Standard, 27 марта 2020 — нормативная спецификация формата результата статического анализа. Она задаёт формат обмена, а не достоверность отдельного result и не решение об исправлении.
- OASIS: SARIF 2.1.0 JSON Schema, 27 марта 2020 — схема формата, доступная вместе со стандартом. Этот fixture собирает учебный объект в памяти и не выполняет валидацию внешним schema validator.
- Semgrep v1.20.0: официальный release, 28 апреля 2023 — версионная точка отсчёта, опубликованная до мая 2023. Она нужна для воспроизводимости, но не говорит, как конкретное правило поведёт себя в вашем репозитории.
- Semgrep v1.20.0: исходник CLI scan.py, опция --sarif — официальный исходник фиксированной версии: CLI объявляет output format SARIF. Он не подтверждает, что этот учебный fixture запускал Semgrep или получил finding из исходного кода.
- NIST SP 800-218, Secure Software Development Framework Version 1.1, Final, 3 февраля 2022 — официальный документ по безопасной разработке, доступный до мая 2023. Здесь он служит рамкой для владения решением и evidence, а не доказательством эффективности правила.