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

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

\n

Обратная ошибка стоит не меньше. Если каждое совпадение называть уязвимостью, review получает ложную срочность. Инженеры начинают закрывать предупреждения по тексту сообщения, а затем перестают доверять анализатору. Результат анализатора — это повод проверить контекст, а не готовый вердикт. В этой статье keep, tune и scoped suppress — названия проектной policy, а не универсальные поля SARIF и не обещание, что любой анализатор понимает эти действия.

\n

Отделите результат от вывода

\n

SARIF (Static Analysis Results Interchange Format) описывает обмен результатами статического анализа. В записи можно найти инструмент, правило, результат, сообщение, расположение в артефакте и уровень сигнала. Эти поля отвечают на вопросы «какая гипотеза сработала» и «где её обнаружили». Они сами по себе не доказывают, что ветка исполняется, значение пришло от внешнего пользователя или опасный вызов достижим в выпущенном артефакте.

\n

У результата есть ещё одна важная граница: fingerprint нужен системе управления результатами для сопоставления логически одинаковых сигналов между запусками. Спецификация допускает, что fingerprint добавит именно result management system после загрузки отчёта; прямой производитель SARIF обычно не должен выдумывать его без устойчивого алгоритма. Поэтому строка, которую команда вручную назвала fingerprint, не становится стабильным идентификатором только из-за имени.

\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

Начинайте с наблюдаемого результата, а не с предполагаемого исправления. Сохраните ruleId, revision правила, URI, строку, уровень, сообщение и идентификатор сопоставления, если его выдала система управления результатами. Затем пройдите значение от входа до операции, на которую указывает правило. На каждом переходе фиксируйте не впечатление, а проверяемый факт: какой тип данных получен, какая функция вызвана и в какой сборочный путь она попадает.

\n

Граница доверия находится там, где данные переходят из внешнего или неуправляемого источника в код, принимающий решение. Для HTTP-запроса такой границей может быть контроллер; для очереди — consumer; для файла конфигурации — загрузчик и права на файл. Валидация меняет риск, но её наличие нужно подтвердить кодом и тестом. Название функции вроде sanitize не является доказательством корректного экранирования.

\n
Симптом, проверка и допустимое решение
Что видноЧего не хватаетПроверкаРешение policy
Один результат выглядит безопаснымИсточник и trust boundaryПроследить значение до sink и проверить достижимостьkeep до завершения triage
Сигнал появляется на безопасном APIПравило различает формы слишком грубоСравнить intent правила с двумя минимальными примерамиtune с новой revision и diff
Один результат мешает выпускуТочный scope, владелец и срокСверить fingerprint, owner, reviewBy и expiresOnТолько scoped suppress
Предлагают выключить правило целикомОценка будущей потери сигналаРассмотреть изменение категории как отдельную policyОтдельное решение с rollback
После изменения непонятен возвратПовторный запуск на том же commitСравнить конфигурацию и новый отчётВернуть узкое исключение или revision
\n

Проверьте правило на минимальной паре

\n

Если подозрение относится к гипотезе правила, сначала подготовьте два маленьких примера: один должен соответствовать намерению правила, второй — быть безопасной формой, которую оно не должно захватывать. Запускайте одну и ту же версию CLI с одной и той же конфигурацией. Команда ниже показывает общий путь для локального Semgrep-скана; имя конфигурации и каталог замените своими. Она сохраняет SARIF-файл, но не отвечает за достижимость кода или эксплуатацию сигнала.

\n
semgrep --version\nsemgrep scan \\\n  --config rules/command-input.yml \\\n  --sarif \\\n  --output /tmp/command-input.sarif \\\n  src/
\n

После запуска сравните не только число строк. Проверьте ruleId, revision, URI и содержимое results. Если CLI обновился, формат дополнительных полей и поддерживаемые опции нужно сверить с документацией именно этой версии. Отдельный diff правила должен объяснять, какой класс безопасных совпадений исключается и какие опасные формы остаются в области проверки.

