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

Расследование инцидента в git: кто и когда закоммитил уязвимый код

Расследование инцидента в git: кто и когда закоммитил уязвимый код
Время чтения: 10 мин.

В 2024 году атаки с использованием действительных учётных данных выросли на 71% — данные IBM X-Force Threat Intelligence Index 2025. По отчёту CrowdStrike за тот же период, 75% вторжений начинались со скомпрометированных credentials. Откуда атакующие их берут? Существенная часть — прямо из git-истории: забытые API-ключи, хардкод-пароли, конфиги с токенами, которые «удалили» из текущей версии файла, но не из объектной базы репозитория. Git помнит всё, и атакующие это знают.

Расследование инцидента в git начинается с одного вопроса: кто и когда внёс уязвимый код. Ниже — полный разбор такого расследования на учебном примере, от первой команды до установления автора, даты и мотивации уязвимого изменения.

Git forensics и анализ истории коммитов git: зачем это нужно

Git хранит каждый коммит как снимок (snapshot) с тремя обязательными атрибутами: меткой времени, именем автора и сообщением. По сути, репозиторий — криминалистическая база, где каждый коммит работает как «улика» с точной хронологией. Подделать след можно только через перезапись истории (force push), а в корпоративных репозиториях force push обычно заблокирован политикой.

В терминологии MITRE ATT&CK — это открытая база тактик и техник атак, где каждая техника имеет свой T-код — поиск учётных данных в файлах описан как Credentials In Files (T1552.001, тактика Credential Access). Атакующие целенаправленно ищут секреты в коде и git-истории. Параллельная техника Code Repositories (T1213.003, тактика Collection) описывает сценарий, когда злоумышленник извлекает ценные данные из репозиториев, включая «удалённые» ветки и коммиты.

Бизнес-логика атаки простая: злоумышленник клонирует публичный (или скомпрометированный приватный) репозиторий, прогоняет всю историю коммитов на предмет строк, похожих на ключи и пароли, и использует найденное для доступа к инфраструктуре. По данным hackerdna.com, облачные ключи, найденные в публичных GitHub-репозиториях, эксплуатировались в течение минут — для запуска криптомайнинг-инфраструктуры. По классификации MITRE ATT&CK это T1078.004 (Cloud Accounts) — использование действительных учётных данных для доступа к облаку.

Если вы умеете делать то же, что атакующий, но делаете это раньше — вы выигрываете. Git forensics (я предпочитаю термин «код-археология») — набор команд для восстановления событий по цифровым следам. Три основных инструмента:

  • git log — хронология: кто, когда и что менял
  • git blame — привязка конкретной строки к конкретному автору и коммиту
  • git bisect — бинарный поиск коммита, который сломал тест или внёс регрессию

Принцип тот же, что в детективной работе: начинаем с общей картины (git log), сужаем круг подозреваемых (git blame), устанавливаем точный момент (git bisect).

Git log при инциденте безопасности: точка входа в расследование

git log выводит историю коммитов в обратном хронологическом порядке. Без параметров он покажет всё — а это обычно тысячи записей. Для расследования инцидента git нужна фильтрация.

Как читать вывод git log и фильтровать результаты

Допустим, SAST-сканер (инструмент статического анализа кода — Semgrep, CodeQL и подобные) обнаружил SQL-инъекцию в файле api/views.py. Первый шаг — просмотреть историю этого конкретного файла: git log --oneline -- api/views.py. Флаг --oneline сжимает вывод до одной строки на коммит: сокращённый хеш (первые 7 символов SHA-1, уникального идентификатора коммита) и сообщение. Двойной дефис -- отделяет пути файлов от остальных параметров.

На выходе — компактный список вроде a1b2c3d fix: обход ORM для сложного запроса. Ищите подозрительные формулировки: «hotfix», «temp», «disable check», «workaround». По моему опыту, уязвимости чаще всего прячутся именно в спешных фиксах — разработчик торопился закрыть тикет и срезал углы.

Фильтры для сужения результатов при анализе истории коммитов git:

  • --author="Ivan" — коммиты конкретного автора, когда подозреваемый уже известен
  • --after="2025-01-01" --before="2025-06-01" — диапазон дат, сужающий поиск до периода, когда предположительно появилась проблема
  • --grep="hotfix" — поиск по сообщению коммита
  • --all — включает все ветки, не только текущую. Без этого флага можно пропустить коммиты из feature-веток

Комбинированный запрос git log --oneline --all --after="2025-01-01" -- "*.py" "*.yml" за минуту сужает тысячи коммитов до десятка, затрагивающих Python- и YAML-файлы в указанном периоде. Именно в этих форматах чаще всего оказываются хардкод-секреты и уязвимые конфигурации.

