Я работаю в IT-консалтинге первый год и параллельно веду несколько пет-проектов вместе с Claude Code. И в какой-то момент меня начало беспокоить одно и то же:

«А код, который мне написал AI, вообще безопасный?»

Несколько личных проектов я собрал в связке с Claude Code, и раз уж большую часть code review я отдаю модели, то вполне реально, что куда-нибудь просочится:

  • опасный паттерн (shell=True, конкатенация строк, из которой получается SQL-инъекция, и тому подобное);

  • содержимое .env или API-ключ, зашитый прямо в код.

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

И тут мне попалась мысль: если поставить Semgrep и gitleaks в два эшелона, то можно ловить и опасные паттерны в коде, и утечки секретов одновременно. Я решил встроить эту связку во все проекты, которых касаюсь, — и в старые, и в новые. Эта статья — журнал той работы.

Semgrep и gitleaks: кто за что отвечает

Инструмент

Роль

Что ловит

Semgrep

Статический анализ кода (SAST)

subprocess.call(cmd, shell=True), опасную конкатенацию строк под SQL-инъекцию и т.п.

gitleaks

Скан секретов

Захардкоженные AWS-ключи, Slack-токены, Stripe secret key и прочее

Сначала я думал, что хватит одного Semgrep. Но Semgrep — это всё-таки SAST по коду, и захардкоженный .env или высокоэнтропийные строки-секреты — не его профиль. А gitleaks, наоборот, заточен под секреты, но до опасных паттернов реализации ему дела нет. Зоны ответственности разные, поэтому два эшелона — это осмысленно, а не дублирование ради дублирования.

Схема: локально (pre-commit) + CI (GitHub Actions), два слоя защиты

При внедрении я завёл две точки проверки.

  • Локально (pre-commit hook) — скан на каждом git commit. Цель — чтобы опасный код и секреты вообще не попадали в репозиторий.

  • CI (GitHub Actions) — ещё один скан на каждый push и PR. Ловит то, что проскользнуло мимо pre-commit или было пропущено через --no-verify.

«Остановить локально» + «если проскочило — поймать в CI». Такая двойная схема становится страховкой в том числе от собственной невнимательности и излишней самоуверенности.

Реальные конфиги

.pre-commit-config.yaml


repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.30.1
    hooks:
      - id: gitleaks
  - repo: https://github.com/semgrep/semgrep
    rev: v1.165.0
    hooks:
      - id: semgrep
        args: ['--config', 'auto', '--error']

С флагом --config auto Semgrep сам подбирает community-набор правил под языковой состав репозитория. Чем сразу выкручивать тонкую настройку правил, проще сначала посмотреть на auto, а если шума много — уже подкручивать. Так старт быстрее.

.gitleaks.toml


title = "gitleaks config"
[extend]
useDefault = true
[allowlist]
description = “Global allowlist”
paths = [
‘’‘..lock‘’',
‘’'node_modules/.’‘’,
‘’‘(.*/)?test(s)?/.fixture.’‘’,
‘’‘.env.example$’‘’,
]

Наследую дефолтные правила (useDefault = true), но при этом добавляю в allowlist то, что «и не страшно, если найдётся»: lock-файлы, тестовые фикстуры и т.п. Написать идеальный список исключений с первого раза нереально, поэтому подход такой — ловим ложное срабатывание в процессе эксплуатации и дописываем по ходу.

.github/workflows/security-scan.yml


name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
gitleaks:
name: gitleaks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
semgrep:
name: semgrep
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v4
- run: semgrep scan --config=auto --error

Команда semgrep ci рассчитана на связку с SaaS Semgrep (через APP_TOKEN), поэтому я выбрал не зависящий от внешнего сервиса вариант — semgrep scan --config=auto --error. Так CI крутится без регистрации дополнительных секретов.

Проверил, что оно реально ловит

Я не стал останавливаться на «написал конфиги — и доволен». Сделал отдельный тестовый репозиторий, специально насовал туда «опасного кода» и «чего-то похожего на секреты», и проверил, что оно действительно срабатывает.

Проверка Semgrep


