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

По данным 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-only(иtrivy 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
Три правила работы с исключениями, которые я вывел на практике:
- Каждое исключение — с обоснованием. «Мешает пайплайну» — не причина. «Компонент не используется в runtime, компенсировано WAF-правилом» — причина.
- Каждое исключение — с датой истечения. Без
expiresOnфайл превращается в кладбище CVE. Когда дата наступит, Trivy снова начнёт сообщать — и это заставит пересмотреть решение. - Файл хранится в репозитории. 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):
-
Неполное моделирование потоков данных. Сканер видит строковую конкатенацию в SQL-запросе и сигнализирует «инъекция!» — но не знает, что ORM (Object-Relational Mapping — прослойка между кодом и базой данных) параметризует все запросы автоматически.
-
Слепота к фреймворку. Django автоматически экранирует вывод шаблонов, Spring Security добавляет CSRF-защиту по умолчанию. Сканер без фреймворк-специфичного набора правил помечает каждую переменную.
-
Отсутствие контекста бизнес-логики. Сканер не знает, что поле принимает значения только из dropdown-списка или что API доступен исключительно из внутренней сети. Кастомные санитайзеры для него невидимы.
-
Слишком широкие дефолтные правила. Вендоры настраивают правила так, чтобы ничего не пропустить — за счёт шума. Для вендора это безопасная стратегия. Для вашей команды — болезненная.
Практический workflow триажа:
- Включите только ERROR-severity / HIGH-confidence на первые 2–4 недели.
- Разберите первые 30 находок вручную. Запишите долю реальных и ложных, выделите повторяющиеся классы FP.
- Для повторяющихся классов создайте Semgrep custom rules или записи в
.trivyignore.yaml. - Через месяц понизьте порог до WARNING и повторите цикл.
- Используйте инкрементальное сканирование: передавайте в Semgrep только изменённые файлы через
git diff --name-only HEAD~1и флаг--include. На проектах с большой кодовой базой это радикально сокращает объём вывода.
Чеклист настройки связки Trivy и Semgrep
Готовый список действий для передачи в команду или включения в документацию:
- Установить Trivy >= 0.50 и Semgrep >= 1.0 на машинах разработчиков и CI-раннерах
- Создать
.trivyignore.yamlв корне репозитория — пустой, с комментарием «добавлять исключения через MR с обоснованием» - Создать
.semgrepignoreс путями:tests/,vendor/,node_modules/,*_test.*,*.spec.* - Первый запуск Trivy:
trivy fs --severity HIGH,CRITICAL --format table .— зафиксировать baseline - Первый запуск Semgrep:
semgrep scan --config p/owasp-top-ten --severity ERROR .— зафиксировать baseline - Подключить framework-specific ruleset, если проект использует фреймворк (
p/django,p/flask,p/express) - Настроить CI-job с
allow_failure: trueна первые 2–4 недели (advisory-режим) - Разобрать top-20 находок, создать записи в
.trivyignore.yamlи custom rules для повторяющихся классов FP - Через 2–4 недели убрать
allow_failureдля Trivy (CRITICAL), ещё через 2 недели — для Semgrep (ERROR) - Настроить кеширование базы Trivy в CI: переменная
TRIVY_CACHE_DIR+ cache между запусками - Добавить JSON/SARIF-отчёты как артефакты сборки
- Раз в квартал пересматривать
.trivyignore.yaml— удалять истёкшие исключения, обновлять обоснования
Большинство руководств по DevSecOps начинают с YAML-конфигурации CI/CD — и в этом корень проблемы. Конфиг для GitLab CI или GitHub Actions — последний шаг, не первый. Если вы не понимаете, что означает каждая строка вывода Trivy и почему Semgrep пометил конкретную строку кода, автоматизация не спасёт: вы будете либо игнорировать все алерты, либо блокировать каждый мёрж-реквест, пока команда не начнёт обходить сканер через отдельные ветки «без проверок». Мой подход — сначала две недели ручного запуска на локальной машине, разбор каждой находки, формирование baseline и только потом перенос в пайплайн. Команда, которая сначала научилась читать вывод, внедряет DevSecOps за спринт. Команда, которая начала с копирования YAML из чужой статьи, откатывает всё через неделю.
И ещё одна неудобная вещь: правила «из коробки» не работают ни для одного реального проекта. Каждый стек, каждый фреймворк, каждая внутренняя библиотека требуют кастомизации. Semgrep custom rules — базовая гигиена, без которой инструмент превращается в генератор шума. Если за первую неделю после внедрения вы не написали хотя бы два-три собственных правила — значит, либо проект слишком маленький, либо результаты никто не разбирает. Если переход в ИБ для вас актуален и хочется выстроить базу системно — на IB Basics показывают не теорию, а как джуны реально решают рабочие задачи.
Эту тему и смежные навыки разбирают на практике в курсе «Аналитик SOC» Codeby Academy.