Git blame — поиск автора уязвимости в git

git blame отвечает на вопрос «кто последний менял конкретную строку». Для каждой строки файла он показывает хеш коммита, автора, дату и содержимое. Логичный следующий шаг: через git log вы определили файл — теперь нужно найти строку и её автора.

Предпосылка: вы знаете файл и примерный диапазон строк (из отчёта SAST-сканера или из ручного код-ревью).

Аудит коммитов репозитория: флаги -w, -M, -C

На реальном проекте базовый git blame часто показывает не того, кто написал уязвимую логику, а того, кто последним прогнал форматтер по файлу. Классическая ловушка. Три флага решают проблему:

-w игнорирует изменения пробелов. Если кто-то прогнал весь проект через black или prettier, blame без -w покажет этого человека как «автора» каждой строки. Флаг пробивается сквозь косметические правки.

-M обнаруживает строки, перемещённые внутри файла. Без него перенос функции на 50 строк вниз «затирает» реального автора.

-C обнаруживает строки, скопированные из других файлов. Уязвимый код часто путешествует через copy-paste между модулями — и без -C вы найдёте того, кто скопировал, а не того, кто написал.

Рабочая комбинация для расследования: git blame -w -M -C -L 42,60 api/views.py. Флаг -L 42,60 ограничивает вывод строками 42–60, теми, где SAST обнаружил проблему. Ожидаемый вывод:

a1b2c3d4 (Ivan Petrov   2025-09-15 42) query = "SELECT * FROM users WHERE id=" + user_id
f7e8d9c0 (Anna Sidorova 2025-03-10 43) result = db.execute(query)
b5c6d7e8 (Ivan Petrov   2025-09-15 44) return result

Читаем: строка 42 — конкатенация SQL-запроса вместо параметризованного (классическая SQL-инъекция). Автор — Ivan Petrov, дата — 15 сентября 2025, коммит a1b2c3d4. Теперь вы знаете, кто закоммитил уязвимый код, и можете перейти к контексту.

Следующая команда — git show a1b2c3d4. Она покажет полный diff (все изменения коммита), сообщение и список затронутых файлов. Сообщение вроде «fix CI tests» или «обход ограничения ORM» объясняет мотивацию: разработчик спешил и отключил проверку. Вот он, root cause.

Для долгосрочной работы полезен файл .git-blame-ignore-revs — список хешей «шумных» коммитов (массовое форматирование, миграция стиля), которые blame будет пропускать. GitHub и GitLab поддерживают этот файл в веб-интерфейсе: после добавления в корень репозитория вся команда видит «чистый» blame.

Git bisect — поиск уязвимости бинарным методом

git bisect решает ситуацию, когда вы знаете «раньше работало, а сейчас сломано», но между двумя точками — сотни коммитов. Проверять каждый вручную — часы работы. Bisect делит историю пополам и за log₂(n) шагов находит виновный коммит. Для 1000 коммитов это примерно 10 проверок вместо тысячи.

Типичная ситуация: security-тест проходил неделю назад, а сейчас падает. Кто-то сломал CSP-заголовок (Content Security Policy — HTTP-заголовок, ограничивающий загрузку ресурсов на странице), убрал rate limiting или изменил конфигурацию CORS (Cross-Origin Resource Sharing — политика браузера, определяющая, какие домены могут обращаться к API).

Предпосылка: вы в корне git-репозитория и знаете «хороший» коммит или тег, где проблемы не было.

Ручной режим: запускаете git bisect start, затем git bisect bad (текущий коммит содержит баг) и git bisect good v2.1.0 (здесь бага не было). Git переключит рабочую директорию на коммит посередине — проверяете, отвечаете git bisect good или git bisect bad. Через 7–10 итераций git выводит first bad commit is … с точным хешем, автором и diff. После завершения — git bisect reset для возврата на исходную ветку.

Если есть тест, воспроизводящий проблему, bisect автоматизируется полностью:

git bisect start HEAD v2.1.0
git bisect run pytest tests/test_csp_header.py -x

Тест должен возвращать код выхода 0 для «хорошего» коммита и ненулевой для «плохого». Код 125 говорит bisect пропустить коммит — полезно, когда он не компилируется. Git прогонит весь бинарный поиск без вашего участия.

Принцип выбора инструмента прост: знаете файл и строку — git blame. Не знаете файл, но можете сформулировать тест «работает / не работает» — git bisect.

Секреты в git-истории: аудит безопасности репозитория

Даже если секрет удалён из текущей версии файла, он остаётся в git-истории навсегда — пока не выполнен force push с перезаписью. Это момент, который новички упускают: git rm удаляет файл из рабочей директории, но не из объектной базы.

