Поиск секретов в репозитории — это не один проход регуляркой по файлам, а несколько вложенных конвейеров с разными режимами: история коммитов, рабочее дерево, индекс, предфильтрация по ключевым словам, многоуровневое декодирование, энтропийные пороги и механизм baselineфайл-снимок уже известных находок, с которым сравнивают новые результаты сканирования, который отделяет уже известные находки от новых. Ниже — как каждый из этих механизмов устроен и в каком порядке срабатывает.
Как gitleaks обходит историю репозитория
Когда пользователь не передал свои опции логирования, gitleaks выполняет:
git -C <sourceClean> log -p -U0 --full-history --all --diff-filter=tuxdbФлаг -p включает вывод патчей для каждого коммита, чтобы анализировать добавленные строки. Флаг -U0 задаёт нулевой контекст: в патче показываются только сами изменённые строки без окружающих. Флаг --full-history заставляет git log проходить всю историю, не упрощая её при слияниях. Флаг --all включает все ссылки — ветки, теги и прочее, а не только текущую ветку.
Как обрабатывается stderr от git
git-команды запускаются, а их stderrстандартный поток ошибок процесса, куда git пишет диагностические сообщения читается в отдельной горутине построчно. Если строка содержит одно из известных безобидных сообщений git — например, про пропуск rename detection, про diff.renameLimit, про git help gc или про auto packing в фоне, — она логируется как Warn и ошибкой не считается. Любая другая строка логируется как Error с префиксом [git] и устанавливает флаг ошибки. Если флаг установлен, в канал ошибок отправляется сообщение stderr is not empty, иначе канал просто закрывается. Потребитель читает этот канал и при получении ошибки прерывает обработку.
Сканирование рабочего дерева и индекса
Для рабочего дерева и индекса используются две команды. git ls-files -z перечисляет файлы, отслеживаемые в индексе, и передаётся в detect-secrets-hookинструмент, который проверяет переданные ему файлы на наличие секретов через xargs -0. git diff --staged --name-only -z перечисляет только имена файлов, попавших в индекс, и тоже передаётся в detect-secrets-hook.
Разница между режимами диффа принципиальна. git diff --staged показывает, что попадёт в следующий коммит: это сравнение индекса и последнего коммита. git diff сравнивает директорию и индекс, то есть незастейдженные изменения. git diff HEAD показывает разницу между рабочей директорией и последним коммитом. Добавленные строки извлекаются через чтение stdout запущенной git-команды: поток оборачивается в blobReaderобёртку над потоком данных, из которой читаются эти данные.
Предфильтрующий матчер: от ключевых слов к правилу
На каждой итерации сканирования строится карта ключевых слов, а исходный фрагмент приводится к нижнему регистру. По нормализованному тексту запускается автомат Ахо–Корасикалгоритм поиска множества подстрок в тексте за один проход, и для каждого найденного вхождения в карту кладётся подстрока, взятая из нормализованного текста. Сам автомат строится один раз при создании детектора из ключевых слов конфигурации.
Далее для каждого правила: если у правила нет ключевых слов, фрагмент всегда сканируется этим правилом; иначе правило применяется только если хотя бы одно из его ключевых слов, приведённое к нижнему регистру, присутствует в построенной карте.
Порядок проверок внутри одного правила
Сначала проверяется путь. Если у правила задан Path, а Regex отсутствует и нет декодированных сегментов, то при совпадении пути сразу создаётся находка и происходит возврат. Если Path задан вместе с Regex, то при несовпадении пути происходит ранний возврат, и регулярное выражение не рассматривается. Затем, если Regex отсутствует, содержимое не проверяется и происходит возврат. После этого при заданном лимите размера вычисляется длина фрагмента в мегабайтах и, если она превышает лимит, фрагмент пропускается. Только затем выполняется поиск регулярным выражением, и при отсутствии совпадений происходит возврат.
Проверки allowlistсписок исключений, при совпадении с которым находка не создаётся по коммиту и пути выполняются отдельно, не внутри этого порядка. Для условия AND коммит и путь проверяются вместе с regex и stopwordsслова-исключения, при наличии которых находка не создаётся, а для OR — только regex и stopwords.
Многоуровневое декодирование
Декодирование выполняется в цикле. На каждой итерации сначала увеличивается счётчик глубины, затем проверяется, не превысил ли он предел, и если превысил — цикл прерывается. После этого текущий текст и набор закодированных сегментов заменяются результатом декодирования, то есть текст декодируется для следующего прохода. Если после декодирования закодированных сегментов не осталось, цикл также прерывается. Глубина растёт на единицу за проход, а цикл останавливается либо при превышении предела, либо когда декодировать больше нечего.
Энтропия: формула и пороги
В плагине detect-secrets энтропия Шеннонамера неопределённости распределения символов в строке вычисляется так: для каждого символа из заданного набора charsetнабор допустимых символов, по которому считается распределение вычисляется доля как отношение числа вхождений этого символа к длине строки, и если доля больше нуля, к энтропии прибавляется произведение доли на логарифм доли по основанию 2 со знаком минус. Для пустой строки возвращается 0.
В gitleaks значение энтропии вычисляется от секрета находки и сохраняется в поле находки. Если в правиле задано ненулевое пороговое значение, находка пропускается при условии, что энтропия меньше или равна порогу.
Пороги по умолчанию и допустимый диапазон
Базовый класс плагинов высокой энтропии принимает charset и limitпорог энтропии, с которым сравнивается вычисленное значение строки и при создании проверяет, что limit находится в диапазоне от 0 до 8 включительно; иначе выбрасывается ValueError с сообщением о допустимых границах. Base64HighEntropyStringплагин detect-secrets, ищущий строки в кодировке base64 с высокой энтропией по умолчанию использует limit=4.5, HexHighEntropyString — limit=3.0.
Для hex-строк дополнительно переопределён расчёт: если строка является числом и её длина больше 1, из энтропии вычитается 1.2 / math.log(len(data), 2). Поправка применяется только при успешном преобразовании строки в число; при ValueError она не применяется. Для строки длиной 1 энтропия возвращается без поправки. Смысл — снизить энтропию чисто цифровых строк, чтобы уменьшить ложные срабатывания, но длинные строки получают меньшую поправку, так как у них выше шанс быть настоящим секретом.
При форматировании результата значение округляется до 3 знаков и сравнивается с лимитом: если энтропия меньше лимита, результат помечается как ложный.
Почему для анализа нужны строки в кавычках
Регулярное выражение плагина высокой энтропии компилируется как ([\'"])([{}]+)(\1) с подстановкой экранированного charset. Оно допускает строки, заключённые в одинарные или двойные кавычки: первая группа захватывает открывающую кавычку, вторая — последовательность символов из charset, третья — обратную ссылку на ту же кавычку, что и в начале. Комментарий в коде объясняет: кавычки требуются, чтобы снизить шум, а группа нужна для обратной ссылки, чтобы закрывающая кавычка совпадала с открывающей. Для Base64HighEntropyString charset включает буквы, цифры, +/, \-_ и =.
Режим нетерпеливого поиска
Для форматов файлов, где строки не обязательно обозначаются одинарными или двойными кавычками, регулярное выражение временно подменяется на вариант без кавычек. Внутри менеджер сохраняет старое выражение, строит новое из символов charset и, если требуется точное совпадение, оборачивает его в ^...$, чтобы совпадение покрывало строку целиком; иначе выражение ищет секрет внутри строки текста. После выхода из блока исходное выражение восстанавливается, поэтому подмена действует только на время работы менеджера.
Baseline: не переоткрывать известные находки
baseline. Создаётся он функцией create, которая рекурсивно сканирует файлы по указанным путям и возвращает коллекцию секретов: создаётся коллекция с заданным root, затем вызывается сканирование файлов по путям из get_files_to_scan.
Загрузка baseline принимает уже разобранный словарь и приводит его к текущей версии. upgradeприведение старого baseline к текущему формату. Если версия baseline не меньше текущей версии программы, словарь возвращается без изменений; иначе к нему по очереди применяются модули обновления, подходящие для его версии, и в конце проставляется текущая версия. Затем настраиваются фильтры и плагины из baseline, и возвращается коллекция секретов. Функция load_from_file читает файл и парсит JSON, возвращая словарь.
format_for_output формирует выходной словарь с полями version, настройками фильтров и плагинов и results. Если slim-режимрежим вывода без служебных полей выключен, добавляется поле generated_at с текущим временем в UTC; если включён, из каждого секрета удаляется поле line_number. save_to_file принимает коллекцию секретов или словарь, при передаче коллекции вызывает format_for_output, затем записывает JSON с отступом 2 и завершающим переводом строки.
Как baseline применяется при повторном сканировании
upgrade вызывается при загрузке baseline и приводит старый baseline к текущей версии: если версия baseline не меньше текущей, он возвращается без изменений; иначе импортируются модули из пакета upgrades, отфильтрованные так, чтобы остались только те, для которых проверка релевантности истинна, затем каждый модуль применяется к baseline, после чего в поле version записывается текущая версия.
Проверка релевантности модуля возвращает функцию, которая по пути модуля извлекает версию: берёт последний сегмент после точки, убирает ведущую v, заменяет _ на ., добавляет .0 и сравнивает версию baseline с полученной.
Повторное сканирование с указанием baseline обновляет его до совместимости с последней версией, добавляет новые найденные секреты и удаляет секреты, которых больше нет в кодовой базе.
Pre-commit hook detect-secrets
Проверка baseline перед коммитом устроена так: если файл baseline входит в список изменённых, но не проиндексированных файлов, печатается сообщение с предложением выполнить git add <filename> и возбуждается ValueError.
Решение об обновлении baseline принимается так: сначала загружается исходный baseline из переданной коллекции секретов, затем текущая коллекция обрезается по отсканированным результатам и списку файлов. Возвращается истина, если версия baseline не совпадает с текущей, или если после обрезки коллекция не полностью равна исходной.
Диагностика печатает заголовок с красным текстом об ошибке, затем каждый найденный секрет, после чего общее количество найденных секретов и список возможных мер: обращение к команде безопасности (из переменной окружения или в канале #security) и пометку ложных срабатываний комментарием pragma: allowlist secret.
Близость вспомогательных находок
вспомогательные находкинаходки, которые проверяются на близость к основной и добавляются к ней, если правило требует наличия других правил. В конвейере они появляются так: для каждой основной находки перебираются найденные required-находкинаходки, которые проверяются на близость к основной и добавляются к ней, если правило требует наличия других правил, и если проверка близости возвращает истину, создаётся вспомогательная находка с идентификатором правила, координатами начала и конца, строкой, совпадением и секретом, которая добавляется в список вспомогательных. Затем, если список непуст и подтверждено, что для каждого required-правила есть хотя бы одна вспомогательная находка, создаётся новая находка: копия основной, к которой добавляются вспомогательные, и она попадает в итоговый список.
Проверка полноты собирает множество идентификаторов правил из вспомогательных находок и возвращает ложь, если хотя бы одного required-идентификатора там нет.
Проверка близости работает так: если у required-правила не заданы ни лимит по строкам, ни лимит по столбцам, близость считается выполненной — находки просто должны быть в одном фрагменте. Если задан лимит по строкам, вычисляется абсолютная разница начальных строк основной и вспомогательной находки, и при превышении лимита возвращается ложь. Если задан лимит по столбцам, аналогично сравниваются начальные столбцы, и при превышении возвращается ложь. Иначе возвращается истина.
Что из этого следует на практике
- Сканирование истории и сканирование рабочего дерева — разные режимы с разными командами.
--stagedвидит только то, что попадёт в следующий коммит,git diff— только незастейдженные изменения, аHEAD— всю разницу с последним коммитом. Выбор команды определяет, какие утечки будут найдены. - Предфильтрация по ключевым словам отсекает правила до применения регулярных выражений: правило без ключевых слов применяется всегда, правило с ключевыми словами — только при совпадении хотя бы одного из них. Это влияет на то, какие правила вообще дойдут до поиска.
- Многоуровневое декодирование ограничено пределом глубины и останавливается, когда декодировать больше нечего. Секрет, спрятанный за числом слоёв кодирования больше предела, найден не будет.
- Порог энтропии применяется только при ненулевом значении в правиле; нулевое значение означает, что проверка не применяется. Для hex-строк действует поправка, снижающая энтропию чисто цифровых строк, поэтому длинные числовые строки с большей вероятностью останутся находками.
- Baseline — это механизм отделения известных находок от новых. При повторном сканировании он обновляется, новые секреты добавляются, исчезнувшие удаляются, а помеченныеСекреты в baseline, отмеченные как ложные срабатывания или известные, которые сохраняются при повторном сканировании и не удаляются. сохраняются. Pre-commit hook отказывается работать, если файл baseline не проиндексирован.
- Вспомогательные находки позволяют правилу требовать наличия других правил рядом. Если лимиты по строкам и столбцам не заданы, достаточно нахождения в одном фрагменте; иначе близость проверяется по абсолютной разнице координат.
Где смотреть в коде
- high_entropy_strings.py: calculate_shannon_entropy
- baseline.py: save_to_file
- high_entropy_strings.py: __init__
- pre_commit_hook.py: raise_exception_if_baseline_file_is_unstaged
- baseline.py: create
- detect.go: AddRequiredFindings
- detect.go: Pos
- detect.go: Trace