Перейти к содержимому

Trivy и Semgrep: настройка CI/CD пайплайна от первого запуска до фильтрации ложных срабатываний

Trivy и Semgrep: настройка CI/CD пайплайна от первого запуска до фильтрации ложных срабатываний
Время чтения: 14 мин.

По данным NIST SATE и отчётов Corgea, precision коммерческих SAST-инструментов на отдельных категориях CWE — от 18 до 36%. Переведу: из каждых ста находок сканера от 64 до 82 — шум. Добавь SCA-сканер, который на типовом Node.js-проекте с тремя сотнями транзитивных зависимостей выбросит ещё полсотни алертов — и через пару дней команда разработки отключает проверки целиком. Реальные уязвимости тонут в потоке ложных. Я проходил через этот сценарий трижды на разных проектах, и каждый раз проблема была не в инструментах, а в отсутствии настройки под конкретный стек. Ниже — пошаговый разбор: как запустить связку Trivy и Semgrep локально, прочитать их вывод и довести долю ложных срабатываний до терпимых 15–20%.

Зачем два инструмента: Trivy SCA и Semgrep SAST в одном пайплайне

Два инструмента нужны потому, что они смотрят на разные поверхности атаки.

Trivy — SCA-сканер (Software Composition Analysis — анализ состава ПО, то есть всех сторонних библиотек и пакетов в проекте). Он разбирает манифесты зависимостей (package-lock.json, requirements.txt, go.sum), Docker-образы и IaC-конфигурации, а затем сверяет найденные пакеты с базами уязвимостей: NVD (National Vulnerability Database — федеральная база уязвимостей США, куда попадают все CVE) и vendor-специфичными advisory. Trivy не анализирует код, который вы написали, — только то, что подключили извне. Проект от Aqua Security, релизы каждые 2–3 недели, репозиторий на GitHub набрал свыше 24 000 звёзд.

Semgrep — SAST-сканер (Static Application Security Testing — статический анализ вашего исходного кода на предмет уязвимых паттернов). Он ищет SQL-инъекции, XSS (межсайтовый скриптинг — когда пользовательский ввод попадает в HTML без экранирования), захардкоженные секреты, небезопасные вызовы API. О версиях сторонних библиотек Semgrep ничего не знает. Поддерживается Semgrep Inc., аналогичная частота обновлений.

Если запустить только один сканер, атакующий пойдёт через незакрытую поверхность. В терминах MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1195.001 — её идентификаторы) уязвимые зависимости — вектор Compromise Software Dependencies and Development Tools (T1195.001, тактика Initial Access), а захардкоженные секреты — Credentials In Files (T1552.001, тактика Credential Access). Два вектора — два инструмента.

По классификации OWASP Top 10 (2021): Trivy закрывает A06 — Vulnerable and Outdated Components, Semgrep — A03 — Injection и частично A05 — Security Misconfiguration.

Инструмент Преимущества Ограничения Когда использовать Когда НЕ использовать
Trivy (SCA) Быстрый детект CVE в зависимостях; Docker, IaC, генерация SBOM Не анализирует ваш код; нет оценки reachability — вызывается ли уязвимая функция Проект с внешними зависимостями или контейнерами Для анализа собственного кода
Semgrep (SAST) Детект уязвимостей в коде; YAML-правила читаются как псевдокод Высокий FP без настройки; слеп к рантайму и зависимостям Проект с собственным исходным кодом Для аудита зависимостей или образов

Что оба инструмента не закрывают

Ни Trivy, ни Semgrep не обнаруживают проблемы, которые проявляются только в рантайме — для этого нужен DAST (Dynamic Application Security Testing, например OWASP ZAP), который шлёт реальные запросы к работающему приложению. Для поиска секретов в git-истории есть Gitleaks — он закрывает смежный вектор T1552.004 (Private Keys), но это тема для отдельного разбора. Semgrep также не заменяет ручной пентест. Задача на текущем этапе — выстроить первый барьер ещё до деплоя.

Требования к окружению

