8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 166,
|
||
"slug": "editorial-2023-05-field-static-analysis",
|
||
"title": "Шумное правило статического анализа: как принять обратимое решение",
|
||
"excerpt": "Один результат статического анализа не объясняет, нужно ли менять правило или подавлять сигнал. Разбираем контекст, scope, срок, rollback и проверяемый критерий готовности.",
|
||
"contentHtml": "<p>В CI появляется результат правила, которое ищет передачу недоверенного значения в построение команды. Команда открывает строку, видит безопасный для своего сценария путь и предлагает выключить правило. На следующем запуске исчезают все результаты этой категории. Цена ошибки — потеря сигнала в коде, который ещё никто не проверил, и отсутствие ответа на простой вопрос: почему правило стало тише и кто разрешил это изменение.</p>\n<p>Обратная ошибка тоже стоит дорого. Если каждое совпадение называть уязвимостью, review получает ложную срочность. Инженеры начинают закрывать предупреждения по тексту сообщения. После нескольких таких итераций доверие к анализатору падает. Поэтому результат анализатора — это повод проверить контекст, а не готовый вердикт.</p>\n<h2>Сначала отделите результат от вывода</h2>\n<p>SARIF хранит сведения об инструменте, правиле, результате и позиции в файле. Эти поля отвечают на вопрос «где и по какой гипотезе сработал анализатор». Они не доказывают, что ветка исполняется, значение пришло из сети или команда действительно запускается. Для этого нужен контекст проекта: источник значения, путь до опасного вызова, граница доверия, владелец кода и область действия решения.</p>\n<p>Возьмём узкую учебную гипотезу: значение из параметра запроса передают в функцию, которая строит команду. Фрагмент показывает форму, которую правило может искать. Он не является результатом реального сканирования и не доказывает уязвимость.</p>\n<pre><code>function runReport(request) {\n const reportName = request.query.name;\n return runShell(`report --name ${reportName}`);\n}\n\n// Учебный контекст: нужно отдельно проверить источник,\n// экранирование, достижимость ветки и фактический sink.</code></pre>\n<p>У этого совпадения есть несколько независимых вопросов. Может ли внешний пользователь менять <code>request.query.name</code>? Проверяет ли код значение до вызова? Принимает ли <code>runShell</code> строку как команду или передаёт аргументы безопасным массивом? Попадает ли функция в собираемый артефакт? Пока ответов нет, допустимы только формулировки «результат требует проверки» и «контекст неполный».</p>\n<h2>Три действия вместо глобального выключателя</h2>\n<p><code>keep</code> оставляет результат видимым. Выбирайте его, когда сигнал понятен, но контекст ещё не собран. Это не признание уязвимости и не отказ от исправления. Это сохранение наблюдаемости до следующей проверки.</p>\n<p><code>tune</code> меняет гипотезу правила. Такое действие нужно, если правило захватывает форму, которая не соответствует его назначению: например, оно не отличает безопасный массив аргументов от конкатенации строки. Tune требует новой версии правила, короткого описания diff и проверки того, какие совпадения перестанут появляться.</p>\n<p><code>suppress</code> временно ограничивает один идентифицируемый результат. У него должны быть точный fingerprint, узкий scope, владелец, причина и дата окончания. Suppress не делает код безопасным. Он только задаёт политику отображения конкретного сигнала.</p>\n<p>Глобальное <code>disable</code> не заменяет ни одно из этих действий. Оно меняет поведение правила для текущих и будущих результатов. Если проекту действительно нужна такая смена policy, её надо рассматривать отдельно: назвать категорию, оценить потерю сигнала, назначить владельца и определить способ вернуть правило. Нельзя прятать решение уровня policy в комментарии к одному результату.</p>\n<h2>Минимальный контракт решения</h2>\n<div class=\"table-scroll\"><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Один результат выглядит безопасным</td><td>Нет источника значения и trust boundary</td><td>Сопоставить ruleId, revision, URI, строку, fingerprint и путь данных</td><td>Оставить <code>keep</code> до сбора контекста</td></tr><tr><td>Правило срабатывает на безопасном API</td><td>Гипотеза не различает строку и массив аргументов</td><td>Прочитать intent правила и проверить diff на минимальной паре примеров</td><td>Выбрать <code>tune</code> с новой revision</td></tr><tr><td>Один результат блокирует выпуск</td><td>Исключение не имеет точного scope или срока</td><td>Проверить fingerprint, owner, reviewBy и expiresOn</td><td>Разрешить только scoped <code>suppress</code></td></tr><tr><td>Предлагают выключить всё правило</td><td>Результат одного участка смешан с policy категории</td><td>Оценить будущие результаты и отдельный rollback policy</td><td>Вынести global disable в отдельное решение</td></tr><tr><td>После изменения непонятно, что вернулось</td><td>Rollback удаляет запись, но не повторяет анализ</td><td>Сверить config diff и повторный отчёт на том же commit</td><td>Вернуть точное исключение или прежнюю revision и повторить проверку</td></tr></tbody></table></div>\n<h2>Как записать evidence</h2>\n<p>Запись должна быть короткой, но достаточной для повторной проверки. Для любого действия укажите <code>owner</code>, <code>reason</code>, <code>action</code> и <code>reviewBy</code>. Для <code>tune</code> добавьте новую <code>ruleRevision</code> и описание изменения. Для <code>suppress</code> добавьте точный <code>fingerprint</code>, scope и <code>expiresOn</code>. Дата следующего review не должна быть позже даты окончания исключения.</p>\n<p>Причина «шум» ничего не объясняет. Хорошая причина связывает решение с проверяемым фактом: «вызов получает массив аргументов после нормализации; правило ожидает конкатенацию строки; diff проверен на двух учебных формах». В настоящем проекте сюда добавляют ссылку на задачу, commit или сохранённый контекст без секретов. Не добавляйте в публичную запись токены, пользовательские данные и полный фрагмент чувствительного кода.</p>\n<h2>Учебная проверка отрицательных путей</h2>\n<p>Небольшой synthetic-пример полезен, когда нужно проверить сам контракт решения. Он должен отклонять неполные варианты: глобальное отключение, suppress без fingerprint, suppress без срока, tune без новой revision и context без trust boundary. Следующий фрагмент запускает только детерминированную проверку объектов в памяти. Он не читает репозиторий, не загружает rule pack, не запускает Semgrep, не меняет CI и не сообщает о найденной уязвимости.</p>\n<pre><code>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.</code></pre>\n<p>Проверка должна быть полезна прежде всего отрицательным исходом. Если убрать fingerprint, изменить scope на общий или поставить <code>reviewBy</code> после <code>expiresOn</code>, план обязан стать недействительным. Если тест проходит при <code>action: 'disable-globally'</code>, контракт слишком слабый. Это проверка формы решения, а не доказательство качества правила и не оценка безопасности приложения.</p>\n<figure><img src=\"/assets/editorial/2023/static-analysis-2023-decision-gate.svg\" alt=\"Гейт решения для результата статического анализа: полный контекст ведёт к keep, tune или scoped suppress; глобальное отключение требует отдельной policy; rollback возвращает прежнюю видимость.\" loading=\"lazy\" /><figcaption>Сначала собирается контекст результата, затем выбирается узкое действие. Схема показывает policy-контракт, а не запуск анализатора, реальные findings или эффект в production.</figcaption></figure>\n<h2>Порядок действий</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Сохраните ruleId, revision, URI, строку, уровень, сообщение и fingerprint. Не начинайте с изменения конфигурации.</li><li><strong>Восстановите контекст.</strong> Найдите источник значения, путь до sink, trust boundary, владельца кода и артефакт, в который попадает модуль.</li><li><strong>Проверьте intent правила.</strong> Прочитайте описание, версию и diff. Отдельно отметьте, что результат показывает, а чего не показывает.</li><li><strong>Выберите действие.</strong> Используйте keep для неполного контекста, tune для неверной гипотезы, scoped suppress для одного проверенного результата. Global disable вынесите в отдельную policy.</li><li><strong>Заполните evidence.</strong> Добавьте owner, reason, scope, fingerprint и даты. Для tune укажите новую revision и ожидаемую границу.</li><li><strong>Проверьте отрицательный путь.</strong> Убедитесь, что неполный контекст, общий suppress и просроченные даты отклоняются.</li><li><strong>Подготовьте rollback.</strong> Для suppress удалите точное исключение и повторите анализ. Для tune верните прежнюю revision и сравните diff. Не считайте rollback выполненным по одному изменению файла.</li></ol>\n<h2>Ограничения</h2>\n<p>Статический анализ не видит весь runtime-контекст. Правило может не знать о конфигурации, feature flag, генерации кода, маршруте данных, правах пользователя и фактическом deploy-артефакте. SARIF не превращает позицию в файле в доказательство исполнения. Одинаковая строка может быть опасной в одном сервисе и безопасной в другом.</p>\n<p>Формат исключений и fingerprint зависит от конкретного анализатора и версии CLI. Не переносите поля из учебного объекта в конфигурацию без проверки официальной документации. Не называйте synthetic result находкой, не заявляйте снижение числа ложных срабатываний без измерения и не утверждайте, что опасные случаи не потеряны без проверки на выбранном наборе кода.</p>\n<p>Rollback тоже имеет границу. Он возвращает видимость правила или убирает scoped exception. Он не отменяет уже выпущенный код и не доказывает безопасность старого состояния. Если исключение успело скрыть другие результаты, их нужно искать отдельным повторным анализом.</p>\n<h2>Критерий готовности</h2>\n<p>Решение готово, когда другой инженер может по записи ответить на пять вопросов: какой result разбирали, какую гипотезу проверяли, почему выбрали keep, tune или suppress, кто и когда пересматривает решение, как вернуть прежнюю видимость. Для tune должна существовать новая revision и проверенный diff. Для suppress должны совпадать fingerprint и scope, а expiry должна быть будущей. Для rollback должен быть выполнен повторный анализ на том же commit или явно зафиксировано, почему это невозможно.</p>\n<p>Если хотя бы один ответ отсутствует, глобальное выключение не является исправлением. Оставьте сигнал видимым, назначьте владельца и доберите контекст. Так статический анализ остаётся управляемым источником технических сигналов, а не безымянным переключателем громкости.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html\" target=\"_blank\" rel=\"noopener noreferrer\">OASIS: Static Analysis Results Interchange Format (SARIF) Version 2.1.0</a> — нормативное описание формата результатов; оно не подтверждает достоверность отдельного результата.</li><li><a href=\"https://github.com/semgrep/semgrep/releases\" target=\"_blank\" rel=\"noopener noreferrer\">Semgrep: официальные релизы</a> — источник для проверки версии CLI и её поведения; версия анализатора должна совпадать с версией проекта.</li><li><a href=\"https://csrc.nist.gov/pubs/sp/800/218/final\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-218: Secure Software Development Framework</a> — официальная рамка для владения, проверки и управления риском; она не заменяет анализ конкретного кода.</li></ul>"
|
||
}
|