webhooks: Add Harbor webhook integration.

This commit is contained in:
chgl 2019-10-20 02:12:00 +02:00 committed by Tim Abbott
parent 37f10509f8
commit bea9e41fbd
10 changed files with 279 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -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'),

View File

View File

@ -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)

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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")

View File

@ -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()