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

DevSecOps на практике: trivy + semgrep в GitLab CI вместо «потом разберёмся»

Интегрируем trivy для сканирования образов и semgrep для SAST в GitLab CI. Уязвимые базовые образы ловятся до merge в main - а не после деплоя в production.

Контекст момента

Shift-left security: интеграция SAST/SCA в CI/CD пайплайны как стандарт DevSecOps

Словосочетание «shift-left security» звучит примерно так же, как «agile-трансформация» - то есть понятно зачем, но непонятно за что браться в первую очередь. У нас недавно закончился первый полный цикл внедрения этой логики на реальном проекте, и есть что рассказать без абстракций.

Клиент - небольшая продуктовая команда, разрабатывает веб-сервисы, использует GitLab, Kubernetes, Docker. До начала работы с нами их CI/CD пайплайн делал ровно то, для чего его писали: собирал, тестировал, деплоил. Про безопасность думали отдельно, в виде периодических ручных проверок, которые делались когда было время - то есть почти никогда.

Почему образы - это первое, с чего начинать

Если задаться вопросом «где чаще всего обнаруживаются реально эксплуатируемые уязвимости», ответ неприятный: в базовых образах. Не в коде клиента - в node:14, python:3.9, nginx:1.19. Команда за ними не следит, потому что это «не их код». А уязвимости там вполне настоящие.

Конкретный случай из этого проекта: при первом прогоне trivy на образы, которые уже несколько месяцев ходили в production, нашлись CVE с оценкой CRITICAL в базовом пакете openssl внутри python:3.9.2-slim. Образ тянулся с Docker Hub без пиннинга конкретного дайджеста, поэтому было не очень понятно, сколько времени эта уязвимость уже в production.

Trivy ставится без зависимостей, умеет сканировать образы, файловые системы и git-репозитории, выдаёт результат в нескольких форматах включая SARIF и JSON. Для GitLab CI интеграция выглядит примерно так:

container-scan:
  stage: test
  image:
    name: aquasec/trivy:0.18.3
    entrypoint: [""]
  script:
    - trivy image
        --exit-code 1
        --severity HIGH,CRITICAL
        --no-progress
        $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  allow_failure: false

--exit-code 1 при находках HIGH/CRITICAL - пайплайн падает. Это принципиально: не предупреждение, не артефакт для просмотра, а блокировка merge. Мы намеренно не включали MEDIUM на первом этапе - иначе первый же прогон выдал бы такое количество строк, что команда просто добавила бы allow_failure: true и забыла.

После того как образы начали пинниться по дайджесту и обновляться при появлении исправленных версий, ситуация выровнялась. Теперь CRITICAL не проскакивают незамеченными, потому что пайплайн не пропустит.

SAST через semgrep: что это даёт в реальности

Со статическим анализом кода история чуть тоньше. Semgrep - это инструмент для поиска паттернов в коде через декларативные правила. В отличие от классических SAST-инструментов он не строит полный граф вызовов и не пытается отследить тейнтинг через весь стек, зато работает быстро и не требует настройки под конкретный фреймворк.

Набор правил из semgrep-rules покрывает типичные веб-уязвимости: SQL-инъекции через конкатенацию строк, eval с пользовательскими данными, хардкод секретов, использование небезопасных функций десериализации. На Python-части проекта сразу нашлись несколько мест с потенциальными проблемами - не боевые эксплойты, но паттерны, которые следовало разобрать.

Интеграция в CI:

sast:
  stage: test
  image: returntocorp/semgrep:0.56.0
  script:
    - semgrep
        --config=p/security-audit
        --config=p/python
        --error
        --metrics=off
        src/
  allow_failure: false

--error - аналог --exit-code 1 у trivy. --metrics=off - отключаем отправку метрик в Semgrep Cloud, у клиента это было важно с точки зрения политики.

Тут важно понимать ограничение: semgrep с набором правил p/security-audit будет давать ложноположительные срабатывания. Не десятки процентов, но регулярно. Поэтому на первых прогонах мы делали allow_failure: true, разбирали результаты совместно с командой, добавляли inline-аннотации # nosemgrep: rule-id там, где срабатывание заведомо нерелевантно. Через несколько итераций шума стало меньше, и allow_failure убрали.

Как пайплайн устроен целиком

После всех изменений CI выглядит так: сначала обычная сборка и unit-тесты, потом сборка образа, потом параллельно trivy и semgrep, потом деплой в staging. Merge в main заблокирован до прохождения всех стадий включая сканирование.

build -> unit-tests -> docker-build -> [trivy || semgrep] -> deploy:staging

Время добавилось: trivy на образ с зависимостями занимает от 30 секунд до пары минут в зависимости от размера. Semgrep на кодовую базу среднего проекта - секунд 20-40. Для команды это не критично.

Что получилось по факту

Главное наблюдение: уязвимые базовые образы теперь не доходят до production. Это не потому что их стало меньше - их стало столько же. Просто раньше они тихо приезжали в production вместе с кодом, а теперь пайплайн говорит «нет» и указывает на конкретный пакет и CVE.

Команда поначалу воспринимала это как дополнительную нагрузку - нужно следить за обновлением образов, разбирать срабатывания semgrep. Но через несколько недель это стало фоновой рутиной, а не чрезвычайной ситуацией. Как CI-тесты: сначала раздражает, что пайплайн падает, потом привыкаешь, что это нормально.

Отдельный эффект - видимость. Теперь в каждом MR есть артефакты со списком уязвимостей и их статусами. Это не отчёт «для безопасников» - это часть обычного процесса разработки. Разработчик, который открывает MR, видит результаты сканирования так же, как видит результаты тестов.

Всё это настраивалось в рамках managed-сопровождения - не как разовый проект, а как итеративное встраивание в существующий пайплайн. На следующем шаге планируем добавить dependency scanning через встроенные механизмы GitLab и посмотреть на управление секретами в CI - там тоже есть что улучшить.

Контакт

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

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