Вариант с pickaxe-поиском: git log -S "AWS_SECRET_ACCESS_KEY" --oneline находит коммиты, в которых указанная строка была добавлена или удалена. Флаг -S считает количество вхождений строки в каждом коммите и показывает те, где это количество изменилось.

Для regex-паттернов (например, любого Bearer-токена) используется флаг -G: git log -G "Bearer [A-Za-z0-9_\-]+" --oneline найдёт все коммиты с изменёнными строками, содержащими токен. Разница: -S ищет точные строки, -G — регулярные выражения. Оба можно ограничить типами файлов, добавив -- "*.py" "*.yml" в конец команды.

Автоматическое сканирование: trufflehog и gitleaks

Ручной pickaxe-поиск работает, когда вы знаете, что искать. Для полного аудита безопасности репозитория нужны специализированные инструменты.

trufflehog сканирует всю git-историю, ищет высокоэнтропийные строки (случайные последовательности, характерные для ключей) и паттерны известных секретов — AWS, GCP, Slack-токены и другие. Запуск для локального репозитория: trufflehog git file://./.

gitleaks делает то же, но лучше заточен под CI/CD-пайплайн. Запуск: gitleaks detect --source=./ --verbose. На выходе — список секретов с указанием файла, строки и хеша коммита. Я предпочитаю gitleaks для автоматизации и trufflehog для разовых аудитов, но оба справляются.

Оба инструмента можно встроить в pre-commit hook, чтобы секреты не попадали в репозиторий в будущем. В терминологии MITRE D3FEND (knowledge graph защитных техник) это реализация D3-FA (File Analysis) — анализ файлов для обнаружения credentials.

Каждый найденный секрет необходимо ротировать — сгенерировать новый ключ и отозвать старый. Удаление строки из кода НЕ удаляет её из git-истории. Повторю, потому что это критически важно: git rm + git commit + git push не решает проблему. Секрет уже в истории.

Практика: расследование инцидента git шаг за шаг

Ниже — полный цикл расследования. Предпосылки: установлен git (версия 2.20+), есть доступ к репозиторию, терминал открыт в корне проекта. Для автоматического сканирования понадобится gitleaks или trufflehog (установка через пакетный менеджер или бинарник с GitHub).

Шаг 1. Общая картина. Смотрим историю подозрительного файла: git log --oneline --all -- api/views.py. На выходе — список коммитов. Ищем подозрительные формулировки в сообщениях: «hotfix», «temp fix», «disable check». Записываем хеши кандидатов.

Шаг 2. Находим автора. Запускаем blame с полным набором флагов: git blame -w -M -C -L 42,60 api/views.py. Для каждой строки видим хеш, имя автора и дату. Фиксируем хеш коммита, где появилась уязвимая конструкция.

Шаг 3. Восстанавливаем контекст. Выполняем git show <hash>. Читаем сообщение коммита — оно объясняет мотивацию. Смотрим diff: уязвимость может затрагивать больше одного файла.

Шаг 4. Ищем регрессию (при необходимости). Если проблема — сломанный security-тест, запускаем git bisect start HEAD v2.1.0 и git bisect run pytest tests/test_auth.py -x. Git за 7–10 шагов найдёт точный коммит.

Шаг 5. Проверяем секреты. Запускаем полное сканирование: gitleaks detect --source=./ --verbose. Каждый найденный секрет — вектор атаки, который требует немедленной ротации.

Шаг 6. Документируем. Фиксируем: хеш коммита, автора, дату, мотивацию из сообщения, масштаб изменений, найденные секреты. Это основа для incident report и для доработки CI/CD — добавления pre-commit hooks, SAST-сканеров и автоматических проверок на утечки.

По опыту, цикл восстановления истории изменений git от первого git log до готового отчёта занимает от 20 минут до пары часов — зависит от размера репозитория и количества подозрительных коммитов.

Девять из десяти команд, с которыми я работал, ставят gitleaks или trufflehog ПОСЛЕ первого инцидента. К этому моменту секреты уже лежат в git-истории месяцами. Индустрия фиксируется на shift-left — перехвате проблем на ранних стадиях пайплайна — но почти никто не делает то, что я называю «shift-past»: аудит уже существующей истории коммитов на предмет секретов, которые давно там лежат. Это как ставить замок на дверь, не проверив, нет ли открытого окна с другой стороны.

Первое, что стоит сделать после прочтения — прогнать gitleaks detect или trufflehog git на рабочем репозитории и посмотреть, сколько «сюрпризов» найдётся в истории. Результат обычно отрезвляет. Если хочешь разобраться в подобных расследованиях системно — на IB Basics эту базу закрывают за пару месяцев, без академического тона.

Эту тему и смежные навыки разбирают на практике в курсе «Git. Система контроля версий» Codeby Academy.