Files
progcode/editorial/agent-rewrites/168.json
T

8 lines
22 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 168,
"slug": "editorial-2023-05-practice-static-analysis",
"title": "Шумное правило статического анализа: как разобрать сигнал и выбрать действие",
"excerpt": "Статический анализ сообщает о совпадении, а не выносит готовый вердикт. Разбираем SARIF-результат, восстанавливаем путь данных и выбираем keep, tune или точечное suppress без глобального отключения правила.",
"contentHtml": "<p>В pull request появляется предупреждение об ошибке: правило увидело значение рядом с функцией, которая запускает команду. Строка выглядит безопасной: значение приходит из внутреннего объекта, выше стоит проверка, а такое сообщение повторяется в десятках файлов. Команда предлагает выключить правило целиком, чтобы review снова стал быстрым.</p>\n<p>Это плохой выбор по двум причинам. Статический анализ мог заметить настоящий путь к опасному sink, а мог увидеть только форму кода, не зная источника данных. Глобальное отключение смешивает эти случаи и убирает следующий сигнал вместе с текущим. Полезная единица работы — не «правило шумное» и не «строка уязвима», а один результат с проверяемым контекстом.</p>\n<p>Ниже — схема разбора для JavaScript и других языков. Она отвечает на один вопрос: что нужно проверить, прежде чем оставить результат, уточнить правило или ограниченно подавить совпадение. Названия анализатора и внутренней системы в примерах условны; поля SARIF соответствуют формату версии 2.1.0.</p>\n<h2>Сначала отделите совпадение от вывода</h2>\n<p>Правило задаёт гипотезу: например, «значение из потенциально недоверенного источника попало в функцию, которая может выполнять команду». Результат сообщает, что инструмент нашёл подходящую форму в конкретной ревизии. Это полезное наблюдение, но оно не доказывает, что внешний пользователь управляет значением, ветка достижима или команда дойдёт до production.</p>\n<p>SARIF стандартизирует обмен результатами анализа. В объекте результата могут быть <code>ruleId</code>, сообщение, <code>locations</code> с артефактом и регионом, а также <code>partialFingerprints</code> для корреляции между запусками. Формат описывает данные, которые собрал инструмент или система результатов; он не исправляет неточность правила и не добавляет отсутствующий runtime-контекст.</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></tr></thead><tbody><tr><td>Rule</td><td>Идентификатор, версия и условие совпадения</td><td>Открыть описание и diff правила; понять язык, sink и source</td><td>Считать <code>ruleId</code> оценкой риска</td></tr><tr><td>Result</td><td>Сообщение, уровень и связь с запуском</td><td>Сверить инструмент, ревизию и повторяемость результата</td><td>Назвать результат уязвимостью без проверки кода</td></tr><tr><td>Location</td><td>URI, строка, столбец или логическое имя</td><td>Открыть ту же ревизию исходника и проверить соседние строки</td><td>Довериться старой строке после изменения файла</td></tr><tr><td>Context</td><td>Источник, граница доверия, entry point и владелец</td><td>Проследить данные до sink и записать неизвестные звенья</td><td>Объявить «false positive», когда данных не хватает</td></tr></tbody></table></div>\n<figure><img src='/assets/editorial/2023/static-analysis-2023-triage-funnel.svg' alt='Воронка разбора сигнала статического анализа: rule и SARIF result проверяются по форме, location и контексту, после чего выбираются keep, tune или ограниченное suppress' loading='lazy' /><figcaption>Порядок triage: сначала идентификация результата и его места, затем путь данных, владелец и точный scope действия. Глобальное отключение находится за пределами обычного разбора одного результата.</figcaption></figure>\n<h2>Минимальный SARIF, который можно прочитать</h2>\n<p>Начните с артефакта, а не со скриншота комментария в review. Для проверки структуры достаточно сохранить ответ анализатора в файл <code>result.sarif</code>. Следующий фрагмент специально мал: в нём есть инструмент, правило, результат, место и частичный отпечаток. Значения <code>demo.untrusted-command/v1</code> и <code>src/export.js</code> придуманы для примера и не являются выводом конкретного scanner.</p>\n<pre><code>{\n &quot;version&quot;: &quot;2.1.0&quot;,\n &quot;runs&quot;: [{\n &quot;tool&quot;: {\n &quot;driver&quot;: {\n &quot;name&quot;: &quot;demo-scanner&quot;,\n &quot;rules&quot;: [{ &quot;id&quot;: &quot;demo.untrusted-command&quot; }]\n }\n },\n &quot;results&quot;: [{\n &quot;ruleId&quot;: &quot;demo.untrusted-command&quot;,\n &quot;level&quot;: &quot;warning&quot;,\n &quot;message&quot;: { &quot;text&quot;: &quot;value reaches a command sink&quot; },\n &quot;locations&quot;: [{\n &quot;physicalLocation&quot;: {\n &quot;artifactLocation&quot;: { &quot;uri&quot;: &quot;src/export.js&quot; },\n &quot;region&quot;: { &quot;startLine&quot;: 8 }\n }\n }],\n &quot;partialFingerprints&quot;: {\n &quot;demo.untrusted-command/v1&quot;: &quot;example-fingerprint&quot;\n }\n }]\n }]\n}</code></pre>\n<p>Здесь <code>level</code> — уровень, который выбрал инструмент, а не универсальная шкала ущерба. <code>startLine</code> помогает открыть место, но не подтверждает достижимость. Частичный отпечаток помогает системе сопоставлять результаты; он не доказывает, что два похожих сообщения относятся к одной причине. Официальная спецификация SARIF отдельно предупреждает, что абсолютный номер строки плохо подходит для устойчивого fingerprint.</p>\n<h2>Воспроизводимая проверка без доверия к интерфейсу</h2>\n<p>После сохранения результата выполните команду в каталоге проекта. Она проверит JSON и напечатает для каждого результата правило, URI и строку:</p>\n<pre><code>node -e &quot;const fs=require('node:fs'); const x=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); const rs=(x.runs||[]).flatMap(r=&gt;r.results||[]); for (const r of rs) { const p=r.locations?.[0]?.physicalLocation; console.log([r.ruleId || '&lt;no-rule&gt;', p?.artifactLocation?.uri || '&lt;no-uri&gt;', p?.region?.startLine || '?'].join('\t')); }&quot; result.sarif</code></pre>\n<p>Ожидаемый вывод для примера — <code>demo.untrusted-command src/export.js 8</code>. Команда проверяет только читаемость и наличие нескольких полей. Она не запускает анализатор, не открывает исходник и не устанавливает безопасность. Для настоящего результата дополнительно зафиксируйте commit, имя инструмента, версию правил и команду запуска. Иначе при повторе можно незаметно сравнить разные ревизии.</p>\n<h2>Восстановите путь данных до sink</h2>\n<p>В отмеченной строке найдите не только аргумент функции, но и его происхождение. Запишите пять точек: source, преобразования, проверку, sink и entry point. Источник может быть HTTP-параметром, сообщением очереди, конфигурацией или внутренней таблицей. «Внутренний объект» — не доказательство доверия: его поля могли быть заполнены раньше из внешнего ввода.</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>request</code>, что именно входит в <code>ALLOWED_COMMANDS</code>, можно ли изменить объект после проверки и вызывается ли функция из внешнего entry point. Если хотя бы одно звено неизвестно, статус должен быть «контекст не собран», а не «безопасно».</p>\n<p>Если проверка подтверждает риск, исправляйте границу данных, а не комментарий анализатору. Для фиксированного набора операций безопаснее сопоставить внешний ключ с заранее заданными исполняемым файлом и аргументами, чем собирать shell-строку:</p>\n<pre><code>const commands = new Map([\n ['weekly', { file: '/usr/bin/report-export', args: ['--weekly'] }],\n]);\n\nconst selected = commands.get(request.options.command);\nif (!selected) throw new Error('unsupported command');\nreturn spawn(selected.file, selected.args, { shell: false });</code></pre>\n<p>Это только иллюстрация для Node.js: абсолютный путь, фиксированные аргументы и <code>shell: false</code> не заменяют проверку прав, окружения, таймаута и обработки ошибок. Документация Node.js предупреждает не передавать непроверенный ввод при включённом shell. На другой платформе и для другого API нужны собственные ограничения.</p>\n<h2>Выберите одно из трёх действий</h2>\n<p><strong>Keep</strong> оставляет результат видимым. Это нормальный исход, когда путь данных опасен или контекст ещё не собран. В записи укажите конкретную недостающую проверку и владельца следующего шага.</p>\n<p><strong>Tune</strong> меняет гипотезу правила. Например, правило можно сузить до подтверждённого sink или потребовать явный признак внешнего источника. У нового варианта должна быть версия, описание изменения и список совпадений, которые теперь перестанут появляться. Снижение количества сообщений само по себе не доказывает улучшение.</p>\n<p><strong>Scoped suppress</strong> ограничивает уже разобранный результат. Укажите устойчивый идентификатор, файл или иной точный scope, причину, владельца и дату пересмотра. Исключение должно быть обратимым отдельным diff. В разных системах действие называется по-разному: например, Semgrep различает ignored и fixed и предлагает указывать причины вроде false positive, acceptable risk или no time to fix. Их нельзя переносить в другую систему без проверки её политики.</p>\n<h2>Порядок разбора в pull request</h2>\n<ol><li>Скачайте или сохраните SARIF для конкретного commit. Зафиксируйте инструмент, версию правила, <code>ruleId</code>, сообщение, URI, регион и fingerprint.</li><li>Откройте описание правила и его revision. Сформулируйте одним предложением, какое условие породило результат.</li><li>Проверьте location в той же ревизии. Если файл изменился, повторите анализ; не исправляйте старый номер строки вручную.</li><li>Проследите source → преобразования → проверка → sink → entry point. Для каждой точки запишите факт или неизвестность.</li><li>Назначьте владельца проверки и scope изменения. Generated-файл связывайте с исходным шаблоном, а не объявляйте автоматически безопасным.</li><li>Выберите keep, tune или scoped suppress. Для suppress запишите причину и review-by; для tune сохраните diff и ожидаемую потерю покрытия.</li><li>Повторите анализ на актуальном commit и проверьте два отрицательных сценария: соседний опасный путь всё ещё виден, а глобальная команда <code>disable-globally</code> не прошла вместо точечного решения.</li></ol>\n<h2>Почему глобальное отключение скрывает проблему</h2>\n<p>Глобальное отключение отвечает на вопрос «нужно ли показывать будущие совпадения этого правила?» и потому имеет масштаб всего проекта или pipeline. Разбор одного результата отвечает на другой вопрос: «что произошло в конкретном месте и что с ним делать?» Эти решения нельзя подменять друг другом.</p>\n<p>Если pattern широк, tune исправляет гипотезу для будущих запусков. Если конкретный участок проверен и правило всё ещё нужно в остальных местах, scoped suppress ограничивает исключение. Если данных не хватает, keep сохраняет сигнал и делает пробел видимым. Раздавать suppress там, где на самом деле неверна модель source/sink, значит терять карту покрытия. Переписывать правило ради одного проверенного участка — значит рисковать соседними результатами.</p>\n<h2>Ограничения применимости</h2>\n<p>Эта схема подходит для triage результатов статического анализа в review и CI, когда доступны исходник, ревизия, описание правила и владелец кода. Она не заменяет динамический тест, threat modeling, ручной security review или проверку разрешений в рабочем окружении.</p>\n<p>Достижимость может зависеть от feature flag, конфигурации, динамического импорта, сгенерированного кода и прав пользователя. Taint-анализ может потерять связь при неизвестном преобразовании. Линтер может проверять только синтаксическую форму. SARIF может не содержать физической строки или может ссылаться на артефакт, которого нет в текущем checkout. В каждом случае утверждение нужно ограничивать тем, что действительно проверено.</p>\n<p>Статья не сообщает precision, recall, coverage, число предотвращённых инцидентов или экономию времени: для таких чисел нужны определение выборки, версии запусков и отдельный измерительный отчёт. Учебные JSON и JavaScript выше показывают форму проверки, но не являются результатом сканирования реального проекта.</p>\n<h2>Критерий готового решения</h2>\n<p>Разбор закончен, когда другой инженер может повторить его без устного объяснения. Есть исходная ревизия, идентификатор правила, точный результат и location; путь данных описан до entry point и sink; неизвестные отмечены; владелец и scope назначены. Для tune виден diff правила и ожидаемая потеря совпадений. Для suppress видны причина, точный идентификатор и дата пересмотра.</p>\n<p>Последний контроль — повторный запуск. Сигнал должен либо исчезнуть из-за исправления, либо остаться с объяснимой причиной. Соседние результаты не должны пропасть из-за широкого исключения, а rollback должен быть отдельным понятным изменением. Пока эти условия не выполнены, видимый сигнал полезнее зелёной проверки без доказательств.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html' target='_blank' rel='noopener noreferrer'>OASIS: SARIF Version 2.1.0 Plus Errata 01</a> — спецификация объектов result, ruleId, locations и partialFingerprints; текущая статья использует формат как контракт данных, а не как оценку риска.</li><li><a href='https://semgrep.dev/docs/for-developers/resolve-findings-through-app' target='_blank' rel='noopener noreferrer'>Semgrep: Resolve findings through Semgrep AppSec Platform</a> — официальное описание состояний finding, причин игнорирования и поведения результатов между ветками; названия действий зависят от конкретной системы.</li><li><a href='https://nodejs.org/api/child_process.html' target='_blank' rel='noopener noreferrer'>Node.js: Child process</a> — официальная документация о <code>spawn</code>, параметре <code>shell</code> и запрете передавать непроверенный ввод shell-интерпретатору.</li><li><a href='https://csrc.nist.gov/projects/ssdf' target='_blank' rel='noopener noreferrer'>NIST: Secure Software Development Framework</a> — официальная риск-ориентированная рамка безопасной разработки; она не подтверждает качество конкретного правила и не заменяет проверку результата.</li></ul>"
}