import subprocess
def run(cmd):
subprocess.call(cmd, shell=True)

При попытке закоммитить это всё останавливается так:


❯❱ python.lang.security.audit.subprocess-shell-true.subprocess-shell-true
   Found 'subprocess' function 'call' with 'shell=True'. This is dangerous because
   this call will spawn the command using a shell process...

shell=True — это классический опасный паттерн: если передать в шелл внешний ввод как есть, можно получить выполнение произвольной команды. Когда просишь Claude Code «напиши функцию, которая выполняет команду», такая реализация вполне может выскочить сама собой, поэтому возможность механически это отсекать — реально успокаивает.

Проверка gitleaks


SLACK_TOKEN = "xoxb-7234659182734-9182734659182-aB3dEfGhIjKlMnOpQrStUvWx"

Detect hardcoded secrets.................................................Failed
Finding:     SLACK_TOKEN = "REDACTED
RuleID:      slack-bot-token

Заблокировалось как надо. Кстати, сначала я попробовал ту самую «образцовую» AWS-строку AKIAIOSFODNN7EXAMPLE, которая часто встречается в официальной документации, — и она не задетектилась. «Что, не работает?» — слегка занервничал я. Но оказалось, что gitleaks специально считает официальные sample-значения AWS заведомо не-секретами, и для защиты от ложных срабатываний это, наоборот, правильное поведение. На реально выглядящем токене всё сработало как надо, так что можно было выдохнуть.

Чтобы и будущие проекты получали это автоматически

Если просто внедрить во все текущие проекты, то на новый проект, который я заведу в следующем месяце, это не распространится. Чтобы убрать «забыл подключить» на уровне механики, я сделал в ~/.zshrc функцию secinit.


secinit() {
  if [ ! -d .git ]; then
    git init
  fi
  cp ~/.git-templates/.pre-commit-config.yaml .
  cp ~/.git-templates/.gitleaks.toml .
  mkdir -p .github/workflows
  cp ~/.git-templates/github-workflows/security-scan.yml .github/workflows/
  pre-commit install
  echo "semgrep + gitleaks готовы (скан запускается автоматически при commit)"
}

Достаточно в каталоге нового проекта набрать secinit, и оно за один заход:

  • (если ещё нет) сделает git init;

  • скопирует весь комплект конфигов;

  • включит хук через pre-commit install.

Сначала я думал, нельзя ли через git config --global init.templateDir подсунуть конфиги прямо в git init. Но на практике убедился: это механизм для шаблонизации содержимого .git/ (хуков и т.п.), и положить файлы в рабочее дерево (в корень проекта) он не может. Честная функция-обёртка оказалась проще и надёжнее.

Что я почувствовал после внедрения

«Работает» и «безопасно» — это разные оси

Скорость vibe coding остаётся при тебе, а то, что можно проверять механически, отдаёшь машине. Прочитать глазами и отревьюить весь код, который написал AI, — нереально, поэтому если хотя бы примесь опасных паттернов и секретов отсекается автоматически, то можно спокойно наращивать скорость разработки.

Умение жить с ложными срабатываниями — это и есть суть эксплуатации

Даже в этой проверке я столкнулся с поведением вокруг ложных срабатываний — в виде «официальный sample-ключ AWS не детектится». Не стоит пытаться написать идеальный allowlist сразу; реалистичнее подкручивать его по ходу, когда всплывает ложное срабатывание.

Механизм «для следующего проекта» — неожиданно важная вещь

Внедрение в текущие проекты — это разовая история. Если не привести всё в состояние «в следующий раз тоже подключится автоматически» через что-то вроде secinit, то в итоге опять забудешь, и вылезет «вот в том проекте как раз и не стоит». Только доведя до механизма, можно сказать, что ты действительно «внедрил».

В заключение

В итоге я внедрил это во все свои текущие пет-проекты и привёл всё в состояние, когда новые проекты получают защиту автоматически.

И Semgrep, и gitleaks бесплатны, а настройка укладывается в несколько десятков строк. Если вы из тех, кто «отдаёт написание кода AI, но переживает за безопасность», — эту связку точно стоит попробовать в первую очередь.