8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 168,
|
||
"slug": "editorial-2023-05-practice-static-analysis",
|
||
"title": "Шумное правило статического анализа: как принять решение по одному сигналу",
|
||
"excerpt": "Статический анализ показывает совпадение, а не готовый вердикт. Разбираем один сигнал по rule, result, location и контексту, затем выбираем проверяемое действие без глобального отключения защиты.",
|
||
"contentHtml": "<p>В pull request появляется предупреждение: правило увидело передачу значения в функцию, которая строит команду. Строка выглядит безопасно. Значение приходит из внутреннего объекта, ветка закрыта проверкой, а правило повторяется в десятках файлов. После нескольких таких комментариев команда просит выключить его целиком.</p>\n<p>Симптом понятен: статический анализ тормозит review и смешивает полезные находки с шумом. Цена ошибки выше, чем время на один комментарий. Глобальное отключение убирает сигнал для следующего участка, который никто ещё не видел. Автоматическое объявление каждой строки уязвимостью создаёт другую проблему: инженеры перестают различать риск и форму совпадения.</p>\n<p>Рабочий тезис простой: результат анализатора — это начало проверки, а не её конец. Сначала нужно отделить правило, результат, позицию и контекст. Потом выбрать одно из трёх действий: оставить сигнал, уточнить правило или временно ограничить один результат. Если контекст не собран, сигнал остаётся видимым.</p>\n<h2>Что именно сообщает анализатор</h2>\n<p>Правило описывает синтаксическую или семантическую гипотезу. Например, оно ищет передачу условно недоверенного значения в <code>runShell</code>. Результат сообщает, что гипотеза совпала в конкретном месте. Позиция даёт URI, строку и иногда отпечаток. Ни одно из этих полей не говорит само по себе, что ветка исполняется, значение действительно приходит извне или команда достигнет production.</p>\n<p>SARIF 2.1.0 полезен как формат обмена этими фактами. В нём можно связать инструмент, версию правила, result, location и fingerprint. Формат не добавляет сведения, которых инструмент не собирал. Поэтому <code>ruleId</code> нельзя читать как готовый security verdict, а <code>startLine</code> — как доказательство достижимости.</p>\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><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Правило</td><td>Одинаковый ruleId повторяется в разных модулях</td><td>Паттерн шире ожидаемого сценария</td><td>Прочитать intent, revision и diff правила</td><td>Оставить или уточнить pattern</td></tr><tr><td>Result</td><td>Есть сообщение и строка, но нет решения</td><td>Совпадение приняли за вывод о коде</td><td>Сверить fingerprint и версию инструмента</td><td>Добавить контекст, не ставить verdict</td></tr><tr><td>Location</td><td>Указан URI, но файл уже изменился</td><td>Результат относится к другой ревизии</td><td>Проверить commit, строку и entry point</td><td>Повторить анализ на актуальной ревизии</td></tr><tr><td>Контекст</td><td>Непонятно, откуда пришло значение</td><td>Trust boundary не записана</td><td>Назначить владельца и назвать источник</td><td>Оставить сигнал видимым</td></tr><tr><td>Решение</td><td>Предлагают выключить правило глобально</td><td>Точечный результат смешали с политикой</td><td>Проверить scope, срок и rollback</td><td>Выбрать keep, tune или scoped suppress</td></tr></tbody></table></div>\n<h2>Учебный пример: форма совпадения</h2>\n<p>Ниже приведён синтетический фрагмент. Он нужен, чтобы показать границу между совпадением и выводом. Имена файла, строки и правило вымышлены. Пример не читает репозиторий, не запускает анализатор и не доказывает наличие уязвимости.</p>\n<pre><code>function exportReport(request) {\n const command = request.options.command;\n\n if (!ALLOWED_COMMANDS.has(command)) {\n throw new Error('unsupported command');\n }\n\n return runShell(command);\n}</code></pre>\n<p>Правило <code>demo.untrusted-command-construction</code> может отметить вызов <code>runShell(command)</code>. Синтаксически это разумный сигнал: функция получает значение, которое прошло через объект запроса. Но по одному совпадению нельзя установить, что <code>request</code> контролирует внешний пользователь, что проверка <code>ALLOWED_COMMANDS</code> корректна или что функция вызывается в интересующем артефакте.</p>\n<p>Проверка должна идти по цепочке данных. Нужно найти источник <code>request</code>, определить границу доверия, проверить содержимое allowlist и проследить вызов до entry point. Если любое звено неизвестно, запись должна сказать «контекст неполный». Это точнее, чем «ложное срабатывание»: отсутствие данных не доказывает безопасность.</p>\n<p>В учебной модели результат можно представить так: <code>ruleId</code> связывает совпадение с правилом, <code>revision</code> фиксирует его версию, <code>uri</code> и <code>startLine</code> указывают место, а <code>fingerprint</code> помогает сопоставить тот же результат после повторного запуска. Fingerprint не является оценкой риска. Он не заменяет чтение актуального исходника.</p>\n<figure><img src=\"/assets/editorial/2023/static-analysis-2023-triage-funnel.svg\" alt=\"Воронка классификации сигнала статического анализа: rule и result проходят проверку location и контекста, после чего выбираются keep, tune или ограниченное suppress\" loading=\"lazy\" /><figcaption>Учебная воронка разбора одного сигнала. Она показывает порядок классификации и не утверждает наличие findings, coverage или production-эффекта.</figcaption></figure>\n<h2>Контекст, без которого решение преждевременно</h2>\n<p>Для одного сигнала достаточно короткой context record. Поле <code>asset</code> называет компонент или артефакт. <code>entryPoint</code> показывает, откуда начинается путь. <code>trustBoundary</code> объясняет, почему значение считают недоверенным. <code>owner</code> называет человека или роль, которая может подтвердить устройство компонента. <code>releaseScope</code> связывает решение с ревизией или изменением, а не со всем продуктом.</p>\n<p>Эти поля не обязаны быть заполнены сразу. Но неизвестное нужно записать как неизвестное. Если не найден entry point, нельзя утверждать, что код недостижим. Если неясен источник данных, нельзя утверждать, что значение безопасно. Если результат относится к generated code, сначала нужно выяснить, какой исходный файл владеет поведением. Контекст не превращает сигнал в уязвимость, но делает следующий вопрос проверяемым.</p>\n<h2>Три действия после классификации</h2>\n<p><strong>Keep.</strong> Правило и результат остаются видимыми. Это правильный исход, когда риск не исключён или данных ещё не хватает. В комментарии достаточно указать, какое поле контекста отсутствует и кто его проверит.</p>\n<p><strong>Tune.</strong> Правило меняют, когда сама гипотеза слишком широка. Например, pattern можно ограничить известным небезопасным sink или потребовать явного признака внешнего источника. Изменение должно получить новую revision и описание того, какие будущие совпадения оно перестанет показывать. «Стало меньше шума» не объясняет trade-off.</p>\n<p><strong>Scoped suppress.</strong> Один результат временно исключают, когда правило нужно сохранить, а конкретный участок уже проверен. Исключение должно ссылаться на точный fingerprint или другую устойчивую идентификацию, иметь scope, владельца, причину и дату пересмотра. Срок не должен превращать временное решение в бессрочное разрешение.</p>\n<h2>Действия по порядку</h2>\n<ol><li>Сохраните <code>ruleId</code>, revision правила, fingerprint, URI, строку и ревизию исходника. Не добавляйте в запись вывод о безопасности.</li><li>Прочитайте intent правила и его diff. Уточните язык, область файлов и условие, которое вызывает совпадение.</li><li>Восстановите путь данных: источник, преобразования, проверка, sink и entry point. Для каждого шага отметьте подтверждённое и неизвестное.</li><li>Заполните asset, trust boundary, owner и release scope. Если поле неизвестно, поставьте статус <code>context-incomplete</code>.</li><li>Выберите keep, tune или точечный suppress. Запишите причину, scope, срок пересмотра и требуемый rollback.</li><li>Повторите анализ на актуальной ревизии. Проверьте, что tune изменил ожидаемую форму, а suppress не скрыл соседние результаты.</li><li>Проверьте отрицательный путь: глобальное действие <code>disable-globally</code> должно быть отклонено политикой, а неполный контекст не должен превращаться в «безопасно».</li></ol>\n<h2>Почему исключение не лечит неточное правило</h2>\n<p>Suppression решает вопрос об одном уже идентифицированном результате. Tune решает вопрос о гипотезе, которую правило применяет к будущим участкам. Если команда раздаёт исключения там, где pattern неправильно понимает boundary, она сохраняет старую ошибку и постепенно теряет карту покрытия. Если команда переписывает правило ради одного проверенного участка, она может скрыть реальные сигналы в других модулях.</p>\n<p>Не стоит путать и другой отрицательный путь. Если анализатор показал результат на synthetic fixture, это доказывает только то, что учебный объект соответствует заданной форме. PASS у такого fixture не означает, что scanner читал файл, запускал ветку или получил finding в реальном проекте. Код примера ограничен учебной задачей и не является production-рецептом.</p>\n<h2>Ограничения метода</h2>\n<p>Статический анализ не видит автоматически весь runtime-контекст. Feature flag может скрыть путь. Generated code может отличаться от исходного шаблона. Динамический импорт, конфигурация окружения и права доступа могут изменить достижимость. SARIF сохраняет результат инструмента, но не подтверждает корректность правила, полноту проекта и отсутствие других путей к sink.</p>\n<p>Метод также не даёт production-метрику. Он не сообщает precision, recall, coverage, число предотвращённых инцидентов или время до исправления. Для таких утверждений нужны отдельные данные: запуски на определённых ревизиях, правила подсчёта и независимая проверка. В этой статье таких измерений нет.</p>\n<h2>Критерий готовности</h2>\n<p>Разбор одного сигнала готов, если другой инженер может повторить решение без устного контекста. В записи есть ruleId и revision, точный result, проверенная ревизия исходника, путь от источника до sink, владелец, scope и выбранное действие. Для tune виден diff правила. Для suppress видны идентификатор результата, причина и срок пересмотра. Для keep ясно, какая проверка ещё не выполнена.</p>\n<p>Отдельно проверьте, что повторный запуск не создаёт новый необъяснимый сигнал, что соседние результаты не исчезли из-за широкого исключения и что rollback можно выполнить отдельным diff. Если одно из этих условий не выполнено, решение ещё не закрыто. Сигнал лучше оставить видимым, чем скрыть неизвестное за удобной зелёной проверкой.</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://semgrep.dev/docs/writing-rules/overview\" target=\"_blank\" rel=\"noopener noreferrer\">Semgrep: Writing rules overview</a> — официальная документация о структуре и назначении правил; пример в статье не является правилом Semgrep и не запускался.</li><li><a href=\"https://csrc.nist.gov/projects/ssdf\" target=\"_blank\" rel=\"noopener noreferrer\">NIST: Secure Software Development Framework</a> — официальная рамка практик безопасной разработки; она не подтверждает эффективность конкретного правила или решения.</li></ul>"
|
||
}
|