Документация / CI/CD и API
Security checker
Security checker - консольная утилита asoc-gate в образе Docker для джобы CI. Она запускает через Core скан коммита в Radar, ждёт разбора отчётов и сверяет находки с порогом. Результат - код выхода: 0 пропускает пайплайн, 1 и 2 его роняют. Вместе с правилом ветки "слияние только после успешного пайплайна" это закрывает слияние кода с неразобранными уязвимостями.
Как работает
- Определяет репозиторий, ветку и коммит из переменных CI или из каталога
.git. - Находит в Core ассет-репозиторий с тем же путём
group/project. Не нашёл - ошибка, код 2. - Запускает аудит продукта этого ассета (
POST /api/v1/ci/pipelines/run/). Остальные ассеты продукта исключаются, сканируется только этот репозиторий. Ветка передаётся как<ветка>:<sha>, если включён пин коммита. - Находит созданный аудит и ждёт пайплайн Radar. В лог печатает смену статусов джоб. Пайплайн "Остановлен" или "Тайм-аут" - код 2.
- Ждёт, пока Core разберёт отчёты всех джоб-сканеров.
- Проверяет, что скан дал хоть какой-то результат. Ноль новых, известных и дублей по всем отчётам - код 2.
- Считает находки по режиму и порогу, печатает таблицы, пишет отчёт JUnit.
- Завершается с кодом 0, 1 или 2.
Сканирует Radar, а не раннер CI: Code Downloader клонирует ветку из системы контроля версий по SSH URL ассета, сканеры берутся из последовательности ассета.
Требования
| Что | Требование |
|---|---|
| Лицензия Core | с доступом к Radar, см. Лицензирование |
| Radar | подключён к Core, в последовательности есть Code Downloader из каталога. Он умеет переключаться на коммит из <ветка>:<sha> |
| Ассет | репозиторий заведён в продукте Core ассетом типа "Репозиторий". Путь из SSH URL (git@gitlab.example.ru:team/payments.git → team/payments) совпадает с путём проекта в CI без учёта регистра |
| Пользователь Core | сервисный пользователь с API-токеном, см. ниже |
| CI | GitLab CI, раннер с исполнителем Docker или Kubernetes, который может скачать образ Security checker |
| Сеть | раннер → Core по HTTPS, путь /api/v1/ |
Роль сервисного пользователя
| Право роли | Уровень | Зачем |
|---|---|---|
| "Продукты" | "Просмотр" | поиск ассета, чтение аудита |
| "Уязвимости" | "Изменение" | чтение находок. Radar отправляет отчёты токеном этого пользователя, а приём отчёта требует "Изменение" |
| "Radar" | "Изменение" | запуск аудита, чтение пайплайна |
| "API токен" | "Просмотр" | получить свой токен в профиле |
| Остальные | "Нет" | - |
Область видимости роли - продукты, чьи репозитории проверяет Security checker. Подробно - Роли и права, Пользователи.
Получение
Образ Security checker: alpine 3.21 и статический бинарь /usr/local/bin/asoc-gate, архитектуры amd64 и arm64. Тег образа - версия Security checker. Команда SecurityFlow размещает образ в реестре площадки и сообщает его путь. В примерах ниже - registry.example.ru/security-checker:<версия>.
Версия печатается первой строкой вывода: asoc-gate <версия>.
Аутентификация
Security checker обращается к Core с заголовком Authorization: Token <токен>.
- Создайте сервисного пользователя и роль с правами из таблицы выше.
- Войдите под ним и скопируйте токен: "Профиль" → "Токен аутентификации API".
- Сохраните токен в CI как маскированную и защищённую переменную на уровне группы или экземпляра GitLab.
Передать токен можно двумя способами:
ASOC_URL=https://core.example.ruиASOC_TOKEN=<токен>;- одной переменной
ASOC_DSN=https://<токен>@core.example.ru- токен в части пользователя URL.
Если задан ASOC_DSN, переменные ASOC_URL и ASOC_TOKEN не читаются.
Переменные окружения
| Имя | Обязательная | По умолчанию | Описание |
|---|---|---|---|
ASOC_URL |
да, если нет ASOC_DSN |
- | адрес Core: https://core.example.ru |
ASOC_TOKEN |
да, если нет ASOC_DSN |
- | API-токен Core |
ASOC_DSN |
да, если нет ASOC_URL |
- | адрес и токен одной строкой: https://<токен>@core.example.ru[:порт] |
ASOC_FAIL_ON |
нет | critical,high |
блокирующие уровни через запятую: info, low, medium, high, critical. none - только отчёт |
ASOC_GATE_MODE |
нет | open |
режим: open или new. Другое значение - ошибка, код 2 |
ASOC_PIN_COMMIT |
нет | 1 |
1 - сканировать коммит джобы, 0 - текущее состояние ветки |
ASOC_TIMEOUT |
нет | 30m |
общий предел от старта: поиск аудита, ожидание пайплайна и разбора. Формат: 45m, 1h30m |
ASOC_POLL |
нет | 10s |
период опроса Core |
ASOC_SETTLE |
нет | 30s |
сколько ждать, если число разобранных отчётов перестало меняться |
ASOC_JUNIT |
нет | security-gate.xml |
путь отчёта JUnit. none - не писать |
ASOC_JOB_SEQUENCE |
нет | - | id последовательности в Core. Не задан - последовательность ассета или по умолчанию |
ASOC_REPO |
нет | из CI | путь репозитория group/project |
ASOC_BRANCH |
нет | из CI | ветка |
ASOC_ALLOW_EMPTY |
нет | - | 1 - скан без единого результата не считать ошибкой |
ASOC_INSECURE |
нет | - | 1 - не проверять сертификат Core |
ASOC_WEB_URL |
нет | "Внешний URL приложения" из Core | адрес Core для ссылок в выводе |
ASOC_MAX_PAGES |
нет | 2000 |
предел страниц при обходе находок, по 24 на страницу |
ASOC_LIST_LIMIT |
нет | 100 |
строк в таблице блокирующих находок |
ASOC_LINK_MODE |
нет | url |
short - ссылка под словом "link" (OSC 8). Такие ссылки понимает не каждый просмотрщик логов |
ASOC_COLOR |
нет | авто | 1 или 0. Авто - цвет включён в GitLab CI и GitHub Actions |
Числа в ASOC_MAX_PAGES и ASOC_LIST_LIMIT - положительные. Ошибка в любой переменной - сообщение asoc-gate: ... в stderr и код 2 без отчёта JUnit.
ASOC_INSECURE
ASOC_INSECURE=1 отключает проверку сертификата Core, и токен уходит по непроверенному соединению. Если Core работает на сертификате корпоративного центра сертификации, передайте файл с цепочкой в переменной SSL_CERT_FILE вместо отключения проверки.
Режимы и порог
Порог - уровни из ASOC_FAIL_ON. Неразобранными считаются статусы "Не обработано", "В работе" и "Назначено". Статусы "Решено", "Отклонено", "Временно принят риск" и "Постоянно принят риск" гейт не держат.
| Режим | Когда код 1 | Для чего |
|---|---|---|
open |
в ветке есть неразобранные находки блокирующих уровней - новые или старые | держать весь долг ветки |
new |
этот скан добавил находки блокирующих уровней | постепенное внедрение: ловить новые уязвимости, не трогая старый долг |
ASOC_FAIL_ON=none |
никогда, только отчёт | пилот и сбор статистики |
Как гейт считает находки:
new- находки репозитория, появившиеся в Core после старта гейта, с веткой джобы.open- неразобранные находки репозитория, у которых в Core указана ровно ветка джобы.- При настройках дедупликации по умолчанию (критерий "Та же ветка" выключен) находка, совпавшая с уже известной в другой ветке, повторно не импортируется и в подсчёт ветки джобы не попадает. Поэтому долг целевой ветки, например
main, не блокирует merge request в неё. См. Дедупликация.
Разобрали находку в Core - решили, отклонили или приняли риск - и следующий запуск гейта её не учитывает. Отдельно снимать блокировку не нужно.
Рекомендуемые сочетания
Режим выбирайте вместе с пином коммита. С пином Security checker передаёт в Core ветку в виде <ветка>:<sha>, и находки скана получают эту ветку. Режим new считает находки этого скана и работает с пином. Режим open считает находки, у которых указана ровно ветка джобы, поэтому сочетайте его с ASOC_PIN_COMMIT=0.
| Цель | Настройки |
|---|---|
| Не пропускать новые уязвимости коммита | ASOC_GATE_MODE=new, пин включён (по умолчанию) |
| Держать весь неразобранный долг ветки | ASOC_GATE_MODE=open, ASOC_PIN_COMMIT=0 |
| Только отчёт | ASOC_FAIL_ON=none |
Пин коммита
При ASOC_PIN_COMMIT=1 Security checker передаёт ветку как <ветка>:<sha>, а Code Downloader после клонирования переключается на этот коммит. Вердикт относится к коммиту джобы, даже если в ветку уже запушили новые.
Пин не применяется, если:
- коммит не определился - предупреждение
commit pin: no commit sha detected; <ветка>:<sha>длиннее 100 символов - предупреждениеbranch name too long.
В обоих случаях сканируется текущее состояние ветки.
Если Code Downloader не может получить коммит джобы, он завершается с сообщением commit <sha> is not reachable from branch <ветка>. Дальше зависит от флага "Разрешить падение" у Code Downloader:
- выключен (рекомендуется, см. Типы джоб) - пайплайн останавливается, Security checker возвращает код 2;
- включён - сканеры проверяют текущее состояние ветки, а не коммит джобы.
Для пайплайнов, где коммит джобы не принадлежит ветке, задайте ASOC_PIN_COMMIT=0.
Коды выхода
| Код | Значение | Типичная причина |
|---|---|---|
| 0 | SECURITY GATE: PASS |
блокирующих находок нет |
| 1 | SECURITY GATE: FAIL - <число> <уровень>, ... |
найдены блокирующие находки |
| 2 | ошибка, вердикта нет | неверная переменная; ассет не найден; ошибка HTTP при запуске; таймаут; пайплайн "Остановлен" или "Тайм-аут"; пустой скан; Core насчитал больше новых находок, чем видно по ветке |
Любая ошибка - стоп: пайплайн не станет зелёным, если проверить не удалось.
Таймауты
ASOC_TIMEOUT(по умолчанию 30 минут) отсчитывается от старта и покрывает поиск аудита, ожидание пайплайна Radar и ожидание разбора.- Один запрос к Core ждёт ответа до 60 секунд. Временные ошибки HTTP в циклах ожидания пропускаются до истечения
ASOC_TIMEOUT. - Таймаут джобы в GitLab задайте больше
ASOC_TIMEOUT, иначе GitLab оборвёт джобу без отчёта. - Время скана - сумма времени джоб последовательности ассета плюс ожидание свободного воркера Radar.
Вывод
Лог джобы
Пример (сокращён):
asoc-gate 0.3.0 repo=team/payments branch=feature/login detected-from=gitlab
commit 9f1c2e7d4b5a6c8e0f1a2b3c4d5e6f7a8b9c0d1e
scan ref feature/login:9f1c2e7d4b5a6c8e0f1a2b3c4d5e6f7a8b9c0d1e (commit pinned)
10:02:11 asset #15 in product "payments" (2 other assets excluded), last audit #120, known findings up to #4810
10:02:23 audit #121 started, waiting for the auditor pipeline
10:02:23 Code Downloader IN_PROGRESS
10:03:40 Gitleaks Scan FINISHED
10:05:02 Semgrep FINISHED
10:05:14 pipeline #87 finished, waiting for the import of 2 reports
┌─────────────────────┬───────┬───────────┬────────┐
│ scanner │ новых │ известных │ дублей │
├─────────────────────┼───────┼───────────┼────────┤
│ Gitleaks Scan │ 0 │ 3 │ 0 │
│ Semgrep JSON Report │ 1 │ 12 │ 0 │
└─────────────────────┴───────┴───────────┴────────┘
Security Gate режим: new
┌──────────┬───────┬───────────┐
│ severity │ новых │ блокирует │
├──────────┼───────┼───────────┤
│ CRITICAL │ 0 │ да │
│ HIGH │ 1 │ да │
│ MEDIUM │ 0 │ нет │
│ LOW │ 0 │ нет │
│ INFO │ 0 │ нет │
└──────────┴───────┴───────────┘
Гейт не пропускает, находок: 1
┌──────────┬─────────────────────┬─────────────────┬───────────────────────────────────────┐
│ severity │ scanner │ location │ ссылка │
├──────────┼─────────────────────┼─────────────────┼───────────────────────────────────────┤
│ HIGH │ Semgrep JSON Report │ app/views.py:42 │ https://core.example.ru/findings/4811 │
└──────────┴─────────────────────┴─────────────────┴───────────────────────────────────────┘
audit #121: 1 new findings (critical 0, high 1, medium 0, low 0, info 0); fail on: critical,high
https://core.example.ru/products/7 (3m4s)
SECURITY GATE: FAIL - 1 high
- Таблица по сканерам: "новых" - добавлено этим аудитом, "известных" - совпало с уже известными, "дублей" - повторы внутри отчёта.
- В режиме
openв таблице уровней есть колонка "не разобрано", а список блокирующих называется "Не разобрано, держит мерж: N". - Medium, low и info попадают в список, только если входят в
ASOC_FAIL_ON. Иначе они видны числом в таблице уровней. - Предупреждение
warning: N of M scanner reports were imported- одна из джоб-сканеров не дала отчёт. Проверьте её лог в Radar.
Отчёт JUnit
Файл ASOC_JUNIT (по умолчанию security-gate.xml), набор тестов asoc security gate:
| Ситуация | Тест-кейсы |
|---|---|
| ошибка (код 2) | один упавший кейс security gate с текстом ошибки |
| новых находок нет | один кейс no new findings со сводкой |
| есть новые находки | кейс на каждую новую находку: [HIGH] <название> (<файл:строка>). Упавшие - только блокирующих уровней |
В режиме open отчёт JUnit содержит только новые находки этого скана. Старые неразобранные находки, из-за которых упала джоба, видны в логе джобы.
GitLab CI
Минимальная джоба. ASOC_URL и ASOC_TOKEN заданы переменными CI/CD группы. Режим new задан явно: он работает с пином коммита, который включён по умолчанию, см. Рекомендуемые сочетания.
security-check:
stage: test
image: registry.example.ru/security-checker:<версия>
variables:
ASOC_GATE_MODE: new
script: [asoc-gate]
artifacts:
when: always
reports:
junit: security-gate.xml
Рабочий шаблон: проверка merge request и ветки по умолчанию, один запуск на ветку, таймаут с запасом:
security-check:
stage: test
image: registry.example.ru/security-checker:<версия>
variables:
ASOC_URL: https://core.example.ru
ASOC_GATE_MODE: new
ASOC_FAIL_ON: critical,high
ASOC_TIMEOUT: 40m
script: [asoc-gate]
timeout: 50m
resource_group: security-check-$CI_COMMIT_REF_SLUG
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
artifacts:
when: always
reports:
junit: security-gate.xml
resource_groupвыполняет запуски на одной ветке по очереди. Так каждый запуск находит свой аудит.- В пайплайне merge request ветка берётся из
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, иначе изCI_COMMIT_REF_NAME; коммит - изCI_COMMIT_SHA.
Неблокирующий запуск для пилота - вердикт в логе и JUnit, пайплайн не падает на находках:
security-check:
stage: test
image: registry.example.ru/security-checker:<версия>
variables:
ASOC_FAIL_ON: none
script: [asoc-gate]
artifacts:
when: always
reports:
junit: security-gate.xml
Ошибки (код 2) роняют джобу и в этом режиме.
Блокировка слияния
Код выхода сам по себе слияние не запрещает. Включите в GitLab:
- В настройках проекта или группы - проверку слияния "Pipelines must succeed" (Settings → Merge requests → Merge checks).
- Для
mainиmaster- защиту ветки без прямого push (Settings → Repository → Protected branches). - Убедитесь, что джоба Security checker попадает в пайплайн merge request: правило
rulesвыше.
Теперь merge request с упавшей джобой нельзя слить, пока находки не разобраны в Core или не исправлены.
Security checker и Quality gate дополняют друг друга: Security checker держит слияние из CI, quality gate пишет статус коммита и комментарии в pull request.
Параллельные запуски и нагрузка
- Запуски по разным веткам или разным коммитам одного репозитория не мешают друг другу: свой аудит Security checker находит по ветке
<ветка>:<sha>в записи аудита. - Запуски на одной ветке выполняйте по очереди: задайте
resource_group. - Каждый запуск - полный скан репозитория в Radar. Число одновременных сканов ограничено числом процессов исполнителя пайплайнов Radar, остальные ждут в очереди, см. Как работает Radar.
Другие CI-системы и локальный запуск
Шаблоны на этой странице - для GitLab CI. В GitHub Actions Security checker берёт репозиторий, ветку и коммит из GITHUB_REPOSITORY, GITHUB_HEAD_REF или GITHUB_REF_NAME, GITHUB_SHA, в Jenkins с плагином Git - из GIT_URL, GIT_BRANCH без префикса origin/, GIT_COMMIT. Адрес и токен Core передавайте через ASOC_URL и ASOC_TOKEN или ASOC_DSN.
Вне CI Security checker читает .git текущего каталога: URL remote origin, текущую ветку и её коммит.
docker run --rm -v "$PWD":/src -w /src \
-e ASOC_URL=https://core.example.ru \
-e ASOC_TOKEN=<токен> \
registry.example.ru/security-checker:<версия>
Radar клонирует ветку из системы контроля версий, поэтому незапушенные изменения не сканируются. С пином коммита локальный коммит должен быть запушен.
Диагностика
| Сообщение | Причина | Что сделать |
|---|---|---|
asoc-gate: set ASOC_DSN=https://host (or ASOC_URL) |
не задан адрес Core | задайте ASOC_URL или ASOC_DSN |
asoc-gate: нет токена портала ... |
не задан токен | задайте ASOC_TOKEN или токен в ASOC_DSN |
cannot detect repository: set ASOC_REPO=group/project |
джоба не в CI и нет .git |
задайте ASOC_REPO |
cannot detect branch: set ASOC_BRANCH |
не определилась ветка, например detached HEAD вне CI | задайте ASOC_BRANCH |
ERROR: repository "<путь>" is not registered in ASOC ... |
нет ассета-репозитория с таким путём. В скобках - похожие ассеты | заведите ассет в продукте или поправьте SSH URL ассета |
... -> HTTP 401 |
токен неверный или ротирован | обновите токен в переменных CI |
... -> HTTP 403 |
у роли нет нужного права или лицензия без доступа к Radar | проверьте роль по таблице выше и тип лицензии |
timeout: auditor pipeline for audit #N did not finish in 30m0s |
Radar не получил задачу или не успел. Ошибка отправки задачи в Radar видна в журнале воркера Core | проверьте "Статус подключения" в Core ("Radar" → "Настройки"), очередь на экране "Пайплайны" Radar, увеличьте ASOC_TIMEOUT |
ERROR: auditor pipeline #N ended with status KILLED |
джоба с выключенным "Разрешить падение" упала или пайплайн остановлен вручную | откройте лог пайплайна в Radar |
ERROR: auditor pipeline #N ended with status TIMED_OUT |
сработал таймаут пайплайна или джобы Radar | увеличьте таймауты в Radar |
audit #N produced no results at all ... |
сканеры не дали ни одной находки, даже известной. Чаще всего Code Downloader не склонировал код | проверьте лог Code Downloader: SSH-ключ, доступ к репозиторию, коммит. Для действительно пустого репозитория - ASOC_ALLOW_EMPTY=1 |
audit #N added X findings but only Y are visible for branch ... |
новые находки аудита не видны по ветке джобы | повторите запуск; не запускайте гейт параллельно на одной ветке |
x509: certificate signed by unknown authority |
сертификат Core выпущен корпоративным центром сертификации | передайте цепочку в SSL_CERT_FILE |