Перед запуском команд убедитесь, что среда готова:

  • ОС: Linux (Ubuntu 20.04+), macOS 12+, WSL2 на Windows. Все команды ниже для bash
  • RAM: минимум 2 ГБ свободных. Trivy при первом запуске скачивает базу уязвимостей (~90 МБ), Semgrep загружает правила
  • Интернет: нужен при первом запуске. Для офлайн-сред: предварительно выполните trivy fs --download-db-onlytrivy fs --download-java-db-only для Java-проектов); для полностью изолированных сред см. документацию Trivy по Air-Gapped Environment и кешируйте набор правил Semgrep
  • Версии: Trivy >= 0.50 (проверить: trivy --version), Semgrep >= 1.0 (проверить: semgrep --version)
  • Установка Trivy: на macOS — brew install trivy; на Debian/Ubuntu — предварительно добавьте репозиторий Aqua Security (wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - и echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee /etc/apt/sources.list.d/trivy.list), затем sudo apt-get update && sudo apt-get install -y trivy
  • Установка Semgrep: pip install semgrep или brew install semgrep
  • Что сканировать: каталог с исходным кодом и манифестом зависимостей (package.json, requirements.txt, go.mod)

Trivy — SCA сканирование уязвимостей в зависимостях

Первый запуск и чтение вывода

Запускаем Trivy в режиме файловой системы — он найдёт манифесты зависимостей и проверит их по базе:

trivy fs --severity HIGH,CRITICAL --format table .

Что делает каждый флаг:

  • fs — режим сканирования файловой системы. Альтернатива: image для Docker-образов
  • --severity HIGH,CRITICAL — показывать только находки уровня HIGH и CRITICAL, отсекая LOW и MEDIUM. На старте это принципиально: если включить всё, поток алертов обесценит результаты за первый же день
  • --format table — человекочитаемый табличный вывод. Для автоматизации: json или sarif (Static Analysis Results Interchange Format — стандартный формат обмена результатами; его понимают GitHub Security tab и GitLab MR)
  • . — текущий каталог

Trivy выведет таблицу. Разберём колонки:

  • Library — имя пакета (например, express, lodash)
  • Vulnerability — идентификатор CVE (Common Vulnerabilities and Exposures — стандартный номер уязвимости, по которому ищут описание и патч)
  • Severity — уровень критичности: CRITICAL, HIGH, MEDIUM, LOW
  • Installed Version — версия пакета у вас
  • Fixed Version — версия с исправлением. Пусто — патча пока нет
  • Title — краткое описание уязвимости

Как убедиться, что всё работает: Trivy завершается с кодом 0, если находок указанной severity нет, и выводит таблицу, если есть. Ошибка FATAL обычно указывает на проблему с доступом к базе — проверьте сеть.

Для CI/CD добавляется --exit-code 1 — при наличии находок процесс завершится с ненулевым кодом и заблокирует пайплайн. Для Docker-образов: trivy image --severity CRITICAL --exit-code 1 --ignore-unfixed myapp:latest. Флаг --ignore-unfixed исключает уязвимости без выпущенного патча — убирает шум от проблем в базовых образах, которые нельзя закрыть прямо сейчас.

Ещё одна полезная штука — генерация SBOM (Software Bill of Materials — полный перечень всех компонентов с версиями): trivy fs --format cyclonedx -o sbom.json . создаёт файл в формате CycloneDX. Зачем это нужно: когда всплывает новый CVE (а они всплывают постоянно), SBOM позволяет за секунды проверить, затронут ли ваш проект, вместо ручного перебора зависимостей.

Конфигурация Trivy: .trivyignore и управление шумом

На реальном проекте часть находок будет нерелевантной: CVE в компоненте, функция которого не вызывается; уязвимость в dev-зависимости вне production-сборки; known issue, компенсированный другими мерами. Trivy поддерживает файл .trivyignore.yaml с обоснованием и сроком каждого исключения:

