Semgrep GitLab CI интеграция: блокируем merge с хардкодным API-ключом

На прошлом проекте разработчик закоммитил STRIPE_SECRET_KEY прямо в settings.py Flask-приложения. Ревьюер пропустил строку среди двухсот изменений — MR (merge request, запрос на слияние кода в основную ветку) влился в main за двенадцать минут. Через два часа ключ уже лежал в Docker-образе, ушедшем в registry. Инцидент закрыли быстро, но вопрос повис: как сделать так, чтобы пайплайн (последовательность автоматических проверок при каждом изменении кода) сам останавливал такие MR?
Ответ — одна job с Semgrep в .gitlab-ci.yml, настроенная на allow_failure: false. Ниже — конкретный YAML, кастомное правило и грабли, на которые наступают все.
Почему хардкод API-ключа в коде — реальная угроза
Токен или пароль, попавший в Git-репозиторий, остаётся в истории коммитов навсегда — даже если удалить строку следующим коммитом. В классификации MITRE ATT&CK (открытая база тактик и техник, которые реально используют злоумышленники; T-коды вроде T1552 — её идентификаторы) эта проблема описана как T1552.001 Credentials In Files: атакующий целенаправленно ищет учётные данные в файлах.
Цепочка «от коммита до инцидента» короткая:
- Разработчик для быстрого фикса вставляет
API_KEY = "sk_live_..."прямо в код - Код попадает в репозиторий — даже приватный
- Атакующий получает доступ через фишинг, утёкший токен или скомпрометированный CI-runner — техника T1213.003 Code Repositories
- Найденный ключ используется для входа в инфраструктуру — техника T1078 Valid Accounts, которая покрывает сразу четыре тактики: начальный доступ, закрепление, повышение привилегий и скрытность
По данным IBM X-Force Threat Intelligence Index 2025, атаки с использованием действительных учётных данных выросли на 71% за год. Хардкодный ключ в репозитории — один из самых дешёвых способов получить эти «действительные» учётные данные.
Злоумышленнику не нужно ломать периметр, если в коде уже лежит ключ от парадной двери. Stripe-ключ — вывод денег. AWS-ключ — майнинг криптовалюты на чужом аккаунте. Telegram-бот-токен — рассылка от имени компании. Предотвращение утечки секретов в репозитории начинается не с политик и регламентов, а с автоматической проверки в пайплайне.
Semgrep как инструмент статического анализа кода в GitLab CI
SAST (Static Application Security Testing, статический анализ безопасности) — проверка исходного кода без его запуска. Сканер читает файлы, применяет правила и сообщает о найденных проблемах. Semgrep — один из таких сканеров, и вот почему именно он удобен для первой интеграции в CI/CD (Continuous Integration / Continuous Delivery, автоматическая сборка и доставка кода):
- Работает без компиляции — не нужно настраивать среду сборки проекта
- Покрывает Python, JavaScript, Go, Java, C# и ещё два десятка языков одним инструментом
- Имеет готовые наборы правил:
p/defaultдля общих уязвимостей,p/secretsдля поиска секретов,p/owasp-top-tenдля рисков из OWASP Top 10 (открытый рейтинг критичных уязвимостей веб-приложений) - Возвращает ненулевой exit-код (код завершения процесса; 0 = успех, всё остальное = ошибка) при находке — без этого блокировка пайплайна не сработает
GitLab предлагает встроенный SAST через шаблон Security/SAST.gitlab-ci.yml, который под капотом тоже использует Semgrep. Но есть нюанс: полноценное управление результатами (Vulnerability Dashboard, MR Widget) доступно только на тире Ultimate. На бесплатном и Premium сканирование запускается, а вот интерфейс для разбора находок урезан. Прямая интеграция Semgrep через Docker-образ semgrep/semgrep работает на любом тире и даёт полный контроль над тем, что блокирует пайплайн.
Пошаговая настройка DevSecOps пайплайна: от job до блокировки merge request
Минимальная job в .gitlab-ci.yml
Прежде чем начать, убедись в трёх вещах: у тебя есть GitLab-проект с файлом .gitlab-ci.yml в корне (если его нет — создай пустой), GitLab Runner (агент, выполняющий job’ы) доступен и использует Docker executor. На GitLab.com shared runners работают из коробки; на self-hosted нужно зарегистрировать runner самостоятельно.
Добавь в .gitlab-ci.yml следующую job. Она запускается на каждый merge request и проверяет код набором правил p/secrets:
semgrep-secrets:
stage: test
image: semgrep/semgrep:latest
script:
- semgrep scan --config p/secrets --error .
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
Разбор ключевых строк:
image: semgrep/semgrep:latest— Docker-образ с уже установленным Semgrep; ничего дополнительно ставить не нужно. Если runner не может скачать образ (air-gapped среда), придётся положить образ в локальный registry--config p/secrets— подключает набор правил для поиска секретов. Можно указать несколько наборов через повторение флага:--config p/secrets --config p/default--error— заставляет Semgrep вернуть exit-код 1 при находке. Без этого флага Semgrep вернёт 0 даже если нашёл проблемы, и пайплайн продолжит как ни в чём не бывало. Частая ошибка — забыть этот флаг и потом удивляться, почему MR с секретом проходитallow_failure: false— говорит GitLab: если job упала, весь пайплайн считается неуспешным. Это значение по умолчанию, но я пишу его явно, чтобы никто случайно не переключил наtruerules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"— job запускается только при создании или обновлении MR, не на каждый push в любую ветку
Что произойдёт: при открытии MR запустится пайплайн, в нём — job semgrep-secrets. Если в файлах проекта (это full scan, не только diff) есть паттерн, похожий на хардкодный ключ, job завершится с ошибкой. MR получит красный статус.
Кастомные Semgrep правила для поиска внутренних ключей
Набор p/secrets покрывает типовые паттерны: AWS-ключи, GCP-сервисные аккаунты, Stripe-токены, GitHub PAT. Но если в проекте используется внутренний API с собственным форматом ключей — нужно кастомное правило. Создай файл .semgrep/internal-api-key.yml в корне репозитория:
rules:
- id: hardcoded-internal-api-key
patterns:
- pattern: $KEY = "MYAPI-..."
message: |
Захардкоженный ключ внутреннего API.
Используйте CI/CD Variable или vault.
severity: ERROR
languages: [python, javascript, go]
$KEY — метапеременная Semgrep, она подставится под любое имя переменной слева от знака присваивания. Паттерн "MYAPI-..." с тремя точками (это синтаксис Semgrep, не опечатка) означает «строка, начинающаяся с MYAPI-, за которой идёт произвольный текст». severity: ERROR — уровень серьёзности находки.
Чтобы подключить кастомные правила вместе с готовыми, измени команду в job: semgrep scan --config p/secrets --config .semgrep/ --error . — Semgrep подхватит все YAML-файлы из папки .semgrep/. При находке в логе job ты увидишь имя файла, номер строки, ID правила и текст из поля message.
Настройка блокировки merge request по результатам сканирования
Одной job с allow_failure: false мало — нужно сказать GitLab, что красный пайплайн означает запрет на merge. Три шага:
- Перейди в Settings → Merge requests → Merge checks и включи «Pipelines must succeed». После этого кнопка Merge в MR станет неактивной, пока пайплайн не позеленеет
- В Settings → Repository → Protected branches для ветки
mainубедись, что «Allowed to push» стоит «No one» или ограничено Maintainers. Иначе кто-нибудь обойдёт блокировку через direct push мимо MR - Опционально: настрой Merge request approval rules, чтобы даже при ручном override (на тирах Premium и Ultimate) требовался approve от Security-команды
Связка замыкается: Semgrep находит ключ → exit-код 1 → job красная → пайплайн красный → GitLab блокирует кнопку Merge.
CI Variable (переменная окружения пайплайна, задаётся в Settings → CI/CD → Variables) позволяет менять поведение без правки .gitlab-ci.yml. Добавь переменную SEMGREP_RULES со значением p/secrets p/owasp-top-ten и в скрипте используй semgrep scan --config $SEMGREP_RULES --error .. Меняешь набор правил через интерфейс, не трогая код — удобно для экспериментов.
Как не утонуть в false positives при поиске секретов в коде
Главная причина, по которой команды отключают SAST через неделю — ложные срабатывания. Semgrep помечает .env.example с API_KEY=your-key-here как утечку. Или тестовый файл с фейковым токеном для unit-тестов. Разработчики видят красный пайплайн на каждый MR и просят «выключить эту штуку». Три приёма, которые снимают проблему:
Файл .semgrepignore в корне репозитория работает как .gitignore, но для Semgrep. Каждая строка — путь или паттерн, который сканер пропускает. Строки tests/fixtures/ и *.example исключат тестовые фикстуры и файлы-шаблоны. Semgrep их просто не увидит.
Inline-комментарий # nosemgrep: rule-id рядом со строкой — точечное подавление конкретного срабатывания. Используй когда значение действительно безопасно (тестовый токен, строка из документации), но паттерн совпал. В Git-истории останется след: кто и когда отключил правило. На code review это видно.
Начинай с allow_failure: true, если команда раньше не работала с SAST. Первые две недели Semgrep запускается, показывает находки в логе, но не блокирует MR. За это время смотришь, какие правила срабатывают ложно, настраиваешь .semgrepignore и добавляешь nosemgrep где нужно. Потом переключаешь на allow_failure: false. Этот подход описан в OWASP DevSecOps Maturity Model (DSOMM — модель зрелости DevSecOps с уровнями Build, Patch, Test): сначала visibility — команда видит результаты, потом enforcement — результаты блокируют процесс.
Semgrep и Gitleaks: выстраиваем CI/CD security gate
Semgrep — универсальный SAST-сканер. Gitleaks — специализированный инструмент именно для поиска секретов. Ключевое отличие: Gitleaks умеет проверять не только текущее состояние файлов, но и всю Git-историю через gitleaks detect --source . --verbose. Semgrep работает с файлами на диске «как есть».
Рабочий подход, который закрывает оба сценария: Semgrep в MR-пайплайне ловит свежие коммиты с секретами и параллельно проверяет код на уязвимости (A03:2021 Injection, A05:2021 Security Misconfiguration из OWASP Top 10). Gitleaks запускаешь по расписанию — ночной full scan — для аудита всей истории. Так покрыты и «кто-то закоммитил ключ сегодня» и «ключ лежит в истории с прошлого года».
В .gitlab-ci.yml это две отдельные job. semgrep-secrets с триггером merge_request_event блокирует MR в реальном времени. Вторая job с Gitleaks на schedules генерирует отчёт для ручного разбора. Первая защищает от новых утечек, вторая находит старые. Обе не требуют GitLab Premium или Ultimate.
Если нужно сканировать только изменённые файлы (diff-aware scan — проверка только тех строк, которые изменились в MR), Semgrep поддерживает это через переменную SEMGREP_BASELINE_REF. В связке с Semgrep AppSec Platform (бесплатный тир доступен) это убирает шум от legacy-кода: старые находки не блокируют новый MR. Согласно документации Semgrep, для GitLab CI/CD diff-aware режим включается автоматически в MR-пайплайнах при подключении к Semgrep AppSec Platform.
Последние полтора года я настраиваю security gate в пайплайнах для разных команд. Главный вывод: техническая часть — написать YAML, добавить правило — занимает час. Политическая часть — убедить команду не выключать сканер после первого ложного срабатывания — занимает недели.
Semgrep с allow_failure: false без предварительной настройки исключений — верный способ получить бунт разработчиков к пятнице. Каждый раз, когда я видел отключённый SAST в продакшн-проекте, история была одинаковая: включили на полную мощность в первый день, к третьему дню команда забросала тикетами «пайплайн блокирует .env.example», к пятому — кто-нибудь с правами Maintainer ставил allow_failure: true и забывал вернуть. Двухнедельная «обкатка» в режиме наблюдения — не компромисс с безопасностью, а единственный рабочий порядок внедрения.
Большинство гайдов по DevSecOps показывают пайплайн с пятью сканерами в одной статье. На практике команда, которая вчера не имела ни одного security-чека, не переварит пять инструментов разом. Один Semgrep с набором p/secrets — уже больше, чем у большинства проектов на рынке. Закрой этот минимум, убедись, что пайплайн стабилен и никто не обходит блокировку через direct push — потом добавляй SCA, container scan и всё остальное. Если ищешь джуниор-роль в ИБ и хочешь показать на собеседовании что-то конкретное — IB Basics на codeby.school закрывает базу за пару месяцев, а настроенный security gate в GitLab CI на pet-проекте уже даёт реальный кейс для портфолио.
Эту тему и смежные навыки разбирают на практике в курсе «Профессия DevSecOps-инженер» Codeby Academy.