\n

Выберите узкое действие

\n

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

\n

tune меняет гипотезу правила. Такое действие оправданно, если правило захватывает форму, которая не соответствует его назначению. Укажите новую revision, покажите минимальный diff и проверьте положительный и отрицательный пример. Не называйте tune снижением false-positive rate без измерения на заранее выбранном наборе кода.

\n

scoped suppress ограничивает один идентифицируемый результат. В нашей policy у него должны быть точный fingerprint, узкий scope, владелец, причина, дата следующей проверки и дата окончания. Suppress не делает код безопасным: он меняет видимость конкретного сигнала. Формат исключения и его область зависят от инструмента, поэтому эти поля нельзя механически перенести в конфигурацию другого анализатора.

\n

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

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

Запишите evidence так, чтобы его повторили

\n

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

\n

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

\n

Проверьте отрицательные пути

\n

До применения policy проверьте, что неполная запись не проходит. Нужна не проверка непустых строк, а минимальный контракт: разрешены только три действия; suppress требует точного scope и двух корректных дат; tune требует новой revision. Фрагмент ниже запускается в Node.js и работает только с объектом в памяти. Он не читает репозиторий, не запускает сканер и не применяет конфигурацию.

\n
node --input-type=module <<'NODE'\nconst decision = {\n  action: 'scoped-suppress',\n  owner: 'security-review',\n  reason: 'Проверены источник, граница доверия и sink',\n  fingerprint: 'result-command-001',\n  scope: 'exact-result',\n  reviewBy: '2023-05-20',\n  expiresOn: '2023-05-27',\n  ruleRevision: 'command-input/v3'\n};\n\nconst allowedActions = new Set(['keep', 'tune', 'scoped-suppress']);\nconst isDate = (value) => {\n  if (!/^\\\\d{4}-\\\\d{2}-\\\\d{2}$/.test(value)) return false;\n  return new Date(value + 'T00:00:00Z').toISOString().startsWith(value);\n};\n\nconst valid =\n  allowedActions.has(decision.action) &&\n  decision.owner && decision.reason &&\n  (decision.action !== 'tune' || decision.ruleRevision !== 'command-input/v2') &&\n  (decision.action !== 'scoped-suppress' ||\n    (decision.fingerprint && decision.scope === 'exact-result' &&\n      isDate(decision.reviewBy) && isDate(decision.expiresOn) &&\n      decision.reviewBy <= decision.expiresOn));\n\nconsole.log(valid ? 'plan-valid' : 'plan-invalid');\nNODE
\n

Положительный путь печатает plan-valid. Для отрицательной проверки удалите fingerprint, поставьте 2023-02-31, замените scope на общий или используйте действие disable-globally: во всех случаях должен получиться plan-invalid. Сравнение дат строкой безопасно только после строгой проверки формата и календаря, как в примере. Без этого значение вроде 31 февраля может пройти поверхностную проверку.

\n

Сделайте rollback проверяемым

\n

Rollback — не удаление строки из конфигурации. Для suppress удалите именно это исключение, повторите анализ на том же commit и проверьте, что ожидаемый результат снова виден. Для tune верните прежнюю revision, повторите минимальную пару и сравните отчёты. Если повторный анализ невозможен, зафиксируйте причину и риск, а не называйте возврат завершённым.

\n

У rollback есть ограничение: он возвращает видимость правила или убирает узкое исключение. Он не отменяет уже выпущенный код и не доказывает безопасность старого состояния. Если исключение успело скрыть другие результаты, их нужно искать отдельным повторным запуском и сверкой baseline.

\n

Ограничения применимости

\n

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

\n

Команда Semgrep в примере — ориентир для CLI, а не зафиксированный контракт всех будущих версий. Закрепите версию в CI, сохраните вывод semgrep --version и проверьте опции в документации перед миграцией. Не переносите названия keep, tune и scoped-suppress в инструмент без адаптера и теста его реального формата.

\n

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

\n

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

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

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

\n

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

\n" }