# .trivyignore.yaml — пример для демонстрации формата
ignoreRules:
  - id: CVE-2023-12345
    reason: "Компонент в base image, функция не вызывается"
    expiresOn: 2025-09-30
  - id: CVE-2022-XXXXX
    reason: "False positive для данной версии в alpine:3.19 (замените на реальный CVE)"
    expiresOn: 2025-12-31

Три правила работы с исключениями, которые я вывел на практике:

  1. Каждое исключение — с обоснованием. «Мешает пайплайну» — не причина. «Компонент не используется в runtime, компенсировано WAF-правилом» — причина.
  2. Каждое исключение — с датой истечения. Без expiresOn файл превращается в кладбище CVE. Когда дата наступит, Trivy снова начнёт сообщать — и это заставит пересмотреть решение.
  3. Файл хранится в репозитории. Code review на правки .trivyignore.yaml — такой же обязательный, как на продовый код. Это принцип «политика как код» (policy as code) — один из базовых элементов OWASP DevSecOps Maturity Model (DSOMM — модель зрелости DevSecOps-практик, описывающая, какие процессы безопасности на каком уровне зрелости внедрять).

Для простых случаев есть формат .trivyignore — список CVE-идентификаторов по одному на строку, без метаданных. Но YAML-формат с обоснованиями лучше для командной работы: через полгода вы сами не вспомните, почему исключили конкретный CVE.

Semgrep — SAST правила настройка под конкретный стек

Выбор ruleset и фильтрация по severity

Semgrep работает с наборами правил (ruleset). Каждый набор — свой баланс покрытия и шума:

  • p/default — широкий охват, много шума на проекте с историей
  • p/owasp-top-ten — правила, привязанные к OWASP Top 10 2021; разумный баланс для старта
  • p/security-audit — максимальный охват; имеет смысл для команд, которые уже разобрались с базовым шумом

Для фреймворк-специфичных проектов доступны отдельные наборы: p/django, p/flask, p/express, p/react. Почему это важно: фреймворковый набор знает, что Django автоматически экранирует вывод шаблонов, и не помечает каждую переменную как XSS. Без этого знания — десятки ложных срабатываний на ровном месте.

Первый запуск:

semgrep scan --config p/owasp-top-ten --severity ERROR .

Флаг --severity ERROR фильтрует по полю severity, оставляя только правила с наивысшим уровнем (ERROR) и отсекая WARNING и INFO. Отдельно: правила в Semgrep Registry имеют поле metadata.confidence (HIGH, MEDIUM, LOW) — это независимый атрибут, не связанный с severity. Фильтрация по confidence через CLI напрямую не поддерживается в OSS-версии — для этого используют кастомные ruleset-ы или Semgrep Cloud Platform. На старте работайте с --severity ERROR и расширяйте охват постепенно.

Вывод Semgrep для каждой находки содержит: rule — идентификатор правила, severity — ERROR/WARNING/INFO, message — описание проблемы, file:line — расположение в коде, fix — предложение исправления, если правило его поддерживает.

Как отличить реальную находку от ложной: откройте файл на указанной строке. Данные приходят от пользователя и попадают в опасную функцию без санитизации (без очистки/валидации) — реальная находка. Данные из внутреннего enum, конфигурации или кастомного санитайзера — false positive (ложное срабатывание, когда сканер кричит «уязвимость!», а её нет).

Semgrep custom rules для подавления ложных срабатываний

Кастомные правила — самый эффективный способ сократить шум. И это не «продвинутая фича для зрелых команд», а базовый шаг настройки. Каждый проект имеет свои обёртки и внутренние библиотеки, о которых стандартные правила не знают.

Пример: команда использует обёртку SafeQuery, которая всегда параметризует SQL. Semgrep стабильно помечает SafeQuery.execute() как инъекцию. Решение — правило-исключение:

# .semgrep/custom-rules.yml
rules:
  - id: sql-without-safequery
    patterns:
      - pattern: $DB.execute($QUERY)
      - pattern-not: SafeQuery.execute(...)
    message: "SQL-запрос без SafeQuery — проверить параметризацию"
    severity: ERROR
    languages: [python]

