Утечка конфиденциальных данных, таких как пароли или RSA-ключи в репозиторий Github, даже в приватный – очень неприятное событие, и хотелось бы иметь представление о том, кто и что пушит в репозитории нашей Github-организации.
Содержание
Утилиты сканирования
Для проверки репозиториев имеется достаточно много утилит:
- Gittyleaks – выглядит неплохо, но последнее обновление репозитория 2 года тому
- Repo Supervisor – есть WebUI, можно потрогать, требует AWS Lambda, классно интегрируется с Github
- Truffle Hog – CLI only, можно потрогать
- Git Hound – плагин для
git
, умеет сканировать только перед коммитом, репозитории в самом Github не умеет - Gitrob – последнее обновление три года тому, увы
- Watchtower – и вроде бы интересное решение, вроде бы даже с WebUI, но на сайте даже прайса нет, так что в пролёте
- GitGuardian – замечательное решение, но стоимость зашкаливает
- gitleaks – CLI only, можно потрогать
Из списка остаются только Truffle Hog и gitleaks, но Truffle Hog как-то не зашёл по своей документации.
Repo Supervisor интересен, потрогаем его попозже.
Из них:
- Gitleaks: просто сканер – указываем URL репозитория, он его сканирует, генерирует отчёт в JSON
- Repo Supervisor: два варианта:
- просто сканер локальных каталогов
- сканирование репозитория по PullRequest/push/etc
Значит, для Gitleaks можно сделать кронджобу в Jenkins или Kubernetes, которая будет брать список репозиториев, сканировать и генерировать отчёты, которые потом можем слать в Slack.
Но тут вопрос куда именно в Slack слать – репозиториев много, команды в проекте разные. Сделать общий канал Слака, в который будут слаться уведомления обо всех репозиториях – девелоперы начнут их игнорить.
Кроме того, Gitleaks умеет в Github Actions – см. тут>>>, но у нас не все команды пользуются ими. Также, умеет в pre-commit hooks.
Планирование
Пока остановимся на варианте с Jenkins, хотя тут тоже возможны варианты:
- триггерить джобу через GitHub Pull Request Builder
- триггерить через GitHub hook trigger for GITScm polling или Poll SCM
- просто запускать по расписанию
Для начала, сделаем вариант с запуском по расписанию, а в будущем будем посмотреть.
Что у нас есть:
- порядка 200 репозитриев
- около 10 команд разработчиков – бекенд, фронтенд, аналитика, iOS и Android приложения, гейминг, девопсы
Что мы можем сделать с Gitleaks:
- для каждой команды создадим задачу в Jenkins
- джоба принимает параметром список репозиториев, которые относятся к команде
- сделаем отдельные слак-каналы на каждую команду, куда заинвайтим нужных людей из конкретной команды
- раз в сутки выполняем проверку, и шлём алерты в нужный канал
Сначала посмотрим руками как оно работает, а потом приступим к автоматизации.
Ручной запуск Gitleaks
Устанавливаем. В Arch Linux – из AUR:
[simterm]
$ yay -S gitleaks
[/simterm]
Github token
Переходим в настройки пользователя, создаём токен:
Разрешаем доступ к repo
:
Запускаем – передём токен, URL репозитория, добавляем --verbose
, результаты сохраняем в файл:
[simterm]
$ gitleaks --access-token=ghp_C6h***3z5 --repo-url=https://github.com/example/BetterBI --verbose --report=analytics-repo.json ... INFO[0036] scan time: 32 seconds 756 milliseconds 672 microseconds INFO[0036] commits scanned: 1893 WARN[0036] leaks found: 111
[/simterm]
Проверяем:
[simterm]
$ less analytics-repo.json
[/simterm]
Например:
... { "line": " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADA***CCaM=\\n-----END PRIVATE KEY-----\\n\",", "lineNumber": 5, "offender": "-----BEGIN PRIVATE KEY-----", "offenderEntropy": -1, "commit": "0f047f0cca3994b3465821ef133dbd3c8b55ee7a", "repo": "BetterBI", "repoURL": "https://github.com/example/BetterBI", "leakURL": "https://github.com/example/BetterBI/blob/0f047f0cca3994b3465821ef133dbd3c8b55ee7a/adslib/roas_automation/example-service-account.json#L5", "rule": "Asymmetric Private Key", "commitMessage": "DT-657 update script for subs and add test\n\nDT-657 create test for check new json (add new subs)", "author": "username", "email": "[email protected]", "file": "adslib/roas_automation/example-service-account.json", "date": "2021-05-11T19:46:46+03:00", "tags": "key, AsymmetricPrivateKey" }, ...
Тут:
line
: что именно обнаруженоoffender
: какое правило затриггирилосьcommit
: ID коммита
Обнаружения выполняются на основе регулярных выражений, описанных в default.go
.
Также, можно создать свой конфиг, и передавать его Gitleaks.
К примеру, приватный ключ был найден по правилу Asymmetric Private Key:
[[rules]] description = "Asymmetric Private Key" regex = '''-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----''' tags = ["key", "AsymmetricPrivateKey"]
Можем сделать отдельные конфиги для каждого репозитория или команды, и в зависимости от того, как будем запускать – передавать их через Kubernetes ConfigMap или просто файлами в Jenkins.
Jenkins job
Pipeline script
Итак, для каждой команды сделаем отдельную Jenkins job, в которую будем передавать список репозиториев команды, которые надо проверить.
Циклы в Groovy
Когда-то делал аналогичное решение, см. Go: проверка списка публичных репозиториев в Github и уведомления в Slack, там тоже в цикле надо было перебрать список, и на Go получилось как-то проще. С Groovy пришлось погуглить.
Создаём новую джобу, назовём её по имени команды, тип Pipeline:
В джобе создаём строковый параметр, в котором перечисляем список репозиториев команды, тут их два:
Пишем скрипт.
Задаём переменную $repos_list
, в которую передаём переменную окружения $TEAM_REPOS
, а затем через split()
по запятой разделяем элементы. Потом с помощью цикла for
выводим их список:
node('master') { def repos_list = "${env.TEAM_REPOS}".split(',') for (repo in repos_list) { println repo } }
Запускаем:
Jenkins Docker plugin
Наш стандартный подход к билдам в Jenkins – через Docker-контейнеры, что бы не засорять хост-систему лишним пакетами.
Добавляем ещё один параметр, тип Password, записываем сюда токен Github:
Используем Jenkins Docker Plugin, с помощью которого создаём контейнер с Gitleaks, которому параметрами передаём токен, репозиторий, и куда записывать репорт, при этом файл репорта будет содержать имя репозитория:
node('master') { def repos_list = "${env.TEAM_REPOS}".split(',') for (repo in repos_list) { stage("Repository ${repo}") { docker.image('zricethezav/gitleaks').inside('--entrypoint=""') { sh "gitleaks --access-token=${GITHUB_TOKEN} --repo-url=https://github.com/example/${repo} --verbose --report=analytics-${repo}-repo.json" } } } }
Тут для каждого репозитория из списка repos_list
создаём отдельный stage, в имени которого тоже используем имя репозитрия – сразу будет видно, в каком именно репозитории возникла проблема.
Запускаем, проверяем:
Ага… И тут возникает проблема – билд останавливается на первом же упавшем stage, так как Gitleaks в репозитории test нашёл секреты, выдал ошибку – код 1, и не проверяет следующие репозитории:
Игнорирование ошибок в Jenkins stage{}
Используем конструкцию try/catch
: вызываем каждый stage в отдельном try
, в случае ошибок – ловим её с catch
, и продолжаем билд:
node('master') { def repos_list = "${env.TEAM_REPOS}".split(',') def build_ok = true for (repo in repos_list) { try { stage("Repository ${repo}") { docker.image('zricethezav/gitleaks').inside('--entrypoint=""') { sh "gitleaks --access-token=${GITHUB_TOKEN} --repo-url=https://github.com/example/${repo} --verbose --report=analytics-${repo}-repo.json" } } } catch(e) { currentBuild.result = 'FAILURE' } } }
Запускаем:
Отлично – теперь следующий стейдж выполняется, несмотря на результат работы предыдущего.
Уведомления в Slack из Jenkins
Следующий шаг – отправка уведомлений в Slack.
Используем плагин Slack Notification. Что бы иметь возможность отправки файлов – потребуется Slack Bot, см. документацию тут>>>.
Создание Slack Bot
Переходим в Slack Apps, создаём новое приложение:
Переходим в Permissions:
Для бота добавляем:
files:write
chat:write
Переходим в OAuth & Permissions, устанавливаем бота в Slack:
Сохраняем полученный токен:
Jenkins credentials
Добавляем токен в Jenkins – переходим в Manage Jenkins > Manage Credentials:
Добавляем новый:
Выбираем тип Secret file, заполняем:
В Slack создаём новый канал:
Инвайтим туда нового бота:
Добавляем в Jenkins-скрипт новую функцию notifySlack()
, и вызываем её в catch{}
:
def notifySlack(String buildStatus = 'STARTED') { // Build status of null means success. buildStatus = buildStatus ?: 'SUCCESS' def color //change for another slack chanel def token = 'gitleaks-slack-bot' if (buildStatus == 'STARTED') { color = '#D4DADF' } else if (buildStatus == 'SUCCESS') { color = '#BDFFC3' } else if (buildStatus == 'UNSTABLE') { color = '#FFFE89' } else { color = '#FF9FA1' } def msg = "${buildStatus}: `${env.JOB_NAME}` #${env.BUILD_NUMBER}:\n${env.BUILD_URL}" slackSend(color: color, message: msg, tokenCredentialId: token, channel: "#devops-alarms-gitleaks-analytics") } node('master') { def repos_list = "${env.TEAM_REPOS}".split(',') for (repo in repos_list) { try { stage("Repository ${repo}") { docker.image('zricethezav/gitleaks').inside('--entrypoint=""') { sh "gitleaks --access-token=${GITHUB_TOKEN} --repo-url=https://github.com/example/${repo} --verbose --report=analytics-${repo}-repo.json" } } } catch(e) { currentBuild.result = 'FAILURE' notifySlack(currentBuild.result) } } }
jenkins.plugins.slack.StandardSlackService postToSlack Response Code: 404
Запускаем билд, и:
12:42:39 ERROR: Slack notification failed. See Jenkins logs for details.
Смотрим лог в https://<JENKINS_URL>/log/all:
Переходим в Manage Jenkins > Configure System, находим настройки Slack-плагина, ставим Custom slack app bot user:
Не обращаем тут внимания на Credential, это дефолтные, мы их переопределяем в скрипте.
В Advanced убираем Override url:
Пробуем ещё раз – и всё работает:
Загрузка файла в Slack
Теперь надо добавить отправку репорта в Slack.
Используем slackUploadFile()
, добавляем:
def notifySlack(String buildStatus = 'STARTED', reportFile) { // Build status of null means success. buildStatus = buildStatus ?: 'SUCCESS' def color //change for another slack chanel def token = 'gitleaks-slack-bot' if (buildStatus == 'STARTED') { color = '#D4DADF' } else if (buildStatus == 'SUCCESS') { color = '#BDFFC3' } else if (buildStatus == 'UNSTABLE') { color = '#FFFE89' } else { color = '#FF9FA1' } def msg = "${buildStatus}: `${env.JOB_NAME}` #${env.BUILD_NUMBER}:\n${env.BUILD_URL}" slackSend(color: color, message: msg, tokenCredentialId: token, channel: "#devops-alarms-gitleaks-analytics") slackUploadFile(credentialId: token, channel: "#devops-alarms-gitleaks-analytics", filePath: "${reportFile}") } node('master') { def repos_list = "${env.TEAM_REPOS}".split(',') for (repo in repos_list) { try { stage("Repository ${repo}") { docker.image('zricethezav/gitleaks').inside('--entrypoint=""') { sh "gitleaks --access-token=${GITHUB_TOKEN} --repo-url=https://github.com/example/${repo} --verbose --report=analytics-${repo}-repo.json" } } } catch(e) { currentBuild.result = 'FAILURE' notifySlack(currentBuild.result, "analytics-${repo}-repo.json") } } }
Канал потом вынесем в параметры.
Тут в функцию notifySlack()
добавляем второй параметр – reportFile
, а затем в его вызове notifySlack()
передаём вторым аргументом имя файла с репортом.
Запускаем, проверяем Slack:
Осталось запускать задачу по расписанию:
Настройки Gitleaks
Коммиты для проверки
Сейчас Gitleasks выполняет полную проверку репозитория – все коммиты, всю историю.
Если мы будем запускать его каждый день – то и в репорте будем получать все старые проблемные коммиты.
Как вариант – сделать две джобы: в одной выполнять полное сканирование, и запускать раз в неделю, а во второй – проверять коммиты только за последние сутки.
Т.е. запускаем джобу каждый день в 12.00, что бы все разработчики были на месте, и видели сообщение в Slack, а в джобе выполняем проверку всех коммитов за предыдущий день.
Для этого у Gitleaks есть опция --commit-since
– добавляем в наш скрипт создание переменной yesterday
с вчерашней датой с помощью вызова метода previous()
класса Date()
, а потом дату подставляем в --commit-since
:
... node('master') { def repos_list = "${env.TEAM_REPOS}".split(',') def yesterday = new Date().format( 'yyyy-MM-dd' ).previous() println yesterday for (repo in repos_list) { try { stage("Repository ${repo}") { docker.image('zricethezav/gitleaks').inside('--entrypoint=""') { sh "gitleaks --access-token=${GITHUB_TOKEN} --repo-url=https://github.com/example/${repo} --verbose --report=analytics-${repo}-repo.json --commit-since=${yesterday}" } } } catch(e) { currentBuild.result = 'FAILURE' notifySlack(currentBuild.result, "analytics-${repo}-repo.json") } } }
Файл правил Gitleaks
И второй момент – добавить файл со своими настройками правил для проверки.
Используем опцию --repo-config-path
, и в каждом репозитории создадим файл правил.
Включаем туда часть дефолтных, и хочется добавить поиск паролей:
... [[rules]] description = "Plaintext password" regex = '''(?i)pass*[a-z]{5}[:|=]? +["|'](.*)["|']''' tags = ["password", "PlainTextPassword"] [allowlist] description = "Allowlisted files" files = ['''^\.?gitleaks.config$''']
В регулярке (?i)pass*[a-z]{5}[:|=]? +["|'](.*)["|']
ищем строки, начинающиеся с pass, после которой могут быть “:” или “=”, за которыми может быть пробел, потом одинарная или двойная кавычка, затем любой текст, и затем снова одинарная или двойная кавычка.
Вроде должно срабатывать на варианты создания переменных с паролями в репозиториях. По крайней мере в этой джобе, у аналитиков, в основном Python, так что подойдёт.
Ага, а вот вариант с именем переменной типа db_password не продумал, потом надо будет обновить.
Хотя нет – вроде тоже сработает:
Сохраняем его в репозиторий как .github/gitleaks.config
, а в джобе добавляем его использование:
... docker.image('zricethezav/gitleaks').inside('--entrypoint=""') { sh "gitleaks --access-token=${GITHUB_TOKEN} --repo-url=https://github.com/example/${repo} --verbose --report=analytics-${repo}-repo.json --commit-since=${yesterday} --repo-config-path=.github/gitleaks.config" } ...
В целом, на этом всё.