DarkRiDDeR13 мин

Lockfile и SBOM: какие факты о зависимости они фиксируют

БезопасностьАрхитектура

После advisory команда часто открывает package.json, меняет видимую версию и считает работу законченной. Цена такой правки проявляется позже: lockfile разрешил другое транзитивное дерево, SBOM относится к старому build, а reviewer не может восстановить, что именно было поставлено. Ошибка не в существовании трёх файлов. Ошибка в ожидании, что manifest, lockfile и инвентарь отвечают на один вопрос.

Полезнее разделить их роли до изменения. Manifest выражает намерение автора: direct dependency и допустимый range. Lockfile фиксирует resolved tree, которое выбрал конкретный installer при известных ему правилах. SBOM описывает компоненты и supply-chain relationships выбранного software artifact. Runtime отвечает ещё на четвёртый вопрос: что реально было загружено в конкретном процессе. Эти слои связаны, но не взаимозаменяемы; у каждого есть свой владелец и свой способ проверки.

Один package — четыре разные границы

Документация npm 7 описывает package-lock.json как автоматически созданное представление dependency tree, которое помогает последующим installs получить то же дерево. Это сильнее, чем range в manifest, но не превращает файл в журнал уже запущенного сервиса. Installer flags, platform, optional dependencies, lifecycle scripts и выбор workspace могут менять условия, в которых дерево превращается в конкретный build. Поэтому проверить lockfile — обязательный шаг, но назвать его production inventory можно только при явной связи с артефактом.

NTIA называет SBOM формальной записью о деталях и отношениях компонентов. В отличие от lockfile, SBOM может жить рядом с поставкой и быть полезен вне конкретного package manager. Но полезность требует provenance: кто и когда сгенерировал документ, из какого commit или image, в каком format, где хранится неизменяемая ссылка. SPDX 2.3 описывает спецификацию формата и versioned fields; он не создаёт честную связь с binary автоматически. Файл без указанного artifact identity остаётся списком, происхождение которого невозможно проверить.

Граница ответственности артефактов
АртефактГлавный вопросВладелец состоянияПроверяемый сигналЧего не обещает
package.jsonкакую direct dependency просит проект?автор changemanifest diff и review rangeточно resolved tree
package-lock.jsonкакое дерево выбрал installer?resolver и committed lockfilelockfile diff, clean install с теми же правиламичто модуль загрузился в process
SBOMкакие компоненты заявлены для named artifact?build pipeline и владелец inventoryartifact identity, format, component recordsчто путь вызова достижим
Runtime evidenceчто произошло в named scenario?запуск и наблюдение средыtest, trace или controlled smokeвсе возможные configuration paths
Advisoryкакой внешний risk signal надо разобрать?источник advisory и triage ownerID, URL, package/version scopeфакт воздействия на этот сервис

Минимальный graph, который можно прочитать

Учебный fixture строит три lockfile rows: demo-service зависит от demo-shell, а demo-shell зависит от demo-parser. SBOM намеренно перечисляет только два component records, потому что пример показывает не полноту инвентаря, а необходимость сравнения. buildSyntheticInventory выдаёт строку для каждого package и отдельно помечает, есть ли synthetic запись в SBOM. Статус synthetic-not-listed означает пропуск в специально заданном массиве, не проблему стороннего генератора и не evidence о настоящей поставке.

import {
  buildSyntheticInventory,
  syntheticDependencyInput,
} from './web/scripts/upgrade-2023-04.mjs';

const inventory = buildSyntheticInventory(syntheticDependencyInput);
console.log(inventory.rows);
// demo-parser: lockfile = synthetic-listed
// demo-parser: sbom = synthetic-listed
// runtime = not-observed-by-fixture

console.log(inventory.deploymentInventory);
// not-proven-by-fixture

Этот пример воспроизводим, потому что все строки определены рядом с функцией. Он честно неполон: не читает package-lock.json, не формирует SPDX или CycloneDX, не строит container image и не знает, какая команда release взяла артефакт. Важно не расширять его обещание. Строка sbom = synthetic-listed подтверждает лишь пересечение двух arrays в памяти. Она не даёт право написать в ticket, что production SBOM обновлён или что transitive dependency больше не существует.

Схема границ: manifest выражает намерение, lockfile фиксирует resolved tree, SBOM связывается с named build artifact, а runtime evidence требует отдельного запуска; стрелки показывают необходимые проверки, а не автоматические гарантии.
Схема объясняет владение данными о synthetic dependency. Она не является SBOM, lockfile реального проекта, контейнерной манифестацией или записью выполненного запуска.

Почему совпадение списков не заканчивает работу