Правило сработает на любой .execute(), кроме SafeQuery.execute() — ложные срабатывания от обёртки исчезают, а детект для остальных вызовов сохраняется. Запускайте свои правила через semgrep scan --config .semgrep/custom-rules.yml . или добавляйте к основному вызову несколько --config.

Для каталогов, которые Semgrep не должен трогать (тесты, сгенерированный код, вендорные зависимости), создайте .semgrepignore в корне и перечислите пути: tests/, vendor/, node_modules/, *.test.js, *.spec.py. Формат аналогичен .gitignore.

Локальный DevSecOps пайплайн — делай раз, делай два, делай три

Цель — скрипт, запускаемый одной командой на машине разработчика до пуша. Тот же принцип shift-left (сдвиг проверок к началу цикла разработки — чем раньше нашли баг, тем дешевле починить), но ещё левее: до CI/CD.

Предпосылки: Trivy >= 0.50 и Semgrep >= 1.0 установлены, проект с манифестом зависимостей в текущем каталоге.

#!/usr/bin/env bash
set -euo pipefail
echo "=== Шаг 1: SCA — Trivy ==="
trivy fs --severity HIGH,CRITICAL --exit-code 0 --format table .
echo "=== Шаг 2: SAST — Semgrep ==="
semgrep scan --config p/owasp-top-ten --severity ERROR --quiet .
echo "=== Шаг 3: JSON-артефакты ==="
trivy fs --severity HIGH,CRITICAL -f json -o trivy.json .
semgrep scan --config p/owasp-top-ten --json -o semgrep.json .
echo "Готово: trivy.json, semgrep.json"

Что происходит на каждом шаге и как понять, что всё работает:

Шаг 1. Trivy сканирует зависимости. --exit-code 0 означает, что скрипт не прерывается при находках — на начальном этапе это правильно, иначе вы не дойдёте до конца скрипта. Если в терминале появилась таблица с колонками Library/Vulnerability/Severity — сканер отработал. Таблицы нет и выход чистый — уязвимостей указанного уровня не обнаружено.

Шаг 2. Semgrep проверяет код. --quiet подавляет вывод правил без находок. Чистый выход — по выбранным правилам ничего не нашлось. Строки с severity: error и путём к файлу — каждую нужно проверить вручную.

Шаг 3. JSON-отчёты нужны для двух задач: загрузка как артефакт в CI/CD и историческое сравнение между запусками. После выполнения в каталоге должны появиться trivy.json и semgrep.json. Валидный JSON с массивом результатов — всё сработало. Для машинной обработки полезна утилита jq — извлекайте нужные поля (CVE ID, severity, имя пакета) из вложенного JSON; точный фильтр зависит от версии Trivy.

Когда команда привыкнет к результатам (через 2–4 недели), замените --exit-code 0 на --exit-code 1 и добавьте --error в вызов Semgrep — скрипт начнёт прерываться при находках. Этот же скрипт переносится в .gitlab-ci.yml или .github/workflows/ с минимальными правками: добавляются image:, artifacts: и rules:.

Ложные срабатывания SAST — корневые причины и workflow триажа

По данным Corgea (со ссылкой на результаты NIST SATE и индустриальные оценки), в проектах со 100 000+ строк кода типичное количество находок SAST за один скан — от 5 000 до 20 000, из которых от 1 500 до 16 000 могут быть ложными. Каждый false positive требует 15–30 минут на разбор. Арифметика простая: если не фильтровать, команда из трёх человек потратит неделю только на триаж.

Четыре главные причины (по данным AppSecSanta и Corgea):

  1. Неполное моделирование потоков данных. Сканер видит строковую конкатенацию в SQL-запросе и сигнализирует «инъекция!» — но не знает, что ORM (Object-Relational Mapping — прослойка между кодом и базой данных) параметризует все запросы автоматически.

  2. Слепота к фреймворку. Django автоматически экранирует вывод шаблонов, Spring Security добавляет CSRF-защиту по умолчанию. Сканер без фреймворк-специфичного набора правил помечает каждую переменную.

  3. Отсутствие контекста бизнес-логики. Сканер не знает, что поле принимает значения только из dropdown-списка или что API доступен исключительно из внутренней сети. Кастомные санитайзеры для него невидимы.

  4. Слишком широкие дефолтные правила. Вендоры настраивают правила так, чтобы ничего не пропустить — за счёт шума. Для вендора это безопасная стратегия. Для вашей команды — болезненная.

