Утечка конфиденциальных данных, таких как пароли или 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:writechat: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"
}
...
В целом, на этом всё.




























