SBOM и происхождение артефактов

Чтобы защищать ПО, нужно сначала точно знать, из чего оно состоит и кто его собрал.

SBOM (Software Bill of Materials, спецификация состава ПО) — это машиночитаемый список всех компонентов внутри артефакта: библиотек, версий, лицензий и хешей. По сути — «состав» на этикетке вашего релиза.

Когда выходит новый громкий CVE, первый вопрос — «а он у нас вообще есть?». Без описи состава ответ ищут вручную по десяткам сервисов и часто ошибаются. SBOM превращает этот вопрос в запрос по списку. А подпись и происхождение (provenance) отвечают на второй вопрос: «можно ли вообще доверять этому артефакту?».

Зачем это знать защитнику

SBOM и provenance — это два столпа доверия к артефакту. SBOM отвечает на «ЧТО внутри», provenance — на «КАК и КЕМ это собрано». Вместе они позволяют: мгновенно находить уязвимые компоненты, обнаруживать подмену артефакта, отличать ваш настоящий релиз от поддельного и выполнять требования регуляторов (во многих странах SBOM уже обязателен для поставок ПО).

SBOM: опись состава

SBOM генерируется автоматически на этапе сборки инструментами вроде Syft, Trivy или CycloneDX-плагинов. Есть два распространённых формата: SPDX и CycloneDX. Упрощённо запись об одном компоненте выглядит так:

{
  "bomFormat": "CycloneDX",
  "components": [
    {
      "type": "library",
      "name": "lodash",
      "version": "4.17.21",
      "purl": "pkg:npm/[email protected]",
      "hashes": [{ "alg": "SHA-256", "content": "a1b2c3..." }],
      "licenses": [{ "license": { "id": "MIT" } }]
    }
  ]
}

Поле purl (package URL) — каноническое имя компонента, по которому SBOM связывается с базами уязвимостей. Имея SBOM, на выход нового CVE отвечают одной командой:

# Сгенерировать SBOM образа и сразу проверить на уязвимости
syft myapp:1.4.0 -o cyclonedx-json > sbom.json
grype sbom:sbom.json   # сверка состава с базами CVE

Подпись артефактов и происхождение

SBOM говорит, что внутри, но не доказывает, что артефакт настоящий. Это задача цифровой подписи. Сборка подписывается приватным ключом, а потребитель проверяет подпись публичным. Если артефакт подменили после подписи, проверка не пройдёт.

Современный подход (например, проект Sigstore с инструментом cosign) использует keyless-подпись: вместо хранения долгоживущего приватного ключа подпись привязывается к удостоверённой личности (OIDC-токену CI) и записывается в публичный прозрачный журнал. Это снимает риск утечки ключа.

# Подписать образ (keyless, личность берётся из CI-токена)
cosign sign myregistry/myapp:1.4.0

# Потребитель проверяет: кто подписал и каким workflow
cosign verify myregistry/myapp:1.4.0 \
  --certificate-identity-regexp 'github.com/acme/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Provenance (происхождение) идёт дальше подписи и фиксирует как собран артефакт: какой коммит исходника, какой пайплайн, какие входные данные, какой билдер. Стандарт для этого — in-toto attestation: подписанное утверждение «артефакт X с хешем H собран из коммита C пайплайном P». Проверив provenance, потребитель убеждается, что бинарник действительно собран из заявленного исходника, а не подменён по дороге.

Уровни SLSA

SLSA (Supply-chain Levels for Software Artifacts, произносится «салса») — это рамка зрелости, описывающая, насколько защищён процесс сборки. Уровни нарастают:

УровеньЧто гарантирует
Build L1Сборка автоматизирована и генерирует provenance (есть запись о происхождении)
Build L2Сборка идёт на управляемой платформе, provenance подписан сервисом
Build L3Сборка изолирована и защищена от вмешательства; provenance невозможно подделать изнутри сборки

Смысл уровней — давать потребителю измеримую гарантию. «Собрано по SLSA L3» означает не просто «есть подпись», а «процесс сборки был изолирован, и происхождению можно доверять».

Как это работает под капотом

В основе всего — криптографические хеши и подписи. Артефакт сводится к одному хешу (digest), например sha256:abc.... SBOM, подпись и provenance ссылаются именно на этот хеш, а не на изменяемый тег вроде :latest. Цепочка доверия выглядит так: исходник → сборка фиксирует digest артефакта → provenance подписывает утверждение про этот digest → подпись кладётся в прозрачный журнал. Потребитель пересчитывает digest скачанного артефакта и проверяет всю цепочку. Любая подмена меняет хеш и рушит проверку — именно поэтому привязка к тегу опасна, а к digest надёжна.

Как защититься

  • Генерируйте SBOM автоматически на каждой сборке и храните рядом с артефактом. Тогда вопрос «затрагивает ли нас новый CVE» решается запросом по описи, а не паникой.
  • Подписывайте артефакты и проверяйте подпись при развёртывании. Кластер/раннер должен принимать только подписанные доверенным workflow образы.
  • Прикрепляйте provenance и сверяйте, что артефакт собран из ожидаемого репозитория и пайплайна, а не «откуда-то».
  • Ссылайтесь на артефакты по digest, а не по тегу. Тег можно переназначить на другой образ; digest однозначно идентифицирует содержимое.
  • Стремитесь к более высокому уровню SLSA поэтапно: сначала автоматизация и provenance (L1), затем подписанный provenance (L2), затем изоляция сборки (L3).

Итоги

  • SBOM — машиночитаемая опись состава ПО (компоненты, версии, хеши, лицензии); отвечает на «ЧТО внутри» и ускоряет реакцию на новые CVE.
  • Подпись артефактов доказывает подлинность, а keyless-подход (Sigstore/cosign) снимает риск утечки ключа.
  • Provenance (in-toto) фиксирует, КАК и из какого исходника собран артефакт; SLSA задаёт уровни зрелости этого процесса (L1–L3).
  • Вся цепочка доверия держится на хешах: ссылайтесь на артефакты по digest, а не по изменяемому тегу.
Проверьте себя
1. Чем SBOM принципиально отличается от provenance (происхождения)?
ASBOM описывает, ЧТО внутри артефакта (состав компонентов), а provenance — КАК и кем он собран
BSBOM — это подпись артефакта, а provenance — его SHA-256 хеш
CSBOM нужен только для лицензий, а provenance — только для уязвимостей
DЭто два названия одного и того же документа в форматах SPDX и CycloneDX
2. Почему артефакты надёжнее идентифицировать по digest (sha256:...), а не по тегу вроде :latest?
AТег короче и его легче опечатать
BDigest однозначно соответствует содержимому, а тег можно переназначить на другой артефакт, что открывает подмену
CDigest содержит дату сборки, а тег — нет
DПо тегу нельзя сгенерировать SBOM