{ "index": 170, "slug": "editorial-2023-04-mechanism-dependency-security", "title": "Advisory, lockfile и SBOM: как доказать состав выпуска", "excerpt": "Имя пакета в advisory ещё не доказывает риск для конкретного выпуска. Разбираем границы manifest, lockfile, SBOM и runtime, показываем проверку npm-командами и критерий, при котором неизвестность не маскируют под «безопасно».", "contentHtml": "

Сканер сообщает об advisory для транзитивного пакета. Команда меняет строку в package.json, получает зелёный pull request и закрывает задачу. Позже выясняется, что lockfile не изменился, SBOM относится к предыдущему образу, а пакет присутствует только в optional-ветке другой платформы. Команда потратила время на ложную аварию или оставила настоящий риск без владельца. В обоих случаях не восстановлен ответ на главный вопрос: какой компонент попал в конкретный выпуск и при каких условиях он достижим?

\n

Безопасность зависимости проверяют не по одному имени и не по одному файлу. Внешний сигнал связывают с точной записью в resolved tree, эту запись — с SBOM того же кандидата, а затем отдельно проверяют достижимость и поведение нужного runtime-сценария. package.json, lockfile, SBOM и runtime evidence описывают разные границы. Совпадение двух списков полезно, но не является вердиктом о безопасности.

\n

Сигнал advisory не равен факту о выпуске

\n

Advisory — сообщение о проблеме в определённом пакете и диапазоне версий. Оно отвечает на вопрос «какой сигнал надо разобрать», но не на вопрос «затронут ли мой образ». Для этого нужны как минимум имя пакета, точная версия, источник пакета и путь, по которому компонент попал в кандидат.

\n

Различайте три утверждения. «Версия попала в дерево» — факт о разрешении зависимостей. «Компонент попал в артефакт» — факт о конкретном build и его SBOM. «Опасный код достижим с контролируемым входом» — вывод анализа кода, конфигурации и runtime. Эти утверждения могут быть истинны независимо друг от друга.

\n

Что фиксирует каждый слой

\n

Manifest фиксирует намерение проекта. Диапазон \"demo-shell\": \"^1.0.0\" задаёт допустимые версии, но не говорит, что установлено сегодня. Lockfile фиксирует resolved tree. В npm он содержит представление дерева, версии, источники и integrity-поля, чтобы повторная установка могла получить ту же структуру.

\n

SBOM — inventory конкретного программного продукта и его компонентов. Он полезен для сопоставления с базами уязвимостей и лицензий, но файл без source revision, build ID или digest нельзя надёжно связать с deployed image. Runtime evidence отвечает на ещё один вопрос: что загрузилось и произошло в выбранном сценарии. Ни один слой не заменяет остальные.

\n
Граница ответственности артефактов
СлойВопросПроверкаЧего не доказывает
package.jsonЧто проект просит установить?Diff manifest и review диапазонаТочную resolved version
package-lock.jsonКакое дерево выбрал installer?Diff lockfile и clean installЗагрузку модуля в процессе
SBOMКакие компоненты заявлены для artifact?Records, relations и identity артефактаДостижимость по коду
Runtime evidenceЧто произошло в named scenario?Тест, trace или controlled smokeВсе возможные пути
AdvisoryКакой внешний сигнал исследовать?ID, package и affected rangeВоздействие на сервис
\n

Граф зависимости: почему одного совпадения мало

\n

Возьмём небольшой учебный граф: demo-service зависит от demo-shell, а demo-shell — от demo-parser. Advisory указывает на demo-parser@1.0.0. Совпадение имени и версии в lockfile и SBOM подтверждает пересечение двух документов. Оно не подтверждает, что пакет попал в production-образ, вызывается из реального entry point или уязвим при фактической конфигурации.

\n
const locked = packages.find(\n  (item) => item.name === advisory.packageName\n    && item.version === advisory.affectedVersion,\n);\n\nconst recorded = components.some(\n  (item) => item.name === advisory.packageName\n    && item.version === advisory.affectedVersion,\n);\n\nreturn {\n  lockfile: locked ? 'entry-matched' : 'entry-not-found',\n  sbom: recorded ? 'component-matched' : 'component-not-found',\n  reachability: 'not-assessed',\n  vulnerabilityStatus: 'not-determined',\n};
\n

Этот фрагмент намеренно не читает настоящий lockfile и не запускает scanner. Он показывает границу доказательства: найденная строка — факт о входных данных, а не решение по уязвимости. Если exact version отсутствует в lockfile, проверяют provenance advisory или другой workspace. Если запись есть в lockfile, но отсутствует в SBOM кандидата, проверяют вход генератора и identity артефакта. Если оба списка совпали, остаётся анализ пути использования.

\n
\"Схема
Схема разделяет намерение проекта, resolved tree, инвентарь связанного артефакта и наблюдаемое выполнение. Это учебная модель, а не SBOM или запись production-запуска.
\n

Воспроизводимая проверка в npm

\n

Ниже — последовательность для проекта с package.json и package-lock.json. Запускайте её в том же commit и с той же версией Node/npm, которые использует сборка. npm ci удаляет существующий node_modules и завершается с ошибкой, если manifest и lockfile не согласованы. Это контроль входа, а не замена тестам приложения.

\n
set -eu\n\n# 1. Чистое дерево кандидата.\nnpm ci\n\n# 2. Почему пакет установлен и где находятся его копии.\nnpm explain demo-parser\nnpm ls demo-parser --all --json > dependency-tree.json\n\n# 3. Известные advisory. Ненулевой код возможен при найденных проблемах.\nset +e\nnpm audit --json > audit.json\naudit_status=$?\nset -e\nprintf 'npm audit exit code: %s\\n' \"$audit_status\"\n\n# 4. SBOM именно после этой установки.\nnpm sbom --sbom-format=spdx > sbom.spdx.json\n\n# 5. Идентичность результатов фиксируется рядом с CI-артефактами.\ngit rev-parse HEAD\nsha256sum package-lock.json sbom.spdx.json
\n

npm explain выводит цепочку зависимостей, из-за которой пакет установлен. npm ls даёт машинно читаемое дерево текущего node_modules, а не доказательство того, что именно это дерево ушло в выпуск. npm audit отправляет описание зависимостей в registry и может завершиться ненулевым кодом; такой статус показывает результат аудита, но не доказывает эксплуатацию. npm sbom создаёт SPDX или CycloneDX. Для production SBOM сохраняйте digest образа или другого выпускаемого артефакта рядом с документом.

\n

Как читать расхождения

\n

Сверку начинайте с identity, а не с имени пакета. Один и тот же name@version в двух документах не означает один и тот же источник, если различаются registry, URL tarball, integrity, source revision или artifact digest. В lockfile эти поля помогают отличить запись от простого совпадения текста.

\n
Решение по результату сверки
НаблюдениеЧто доказаноЧто неизвестноСледующий шаг
Advisory не совпадает с exact versionСигнал не подтверждает affected version в этом lockfileКорректность advisory, другой workspace или артефактПроверить источник и границы affected range
Exact version есть в lockfile, нет в SBOMКандидатное дерево содержит компонентДругой вход генератора или неполный SBOMСверить commit, digest, omit и генератор
Компонент есть в SBOM, но omitted при installОн разрешён и описан документомПопал ли на диск и в runtime-образПроверить build-команду и финальный слой
Компонент достижим, но опасная ветка выключенаПуть существует при определённом условииФлаг и входы в deployed окруженииПроверить условие smoke-тестом
Есть только локальный npm auditRegistry вернул результат для локального дереваСостояние конкретного deployed artifactПовторить для candidate и привязать к digest
\n

Почему SBOM не закрывает достижимость

\n

SBOM — инвентарная модель компонентов и отношений между ними. Она помогает сопоставлять компонент с базой уязвимостей, лицензий или provenance. Но инвентарь не знает, вызывается ли экспорт, включён ли feature flag, доступен ли endpoint извне и может ли атакующий передать опасный вход. Эти вопросы относятся к анализу кода, конфигурации и runtime.

\n

Работает и обратная граница: успешный smoke-тест одного endpoint не доказывает отсутствие риска во всех путях. Native addon, bundled dependency, generated bundle, private registry и зависимости операционной системы могут выпасть из npm-сценария. Если инструмент не покрывает такой слой, результат помечают как unknown и назначают отдельную проверку.

\n

Критерий готовности и ограничения

\n

Решение об обновлении можно принимать, когда для одного кандидата восстановлена цепочка advisory → exact package@version → lockfile commit → SBOM → artifact digest → runtime-сценарий. Для каждого перехода есть файл, ссылка или команда, которую другой инженер может повторить. Lockfile diff просмотрен, clean install завершился ожидаемо, проектные тесты прошли, а неизвестные пути перечислены отдельно.

\n

Критерий не вычисляет вероятность эксплуатации и не отменяет ручную оценку. Он также не обещает полноту SBOM: она зависит от генератора, режима установки и того, что команда называет артефактом. Для монорепозитория отдельно проверяйте workspace, production-режим и образ, который публикуется. Для менеджеров, отличных от npm, переносите принцип слоёв, но не копируйте команды без сверки с документацией своего инструмента.

\n

Порядок действий для команды

\n
  1. Сохранить ID advisory, источник, affected range, время получения и имя пакета.
  2. Проверить exact package и version в lockfile кандидата, затем выполнить npm explain и зафиксировать путь от direct dependency.
  3. Убедиться, что кандидат собирается теми же Node/npm и установочными флагами, что и pipeline.
  4. Сгенерировать SBOM после clean install, связать его с commit и digest выпуска, сохранить формат и версию генератора.
  5. Сравнить lockfile, SBOM и финальный образ: версии, integrity, registry, optional/dev-границы и bundled-файлы.
  6. Отдельно проверить достижимость: entry point, import, conditional export, feature flag, конфигурацию и контролируемый вход.
  7. Запустить тесты и runtime smoke для затронутого сценария, затем записать решение, оставшиеся unknown и rollback-артефакт.
\n

Если отсутствует хотя бы одно звено, корректный статус — «требует дополнительной проверки». Такая формулировка сохраняет владельца и следующий шаг. Она точнее, чем «ложное срабатывание» при несовпавшей версии и чем «безопасно» при одном зелёном отчёте.

\n

Проверяемые источники

" }