mirror of https://github.com/zulip/zulip.git
webhooks: Add Harbor webhook integration.
This commit is contained in:
parent
37f10509f8
commit
bea9e41fbd
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
|
@ -287,6 +287,7 @@ WEBHOOK_INTEGRATIONS = [
|
|||
WebhookIntegration('gosquared', ['marketing'], display_name='GoSquared'),
|
||||
WebhookIntegration('greenhouse', ['hr'], display_name='Greenhouse'),
|
||||
WebhookIntegration('groove', ['customer-support'], display_name='Groove'),
|
||||
WebhookIntegration('harbor', ['deployment', 'productivity'], display_name='Harbor'),
|
||||
WebhookIntegration('hellosign', ['productivity', 'hr'], display_name='HelloSign'),
|
||||
WebhookIntegration('helloworld', ['misc'], display_name='Hello World'),
|
||||
WebhookIntegration('heroku', ['deployment'], display_name='Heroku'),
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
Get Zulip notifications for your [Harbor](https://goharbor.io/) projects!
|
||||
|
||||
Harbor's webhooks feature is available in version 1.9 and later.
|
||||
|
||||
1. {!create-stream.md!}
|
||||
|
||||
1. {!create-bot-construct-url-indented.md!}
|
||||
|
||||
1. Go to your Harbor **Projects** page. Open a project and click on the **Webhooks** tab.
|
||||
|
||||
1. Set **Endpoint URL** to the URL constructed above and click on **Continue**. We
|
||||
currently support the following events:
|
||||
* pushImage
|
||||
* scanningCompleted
|
||||
|
||||
{!congrats.md!}
|
||||
|
||||
![](/static/images/integrations/harbor/001.png)
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"type": "deleteImage",
|
||||
"occur_at": 1571334977,
|
||||
"event_data": {
|
||||
"resources": [
|
||||
{
|
||||
"tag": "latest",
|
||||
"resource_url": "harbor.example.com/example/test:latest"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"date_created": 1571333978,
|
||||
"name": "test",
|
||||
"namespace": "example",
|
||||
"repo_full_name": "example/test",
|
||||
"repo_type": "private"
|
||||
}
|
||||
},
|
||||
"operator": "admin"
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"type": "pushImage",
|
||||
"occur_at": 1571334632,
|
||||
"event_data": {
|
||||
"resources": [
|
||||
{
|
||||
"digest": "sha256:d72f37f783ed6a8d58ac70b4db052707d1ec9d4ea010ef5ccd84652faf9ed844",
|
||||
"tag": "latest",
|
||||
"resource_url": "harbor.example.com/example/test:latest"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"date_created": 1571334632,
|
||||
"name": "test",
|
||||
"namespace": "example",
|
||||
"repo_full_name": "example/test",
|
||||
"repo_type": "private"
|
||||
}
|
||||
},
|
||||
"operator": "admin"
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"type": "scanningCompleted",
|
||||
"occur_at": 1571341880,
|
||||
"event_data": {
|
||||
"resources": [
|
||||
{
|
||||
"digest": "sha256:d55414535f65d9b0ca180a9a591868c93d72e6f7d1de03a315215220e63e622e",
|
||||
"tag": "latest",
|
||||
"resource_url": "harbor.example.com/example/test:latest",
|
||||
"scan_overview": {
|
||||
"image_digest": "sha256:d55414535f65d9b0ca180a9a591868c93d72e6f7d1de03a315215220e63e622e",
|
||||
"scan_status": "finished",
|
||||
"job_id": 5,
|
||||
"severity": 5,
|
||||
"components": {
|
||||
"total": 168,
|
||||
"summary": [
|
||||
{
|
||||
"severity": 5,
|
||||
"count": 12
|
||||
},
|
||||
{
|
||||
"severity": 4,
|
||||
"count": 16
|
||||
},
|
||||
{
|
||||
"severity": 2,
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"severity": 3,
|
||||
"count": 7
|
||||
},
|
||||
{
|
||||
"severity": 1,
|
||||
"count": 131
|
||||
}
|
||||
]
|
||||
},
|
||||
"details_key": "71ffc66d04c94f01d2990d7ae908f66e6266bc82e45fdffa12181ea521e3a30c",
|
||||
"creation_time": "2019-10-17T19:50:10.728751Z",
|
||||
"update_time": "2019-10-17T19:51:20.370425Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"name": "test",
|
||||
"namespace": "example",
|
||||
"repo_full_name": "example/test",
|
||||
"repo_type": "private"
|
||||
}
|
||||
},
|
||||
"operator": "auto"
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from mock import MagicMock, patch
|
||||
|
||||
from zerver.lib.test_classes import WebhookTestCase
|
||||
|
||||
class HarborHookTests(WebhookTestCase):
|
||||
STREAM_NAME = "harbor"
|
||||
URL_TEMPLATE = u"/api/v1/external/harbor?api_key={api_key}&stream={stream}"
|
||||
|
||||
def test_push_image(self) -> None:
|
||||
expected_topic = "example/test"
|
||||
expected_message = """**admin** pushed image `example/test:latest`"""
|
||||
self.send_and_test_stream_message(
|
||||
"push_image", expected_topic, expected_message)
|
||||
|
||||
@patch('zerver.lib.webhooks.common.check_send_webhook_message')
|
||||
def test_delete_image_ignored(
|
||||
self, check_send_webhook_message_mock: MagicMock) -> None:
|
||||
self.url = self.build_webhook_url()
|
||||
payload = self.get_body('delete_image')
|
||||
result = self.client_post(self.url, payload, content_type="application/json")
|
||||
self.assertFalse(check_send_webhook_message_mock.called)
|
||||
self.assert_json_success(result)
|
||||
|
||||
def test_scanning_completed(self) -> None:
|
||||
expected_topic = "example/test"
|
||||
|
||||
expected_message = """
|
||||
Image scan completed for `example/test:latest`. Vulnerabilities by severity:
|
||||
|
||||
* High: **12**
|
||||
* Medium: **16**
|
||||
* Low: **7**
|
||||
* Unknown: **2**
|
||||
* None: **131**
|
||||
""".strip()
|
||||
|
||||
self.send_and_test_stream_message(
|
||||
"scanning_completed", expected_topic, expected_message)
|
||||
|
||||
def get_body(self, fixture_name: str) -> str:
|
||||
return self.webhook_fixture_data("harbor", fixture_name, file_type="json")
|
|
@ -0,0 +1,123 @@
|
|||
# Webhooks for external integrations.
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from django.db.models import Q
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from zerver.decorator import api_key_only_webhook_view
|
||||
from zerver.lib.request import REQ, has_request_variables
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.webhooks.common import check_send_webhook_message, \
|
||||
UnexpectedWebhookEventType
|
||||
from zerver.models import Realm, UserProfile
|
||||
|
||||
IGNORED_EVENTS = [
|
||||
"downloadChart",
|
||||
"deleteChart",
|
||||
"uploadChart",
|
||||
"pullImage",
|
||||
"deleteImage",
|
||||
"scanningFailed"
|
||||
]
|
||||
|
||||
|
||||
def guess_zulip_user_from_harbor(harbor_username: str, realm: Realm) -> Optional[UserProfile]:
|
||||
try:
|
||||
# Try to find a matching user in Zulip
|
||||
# We search a user's full name, short name,
|
||||
# and beginning of email address
|
||||
user = UserProfile.objects.filter(
|
||||
Q(full_name__iexact=harbor_username) |
|
||||
Q(short_name__iexact=harbor_username) |
|
||||
Q(email__istartswith=harbor_username),
|
||||
is_active=True,
|
||||
realm=realm).order_by("id")[0]
|
||||
return user # nocoverage
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
def handle_push_image_event(payload: Dict[str, Any],
|
||||
user_profile: UserProfile,
|
||||
operator_username: str) -> str:
|
||||
image_name = payload["event_data"]["repository"]["repo_full_name"]
|
||||
image_tag = payload["event_data"]["resources"][0]["tag"]
|
||||
|
||||
return u"{author} pushed image `{image_name}:{image_tag}`".format(
|
||||
author=operator_username,
|
||||
image_name=image_name,
|
||||
image_tag=image_tag
|
||||
)
|
||||
|
||||
|
||||
VULNERABILITY_SEVERITY_NAME_MAP = {
|
||||
1: "None",
|
||||
2: "Unknown",
|
||||
3: "Low",
|
||||
4: "Medium",
|
||||
5: "High",
|
||||
}
|
||||
|
||||
SCANNING_COMPLETED_TEMPLATE = """
|
||||
Image scan completed for `{image_name}:{image_tag}`. Vulnerabilities by severity:
|
||||
|
||||
{scan_results}
|
||||
""".strip()
|
||||
|
||||
|
||||
def handle_scanning_completed_event(payload: Dict[str, Any],
|
||||
user_profile: UserProfile,
|
||||
operator_username: str) -> str:
|
||||
scan_results = u""
|
||||
scan_summaries = payload["event_data"]["resources"][0]["scan_overview"]["components"]["summary"]
|
||||
summaries_sorted = sorted(
|
||||
scan_summaries, key=lambda x: x["severity"], reverse=True)
|
||||
for scan_summary in summaries_sorted:
|
||||
scan_results += u"* {}: **{}**\n".format(
|
||||
VULNERABILITY_SEVERITY_NAME_MAP[scan_summary["severity"]], scan_summary["count"])
|
||||
|
||||
return SCANNING_COMPLETED_TEMPLATE.format(
|
||||
image_name=payload["event_data"]["repository"]["repo_full_name"],
|
||||
image_tag=payload["event_data"]["resources"][0]["tag"],
|
||||
scan_results=scan_results
|
||||
)
|
||||
|
||||
|
||||
EVENT_FUNCTION_MAPPER = {
|
||||
"pushImage": handle_push_image_event,
|
||||
"scanningCompleted": handle_scanning_completed_event,
|
||||
}
|
||||
|
||||
|
||||
@api_key_only_webhook_view("Harbor")
|
||||
@has_request_variables
|
||||
def api_harbor_webhook(request: HttpRequest, user_profile: UserProfile,
|
||||
payload: Dict[str, Any] = REQ(argument_type='body')) -> HttpResponse:
|
||||
|
||||
operator_username = u"**{}**".format(payload["operator"])
|
||||
|
||||
if operator_username != "auto":
|
||||
operator_profile = guess_zulip_user_from_harbor(
|
||||
operator_username, user_profile.realm)
|
||||
|
||||
if operator_profile:
|
||||
operator_username = u"@**{}**".format(operator_profile.full_name) # nocoverage
|
||||
|
||||
event = payload["type"]
|
||||
topic = payload["event_data"]["repository"]["repo_full_name"]
|
||||
|
||||
if event in IGNORED_EVENTS:
|
||||
return json_success()
|
||||
|
||||
content_func = EVENT_FUNCTION_MAPPER.get(event)
|
||||
|
||||
if content_func is None:
|
||||
raise UnexpectedWebhookEventType('Harbor', event)
|
||||
|
||||
content = content_func(payload, user_profile,
|
||||
operator_username) # type: str
|
||||
|
||||
check_send_webhook_message(request, user_profile,
|
||||
topic, content,
|
||||
unquote_url_parameters=True)
|
||||
return json_success()
|
Loading…
Reference in New Issue