Как читать авторский курс и нормативные материалы
Это учебные материалы, а не заключение о соответствии конкретного продукта. Организационные модели курса отделяйте от терминов стандарта: ППК в опубликованном проекте — машиночитаемый перечень, а живой реестр его версий является практикой сопровождения. Термин OSA в разных материалах имеет разный охват. Статус и обязательность нормативного документа проверяются отдельно; проект КАПО опубликован как проект первой редакции.
П38. Честный SBOM контейнерного образа
Цель
Сгенерировать SBOM одного и того же образа двумя независимыми генераторами (Syft и Trivy), сравнить число компонентов и доказать, что «честность» SBOM измеряется не количеством строк, а источником факта для каждого компонента. Проверяемый результат — таблица расхождений + объяснение, откуда взялся каждый «лишний» или «пропущенный» пакет.
Что значит «нечестный» SBOM образа SBOM по образу опирается на следы: базы пакетных менеджеров (
/var/lib/dpkg/status,/lib/apk/db/installed,/var/lib/rpm), lockfile'ы языков и метаданные бинарей. Если ПО положено в образCOPY-ом мимо пакетного менеджера, метаданные стёрты (mint --obfuscate-metadata) или бинарь упакован UPX — генератор не видит компонент, и SBOM покажет «0 уязвимостей» при дырявом образе. Честный SBOM явно перечисляет, какие источники фактов использованы и где их нет.
Команда или действие
Возьмите реальный публичный образ среднего размера и снимите два SBOM в одном формате (CycloneDX JSON), чтобы их можно было сравнивать:
IMG=ghcr.io/benc-uk/nodejs-demoapp:latest
# 1. SBOM генератором Syft
syft "$IMG" -o cyclonedx-json=syft.cdx.json
# 2. SBOM генератором Trivy
trivy image --format cyclonedx --output trivy.cdx.json "$IMG"
# 3. сколько компонентов нашёл каждый
echo "syft : $(jq '.components | length' syft.cdx.json)"
echo "trivy: $(jq '.components | length' trivy.cdx.json)"
# 4. множества PURL и их разница (что нашёл один и не нашёл другой)
jq -r '.components[].purl' syft.cdx.json | sort -u > syft.purls
jq -r '.components[].purl' trivy.cdx.json | sort -u > trivy.purls
echo "--- только в syft ---"; comm -23 syft.purls trivy.purls
echo "--- только в trivy ---"; comm -13 syft.purls trivy.purls
# 5. проверка «честности»: сколько компонентов БЕЗ purl (висячая идентичность)
jq '[.components[] | select(.purl == null)] | length' syft.cdx.json
Для CI-гейта по качеству SBOM (а не только по severity) добавьте порог на долю компонентов без PURL:
TOTAL=$(jq '.components | length' syft.cdx.json)
NOPURL=$(jq '[.components[] | select(.purl == null)] | length' syft.cdx.json)
awk -v t="$TOTAL" -v n="$NOPURL" 'BEGIN{ r=n/t; printf "no-purl ratio: %.2f\n", r; exit (r>0.10)}'
# код возврата 1, если больше 10% компонентов без PURL — пайплайн падает
Что вы должны увидеть
Числа у двух генераторов различаются — это норма, а не ошибка. Для node-демо ориентир — десятки компонентов (apk-пакеты базового Alpine + npm-зависимости приложения), и наборы PURL пересекаются не полностью:
syft : 213
trivy: 198
--- только в syft ---
pkg:npm/path-to-regexp@0.1.10
pkg:apk/alpine/libcrypto3@3.3.2-r0?...
--- только в trivy ---
pkg:apk/alpine/.python-rundeps@...
Скелет компонента в CycloneDX, по которому видно источник факта (поле purl + properties):
{
"type": "library",
"name": "axios",
"version": "1.7.7",
"purl": "pkg:npm/axios@1.7.7",
"properties": [
{ "name": "syft:package:foundBy", "value": "javascript-package-cataloger" },
{ "name": "syft:location:0:path", "value": "/app/node_modules/axios/package.json" }
]
}
CI-проверка печатает долю компонентов без PURL и завершается кодом 0, если она ≤ 10 %:
no-purl ratio: 0.03
Если не получилось
jq: error ... Cannot index .... У Trivy и Syft немного разные ветви CycloneDX (componentsvs вложенность вmetadata.component). Проверьтеjq 'keys' trivy.cdx.jsonиjq '.components[0]'— еслиcomponentsотсутствует, у вас SBOM без зависимостей (пустой образ или ошибка скана).syftиtrivyдают одинаковые числа. Маловероятно для реального образа; чаще значит, что вы сравниваете не PURL, а строки целиком (с разными query-параметрами?arch=...&distro=...). Сравнивайте по нормализованному PURL (имя+версия+тип), отбросив query.0 vulnerabilities/ пустой SBOM на «чистом» образе. Возможна обфускация: проверьте образ наmint --obfuscate-metadataили UPX (trivy image --scanners vuln,secret+ ручнойdive). Нулевой результат без проверки coverage — не доказательство безопасности, а красный флаг.- CI-гейт всегда зелёный. Убедитесь, что
awk ... exit (r>0.10)действительно возвращает код в пайплайн (set -e, проверка$?). Частая ошибка — порог считается, но код возврата теряется в конвейере.
Критерий зачёта
Сдаёте: syft.cdx.json, trivy.cdx.json, файл с разницей PURL и письменный разбор 3–5 расхождений (для каждого — почему один генератор нашёл компонент, а другой нет: разный каталогизатор, версия БД, обработка бинарей). Плюс рабочий CI-фрагмент с порогом по доле компонентов без PURL и демонстрация того, что он валит сборку на искусственно «ослеплённом» SBOM.
Два генератора — это фича, а не баг На приёмке полезно держать кросс-проверку: расхождение наборов PURL подсвечивает компоненты со слабой идентичностью и зоны, где одному инструменту не хватает каталогизатора. Это и есть переход от «у нас есть SBOM» к «у нас есть честный SBOM».
Нормативная связка
- Модуль М16 — разрыв между декларацией и реальным составом образа.
- Смежные практики: П37. Контейнерный образ слои секреты и удаленные файлы, П39. SBOM for AI minimum elements, П40. Огнестойкость РБПО и SCA процесс.
- Инструменты: Syft, Trivy.
- Шаблоны: Чек-лист честного SBOM контейнерного образа, Требования к CI security gate, Чек-лист качества CycloneDX.
- Нормативка: CISA Minimum Elements for SBOM, BSI TR-03183-2, ГОСТ Р 56939-2024.