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

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

\n

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

\n

Четыре вопроса вместо одного

\n

Manifest отвечает на вопрос «что проект просит установить». Для прямой зависимости он хранит имя и допустимый диапазон версий. Запись \"demo-shell\": \"^1.0.0\" не говорит, какая версия окажется в текущем дереве.

\n

Lockfile отвечает на другой вопрос: какое дерево выбрал установщик при конкретных правилах разрешения. Для npm это точное представление дерева, созданного установкой. В нём видны транзитивные пакеты, версии, источники и integrity-поля. Но lockfile не является журналом уже запущенного процесса. Его нужно связать с commit, командой установки и артефактом, который действительно собирает pipeline.

\n

SBOM отвечает на вопрос «какие компоненты заявлены для named artifact и как они связаны». Он может быть независим от конкретного package manager и пригоден для анализа поставки. Но файл без build ID, digest или другой неизменяемой привязки не доказывает состав текущего образа. Runtime evidence отвечает на четвёртый вопрос: что загрузилось и выполнилось в выбранном сценарии. Ни один из этих слоёв не заменяет остальные.

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

Учебный graph: почему одного совпадения мало

\n

Рассмотрим ограниченный fixture. demo-service зависит от demo-shell, а demo-shell — от demo-parser. Advisory указывает на demo-parser@1.0.0. В synthetic lockfile пакет есть. В synthetic 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 и не строит call graph. Он показывает важную границу: найденная строка — это факт о входных данных, а не вердикт о безопасности. Если имя отсутствует в lockfile, вопрос меняется на provenance advisory или на несовпадение версии. Если имя есть в lockfile, но отсутствует в SBOM нужного build, нужно проверять генерацию и identity артефакта. Если оба списка совпали, всё ещё остаётся путь использования.

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

Симптом → причина → проверка → действие

\n
Диагностика несвязанной цепочки поставки
СимптомПричинаПроверкаДействие
Изменился package.json, но lockfile остался прежнимПроверили намерение, но не разрешённое деревоСравнить direct range, lockfile entry и install commandПересобрать candidate tree и review diff
Lockfile и SBOM показывают разные версииSBOM создан из другого commit или buildСверить source revision, artifact digest и время генерацииСгенерировать SBOM для того же candidate artifact
Пакет есть в SBOM, но не найден в пути вызоваСостав поставки смешан с reachabilityПроверить import, entry point, flags и optional branchОставить риск в review до доказанного исключения
После patch update изменилось много строк lockfileResolver обновил транзитивное деревоРазделить added, removed, version и integrity changesПроверить каждую существенную ветку и тесты
Fixture завершился PASSPASS проверяет только synthetic contractПосмотреть статусы provenance, runtime и deploymentНе прикладывать PASS как доказательство production safety
\n

Порядок проверки advisory

\n
  1. Сохраните immutable источник сигнала: URL или ID advisory, имя пакета, affected range и дату получения. Не называйте совпадение имени подтверждённой уязвимостью.
  2. Найдите exact package и version в lockfile того commit, из которого собирается candidate. Запишите путь в дереве: direct dependency, parent и транзитивная ветка.
  3. Сопоставьте компонент с SBOM, сгенерированным для того же артефакта. Зафиксируйте format, generator, source revision и artifact digest.
  4. Проверьте достижимость отдельным методом. Ищите entry point, imports, conditional exports, feature flags, optional dependencies и платформенные ветки. Если метод не покрывает часть пути, запишите unknown.
  5. Сформируйте candidate update и просмотрите полный lockfile diff. Убедитесь, что изменение не добавило другой риск и не изменило дерево шире, чем ожидалось.
  6. Выполните clean install с правилами pipeline, затем проектные тесты и короткий runtime smoke для сценария, где используется затронутая ветка. Результат должен ссылаться на конкретный build.
  7. Запишите решение и rollback. Если доказательств не хватает, статус должен быть «требует проверки проекта», а не «ложное срабатывание» и не «безопасно».
\n

Почему обновление иногда увеличивает область риска

\n

Patch update не ограничивается одной строкой manifest. Новый диапазон может выбрать другую транзитивную версию. Installer может иначе обработать optional dependency на другой платформе. Lifecycle script может изменить содержимое build. Поэтому после обновления смотрят lockfile diff, а не только diff package.json. Важны added и removed packages, version shifts, integrity и source fields.

\n

Отдельная ловушка — SBOM, который лежит рядом с репозиторием, но не рядом с выпуском. Такой документ может быть полезен для разработки и одновременно бесполезен для ответа о deployed image. Минимальный критерий свежести задаёт сам pipeline: SBOM создан из того же source revision и относится к тому же artifact digest, что и release candidate. Это инженерный критерий, который нужно реализовать, а не свойство любого файла с названием SBOM.

\n

Ограничения и отрицательный путь

\n

Учебная модель не знает реальную advisory database, формат конкретного SBOM, private registry, зависимости ОС, native addons, bundle, generated source или runtime configuration. Она не вычисляет vulnerable range и не доказывает отсутствие эксплуатации. Даже реальный путь импорта не равен доказанной уязвимости: нужно понимать, достигается ли опасный код с контролируемыми входными данными и при каких настройках.

\n

Отрицательный путь обязателен. Advisory может не совпасть с exact resolved version. Компонент может быть в lockfile, но отсутствовать в named artifact. SBOM может быть старше образа. Достижимость может зависеть от выключенного по умолчанию флага. В каждом случае нельзя подменять неизвестность уверенным исключением. Укажите, что именно не проверено, каким методом это проверят и кто владеет следующим действием.

\n

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

\n

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

\n

Если хотя бы одно звено отсутствует, работа может быть готова к следующему этапу, но не к утверждению безопасности выпуска. Это не бюрократическая формальность. Такая запись позволяет отличить реальный риск от неполного inventory и не потерять его при следующем обновлении.

\n

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

" }