Files

8 lines
25 KiB
JSON
Raw Permalink 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": 171,
"slug": "editorial-2023-04-practice-dependency-security",
"title": "Advisory зависимости: как доказать, что сигнал относится к вашему выпуску",
"excerpt": "Совпадение package и версии с advisory ещё не доказывает риск в приложении. Разбираем lockfile, SBOM, путь зависимости и безопасный порядок обновления с воспроизводимыми командами.",
"contentHtml": "<p>Сбой в CI сопровождается advisory: пакет совпал с уязвимой версией. Первая реакция понятна — поднять версию в <code>package.json</code> и закрыть тикет после зелёного pipeline. Но такой сигнал отвечает только на один вопрос: известна запись о компоненте с подходящим диапазоном версий. Он не отвечает, попал ли компонент в конкретный образ, откуда он пришёл, выполняется ли опасная ветка и относится ли найденный SBOM к этому выпуску.</p>\n<p>Практический риск здесь двойной. Ложное «уязвимо» заставляет срочно менять большое дерево зависимостей и ломает unrelated-сценарии. Ложное «исправлено» оставляет старый образ или транзитивный путь без владельца. Поэтому advisory нужно разбирать как цепочку доказательств: внешний сигнал → exact package@version → запись в lockfile → компонент в том же артефакте → проверенный путь выполнения → решение с границами применимости.</p>\n<h2>Начните с вопроса, который можно проверить</h2>\n<p>Не формулируйте задачу как «проверить безопасность пакета». Это слишком широкое обещание. Запишите узкий вопрос: «Есть ли <code>package@version</code> из advisory в lockfile commit X, в SBOM artifact digest Y и в runtime-сценарии Z?» Если один из переходов пока неизвестен, это часть результата расследования.</p>\n<p>Сначала сохраните идентификатор сигнала: URL или ID advisory, имя пакета, затронутый диапазон версий, источник и время получения. Не переносите в тикет более сильную формулировку, чем есть в источнике. «Совпала версия из диапазона» и «уязвимый код достижим из внешнего запроса» — разные утверждения и требуют разных проверок.</p>\n<table><caption>Что доказывает каждый слой проверки</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Проверяемый вопрос</th><th scope=\"col\">Подходящий evidence</th><th scope=\"col\">Чего он не доказывает</th></tr></thead><tbody><tr><td>Advisory</td><td>Какие package и version range описаны внешним источником?</td><td>URL или ID, дата, диапазон, severity и описание условия</td><td>Воздействие на конкретный сервис</td></tr><tr><td><code>package.json</code></td><td>Какую direct dependency просит проект?</td><td>Commit и diff manifest</td><td>Точную транзитивную версию</td></tr><tr><td>Lockfile</td><td>Какое дерево выбрал установщик?</td><td>Запись пакета, parent path, resolved и integrity-поля</td><td>Загрузку модуля процессом</td></tr><tr><td>SBOM</td><td>Какие компоненты заявлены для артефакта?</td><td>Формат, component record и commit/digest артефакта</td><td>Достижимость опасной функции</td></tr><tr><td>Runtime evidence</td><td>Что произошло в названном сценарии?</td><td>Тест, trace или controlled smoke с build identity</td><td>Все возможные платформы и флаги</td></tr></tbody></table>\n<h2>Почему lockfile важнее одной строки manifest</h2>\n<p>Manifest описывает намерение: например, <code>\"demo-shell\": \"^1.4.0\"</code> разрешает установщику выбрать совместимую версию. Lockfile фиксирует результат разрешения для конкретного состояния проекта. В npm это точное дерево, из которого последующие установки могут восстановить те же версии, если соблюдены правила и версия инструмента.</p>\n<p>Проверяйте не только наличие имени. Для транзитивной зависимости нужен путь: какая direct dependency привела к пакету, какая версия родителя была выбрана и не существует ли второй копии в другом поддереве. Две записи одного имени могут иметь разные версии и разные условия попадания в bundle. Удаление прямого импорта не исключает транзитивную, optional, plugin или dynamic-import ветку.</p>\n<p>Команда <code>npm explain</code> предназначена именно для восстановления цепочки, которая привела пакет в установленное дерево. Но она описывает установленное дерево в конкретной рабочей директории. Если проверяется другой build, сначала получите чистую установку из его lockfile и только затем интерпретируйте вывод.</p>\n<pre><code>set -eu\nPACKAGE='имя-пакета-из-advisory'\n\nprintf 'commit: '; git rev-parse HEAD\nnode --version\nnpm --version\n\n# Чистое дерево должно соответствовать lockfile candidate-коммита.\nnpm ci\n\n# Полный путь от root до транзитивного пакета.\nnpm explain \"$PACKAGE\"\n\n# Все найденные экземпляры и их типы зависимости.\nnpm ls \"$PACKAGE\" --all --json &gt; artifacts/package-tree.json</code></pre>\n<p>Этот блок не выдаёт автоматически решение. Он сохраняет контекст инструмента и показывает, где искать parent path. Если <code>npm ci</code> не проходит, сначала разберите несовместимость manifest и lockfile. Нельзя считать отсутствие результата <code>npm explain</code> доказательством отсутствия компонента в уже опубликованном образе.</p>\n<h2>Свяжите lockfile с конкретным SBOM</h2>\n<p>SBOM — это инвентарь компонентов, а не заключение о безопасности. NTIA описывает для него поля идентификации компонентов, поддержку автоматической обработки и процессы формирования. На практике этого достаточно, чтобы сопоставлять компонент с базой advisory, но недостаточно, чтобы утверждать его достижимость или отсутствие эксплуатации.</p>\n<p>Для npm можно получить SBOM командой <code>npm sbom</code>. Режим <code>--package-lock-only</code> строит результат по lockfile, поэтому он полезен для проверки resolved tree, но не заменяет SBOM контейнерного образа: в нём могут быть системные библиотеки, native runtime и дополнительные файлы. Если вопрос относится к deployed image, SBOM нужно создавать в build-процессе для того же digest, который будет развёрнут.</p>\n<pre><code># Современный npm CLI: проверить поддерживаемые опции перед запуском.\nnpm help sbom\n\n# SBOM по lockfile, без dev-зависимостей в установленном дереве.\nnpm sbom --package-lock-only --omit=dev --sbom-format=cyclonedx \\\n &gt; artifacts/sbom-lockfile.cdx.json\n\n# Аудит npm использует lockfile; JSON сохраняет детали сигнала.\nnpm audit --omit=dev --json &gt; artifacts/npm-audit.json</code></pre>\n<p>У команд есть границы. npm docs указывают, что <code>npm audit</code> отправляет описание зависимостей в настроенный registry для получения отчёта; проверьте правила обращения с именами private-пакетов и registry в своей организации. Опция <code>--omit=dev</code> меняет проверяемое установленное дерево, но не означает, что dev-записи исчезли из lockfile. Устаревший npm может не знать <code>npm sbom</code>; тогда используйте одобренный генератор SBOM и зафиксируйте его версию.</p>\n<figure><img src=\"/assets/editorial/2023/dependency-security-2023-advisory-triage.svg\" alt=\"Схема проверки advisory: точный пакет в lockfile сопоставляется с SBOM артефакта и проверяется по пути от точки входа\" loading=\"lazy\" /><figcaption>Advisory запускает расследование: после lockfile и SBOM остаётся отдельный вопрос о достижимости. Схема учебная и не содержит данных production.</figcaption></figure>\n<h2>Совпадение двух списков не равно достижимости</h2>\n<p>Рассмотрим воспроизводимую модель без реального registry. Входные массивы ниже заранее заданы в памяти: один имитирует lockfile, второй — SBOM. Имена synthetic, поэтому пример не подтверждает конкретный CVE, версию библиотеки, build или runtime. Он нужен, чтобы не смешивать факт присутствия с выводом о поведении.</p>\n<pre><code>const advisory = {\n packageName: 'demo-parser',\n affectedVersion: '1.0.0',\n};\n\nconst lockfile = [\n { name: 'demo-parser', version: '1.0.0',\n path: 'demo-app &gt; demo-shell &gt; demo-parser' },\n];\n\nconst sbom = [\n { name: 'demo-parser', version: '1.0.0',\n artifactDigest: 'sha256:example' },\n];\n\nconst locked = lockfile.find((item) =&gt;\n item.name === advisory.packageName\n &amp;&amp; item.version === advisory.affectedVersion\n);\nconst recorded = sbom.find((item) =&gt;\n item.name === advisory.packageName\n &amp;&amp; item.version === advisory.affectedVersion\n);\n\nconsole.log({\n lockfileMatch: Boolean(locked),\n sbomMatch: Boolean(recorded),\n parentPath: locked?.path ?? 'unknown',\n artifact: recorded?.artifactDigest ?? 'unknown',\n reachability: 'not-assessed',\n decision: 'not-determined',\n});</code></pre>\n<p>Ожидаемый результат показывает <code>true</code> для двух совпадений и одновременно <code>not-assessed</code> для достижимости. Это правильный результат модели. Код не читает настоящий lockfile, не строит call graph, не запускает контейнер и не проверяет входные данные. Подменять его PASS-выводом production-сканера нельзя.</p>\n<h2>Разделите reachability и exploitability</h2>\n<p>Достижимость отвечает на вопрос «может ли выполнение попасть в нужный модуль или функцию при заданном entry point и конфигурации». Ищите импорт, регистрацию plugin, conditional export, dynamic import, feature flag и платформенную ветку. Зафиксируйте, какой метод использован: статический граф показывает возможную связь, тест — выбранный сценарий, trace — один наблюдаемый запуск.</p>\n<p>Exploitability — ещё более сильный вывод. Даже если модуль достижим, нужно знать, достигается ли уязвимая ветка, какие входные данные до неё доходят и какие защитные условия действуют. Ни lockfile, ни SBOM, ни успешный <code>npm audit</code> не вычисляют это автоматически для вашего приложения. Для такого вывода нужны анализ advisory, кодовый review и подходящие security-тесты.</p>\n<table><caption>Как интерпретировать результаты расследования</caption><thead><tr><th scope=\"col\">Наблюдение</th><th scope=\"col\">Допустимый вывод</th><th scope=\"col\">Следующий шаг</th></tr></thead><tbody><tr><td>Package/version совпали только в advisory и manifest</td><td>Есть сигнал и заявленный диапазон, resolved tree не доказан</td><td>Проверить lockfile candidate-коммита</td></tr><tr><td>Пакет найден в lockfile, parent path известен</td><td>Компонент выбран resolver для этого дерева</td><td>Сверить SBOM и artifact identity</td></tr><tr><td>Пакет найден в SBOM без digest</td><td>Есть неподтверждённая inventory-запись</td><td>Найти provenance или пересоздать SBOM в build</td></tr><tr><td>SBOM и digest совпали, путь не проверен</td><td>Компонент заявлен в конкретном артефакте</td><td>Проверить entry point, flags и runtime-сценарий</td></tr><tr><td>Путь найден, опасная ветка подтверждена входом</td><td>Риск относится к названному сценарию и scope</td><td>Оценить remediation, тесты и rollback</td></tr></tbody></table>\n<h2>Безопасное обновление — это отдельная проверка</h2>\n<p>Обновление одной direct dependency может перестроить всё дерево. Меняются peer dependencies, optional-пакеты, integrity, формат модулей и lifecycle scripts. Поэтому сначала сохраните baseline, затем создайте candidate и просмотрите полный diff lockfile. Широкий diff не означает, что обновление ошибочно; он означает, что область совместимости стала шире ожидаемой и требует объяснения.</p>\n<pre><code># Перед изменением сохраните baseline. Не включайте секреты в artifacts.\ngit rev-parse HEAD &gt; artifacts/baseline-commit.txt\ncp package-lock.json artifacts/baseline-package-lock.json\n\n# В отдельной ветке обновите только заявленный пакет.\nnpm install --save-exact PACKAGE@FIXED_VERSION\ngit diff -- package.json package-lock.json\n\n# Повторите установку и проверки в чистой среде candidate.\nnpm ci\nnpm test\nnpm audit --omit=dev --audit-level=high</code></pre>\n<p>Замените <code>PACKAGE</code> и <code>FIXED_VERSION</code> на значения из вашего advisory и учитывайте менеджер пакетов проекта. Не запускайте <code>npm audit fix --force</code> как универсальную кнопку: npm docs предупреждают, что <code>--force</code> разрешает изменения за пределами обычных ограничений и может привести к major-обновлениям. Если требуется major version, сначала нужен совместимый change plan и тестовый rollback.</p>\n<h2>Порядок triage от сигнала до решения</h2>\n<ol><li><strong>Зафиксируйте advisory.</strong> Сохраните источник, ID, package, affected range и дату. Отдельно отметьте, что является фактом источника, а что пока гипотеза.</li><li><strong>Назовите baseline.</strong> Привяжите проверку к commit, lockfile и правилам установки. Для монорепозитория назовите workspace и тип зависимости.</li><li><strong>Восстановите dependency path.</strong> Выполните <code>npm explain</code> и <code>npm ls --all</code>, сохраните parent path и все найденные версии.</li><li><strong>Сверьте артефакт.</strong> Найдите SBOM с commit и digest candidate-образа. Если документ относится к другому build, вернитесь к шагу генерации.</li><li><strong>Проверьте runtime scope.</strong> Назовите entry point, режим, флаг и входные данные. Отметьте, что покрывает статический анализ, тест и trace.</li><li><strong>Сформируйте candidate update.</strong> Укажите from/to, ожидаемый lockfile diff, peer/optional branches и способ rollback. Не смешивайте unrelated upgrades.</li><li><strong>Пройдите gates.</strong> Выполните clean install, targeted tests, security checks и controlled smoke. Каждый результат должен ссылаться на этот candidate.</li><li><strong>Запишите решение.</strong> Формулировки «исправлено», «не входит в этот артефакт» и «требует project review» допустимы только с указанным scope и оставшимися неизвестными.</li></ol>\n<h2>Отрицательный путь нельзя пропускать</h2>\n<p>Зелёный путь показывает, что выбранная версия устанавливается и тестовый сценарий проходит. Он не показывает, что старая версия не поставляется другим job, не остаётся в старом образе и не приходит через optional branch. Проверьте как минимум четыре отрицательных условия: exact version не совпадает с advisory range; package отсутствует в candidate SBOM; SBOM относится к другому digest; entry point не может включить ветку при заданной конфигурации.</p>\n<p>Для каждого отрицательного результата фиксируйте границу. «Не найдено в SBOM digest Y» не равно «пакет нигде не существует». «Не импортируется этим entry point» не равно «уязвимость невозможна во всех режимах». Если проверка не покрывает dynamic loading или native code, оставьте это как unknown и назначьте следующий способ проверки.</p>\n<h2>Ограничения применимости</h2>\n<p>Схема рассчитана на проекты, где можно получить lockfile и идентичность build. Она не заменяет аудит private registry, зависимостей ОС, контейнерных слоёв, generated code, native addons, browser bundle, runtime flags, XSS или authorization. Формат SBOM и качество его генератора тоже влияют на результат. Для yarn, pnpm, Composer, Maven и других экосистем команды и поля будут другими; переносить npm-команды без адаптации нельзя.</p>\n<p>Ссылки на npm CLI относятся к текущей документации. Поведение и доступность команд зависят от версии npm, настроек registry и package-manager policy. Поэтому сохраняйте <code>node --version</code>, <code>npm --version</code>, параметры <code>omit</code> и источник SBOM рядом с результатом. Это не делает проверку вечной, но позволяет повторить её в том же контракте и увидеть, что изменилось.</p>\n<h2>Критерий готовности</h2>\n<p>Расследование готово к решению, когда другой инженер может восстановить цепочку для одного candidate release: advisory → exact package@version → lockfile commit → parent path → SBOM → artifact digest → entry point и configuration → evidence достижимости → install, tests и smoke → решение и rollback. Для каждого перехода есть ссылка или сохранённый результат. Неизвестные пути перечислены, а не скрыты под словом «безопасно».</p>\n<p>Такой критерий не обещает отсутствие уязвимостей. Он делает вывод проверяемым и ограниченным: понятно, какой компонент и какой артефакт исследованы, какой сценарий покрыт и где требуется дополнительная работа. Это достаточная основа для инженерного решения и гораздо надёжнее, чем совпадение имени в отчёте сканера.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://docs.npmjs.com/files/package-lock.json/\" target=\"_blank\" rel=\"noopener noreferrer\">npm Docs: package-lock.json</a> — описание lockfile как точного дерева зависимостей, созданного установщиком.</li><li><a href=\"https://docs.npmjs.com/cli/audit/\" target=\"_blank\" rel=\"noopener noreferrer\">npm Docs: npm audit</a> — назначение аудита, требования к lockfile, уровни выхода и ограничения автоматического исправления.</li><li><a href=\"https://docs.npmjs.com/cli/v12/commands/npm-explain/\" target=\"_blank\" rel=\"noopener noreferrer\">npm Docs: npm explain</a> — восстановление цепочки зависимостей, из-за которой пакет установлен.</li><li><a href=\"https://docs.npmjs.com/cli/v12/commands/npm-sbom/\" target=\"_blank\" rel=\"noopener noreferrer\">npm Docs: npm sbom</a> — генерация SBOM в SPDX или CycloneDX и режим на основе lockfile.</li><li><a href=\"https://www.ntia.gov/report/2021/minimum-elements-software-bill-materials-sbom\" target=\"_blank\" rel=\"noopener noreferrer\">NTIA: The Minimum Elements for a Software Bill of Materials (SBOM)</a> — минимальные элементы идентификации, автоматизации и процессов SBOM.</li><li><a href=\"https://doi.org/10.6028/NIST.SP.800-218\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-218: Secure Software Development Framework</a> — официальный набор практик безопасной разработки и управления риском компонентов.</li></ul>"
}