SBOM как базовый гигиена: CycloneDX через Syft, аттестации в Harbor
SolarWinds катализирует разговор о SBOM. Запускаем генерацию Software Bill of Materials для всех образов через Syft в CycloneDX-формате, храним в Harbor как attestation.
SolarWinds инцидент запускает индустриальную дискуссию о SBOM как стандарте прозрачности supply chain
После трёх недель разбора SolarWinds - хеши DLL, изоляция серверов, IR-playbook - у нас накопился практический вопрос: а что было бы, если бы у клиента был список компонентов в каждом задеплоенном образе? Не «обновите Orion», а конкретный ответ: вот все системы, где есть библиотека версии X, вот где нет. Индустрия, судя по всему, задала себе тот же вопрос - и слово SBOM (Software Bill of Materials) вдруг начали произносить не только на конференциях по supply chain безопасности.
Мы решили не ждать, пока это оформится в регуляторное требование, а поставить базовый пайплайн прямо сейчас.
Что такое SBOM в контексте контейнеров
SBOM - это манифест компонентов: пакеты, библиотеки, версии, лицензии, хеши. Для контейнерного образа это значит: взял ubuntu:20.04 как базу, добавил OpenSSL 1.1.1g, curl 7.68.0, и вот ещё 80 транзитивных пакетов apt, о которых ты, может, и не знаешь. SBOM делает этот список явным и машиночитаемым.
Форматов несколько. Мы остановились на CycloneDX - он компактнее SPDX для наших задач, нативно поддерживается Syft, и Harbor умеет хранить его как attestation-объект рядом с образом. SPDX тоже валидный вариант, просто мы выбирали исходя из инструментов, которые уже есть в пайплайне.
Syft: генерация SBOM из образа
Syft от Anchore - инструмент, который сканирует образ и выдаёт SBOM в нескольких форматах. Установка - один бинарник, никаких агентов или демонов:
# Установка
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# SBOM для конкретного образа в CycloneDX JSON
syft registry.example.com/app/backend:1.2.3 \
-o cyclonedx-json > sbom-backend-1.2.3.json
Syft смотрит внутрь слоёв образа: находит dpkg-базу данных, pip freeze, npm-пакеты, gem-ы, jar-файлы. На практике при первом прогоне на наших production-образах обнаружилось несколько пакетов, которые никто намеренно не устанавливал - они пришли транзитивно через apt-зависимости базового образа. Это не инцидент, но это знание, которого не было.
Хранение в Harbor как attestation
Harbor начиная с версии 2.x поддерживает OCI-совместимые артефакты - не только образы, но и подписи, аттестации, любые вложения к дайджесту образа. SBOM хранится рядом с образом, привязанный к конкретному sha256-дайджесту, а не к тегу (теги мутабельны, дайджесты нет).
Для загрузки SBOM как OCI-артефакта используем oras - CLI для работы с OCI-реестрами:
# Получаем дайджест образа
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
registry.example.com/app/backend:1.2.3 | cut -d@ -f2)
# Загружаем SBOM как OCI-артефакт
oras push registry.example.com/app/backend@${DIGEST} \
--manifest-config /dev/null:application/vnd.cyclonedx+json \
sbom-backend-1.2.3.json:application/vnd.cyclonedx+json
В интерфейсе Harbor это видно как вложение к образу. Если у Harbor включена политика сканирования - к этому добавляется ещё и Trivy-отчёт, и получается связка: SBOM + vulnerability report к одному дайджесту.
Интеграция в CI
В GitLab CI это выглядит как дополнительный job после сборки образа:
generate-sbom:
stage: post-build
image: alpine:3.12
before_script:
- apk add curl
- curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- curl -sSfL https://github.com/deislabs/oras/releases/download/v0.8.1/oras_0.8.1_linux_amd64.tar.gz | tar xz -C /usr/local/bin
script:
- |
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} | cut -d@ -f2)
- syft ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} -o cyclonedx-json > sbom.json
- oras push ${CI_REGISTRY_IMAGE}@${DIGEST}
--manifest-config /dev/null:application/vnd.cyclonedx+json
sbom.json:application/vnd.cyclonedx+json
artifacts:
paths:
- sbom.json
expire_in: 90 days
Job не блокирует деплой - он параллельный. Если Syft не смог разобрать какой-то слой, это warning, не падение пайплайна. Подход спорный: можно было бы сделать блокирующим, но для первой итерации решили собирать SBOM там, где получается, и не ломать CI там, где что-то пошло не так.
Что это даёт при реальном инциденте
Вот конкретный сценарий в стиле SolarWinds: допустим, завтра появляется CVE в OpenSSL 1.1.1g, и нужно за час понять, какие сервисы затронуты.
Без SBOM - берёшь список образов в production, для каждого делаешь docker exec или пересобираешь локально, запускаешь dpkg-query или pip list, сравниваешь вручную. На сотне сервисов это занимает несколько часов и кто-нибудь обязательно ошибается.
С SBOM в Harbor - пишешь скрипт, который через API Harbor вытаскивает SBOM для каждого образа и фильтрует по компоненту:
import json, requests
HARBOR_URL = "https://registry.example.com/api/v2.0"
TARGET_COMPONENT = "openssl"
TARGET_VERSION = "1.1.1g"
# псевдокод - реальный запрос к API Harbor для получения артефактов
for project in get_projects(HARBOR_URL):
for repo in get_repositories(project):
for artifact in get_artifacts(repo):
sbom = get_sbom_attachment(artifact)
if sbom:
for component in sbom.get("components", []):
if (component.get("name") == TARGET_COMPONENT and
component.get("version") == TARGET_VERSION):
print(f"AFFECTED: {artifact['digest'][:16]} in {repo}")
Результат - список дайджестов образов с конкретной версией конкретной библиотеки. Не «проверьте все сервисы», а точный список. Это разница между часами и минутами при реагировании.
Что ещё не закрыто
Честно - несколько вещей не решены.
Первое - покрытие. Пайплайн работает для образов, собираемых в нашем CI. Образы, которые деплоятся из upstream без пересборки - например, готовые официальные образы postgres или redis - SBOM не получают. Там нужно либо добавить отдельный job «генерируй SBOM для внешних образов при первом использовании», либо смириться с пробелом.
Второе - политика использования. SBOM собирается и хранится. Но автоматической проверки «этот образ содержит компонент из списка запрещённых или уязвимых» в деплой-пайплайне нет. Это следующий шаг - интеграция с OPA или admission webhook в Kubernetes.
Третье - свежесть. SBOM актуален на момент сборки. Если в production продолжает работать образ, собранный три месяца назад, его SBOM описывает состояние на момент сборки, а не сейчас. Для контейнеров это обычно нормально - образы иммутабельны. Но если кто-то ставит что-то внутрь запущенного контейнера, SBOM этого не поймает.
Запустить аудит по тому, как выглядит ваш supply chain прямо сейчас - разумный первый шаг перед построением пайплайна. Мы увидели несколько неожиданных вещей при первых прогонах Syft на production-образах, и это нормально - SBOM именно для этого и нужен.