Trivy + SonarQube: сканирование безопасности в пайплайне GitLab CI
Published: 2026-04-07
Infra-репозиторий запускает два вида Trivy-сканирования: файловой системы (ошибки конфигурации IaC в репозитории) и живого Kubernetes-кластера. Результаты попадают в SonarQube как внешние issue.
Зачем оба сканирования
| Тип сканирования | Что находит |
|---|---|
trivy fs |
Секреты в git, уязвимые теги образов в HelmRelease values, ошибки в K8s-манифестах |
trivy k8s |
Уязвимости задеплоенных образов, избыточные RBAC-права, ошибки конфигурации в рантайме |
Сканирование файловой системы находит проблемы в git. Сканирование кластера проверяет то, что уже работает в production, — например, образы, задеплоенные до публикации CVE.
Структура пайплайна
Стадия безопасности запускается вручную:
yamlstart:security:
stage: validate:security
when: manual
allow_failure: true
script:
- echo "Security checks started"
Все джобы безопасности имеют needs: [start:security] — запускаются только после ручного триггера.
Сканирование файловой системы
yamltrivy:scan-repo:
image: registry.example.com/aquasec/trivy:0.70.0
needs: [start:security]
cache:
key: trivy-db
paths: [.trivy-cache/]
policy: pull-push
script:
- |
trivy fs \
--cache-dir "${TRIVY_CACHE_DIR}" \
--format sarif \
--output trivy.sarif \
--severity HIGH,CRITICAL \
--exit-code 0 \
.
artifacts:
paths: [trivy.sarif]
--exit-code 0 — джоб не падает при наличии находок. Цель — видимость в SonarQube, а не жёсткая блокировка. --format sarif генерирует SARIF-файл, который SonarQube принимает нативно через sonar.sarifReportPaths.
Сканирование Kubernetes-кластера
yamltrivy:k8s:
needs: [start:security]
cache:
key: trivy-db
paths: [.trivy-cache/]
policy: pull-push
script:
- |
trivy k8s \
--format json \
--output "trivy-k8s-infra.json" \
--severity HIGH,CRITICAL \
--context infra-k8s \
all
artifacts:
paths: ["trivy-k8s-*.json"]
Формат JSON, не SARIF — сканирование trivy k8s не генерирует валидный SARIF напрямую. Конвертация через Python-скрипт.
JSON → формат внешних issue SonarQube
yamltrivy:convert-k8s:
needs: [trivy:k8s]
script:
- |
for source_file in trivy-k8s-*.json; do
target_env="${source_file#trivy-k8s-}"
target_env="${target_env%.json}"
python3 scripts/trivy-k8s-json-to-sonar.py \
"$source_file" \
"trivy-k8s-${target_env}-sonar.json"
done
trivy-k8s-json-to-sonar.py преобразует находки в формат внешних issue SonarQube:
json{
"issues": [
{
"engineId": "trivy",
"ruleId": "CVE-2024-1234",
"severity": "CRITICAL",
"type": "VULNERABILITY",
"primaryLocation": {
"message": "nginx:1.25.3 — известная RCE-уязвимость",
"filePath": "fluxcd/projects/dev/kustomization/hub/patches/nginx.yaml"
}
}
]
}
Импорт в SonarQube
yamlsonarqube:trivy:
image: registry.example.com/sonarsource/sonar-scanner-cli:5.0.1
needs: [trivy:scan-repo, trivy:convert-k8s]
variables:
GIT_STRATEGY: clone
GIT_DEPTH: "0" # нужен для git blame
script:
- |
sonar_k8s_reports=$(ls trivy-k8s-*-sonar.json 2>/dev/null | tr '\n' ',' | sed 's/,$//')
sonar-scanner \
-Dsonar.host.url=${SONAR_HOST_URL} \
-Dsonar.token=${SONAR_TOKEN} \
-Dsonar.projectKey=trivy_infra \
-Dsonar.sources=. \
-Dsonar.python.version=3.12 \
-Dsonar.sarifReportPaths=trivy.sarif \
${sonar_k8s_reports:+-Dsonar.externalIssuesReportPaths=${sonar_k8s_reports}}
GIT_DEPTH: "0" — SonarQube использует git blame для привязки находок к коммитам и авторам. Shallow clone ломает blame.
${sonar_k8s_reports:+...} — флаг добавляется только если переменная непустая.
Кэш базы данных Trivy
yamlcache:
key: trivy-db
paths: [.trivy-cache/]
policy: pull-push
База уязвимостей занимает ~300 МБ. Без кэша каждое сканирование скачивает её с GitHub — несколько минут на медленном соединении.
Уведомление
yamlnotify:security:
when: on_success
needs: [sonarqube:trivy]
Отправляет сообщение в Telegram и Mattermost с прямой ссылкой на дашборд SonarQube.
Частые проблемы
Trivy: TOOMANYREQUESTS — rate limit GitHub API при скачивании базы. Решение: GitLab CI cache или зеркало Trivy (--db-repository).
SonarQube — нет информации об авторе — GIT_DEPTH не был "0"; issue остаются без владельца.
trivy k8s завершается по таймауту — добавить --timeout 30m для больших кластеров. Стандартных 5 минут не хватает на 20+ namespace.
SARIF-импорт не работает — убедиться, что версия SonarQube поддерживает SARIF (v9.9+).