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

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

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

На прошлом проекте разработчик закоммитил 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: атакующий целенаправленно ищет учётные данные в файлах.

Цепочка «от коммита до инцидента» короткая:

  1. Разработчик для быстрого фикса вставляет API_KEY = "sk_live_..." прямо в код
  2. Код попадает в репозиторий — даже приватный
  3. Атакующий получает доступ через фишинг, утёкший токен или скомпрометированный CI-runner — техника T1213.003 Code Repositories
  4. Найденный ключ используется для входа в инфраструктуру — техника 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 упала, весь пайплайн считается неуспешным. Это значение по умолчанию, но я пишу его явно, чтобы никто случайно не переключил на true
  • rules: - 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. Три шага:

  1. Перейди в Settings → Merge requests → Merge checks и включи «Pipelines must succeed». После этого кнопка Merge в MR станет неактивной, пока пайплайн не позеленеет
  2. В Settings → Repository → Protected branches для ветки main убедись, что «Allowed to push» стоит «No one» или ограничено Maintainers. Иначе кто-нибудь обойдёт блокировку через direct push мимо MR
  3. Опционально: настрой 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.