Files

8 lines
24 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": 169,
"slug": "editorial-2023-04-field-dependency-security",
"title": "Безопасность зависимостей: как проверить транзитивный пакет до релиза",
"excerpt": "Сканер нашёл пакет, которого нет в package.json. Разбираем путь через npm-дерево, lockfile, production-артефакт и runtime, затем проверяем обновление и откат без ложного PASS.",
"contentHtml": "<p>Сканер сообщает об уязвимой версии пакета, но в <code>package.json</code> этого имени нет. Команда удаляет случайную строку, получает зелёный install и закрывает задачу. Позже выясняется, что пакет пришёл транзитивно, остался в другом production-артефакте или уже был упакован в образ. Обратный сценарий тоже опасен: обновление без проверки меняет peer-зависимость, требует другой Node.js и останавливает деплой.</p>\n<p>Проверяемый вывод должен быть уже: какой пакет найден, кто его привёл, в каком артефакте он оказался, достигается ли уязвимая ветка в названном сценарии и можно ли вернуться к исходному digest. Предупреждение scanner — вход в расследование, а не доказательство ни безопасности, ни эксплуатации.</p>\n<h2>Не начинайте с удаления строки</h2>\n<p>Зависимость может отсутствовать в корневом манифесте и всё равно входить в установленное дерево. Приложение зависит от <code>http-client</code>, тот — от <code>parser</code>, а advisory указывает на старую версию <code>parser</code>. Нужно проверить не только имя и версию, но и путь от production root до компонента.</p>\n<p>Есть и обратная граница. Запись в lockfile описывает результат разрешения, но не доказывает, что пакет физически попал в конкретный образ. Сборка может исключить dev-зависимость, заменить optional-ветку, собрать другой workspace или использовать другой lockfile. Поэтому после дерева проверяют именно выходной артефакт и его digest.</p>\n<h2>Четыре слоя доказательств</h2>\n<p>Разделите объекты до первого изменения. Манифест описывает намерение проекта и диапазоны прямых зависимостей. Lockfile фиксирует разрешённое дерево. SBOM (Software Bill of Materials) перечисляет компоненты и отношения поставки для названного артефакта. Runtime evidence показывает наблюдение конкретного процесса и сценария. Каждый слой отвечает на свой вопрос и не заменяет соседний.</p>\n<p>Сначала зафиксируйте <code>baseline</code>: commit, package manager, Node.js, настройки <code>.npmrc</code>, команду установки и digest текущего артефакта. Затем назовите <code>candidate</code>: пакет, исходную и целевую версии, advisory и предполагаемый путь обновления. Без baseline нельзя отличить удаление уязвимого узла от его перемещения в другую ветку.</p>\n<div class=\"table-scroll\"><table><caption>Что именно проверяет каждый слой</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Вопрос</th><th scope=\"col\">Что сохранить</th></tr></thead><tbody><tr><td><code>package.json</code></td><td>Какие direct dependencies и диапазоны объявлены?</td><td>Commit и diff манифеста</td></tr><tr><td><code>package-lock.json</code></td><td>Какое дерево разрешено зафиксированным проектом?</td><td>Полный diff, integrity и настройки resolver</td></tr><tr><td><code>node_modules</code> / image</td><td>Что физически установлено в проверенном артефакте?</td><td>Имя образа, digest и состав слоя</td></tr><tr><td>SBOM</td><td>Какие компоненты заявлены для named artifact?</td><td>Формат, источник генерации и связь с digest</td></tr><tr><td>Runtime</td><td>Что произошло в названном entry point?</td><td>Команду, вход, ответ, лог и отрицательный путь</td></tr></tbody></table></div>\n<h2>Воспроизводимый маршрут для npm</h2>\n<p>Ниже команды для проекта на npm. Подставьте в <code>PACKAGE</code> имя из advisory и выполняйте их в том workspace, который собирается в production. Если pipeline использует <code>--legacy-peer-deps</code>, workspaces или иной <code>.npmrc</code>, повторите те же настройки: разрешение зависит не только от текста манифеста.</p>\n<pre><code>PACKAGE=parser\n\nnode --version\nnpm --version\nnpm ci\nnpm ls \"$PACKAGE\" --all --omit=dev --json &gt; /tmp/dependency-tree.json\nnpm explain \"$PACKAGE\"\nnpm audit --omit=dev --json &gt; /tmp/npm-audit.json</code></pre>\n<p><code>npm ci</code> — контрольная точка. По официальной документации ему нужен существующий lockfile; при расхождении с <code>package.json</code> команда завершается ошибкой, удаляет имеющийся <code>node_modules</code> и не переписывает манифест или lockfile. Если установка упала, это отдельный результат: сначала разберите peer-диапазоны, версию npm и флаги, с которыми lockfile был создан.</p>\n<p>Не заменяйте <code>npm ci</code> на <code>npm install</code> ради зелёного exit code: обычная установка может пересчитать lockfile. Флаг <code>--omit=dev</code> убирает dev-зависимости с диска, но записи о них остаются разрешёнными в lockfile. Если build использует dev-инструменты, описывайте build-артефакт отдельной проверкой.</p>\n<p><code>npm ls --all --omit=dev --json</code> показывает логическое дерево установленных пакетов и может отметить missing, invalid или extraneous узлы. <code>npm explain</code> даёт обратный путь — почему пакет присутствует. Нулевой вывод не закрывает advisory, пока не проверены правильные workspace, lockfile, режим установки и artifact.</p>\n<h2>Найдите каждый путь в дереве</h2>\n<p>Одна строка <code>parser@1.0.0</code> отвечает только на вопрос о версии. Один пакет может присутствовать несколько раз из-за несовместимых диапазонов. Логическое дерево npm также не равно физическому расположению на диске: deduplication и peer-зависимости меняют картину. Для решения нужны все пути от корня.</p>\n<pre><code>function findPaths(node, wanted, path = []) {\n const name = node.name || '&lt;root&gt;';\n const version = node.version || 'unknown';\n const current = path.concat(name + '@' + version);\n const matches = node.name === wanted\n ? [{ version: version, path: current }]\n : [];\n\n for (const entry of Object.entries(node.dependencies || {})) {\n const childName = entry[0];\n const child = entry[1];\n matches.push(...findPaths(\n Object.assign({ name: childName }, child),\n wanted,\n current,\n ));\n }\n\n return matches;\n}\n\nconst tree = JSON.parse(require('fs').readFileSync(0, 'utf8'));\nconst packageName = process.argv[2] || 'parser';\nconsole.log(JSON.stringify(findPaths(tree, packageName), null, 2));</code></pre>\n<p>Сохраните фрагмент как <code>find-paths.js</code> и выполните <code>node find-paths.js parser &lt; /tmp/dependency-tree.json</code>. Учебный вывод может показать две цепочки — через <code>http-client@4</code> и через <code>markdown-tool@2</code>. Это доказывает две записи в переданном JSON, но не доказывает, что обе попали в image и что опасная ветка вызывается. Учитывайте это ограничение при формулировке результата.</p>\n<h2>Сопоставьте дерево с артефактом</h2>\n<figure><img src='/assets/editorial/2023/dependency-security-2023-lockfile-sbom.svg' alt='Схема границ manifest, lockfile, SBOM и runtime evidence: каждый следующий слой требует отдельной проверки и не является автоматической гарантией предыдущего' loading='lazy' /><figcaption>Manifest выражает намерение, lockfile — разрешённое дерево, SBOM — состав названного артефакта, runtime evidence — наблюдение конкретного сценария. Стрелки означают проверку связи, а не доказанную безопасность.</figcaption></figure>\n<p>SBOM полезен только вместе с идентичностью сборки: commit, digest, временем и понятным источником генерации. Файл с названием <code>sbom.json</code> без связи с release candidate может относиться к соседнему образу. Сверяйте компонент, версию, источник и relationship; расхождение с lockfile — сигнал расследования, а не повод выбрать более удобный результат.</p>\n<p>Сканируйте immutable digest именно публикуемого образа или архива. Локальный <code>node_modules</code>, staging-образ и предыдущий digest не являются заменой. Если сборка создаёт несколько image layers, выясните, где лежит компонент и не остаётся ли старая копия в другом слое.</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>Пакета нет в <code>package.json</code></td><td>Он транзитивный или пришёл из другого workspace</td><td><code>npm ls --all</code> и <code>npm explain</code> в production root</td><td>Назвать родителя и проверить каждый путь</td></tr><tr><td>Lockfile изменился, advisory остался</td><td>Осталась другая копия или ветка</td><td>Сравнить все записи и версии в дереве и SBOM</td><td>Обновить каждого владельца либо обосновать исключение</td></tr><tr><td><code>npm ci</code> зелёный, сервис не стартует</td><td>Не совпали Node.js, peer range, ABI или module format</td><td>Smoke в том же image и runtime</td><td>Остановить выпуск и сузить candidate</td></tr><tr><td>Сканер не видит старую версию</td><td>Проверен другой digest или слой</td><td>Сопоставить digest отчёта и release</td><td>Пересканировать публикуемый artifact</td></tr><tr><td>Rollback вернул версию, но данные не читаются</td><td>Изменился формат или внешний протокол</td><td>Проверить обратную совместимость миграции</td><td>Разделить security update и изменение данных</td></tr></tbody></table></div>\n<h2>Отделите наличие от достижимости</h2>\n<p>Наличие пакета и достижимость уязвимого кода — разные утверждения. Назовите entry point, импорт или вызов, входные данные и режим исполнения. Для сервера это HTTP-обработчик, для CLI — команда, для worker — сообщение из очереди. Формулировка «ветка не достигнута в проверенном сценарии» честнее, чем «уязвимости нет».</p>\n<p>Проверьте optional-зависимости на целевой платформе, peer-зависимости, bundled packages, worker, cron и отдельный CLI. Сборщик может исключить статический импорт, а динамический <code>require</code> сохранить путь. Строковый поиск находит имя, но не учитывает условие, экспорт, bundler и конфигурацию.</p>\n<p>Минимальное evidence — связка dependency path, содержимого production-артефакта и названного сценария с ожидаемым результатом. Добавьте отрицательный путь: некорректный вход должен быть отклонён ожидаемым способом, а недоступная optional-часть не должна молча создавать видимость исправности.</p>\n<h2>Обновление как контролируемое изменение</h2>\n<p>Сформируйте небольшой candidate. Обновляйте прямого родителя, если он выпускает совместимую версию, или добавляйте явное разрешение с объяснением владельца риска. Не меняйте одновременно Node.js, package manager и несколько крупных библиотек: rollback перестанет показывать причину отказа.</p>\n<p>Прочитайте весь diff lockfile: resolved URL, integrity, peer и optional-поля, количество копий и новые install scripts. Поле <code>engines</code> выражает заявленный диапазон совместимости. Без <code>engine-strict</code> npm может оставить предупреждение и продолжить, а строгая проверка всё равно не запускает приложение и не проверяет native addon.</p>\n<p>Повторите clean install с параметрами pipeline. Затем запустите startup, импорт компонента, валидный и невалидный вход, сборку, сетевой вызов и затронутый CLI. У каждого теста должны быть команда, окружение и exit code. Фраза «всё прошло» без этого набора не является доказательством.</p>\n<h2>Порядок действий перед выпуском</h2>\n<ol><li>Скопируйте advisory: пакет, затронутый диапазон, исправленную версию и источник. Отделите утверждение advisory от гипотезы о вашем приложении.</li><li>Определите production root и baseline: commit, lockfile, Node.js, npm, <code>.npmrc</code> и digest текущего артефакта.</li><li>Выполните <code>npm ci</code>, затем сохраните <code>npm ls --all --omit=dev --json</code> и <code>npm explain PACKAGE</code>. Разберите каждый путь и копию.</li><li>Соберите candidate с минимальным diff. Проверьте манифест, lockfile, integrity, peer/optional-ветки и install scripts.</li><li>Повторите clean install и тесты на том же runtime. Проверьте успешный и отрицательный сценарий затронутого entry point.</li><li>Соберите image или архив. Сканируйте его immutable digest и сопоставьте результат с SBOM, lockfile и commit.</li><li>Сделайте runtime smoke в том же окружении: старт, безопасный запрос, ожидаемый отказ и нужные worker/CLI paths.</li><li>Проверьте rollback к baseline. При изменении формата данных, миграции или протокола отдельно подтвердите обратную совместимость.</li></ol>\n<h2>Ложные зелёные результаты</h2>\n<p>Зелёный <code>npm audit</code> не означает, что внешний scanner ошибся: базы advisory, области установки и правила инструментов различаются. Красный scanner тоже не доказывает достижимость вызова. Зафиксируйте источник, версию базы, путь пакета и artifact, затем согласуйте решение с владельцем риска.</p>\n<p>Установка с <code>--ignore-scripts</code> не проверяет install script, который нужен приложению. macOS-проверка не заменяет Linux-образ, если native dependency собирается в CI. Локальная папка не заменяет image digest. Эти ограничения пишутся рядом с результатом, иначе отчёт создаёт ложную уверенность.</p>\n<h2>Ограничения применимости</h2>\n<p>Маршрут рассчитан на npm-проекты с <code>package-lock.json</code>. Для Yarn, pnpm, Cargo, Maven или системных пакетов команды и формат lockfile будут другими, хотя разделение manifest, resolved tree, artifact и runtime остаётся полезной моделью. Не переносите npm-команды в другой менеджер без сверки его документации.</p>\n<p>Статья не определяет exploitability и не заменяет threat model, code review или расследование инцидента. Достижимость зависит от кода, конфигурации, прав, входных данных и внешних сервисов. Если не удалось получить точный digest, выполнить сценарий или сопоставить SBOM с build, статус должен быть <code>unknown</code>, а не <code>safe</code>.</p>\n<p>Major-обновление требует отдельной оценки API-изменений. Для native addon добавьте проверку ABI и целевой платформы. Для private registry сохраняйте provenance, но не публикуйте токены и приватные URL в отчёте.</p>\n<h2>Критерий готовности</h2>\n<p>Обновление готово к выпуску, когда одна связанная запись содержит advisory и scope, baseline и candidate, все dependency paths, diff lockfile, clean install, тесты затронутого контракта, runtime smoke, SBOM или иной состав артефакта, сканирование того же digest и проверяемый rollback. У каждого результата есть команда, окружение и exit code либо наблюдаемый ответ.</p>\n<p>Любой пропуск получает явную отметку: <code>not run</code>, <code>not applicable</code> с причиной или <code>unknown</code>. Пока не доказана связь между пакетом, образом и runtime-сценарием, предупреждение не закрыто. Так команда принимает не «самую новую версию», а изменение с понятной областью действия и обратным ходом.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://docs.npmjs.com/cli/v8/commands/npm-ci\" target=\"_blank\" rel=\"noopener noreferrer\">npm Docs: npm ci</a> — требования к lockfile, согласованности с <code>package.json</code>, очистке <code>node_modules</code> и неизменности lockfile во время CI-установки.</li><li><a href=\"https://docs.npmjs.com/cli/v8/commands/npm-ls\" target=\"_blank\" rel=\"noopener noreferrer\">npm Docs: npm ls</a> — логическое дерево установленных пакетов, флаги <code>--all</code>, <code>--json</code> и ограничение: вывод не равен физическому расположению на диске.</li><li><a href=\"https://docs.npmjs.com/cli/v8/commands/npm-explain\" target=\"_blank\" rel=\"noopener noreferrer\">npm Docs: npm explain</a> — обратный ответ на вопрос, почему пакет присутствует в дереве.</li><li><a href=\"https://docs.npmjs.com/cli/v8/configuring-npm/package-json\" target=\"_blank\" rel=\"noopener noreferrer\">npm Docs: package.json</a> — назначение поля <code>engines</code> и его advisory-характер без <code>engine-strict</code>.</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</a> — состав и назначение SBOM, включая компоненты, связи поставки и границу: SBOM не решает все проблемы безопасности сам по себе.</li></ul>"
}