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

Перейдем к разбору наиболее популярных событий.
Основные события webhook
SCANNING_STOPPED Это событие срабатывает, если вручную остановить сканирование образа.
PULL_ARTIFACT Происходит, когда кто-то скачивает ( pulls ) образ.
SCANNING_COMPLETED Сообщает о завершении сканирования образа и содержит краткую информацию о результатах.
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. В дальнейшем хочу реализовать возможность редактирования настроек контейнера прямо через веб-интерфейс — на мой взгляд, это значительно упрощает работу. Надеюсь, статья оказалась полезной. Следите за обновлениями! Если кому необходимо, вот репозиторий с проектом.
dyadyaSerezha
Увидев, что у первой части нет ни одного комментария, вы написали вторую часть.
Не хватает:
1) в первой части и/или тут кратко объяснить, что такое Harbour, как вы его используете, и дать ссылку на него (далеко не все знают, что это вообще такое).
2) тут, в самом начале, дать ссылку на первую часть.
sergey-akhmineev Автор
Благодарю за конструктивные замечания — учёл их и внёс соответствующие правки в статью. Что касается публикации статей без комментариев: для меня первоочередная цель поделиться полезной информацией с теми, кому она необходима, а не ориентироваться на количество откликов. Я уверен, что ценность материала определяется содержанием, а не активностью в обсуждение.
dyadyaSerezha
А как вы определите качество и актуальность содержания? Пока только два варианта: кол-во плюсов/минусов за статью и комментарии (и их содержимое).
sergey-akhmineev Автор
Вы правы, комментарии и оценки — один из способов обратной связи. Но опыт показывает, что качественные статьи могут долго оставаться “тихими”, пока не начинаются активные обсуждения или не меняются тренды. Я делаю ставку на содержательность материала, а не на мгновенную реакцию аудитории.