ADG Оставить заявку
Блог Информационная безопасность 5 мин чтения

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 именно для этого и нужен.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.