Trivy в GitLab CI: блокируем merge при критических CVE в контейнерных образах
Aqua Security Trivy 0.19 добавляет SBOM-сканирование и Cosign для верификации подписей. Встраиваем в GitLab CI, сравниваем с Grype на Alpine-пакетах.
Aqua Security выпускает Trivy 0.19 со сканированием SBOM и поддержкой Cosign для проверки подписей container-образов
Две недели назад мы писали про SBOM и Syft+Grype для инвентаризации контейнерных образов. Пока тот пост выходил, Aqua Security выкатила Trivy 0.19 - с поддержкой Cosign для проверки подписей образов и встроенным SBOM-сканированием. Стало интересно сравнить, и мы взялись встраивать Trivy в GitLab CI вместо того, что было раньше.
Спойлер: на Alpine-образах Trivy выигрывает у Grype заметно. Почему - ниже.
Что изменилось в Trivy 0.19
До этой версии Trivy сканировал уязвимости в OS-пакетах и нескольких языковых экосистемах. В 0.19 появилось несколько важных вещей.
Поддержка Cosign. Sigstore/Cosign - это инициатива по подписи артефактов в supply chain. Trivy теперь умеет верифицировать подпись образа перед сканированием: если подпись не сходится или её нет - можно заблокировать. Для нас это пока эксперимент, подписывание образов в пайплайне ещё не выстроено, но функция нужная, особенно после истории с supply chain атаками.
SBOM как входной формат. Trivy умел генерировать SBOM, теперь умеет принимать его на вход для сканирования - как Grype с его sbom: префиксом. Это открывает схему «сгенерировал один раз - сканируй разными инструментами».
Расширенное покрытие языковых экосистем. В 0.19 улучшена детекция Go-модулей и Rust-пакетов. Для нас актуален Go.
Alpine vs. Debian: почему Trivy выигрывает
Когда мы прогнали одни и те же Alpine-базированные образы через Grype и Trivy, разница оказалась ощутимой: Trivy находил уязвимости в apk-пакетах, которые Grype пропускал или классифицировал с меньшей точностью.
Причина инфраструктурная. Alpine использует собственную базу безопасности (secdb) - secfixes в APKBUILD-файлах репозитория. Trivy напрямую парсит Alpine secdb и связывает CVE с конкретными версиями пакетов, включая backport-фиксы (Alpine часто бэкпортирует патчи без повышения версии пакета). Grype опирается преимущественно на NVD и GitHub Advisory - там данные по Alpine менее полные, а backport-ситуации почти не учитываются.
На практике это выглядит так: libssl1.1 в Alpine 3.13 может числиться уязвимым в NVD, но в Alpine secdb для этой конкретной сборки уязвимость уже закрыта бэкпортом. Trivy это знает, Grype - нет, и выдаёт false positive. На Debian-базированных образах разница меньше, там Grype работает вполне сопоставимо.
Для нас, учитывая что большинство сервисных образов собирается на Alpine, это было решающим аргументом.
Интеграция в GitLab CI
Цель была простая: каждый MR должен сканироваться, при CRITICAL CVE - merge заблокирован. Схема через GitLab security scanning templates существует, но она завязана на GitLab Ultimate. Мы сделали через прямой вызов Trivy.
container-scanning:
stage: test
image:
name: aquasec/trivy:0.19.2
entrypoint: [""]
variables:
TRIVY_NO_PROGRESS: "true"
TRIVY_CACHE_DIR: ".trivycache"
# Сканировать только fixable уязвимости
TRIVY_IGNORE_UNFIXED: "true"
cache:
paths:
- .trivycache/
script:
- trivy image
--exit-code 1
--severity CRITICAL
--format table
${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Несколько моментов из практики.
--ignore-unfixed - почти обязательный флаг. Без него пайплайн валится на уязвимостях, для которых нет фикса в upstream. В базовом alpine:3.14 живут несколько CRITICAL без фиксов - это известные ситуации, где апстрим или не признаёт CVE применимым, или фикс идёт в следующую версию Alpine. Блокировать на них CI - значит парализовать разработку.
Кеш базы уязвимостей важен. Trivy скачивает базу при каждом запуске, если нет кеша. Это 30-40 секунд и несколько сотен мегабайт. С кешем на уровне GitLab runner - первый запуск медленный, дальше база обновляется инкрементально. Время сканирования типичного образа на 200-300 МБ - порядка 20-30 секунд с кешем.
--exit-code 1 только на нужный severity. Мы ставим --exit-code 1 только на CRITICAL. HIGH и ниже - отдельный шаг с --exit-code 0, который генерирует репорт как артефакт, но не блокирует.
container-scanning-report:
stage: test
image:
name: aquasec/trivy:0.19.2
entrypoint: [""]
variables:
TRIVY_NO_PROGRESS: "true"
TRIVY_CACHE_DIR: ".trivycache"
TRIVY_IGNORE_UNFIXED: "true"
cache:
paths:
- .trivycache/
script:
- trivy image
--exit-code 0
--severity HIGH,MEDIUM
--format json
--output trivy-report.json
${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
artifacts:
paths:
- trivy-report.json
expire_in: 14 days
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Что с SBOM-функциональностью
Trivy умеет генерировать SBOM в CycloneDX и SPDX форматах:
trivy image --format cyclonedx --output sbom.json nginx:1.21-alpine
И принимать SBOM как вход для сканирования:
trivy sbom sbom.json
На практике мы пока генерируем SBOM через Syft (покрытие языковых экосистем у него чуть шире по ощущениям), но сканируем через Trivy. Это нормально работает - форматы стандартные.
Где Grype всё ещё полезен
Несправедливо было бы хоронить Grype. Для Python-экосистемы и детальной работы с уже готовыми SBOM-файлами у него удобный CLI. Есть .grype.yaml для тонкой настройки игнорируемых CVE - у Trivy аналог это .trivyignore, тоже есть, но синтаксис проще. У Grype более удобный вывод для человека при интерактивном использовании.
Если стек преимущественно Debian/Ubuntu-образы и нет сильной зависимости от Alpine - выбор между ними менее принципиален.
Где сейчас
Trivy встал в MR-пайплайн, работает около недели. За это время один MR заблокировался на CRITICAL в старой версии openssl - разработчик не обновил базовый образ с прошлого квартала. Нашли, исправили, пошли дальше.
Следующий шаг - попробовать Cosign для подписи образов, которые уходят в прод. Sigstore набирает momentum в сообществе, и иметь верификацию подписи в пайплайне выглядит разумно. Но это отдельная история и отдельный пост.
- SBOM на практике: Syft + Grype для container images и интеграция в CI · 13 сентября 2021
- Kaseya VSA и REvil: когда ваш RMM становится вектором атаки на клиентов · 12 июля 2021