{ "index": 171, "slug": "editorial-2023-04-practice-dependency-security", "title": "Advisory зависимости: как доказать, что сигнал относится к вашему выпуску", "excerpt": "Совпадение package и версии с advisory ещё не доказывает риск в приложении. Разбираем lockfile, SBOM, путь зависимости и безопасный порядок обновления с воспроизводимыми командами.", "contentHtml": "
Сбой в CI сопровождается advisory: пакет совпал с уязвимой версией. Первая реакция понятна — поднять версию в package.json и закрыть тикет после зелёного pipeline. Но такой сигнал отвечает только на один вопрос: известна запись о компоненте с подходящим диапазоном версий. Он не отвечает, попал ли компонент в конкретный образ, откуда он пришёл, выполняется ли опасная ветка и относится ли найденный SBOM к этому выпуску.
Практический риск здесь двойной. Ложное «уязвимо» заставляет срочно менять большое дерево зависимостей и ломает unrelated-сценарии. Ложное «исправлено» оставляет старый образ или транзитивный путь без владельца. Поэтому advisory нужно разбирать как цепочку доказательств: внешний сигнал → exact package@version → запись в lockfile → компонент в том же артефакте → проверенный путь выполнения → решение с границами применимости.
\nНе формулируйте задачу как «проверить безопасность пакета». Это слишком широкое обещание. Запишите узкий вопрос: «Есть ли package@version из advisory в lockfile commit X, в SBOM artifact digest Y и в runtime-сценарии Z?» Если один из переходов пока неизвестен, это часть результата расследования.
Сначала сохраните идентификатор сигнала: URL или ID advisory, имя пакета, затронутый диапазон версий, источник и время получения. Не переносите в тикет более сильную формулировку, чем есть в источнике. «Совпала версия из диапазона» и «уязвимый код достижим из внешнего запроса» — разные утверждения и требуют разных проверок.
\n| Слой | Проверяемый вопрос | Подходящий evidence | Чего он не доказывает |
|---|---|---|---|
| Advisory | Какие package и version range описаны внешним источником? | URL или ID, дата, диапазон, severity и описание условия | Воздействие на конкретный сервис |
package.json | Какую direct dependency просит проект? | Commit и diff manifest | Точную транзитивную версию |
| Lockfile | Какое дерево выбрал установщик? | Запись пакета, parent path, resolved и integrity-поля | Загрузку модуля процессом |
| SBOM | Какие компоненты заявлены для артефакта? | Формат, component record и commit/digest артефакта | Достижимость опасной функции |
| Runtime evidence | Что произошло в названном сценарии? | Тест, trace или controlled smoke с build identity | Все возможные платформы и флаги |
Manifest описывает намерение: например, \"demo-shell\": \"^1.4.0\" разрешает установщику выбрать совместимую версию. Lockfile фиксирует результат разрешения для конкретного состояния проекта. В npm это точное дерево, из которого последующие установки могут восстановить те же версии, если соблюдены правила и версия инструмента.
Проверяйте не только наличие имени. Для транзитивной зависимости нужен путь: какая direct dependency привела к пакету, какая версия родителя была выбрана и не существует ли второй копии в другом поддереве. Две записи одного имени могут иметь разные версии и разные условия попадания в bundle. Удаление прямого импорта не исключает транзитивную, optional, plugin или dynamic-import ветку.
\nКоманда npm explain предназначена именно для восстановления цепочки, которая привела пакет в установленное дерево. Но она описывает установленное дерево в конкретной рабочей директории. Если проверяется другой build, сначала получите чистую установку из его lockfile и только затем интерпретируйте вывод.
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 > artifacts/package-tree.json\nЭтот блок не выдаёт автоматически решение. Он сохраняет контекст инструмента и показывает, где искать parent path. Если npm ci не проходит, сначала разберите несовместимость manifest и lockfile. Нельзя считать отсутствие результата npm explain доказательством отсутствия компонента в уже опубликованном образе.
SBOM — это инвентарь компонентов, а не заключение о безопасности. NTIA описывает для него поля идентификации компонентов, поддержку автоматической обработки и процессы формирования. На практике этого достаточно, чтобы сопоставлять компонент с базой advisory, но недостаточно, чтобы утверждать его достижимость или отсутствие эксплуатации.
\nДля npm можно получить SBOM командой npm sbom. Режим --package-lock-only строит результат по lockfile, поэтому он полезен для проверки resolved tree, но не заменяет SBOM контейнерного образа: в нём могут быть системные библиотеки, native runtime и дополнительные файлы. Если вопрос относится к deployed image, SBOM нужно создавать в build-процессе для того же digest, который будет развёрнут.
# Современный npm CLI: проверить поддерживаемые опции перед запуском.\nnpm help sbom\n\n# SBOM по lockfile, без dev-зависимостей в установленном дереве.\nnpm sbom --package-lock-only --omit=dev --sbom-format=cyclonedx \\\n > artifacts/sbom-lockfile.cdx.json\n\n# Аудит npm использует lockfile; JSON сохраняет детали сигнала.\nnpm audit --omit=dev --json > artifacts/npm-audit.json\nУ команд есть границы. npm docs указывают, что npm audit отправляет описание зависимостей в настроенный registry для получения отчёта; проверьте правила обращения с именами private-пакетов и registry в своей организации. Опция --omit=dev меняет проверяемое установленное дерево, но не означает, что dev-записи исчезли из lockfile. Устаревший npm может не знать npm sbom; тогда используйте одобренный генератор SBOM и зафиксируйте его версию.
Рассмотрим воспроизводимую модель без реального registry. Входные массивы ниже заранее заданы в памяти: один имитирует lockfile, второй — SBOM. Имена synthetic, поэтому пример не подтверждает конкретный CVE, версию библиотеки, build или runtime. Он нужен, чтобы не смешивать факт присутствия с выводом о поведении.
\nconst 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 > demo-shell > 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) =>\n item.name === advisory.packageName\n && item.version === advisory.affectedVersion\n);\nconst recorded = sbom.find((item) =>\n item.name === advisory.packageName\n && 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});\nОжидаемый результат показывает true для двух совпадений и одновременно not-assessed для достижимости. Это правильный результат модели. Код не читает настоящий lockfile, не строит call graph, не запускает контейнер и не проверяет входные данные. Подменять его PASS-выводом production-сканера нельзя.
Достижимость отвечает на вопрос «может ли выполнение попасть в нужный модуль или функцию при заданном entry point и конфигурации». Ищите импорт, регистрацию plugin, conditional export, dynamic import, feature flag и платформенную ветку. Зафиксируйте, какой метод использован: статический граф показывает возможную связь, тест — выбранный сценарий, trace — один наблюдаемый запуск.
\nExploitability — ещё более сильный вывод. Даже если модуль достижим, нужно знать, достигается ли уязвимая ветка, какие входные данные до неё доходят и какие защитные условия действуют. Ни lockfile, ни SBOM, ни успешный npm audit не вычисляют это автоматически для вашего приложения. Для такого вывода нужны анализ advisory, кодовый review и подходящие security-тесты.
| Наблюдение | Допустимый вывод | Следующий шаг |
|---|---|---|
| Package/version совпали только в advisory и manifest | Есть сигнал и заявленный диапазон, resolved tree не доказан | Проверить lockfile candidate-коммита |
| Пакет найден в lockfile, parent path известен | Компонент выбран resolver для этого дерева | Сверить SBOM и artifact identity |
| Пакет найден в SBOM без digest | Есть неподтверждённая inventory-запись | Найти provenance или пересоздать SBOM в build |
| SBOM и digest совпали, путь не проверен | Компонент заявлен в конкретном артефакте | Проверить entry point, flags и runtime-сценарий |
| Путь найден, опасная ветка подтверждена входом | Риск относится к названному сценарию и scope | Оценить remediation, тесты и rollback |
Обновление одной direct dependency может перестроить всё дерево. Меняются peer dependencies, optional-пакеты, integrity, формат модулей и lifecycle scripts. Поэтому сначала сохраните baseline, затем создайте candidate и просмотрите полный diff lockfile. Широкий diff не означает, что обновление ошибочно; он означает, что область совместимости стала шире ожидаемой и требует объяснения.
\n# Перед изменением сохраните baseline. Не включайте секреты в artifacts.\ngit rev-parse HEAD > 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\nЗамените PACKAGE и FIXED_VERSION на значения из вашего advisory и учитывайте менеджер пакетов проекта. Не запускайте npm audit fix --force как универсальную кнопку: npm docs предупреждают, что --force разрешает изменения за пределами обычных ограничений и может привести к major-обновлениям. Если требуется major version, сначала нужен совместимый change plan и тестовый rollback.
npm explain и npm ls --all, сохраните parent path и все найденные версии.Зелёный путь показывает, что выбранная версия устанавливается и тестовый сценарий проходит. Он не показывает, что старая версия не поставляется другим job, не остаётся в старом образе и не приходит через optional branch. Проверьте как минимум четыре отрицательных условия: exact version не совпадает с advisory range; package отсутствует в candidate SBOM; SBOM относится к другому digest; entry point не может включить ветку при заданной конфигурации.
\nДля каждого отрицательного результата фиксируйте границу. «Не найдено в SBOM digest Y» не равно «пакет нигде не существует». «Не импортируется этим entry point» не равно «уязвимость невозможна во всех режимах». Если проверка не покрывает dynamic loading или native code, оставьте это как unknown и назначьте следующий способ проверки.
\nСхема рассчитана на проекты, где можно получить lockfile и идентичность build. Она не заменяет аудит private registry, зависимостей ОС, контейнерных слоёв, generated code, native addons, browser bundle, runtime flags, XSS или authorization. Формат SBOM и качество его генератора тоже влияют на результат. Для yarn, pnpm, Composer, Maven и других экосистем команды и поля будут другими; переносить npm-команды без адаптации нельзя.
\nСсылки на npm CLI относятся к текущей документации. Поведение и доступность команд зависят от версии npm, настроек registry и package-manager policy. Поэтому сохраняйте node --version, npm --version, параметры omit и источник SBOM рядом с результатом. Это не делает проверку вечной, но позволяет повторить её в том же контракте и увидеть, что изменилось.
Расследование готово к решению, когда другой инженер может восстановить цепочку для одного candidate release: advisory → exact package@version → lockfile commit → parent path → SBOM → artifact digest → entry point и configuration → evidence достижимости → install, tests и smoke → решение и rollback. Для каждого перехода есть ссылка или сохранённый результат. Неизвестные пути перечислены, а не скрыты под словом «безопасно».
\nТакой критерий не обещает отсутствие уязвимостей. Он делает вывод проверяемым и ограниченным: понятно, какой компонент и какой артефакт исследованы, какой сценарий покрыт и где требуется дополнительная работа. Это достаточная основа для инженерного решения и гораздо надёжнее, чем совпадение имени в отчёте сканера.
\n