Webhook у Harbor или как я оповещения о пушах docker images нашей команды делал часть — 2

от автора

В прошлой статье мы разобрали пуши в Harbor. Сегодня же я расскажу о других методах, с которыми работает его webhook. Кстати, все эти методы приведены на изображении ниже:

События для webhook

События для webhook

Перейдем к разбору наиболее популярных событий.

Основные события webhook

  1. SCANNING_STOPPED Это событие срабатывает, если вручную остановить сканирование образа.

  2. PULL_ARTIFACT Происходит, когда кто-то скачивает ( pulls ) образ.

  3. SCANNING_COMPLETED Сообщает о завершении сканирования образа и содержит краткую информацию о результатах.

  4. DELETE_ARTIFACT Срабатывает при удалении образа.

Настройка конфигурации

Для удобства мы вынесем некоторые параметры в отдельный конфиг, например, список символов для экранирования и типы уведомлений, чтобы не сбивать лишней информацией там, где она не нужна.

# conf.toml  [telegram] bot_token        = "YOUR_BOT_TOKEN" chat_id          = "-1001234567890" message_thread_id = "1234"      # или оставьте пустым/удалите, если не нужно  [markdown] # Список символов, которые будем экранировать escape_chars = ["\\", "_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", "!"]  [events] # Включаем/выключаем оповещения по типам событий PUSH_ARTIFACT        = true PULL_ARTIFACT        = true DELETE_ARTIFACT      = true SCANNING_STOPPED     = true SCANNING_COMPLETED   = true

Разбор каждого события

Для наглядности я подготовил общий JSON с описанием событий:

[   {     "type": "SCANNING_STOPPED",     "occur_at": 1746597835,     "operator": "auto",     "event_data": {       "resources": [         {           "digest": "sha256:280b85260a6613bf08bd1234512341234123412341234123412gvsdfgdf",           "tag": "1.1.1",           "resource_url": "127.0.0.1/repo/repo_image:1.1.1",           "scan_overview": {             "application/vnd.security.vulnerability.report; version=1.1": {               "report_id": "ec84315d-3f08-42cc-bdc7-491953540cda",               "scan_status": "Stopped",               "severity": "",               "duration": 13,               "summary": null,               "start_time": "2025-05-07T06:03:42Z",               "end_time": "2025-05-07T06:03:55Z",               "complete_percent": 0             }           }         }       ],       "repository": {         "name": "repo_image",         "namespace": "repo",         "repo_full_name": "repo/repo_image",         "repo_type": "public"       }     }   },   {     "type": "PULL_ARTIFACT",     "occur_at": 1746597854,     "operator": "robot$repo+Trivy-16c5e31b-2b09-11f0-87be-0242ac180003",     "event_data": {       "resources": [         {           "digest": "sha256:280b85260a6613bf08bd1234512341234123412341234123412gvsdfgdf",           "tag": "sha256:280b85260a6613bf08bd1234512341234123412341234123412gvsdfgdf",           "resource_url": "127.0.0.1/repo/repo_image:@sha256:280b85260a6613bf08bd1234512341234123412341234123412gvsdfgdf"         }       ],       "repository": {         "date_created": 1733465413,         "name": "repo_image",         "namespace": "repo",         "repo_full_name": "repo/repo_image",         "repo_type": "public"       }     }   },   {     "type": "SCANNING_COMPLETED",     "occur_at": 1746597860,     "operator": "auto",     "event_data": {       "resources": [         {           "digest": "sha256:280b85260a6613bf08bd1234512341234123412341234123412gvsdfgdf",           "tag": "1.1.1",           "resource_url": "127.0.0.1/repo/repo_image:1.1.1",           "scan_overview": {             "application/vnd.security.vulnerability.report; version=1.1": {               "report_id": "39ba459d-3642-4de6-aaac-865b94cdb006",               "scan_status": "Success",               "severity": "Critical",               "duration": 12,               "summary": {                 "total": 1005,                 "fixable": 7,                 "summary": {                   "Critical": 2,                   "High": 73,                   "Low": 422,                   "Medium": 506,                   "Unknown": 2                 }               },               "start_time": "2025-05-07T06:04:08Z",               "end_time": "2025-05-07T06:04:20Z",               "scanner": {                 "name": "Trivy",                 "vendor": "Aqua Security",                 "version": "v0.45.0"               },               "complete_percent": 100             }           }         }       ],       "repository": {         "name": "repo_image",         "namespace": "repo",         "repo_full_name": "repo/repo_image",         "repo_type": "public"       }     }   },   {     "type": "DELETE_ARTIFACT",     "occur_at": 1746597914,     "operator": "Admin",     "event_data": {       "resources": [         {           "digest": "sha256:280b85260a6613bf08bd1234512341234123412341234123412gvsdfgdf123123123123123123",           "tag": "1.0.0",           "resource_url": "127.0.0.1/repo/repo_image:1.0.0"         }       ],       "repository": {         "date_created": 1733465413,         "name": "repo_image",         "namespace": "repo",         "repo_full_name": "repo/repo_image",         "repo_type": "public"       }     }   } ]

