{ "index": 166, "slug": "editorial-2023-05-field-static-analysis", "title": "Шумное правило статического анализа: как принять обратимое решение", "excerpt": "Один результат статического анализа не объясняет, нужно ли менять правило или подавлять сигнал. Разбираем контекст, scope, срок, rollback и проверяемый критерий готовности.", "contentHtml": "

В CI появляется результат правила, которое ищет передачу недоверенного значения в построение команды. Команда открывает строку, видит безопасный для своего сценария путь и предлагает выключить правило. На следующем запуске исчезают все результаты этой категории. Цена ошибки — потеря сигнала в коде, который ещё никто не проверил, и отсутствие ответа на простой вопрос: почему правило стало тише и кто разрешил это изменение.

\n

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

\n

Сначала отделите результат от вывода

\n

SARIF хранит сведения об инструменте, правиле, результате и позиции в файле. Эти поля отвечают на вопрос «где и по какой гипотезе сработал анализатор». Они не доказывают, что ветка исполняется, значение пришло из сети или команда действительно запускается. Для этого нужен контекст проекта: источник значения, путь до опасного вызова, граница доверия, владелец кода и область действия решения.

\n

Возьмём узкую учебную гипотезу: значение из параметра запроса передают в функцию, которая строит команду. Фрагмент показывает форму, которую правило может искать. Он не является результатом реального сканирования и не доказывает уязвимость.

\n
function runReport(request) {\n  const reportName = request.query.name;\n  return runShell(`report --name ${reportName}`);\n}\n\n// Учебный контекст: нужно отдельно проверить источник,\n// экранирование, достижимость ветки и фактический sink.
\n

У этого совпадения есть несколько независимых вопросов. Может ли внешний пользователь менять request.query.name? Проверяет ли код значение до вызова? Принимает ли runShell строку как команду или передаёт аргументы безопасным массивом? Попадает ли функция в собираемый артефакт? Пока ответов нет, допустимы только формулировки «результат требует проверки» и «контекст неполный».

\n

Три действия вместо глобального выключателя

\n

keep оставляет результат видимым. Выбирайте его, когда сигнал понятен, но контекст ещё не собран. Это не признание уязвимости и не отказ от исправления. Это сохранение наблюдаемости до следующей проверки.

\n

tune меняет гипотезу правила. Такое действие нужно, если правило захватывает форму, которая не соответствует его назначению: например, оно не отличает безопасный массив аргументов от конкатенации строки. Tune требует новой версии правила, короткого описания diff и проверки того, какие совпадения перестанут появляться.

\n

suppress временно ограничивает один идентифицируемый результат. У него должны быть точный fingerprint, узкий scope, владелец, причина и дата окончания. Suppress не делает код безопасным. Он только задаёт политику отображения конкретного сигнала.

\n

Глобальное disable не заменяет ни одно из этих действий. Оно меняет поведение правила для текущих и будущих результатов. Если проекту действительно нужна такая смена policy, её надо рассматривать отдельно: назвать категорию, оценить потерю сигнала, назначить владельца и определить способ вернуть правило. Нельзя прятать решение уровня policy в комментарии к одному результату.

\n

Минимальный контракт решения

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
Один результат выглядит безопаснымНет источника значения и 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 и повторить проверку
\n

Как записать evidence

\n

Запись должна быть короткой, но достаточной для повторной проверки. Для любого действия укажите owner, reason, action и reviewBy. Для tune добавьте новую ruleRevision и описание изменения. Для suppress добавьте точный fingerprint, scope и expiresOn. Дата следующего review не должна быть позже даты окончания исключения.

\n

Причина «шум» ничего не объясняет. Хорошая причина связывает решение с проверяемым фактом: «вызов получает массив аргументов после нормализации; правило ожидает конкатенацию строки; diff проверен на двух учебных формах». В настоящем проекте сюда добавляют ссылку на задачу, commit или сохранённый контекст без секретов. Не добавляйте в публичную запись токены, пользовательские данные и полный фрагмент чувствительного кода.

\n

Учебная проверка отрицательных путей

\n

Небольшой synthetic-пример полезен, когда нужно проверить сам контракт решения. Он должен отклонять неполные варианты: глобальное отключение, suppress без fingerprint, suppress без срока, tune без новой revision и context без trust boundary. Следующий фрагмент запускает только детерминированную проверку объектов в памяти. Он не читает репозиторий, не загружает rule pack, не запускает Semgrep, не меняет CI и не сообщает о найденной уязвимости.

\n
const 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', контракт слишком слабый. Это проверка формы решения, а не доказательство качества правила и не оценка безопасности приложения.

\n
\"Гейт
Сначала собирается контекст результата, затем выбирается узкое действие. Схема показывает policy-контракт, а не запуск анализатора, реальные findings или эффект в production.
\n

Порядок действий

\n
  1. Зафиксируйте симптом. Сохраните ruleId, revision, URI, строку, уровень, сообщение и fingerprint. Не начинайте с изменения конфигурации.
  2. Восстановите контекст. Найдите источник значения, путь до sink, trust boundary, владельца кода и артефакт, в который попадает модуль.
  3. Проверьте intent правила. Прочитайте описание, версию и diff. Отдельно отметьте, что результат показывает, а чего не показывает.
  4. Выберите действие. Используйте keep для неполного контекста, tune для неверной гипотезы, scoped suppress для одного проверенного результата. Global disable вынесите в отдельную policy.
  5. Заполните evidence. Добавьте owner, reason, scope, fingerprint и даты. Для tune укажите новую revision и ожидаемую границу.
  6. Проверьте отрицательный путь. Убедитесь, что неполный контекст, общий suppress и просроченные даты отклоняются.
  7. Подготовьте rollback. Для suppress удалите точное исключение и повторите анализ. Для tune верните прежнюю revision и сравните diff. Не считайте rollback выполненным по одному изменению файла.
\n

Ограничения

\n

Статический анализ не видит весь runtime-контекст. Правило может не знать о конфигурации, feature flag, генерации кода, маршруте данных, правах пользователя и фактическом deploy-артефакте. SARIF не превращает позицию в файле в доказательство исполнения. Одинаковая строка может быть опасной в одном сервисе и безопасной в другом.

\n

Формат исключений и fingerprint зависит от конкретного анализатора и версии CLI. Не переносите поля из учебного объекта в конфигурацию без проверки официальной документации. Не называйте synthetic result находкой, не заявляйте снижение числа ложных срабатываний без измерения и не утверждайте, что опасные случаи не потеряны без проверки на выбранном наборе кода.

\n

Rollback тоже имеет границу. Он возвращает видимость правила или убирает scoped exception. Он не отменяет уже выпущенный код и не доказывает безопасность старого состояния. Если исключение успело скрыть другие результаты, их нужно искать отдельным повторным анализом.

\n

Критерий готовности

\n

Решение готово, когда другой инженер может по записи ответить на пять вопросов: какой result разбирали, какую гипотезу проверяли, почему выбрали keep, tune или suppress, кто и когда пересматривает решение, как вернуть прежнюю видимость. Для tune должна существовать новая revision и проверенный diff. Для suppress должны совпадать fingerprint и scope, а expiry должна быть будущей. Для rollback должен быть выполнен повторный анализ на том же commit или явно зафиксировано, почему это невозможно.

\n

Если хотя бы один ответ отсутствует, глобальное выключение не является исправлением. Оставьте сигнал видимым, назначьте владельца и доберите контекст. Так статический анализ остаётся управляемым источником технических сигналов, а не безымянным переключателем громкости.

\n

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

\n" }