Практический workflow триажа:

  1. Включите только ERROR-severity / HIGH-confidence на первые 2–4 недели.
  2. Разберите первые 30 находок вручную. Запишите долю реальных и ложных, выделите повторяющиеся классы FP.
  3. Для повторяющихся классов создайте Semgrep custom rules или записи в .trivyignore.yaml.
  4. Через месяц понизьте порог до WARNING и повторите цикл.
  5. Используйте инкрементальное сканирование: передавайте в Semgrep только изменённые файлы через git diff --name-only HEAD~1 и флаг --include. На проектах с большой кодовой базой это радикально сокращает объём вывода.

Чеклист настройки связки Trivy и Semgrep

Готовый список действий для передачи в команду или включения в документацию:

  1. Установить Trivy >= 0.50 и Semgrep >= 1.0 на машинах разработчиков и CI-раннерах
  2. Создать .trivyignore.yaml в корне репозитория — пустой, с комментарием «добавлять исключения через MR с обоснованием»
  3. Создать .semgrepignore с путями: tests/, vendor/, node_modules/, *_test.*, *.spec.*
  4. Первый запуск Trivy: trivy fs --severity HIGH,CRITICAL --format table . — зафиксировать baseline
  5. Первый запуск Semgrep: semgrep scan --config p/owasp-top-ten --severity ERROR . — зафиксировать baseline
  6. Подключить framework-specific ruleset, если проект использует фреймворк (p/django, p/flask, p/express)
  7. Настроить CI-job с allow_failure: true на первые 2–4 недели (advisory-режим)
  8. Разобрать top-20 находок, создать записи в .trivyignore.yaml и custom rules для повторяющихся классов FP
  9. Через 2–4 недели убрать allow_failure для Trivy (CRITICAL), ещё через 2 недели — для Semgrep (ERROR)
  10. Настроить кеширование базы Trivy в CI: переменная TRIVY_CACHE_DIR + cache между запусками
  11. Добавить JSON/SARIF-отчёты как артефакты сборки
  12. Раз в квартал пересматривать .trivyignore.yaml — удалять истёкшие исключения, обновлять обоснования

Большинство руководств по DevSecOps начинают с YAML-конфигурации CI/CD — и в этом корень проблемы. Конфиг для GitLab CI или GitHub Actions — последний шаг, не первый. Если вы не понимаете, что означает каждая строка вывода Trivy и почему Semgrep пометил конкретную строку кода, автоматизация не спасёт: вы будете либо игнорировать все алерты, либо блокировать каждый мёрж-реквест, пока команда не начнёт обходить сканер через отдельные ветки «без проверок». Мой подход — сначала две недели ручного запуска на локальной машине, разбор каждой находки, формирование baseline и только потом перенос в пайплайн. Команда, которая сначала научилась читать вывод, внедряет DevSecOps за спринт. Команда, которая начала с копирования YAML из чужой статьи, откатывает всё через неделю.

И ещё одна неудобная вещь: правила «из коробки» не работают ни для одного реального проекта. Каждый стек, каждый фреймворк, каждая внутренняя библиотека требуют кастомизации. Semgrep custom rules — базовая гигиена, без которой инструмент превращается в генератор шума. Если за первую неделю после внедрения вы не написали хотя бы два-три собственных правила — значит, либо проект слишком маленький, либо результаты никто не разбирает. Если переход в ИБ для вас актуален и хочется выстроить базу системно — на IB Basics показывают не теорию, а как джуны реально решают рабочие задачи.

Эту тему и смежные навыки разбирают на практике в курсе «Аналитик SOC» Codeby Academy.