{ "index": 166, "slug": "editorial-2023-05-field-static-analysis", "title": "Шумное правило статического анализа: как принять обратимое решение", "excerpt": "Один результат статического анализа не объясняет, нужно ли менять правило или подавлять сигнал. Разбираем контекст, scope, срок, rollback и проверяемый критерий готовности.", "contentHtml": "
В CI появляется результат правила, которое ищет передачу недоверенного значения в построение команды. Команда открывает строку, видит безопасный для своего сценария путь и предлагает выключить правило. На следующем запуске исчезают все результаты этой категории. Цена ошибки — потеря сигнала в коде, который ещё никто не проверил, и отсутствие ответа на простой вопрос: почему правило стало тише и кто разрешил это изменение.
\nОбратная ошибка тоже стоит дорого. Если каждое совпадение называть уязвимостью, review получает ложную срочность. Инженеры начинают закрывать предупреждения по тексту сообщения. После нескольких таких итераций доверие к анализатору падает. Поэтому результат анализатора — это повод проверить контекст, а не готовый вердикт.
\nSARIF хранит сведения об инструменте, правиле, результате и позиции в файле. Эти поля отвечают на вопрос «где и по какой гипотезе сработал анализатор». Они не доказывают, что ветка исполняется, значение пришло из сети или команда действительно запускается. Для этого нужен контекст проекта: источник значения, путь до опасного вызова, граница доверия, владелец кода и область действия решения.
\nВозьмём узкую учебную гипотезу: значение из параметра запроса передают в функцию, которая строит команду. Фрагмент показывает форму, которую правило может искать. Он не является результатом реального сканирования и не доказывает уязвимость.
\nfunction runReport(request) {\n const reportName = request.query.name;\n return runShell(`report --name ${reportName}`);\n}\n\n// Учебный контекст: нужно отдельно проверить источник,\n// экранирование, достижимость ветки и фактический sink.\nУ этого совпадения есть несколько независимых вопросов. Может ли внешний пользователь менять request.query.name? Проверяет ли код значение до вызова? Принимает ли runShell строку как команду или передаёт аргументы безопасным массивом? Попадает ли функция в собираемый артефакт? Пока ответов нет, допустимы только формулировки «результат требует проверки» и «контекст неполный».
keep оставляет результат видимым. Выбирайте его, когда сигнал понятен, но контекст ещё не собран. Это не признание уязвимости и не отказ от исправления. Это сохранение наблюдаемости до следующей проверки.
tune меняет гипотезу правила. Такое действие нужно, если правило захватывает форму, которая не соответствует его назначению: например, оно не отличает безопасный массив аргументов от конкатенации строки. Tune требует новой версии правила, короткого описания diff и проверки того, какие совпадения перестанут появляться.
suppress временно ограничивает один идентифицируемый результат. У него должны быть точный fingerprint, узкий scope, владелец, причина и дата окончания. Suppress не делает код безопасным. Он только задаёт политику отображения конкретного сигнала.
Глобальное disable не заменяет ни одно из этих действий. Оно меняет поведение правила для текущих и будущих результатов. Если проекту действительно нужна такая смена policy, её надо рассматривать отдельно: назвать категорию, оценить потерю сигнала, назначить владельца и определить способ вернуть правило. Нельзя прятать решение уровня policy в комментарии к одному результату.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Один результат выглядит безопасным | Нет источника значения и trust boundary | Сопоставить ruleId, revision, URI, строку, fingerprint и путь данных | Оставить keep до сбора контекста |
| Правило срабатывает на безопасном API | Гипотеза не различает строку и массив аргументов | Прочитать intent правила и проверить diff на минимальной паре примеров | Выбрать tune с новой revision |
| Один результат блокирует выпуск | Исключение не имеет точного scope или срока | Проверить fingerprint, owner, reviewBy и expiresOn | Разрешить только scoped suppress |
| Предлагают выключить всё правило | Результат одного участка смешан с policy категории | Оценить будущие результаты и отдельный rollback policy | Вынести global disable в отдельное решение |
| После изменения непонятно, что вернулось | Rollback удаляет запись, но не повторяет анализ | Сверить config diff и повторный отчёт на том же commit | Вернуть точное исключение или прежнюю revision и повторить проверку |
Запись должна быть короткой, но достаточной для повторной проверки. Для любого действия укажите owner, reason, action и reviewBy. Для tune добавьте новую ruleRevision и описание изменения. Для suppress добавьте точный fingerprint, scope и expiresOn. Дата следующего review не должна быть позже даты окончания исключения.
Причина «шум» ничего не объясняет. Хорошая причина связывает решение с проверяемым фактом: «вызов получает массив аргументов после нормализации; правило ожидает конкатенацию строки; diff проверен на двух учебных формах». В настоящем проекте сюда добавляют ссылку на задачу, commit или сохранённый контекст без секретов. Не добавляйте в публичную запись токены, пользовательские данные и полный фрагмент чувствительного кода.
\nНебольшой synthetic-пример полезен, когда нужно проверить сам контракт решения. Он должен отклонять неполные варианты: глобальное отключение, suppress без fingerprint, suppress без срока, tune без новой revision и context без trust boundary. Следующий фрагмент запускает только детерминированную проверку объектов в памяти. Он не читает репозиторий, не загружает rule pack, не запускает Semgrep, не меняет CI и не сообщает о найденной уязвимости.
\nconst decision = {\n action: 'suppress',\n owner: 'security-review',\n reason: 'Проверен один synthetic result',\n fingerprint: 'synthetic-fingerprint-command-001',\n scope: 'exact-result',\n reviewBy: '2023-05-20',\n expiresOn: '2023-05-27'\n};\n\nconst valid =\n decision.action === 'suppress' &&\n decision.fingerprint &&\n decision.scope === 'exact-result' &&\n decision.reviewBy <= decision.expiresOn;\n\nconsole.log(valid ? 'plan-valid' : 'plan-invalid');\n// Synthetic plan only: configuration is not applied.\nПроверка должна быть полезна прежде всего отрицательным исходом. Если убрать fingerprint, изменить scope на общий или поставить reviewBy после expiresOn, план обязан стать недействительным. Если тест проходит при action: 'disable-globally', контракт слишком слабый. Это проверка формы решения, а не доказательство качества правила и не оценка безопасности приложения.
Статический анализ не видит весь runtime-контекст. Правило может не знать о конфигурации, feature flag, генерации кода, маршруте данных, правах пользователя и фактическом deploy-артефакте. SARIF не превращает позицию в файле в доказательство исполнения. Одинаковая строка может быть опасной в одном сервисе и безопасной в другом.
\nФормат исключений и fingerprint зависит от конкретного анализатора и версии CLI. Не переносите поля из учебного объекта в конфигурацию без проверки официальной документации. Не называйте synthetic result находкой, не заявляйте снижение числа ложных срабатываний без измерения и не утверждайте, что опасные случаи не потеряны без проверки на выбранном наборе кода.
\nRollback тоже имеет границу. Он возвращает видимость правила или убирает scoped exception. Он не отменяет уже выпущенный код и не доказывает безопасность старого состояния. Если исключение успело скрыть другие результаты, их нужно искать отдельным повторным анализом.
\nРешение готово, когда другой инженер может по записи ответить на пять вопросов: какой result разбирали, какую гипотезу проверяли, почему выбрали keep, tune или suppress, кто и когда пересматривает решение, как вернуть прежнюю видимость. Для tune должна существовать новая revision и проверенный diff. Для suppress должны совпадать fingerprint и scope, а expiry должна быть будущей. Для rollback должен быть выполнен повторный анализ на том же commit или явно зафиксировано, почему это невозможно.
\nЕсли хотя бы один ответ отсутствует, глобальное выключение не является исправлением. Оставьте сигнал видимым, назначьте владельца и доберите контекст. Так статический анализ остаётся управляемым источником технических сигналов, а не безымянным переключателем громкости.
\n