Совпадение name и version между lockfile и SBOM — хороший consistency check, но оно оставляет важные вопросы. Какой graph branch выбрал resolver? Не попал ли optional component в другой platform? Совпадает ли current commit с build, для которого получен SBOM? Был ли SBOM regenerated после overrides? Есть ли bundle, native addon или generated source, чей состав не виден в простом списке npm packages? Ответы зависят от build pipeline, а не от красивого JSON-формата.

Особенно опасно обходить это distinction после patch update. Direct dependency может выглядеть как единственная изменённая строка, но новый resolution меняет transitive children. Поэтому в review полезно смотреть не только manifest, но и lockfile diff: added, removed, version-shifted, integrity или source changes. Затем SBOM generation запускают для candidate artifact и сопоставляют его с тем же commit. Если процесс не умеет такого связывания, результат не «SBOM не нужен», а явный пробел в цепочке evidence, который надо закрыть до уверенного выпуска.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. В pull request изменился package.json, но lockfile не изменился, изменился неожиданно широко или рядом лежит SBOM без указания build.
  2. Причина. Намерение автора, resolved tree и inventory artifact смешаны в один этап. Поэтому нельзя понять, какой файл устарел и кто должен его обновить.
  3. Проверка. Назовите commit, installer command и relevant flags для lockfile; затем назовите artifact identity, generator и format для SBOM. Сверяйте exact name/version, но сохраняйте различие уровней.
  4. Действие. Сделайте таблицу соответствий для candidate release: manifest request → lockfile resolution → SBOM component → build artifact. Отсутствующее звено пишите как unknown, не как совпадение.
  5. Проверка контракта. Запустите fixture. Он должен оставить deploymentInventory равным not-proven-by-fixture и не превратить synthetic arrays в настоящий inventory.
  6. Фиксация. В release record храните ссылки на lockfile diff, generated SBOM и artifact ID. Это даёт следующему reviewer точку сравнения для следующего advisory.

Как выбирать минимально достаточный SBOM

Не надо начинать с обещания «соберём идеальный список всего». Для одного pipeline достаточно установить contract: какой artifact считается release unit, кто запускает generator, какой format принимается, где лежит document, как он связывается с immutable build identity и кто проверяет failure. Минимальные элементы NTIA помогают не забыть о data fields, automation support и practices/processes. Но они не диктуют команду package manager и не решают, какие части monorepo являются отдельными поставками.

Внутри команды полезно хранить не только generated file, но и criterion freshness. Например, SBOM считается актуальным, если он был generated из того же source revision и относится к тому же artifact digest, что release candidate. Это criterion, а не факт в этой статье: его надо реализовать и проверить своим pipeline. Если такой связи нет, честнее назвать документ inventory candidate и не закрывать им вопрос о deployed package. Слова «lockfile» и «SBOM» здесь не являются печатями качества.

Ограничение модели и следующий шаг

Функция buildSyntheticInventory не выполняет install, не парсит реальный lockfile, не создаёт SPDX, не проверяет signature, digest, license или provenance. Она не видит environment variables, bundles, generated assets и runtime imports. Её единственное назначение — показать, что lockfile listing, SBOM listing и runtime observation имеют разные статусы даже у одного demo-parser. Из-за этого PASS fixture не может быть приложен как доказательство состава production image.

Следующий шаг — взять один существующий build pipeline и нарисовать его по таблице: где появляется manifest diff, где resolver создаёт lockfile, где generator получает artifact, где сохраняется SBOM и какой review видит оба указателя. Если ваш инструментарий не поддерживает часть цепочки, это нормальный результат диагностики. Сначала зафиксируйте precise gap и владельца, затем выберите инструмент. Так update перестаёт быть заменой строки и становится проверяемым изменением supply chain.

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

  • npm Docs v7.24.2 (Legacy): package-lock.json, 22 сентября 2021 — официальная legacy-документация npm 7, доступная до апреля 2023: lockfile описывает точное дерево, созданное установщиком, и предназначен для коммита. Это не снимок уже запущенного процесса.
  • NTIA: The Minimum Elements for a Software Bill of Materials, 12 июля 2021 — официальный первичный документ: SBOM назван формальной записью о компонентах и связях цепочки поставки. Формат записи не доказывает, что компонент загружен в runtime.
  • SPDX Specification 2.3.0, Conformance, 2022 — официальная спецификация SPDX 2.3, опубликованная в 2022 году. Она помогает именовать формат и версию инвентаря, но не генерирует его из данного fixture.
  • NIST SP 800-218 SSDF Version 1.1, Final, 3 февраля 2022 — официальный финальный документ, доступный до апреля 2023. Он задаёт практики безопасной разработки и работы с компонентами, но не аттестует отдельный выпуск.