PULL_ARTIFACT

PULL_ARTIFACT — это оповещение, которое можно использовать для отслеживания, когда кто-то скачивает образ. Важное замечание — чаще всего это инициирует автоматическое сканирование или другие процессы. В моем случае я решил оставить его как информативное событие, чтобы знать, когда образ попал в использование.

    elif event_type == "PULL_ARTIFACT":         for r in resources:             tag = escape_markdown(r.get("tag", "—"))             url = r.get("resource_url", "—")             msg = (                 "📥 *Pull в Harbor*\n"                 f"*Репозиторий:* {repo_name}\n"                 f"*Тег:* {tag}\n"                 f"*URL:* `{url}`\n"                 f"*Пользователь:* {operator}"             )             messages.append(msg)

DELETE_ARTIFACT

DELETE_ARTIFACT — это событие помогает отслеживать, кто и когда удаляет образы, особенно актуально, если у вас несколько Harbor-репозиториев. Вот пример сообщения:

    elif event_type == "DELETE_ARTIFACT":         for r in resources:             tag = escape_markdown(r.get("tag", "—"))             url = r.get("resource_url", "—")             msg = (                 "🗑️ *Удаление образа в Harbor*\n"                 f"*Репозиторий:* {repo_name}\n"                 f"*Тег:* {tag}\n"                 f"*URL:* `{url}`\n"                 f"*Пользователь:* {operator}"             )             messages.append(msg)

Хотя смайлики — не обязательная часть сообщений, я считаю, что они привлекают больше внимания и делают оповещения более дружелюбными.

SCANNING_STOPPED

SCANNING_STOPPED — это относительно редкое событие, срабатывающее при ручной остановке процесса сканирования. В моем опыте — очень редко, но бывает полезным для ситуаций, когда нужно срочно прервать проверку образа.

    elif event_type == "SCANNING_STOPPED":         for r in resources:             tag     = escape_markdown(r.get("tag", "—"))             url     = r.get("resource_url", "—")             overview = r.get("scan_overview", {})             detail  = next(iter(overview.values()), {})             status  = escape_markdown(detail.get("scan_status", "—"))             duration = detail.get("duration", 0)             msg = (                 "🔍 *Сканирование остановлено*\n"                 f"*Репозиторий:* {repo_name}\n"                 f"*Тег:* {tag}\n"                 f"*URL:* `{url}`\n"                 f"*Статус скана:* {status}\n"                 f"*Продолжительность:* {duration}s\n"                 f"*Пользователь:* {operator}"             )             messages.append(msg)

SCANNING_COMPLETED

SCANNING_COMPLETED — на мой взгляд, одно из самых важных событий. После завершения сканирования Harbor сообщает о найденных уязвимостях и предлагает варианты устранения (например, обновление пакетов). Это очень удобно для быстрого анализа состояния образа и сравнения с предыдущими версиями.

    elif event_type == "SCANNING_COMPLETED":         for r in resources:             tag      = escape_markdown(r.get("tag", "—"))             url      = r.get("resource_url", "—")             overview = r.get("scan_overview", {})             detail   = next(iter(overview.values()), {})             status   = escape_markdown(detail.get("scan_status", "—"))             duration = detail.get("duration", 0)             severity = escape_markdown(detail.get("severity", "—"))             summary  = detail.get("summary", {}) or {}             total    = summary.get("total", 0)             fixable  = summary.get("fixable", 0)             counts   = summary.get("summary", {})              # Формируем разбивку по уровням критичности             counts_lines = ""             for lvl, cnt in counts.items():                 lvl_esc = escape_markdown(lvl)                 counts_lines += f"*{lvl_esc}:* {cnt}\n"              scanner = detail.get("scanner", {})             sc_name = escape_markdown(scanner.get("name", "—"))             sc_ver  = escape_markdown(scanner.get("version", "—"))              msg = (                 "🔍 *Сканирование завершено*\n"                 f"*Репозиторий:* {repo_name}\n"                 f"*Тег:* {tag}\n"                 f"*URL:* `{url}`\n"                 f"*Статус скана:* {status}\n"                 f"*Продолжительность:* {duration}s\n"                 f"*Критичность (макс):* {severity}\n"                 f"*Всего уязвимостей:* {total}\n"                 f"*Исправимо:* {fixable}\n"                 f"{counts_lines}"                 f"*Сканер:* {sc_name} {sc_ver}\n"                 f"*Пользователь:* {operator}"             )             messages.append(msg)

Это уведомление позволяет быстро оценить безопасность образа и принять меры.

Заключение и планы

На данный момент я собираю JSON-описания по другим событиям webhook. В дальнейшем хочу реализовать возможность редактирования настроек контейнера прямо через веб-интерфейс — на мой взгляд, это значительно упрощает работу. Надеюсь, статья оказалась полезной. Следите за обновлениями! Если кому необходимо, вот репозиторий с проектом.


ссылка на оригинал статьи https://habr.com/ru/articles/908460/