mirror of https://github.com/zulip/zulip.git
integrations: Add Thinkst Canary integration.
This commit adds an integration for Thinkst Canaries - physical, VM and cloud-based canaries for detecting attackers to a network. Thinkst Canaries can send webhook alerts when canaries have been tripped, and this integration will post Zulip messages when these webhooks are received. Signed-off-by: David Wood <david@davidtw.co>
This commit is contained in:
parent
43bf6a0b1a
commit
f0f42f7a94
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
|
@ -389,6 +389,7 @@ WEBHOOK_INTEGRATIONS: List[WebhookIntegration] = [
|
|||
WebhookIntegration('stripe', ['financial'], display_name='Stripe'),
|
||||
WebhookIntegration('taiga', ['project-management']),
|
||||
WebhookIntegration('teamcity', ['continuous-integration']),
|
||||
WebhookIntegration('thinkst', ['monitoring']),
|
||||
WebhookIntegration('transifex', ['misc']),
|
||||
WebhookIntegration('travis', ['continuous-integration'], display_name='Travis CI'),
|
||||
WebhookIntegration('trello', ['project-management']),
|
||||
|
@ -619,6 +620,7 @@ DOC_SCREENSHOT_CONFIG: Dict[str, List[ScreenshotConfig]] = {
|
|||
'stripe': [ScreenshotConfig('charge_succeeded__card.json')],
|
||||
'taiga': [ScreenshotConfig('userstory_changed_status.json')],
|
||||
'teamcity': [ScreenshotConfig('success.json'), ScreenshotConfig('personal.json', '002.png')],
|
||||
'thinkst': [ScreenshotConfig('canarytoken_real.json')],
|
||||
'transifex': [ScreenshotConfig('', extra_params={'project': 'Zulip Mobile',
|
||||
'language': 'en',
|
||||
'resource': 'file',
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
See your Thinkst Canary alerts in Zulip!
|
||||
|
||||
1. {!create-stream.md!}
|
||||
|
||||
1. {!create-bot-construct-url-indented.md!}
|
||||
|
||||
1. Go to your Thinkst Canary settings, and click on **Webhooks** on
|
||||
the left sidebar. Select the **Generic** tab. Press
|
||||
**Add Generic Webhook** and enter the constructed URL above. Finally,
|
||||
click **Save**.
|
||||
|
||||
{!congrats.md!}
|
||||
|
||||
![](/static/images/integrations/thinkst/001.png)
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"ReverseDNS": "attacker-ip.local",
|
||||
"CanaryName": "0000000testnode",
|
||||
"Description": "Dummy Incident",
|
||||
"Timestamp": "2020-06-09 13:59:38 (UTC)",
|
||||
"CanaryIP": "1.1.1.1",
|
||||
"AlertType": "CanaryIncident",
|
||||
"Intro": "This is a dummy incident.",
|
||||
"IncidentHash": "aa875f255f94e3ffe40dc85cf1a8b1e0",
|
||||
"CanaryPort": 8080,
|
||||
"SourceIP": "2.2.2.2",
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"Field1",
|
||||
"VALUE1"
|
||||
],
|
||||
[
|
||||
"Field2",
|
||||
"VALUE2"
|
||||
],
|
||||
[
|
||||
"Field3",
|
||||
"VALUE3"
|
||||
]
|
||||
],
|
||||
"CanaryID": "0000000testnode"
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"CanaryName": "0000000testnode",
|
||||
"Description": "Dummy Incident",
|
||||
"Timestamp": "2020-06-09 13:59:38 (UTC)",
|
||||
"CanaryIP": "1.1.1.1",
|
||||
"AlertType": "CanaryIncident",
|
||||
"Intro": "This is a dummy incident.",
|
||||
"IncidentHash": "aa875f255f94e3ffe40dc85cf1a8b1e0",
|
||||
"CanaryPort": 8080,
|
||||
"SourceIP": "2.2.2.2",
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"Field1",
|
||||
"VALUE1"
|
||||
],
|
||||
[
|
||||
"Field2",
|
||||
"VALUE2"
|
||||
],
|
||||
[
|
||||
"Field3",
|
||||
"VALUE3"
|
||||
]
|
||||
],
|
||||
"CanaryID": "0000000testnode"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"manage_url": "http://example.com/test/url/for/webhook",
|
||||
"memo": "Congrats! The newly saved webhook works",
|
||||
"additional_data": {
|
||||
"src_ip": "1.1.1.1",
|
||||
"useragent": "Mozilla/5.0...",
|
||||
"referer": "http://example.com/referrer",
|
||||
"location": "http://example.com/location"
|
||||
},
|
||||
"channel": "HTTP",
|
||||
"time": "2020-06-09 14:04:39"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"manage_url": "https://canarytokens.org/manage?token=foo&auth=bar",
|
||||
"memo": "Canarytoken example",
|
||||
"additional_data": {
|
||||
"src_ip": "81.151.13.3",
|
||||
"useragent": "Mozilla/5.0 (X11; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0",
|
||||
"referer": null,
|
||||
"location": null
|
||||
},
|
||||
"channel": "HTTP",
|
||||
"time": "2020-06-09 14:04:47 (UTC)"
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
from zerver.lib.test_classes import WebhookTestCase
|
||||
|
||||
|
||||
class ThinkstHookTests(WebhookTestCase):
|
||||
STREAM_NAME = 'travis'
|
||||
URL_TEMPLATE = "/api/v1/external/thinkst?stream={stream}&api_key={api_key}"
|
||||
FIXTURE_DIR_NAME = 'thinkst'
|
||||
|
||||
def test_canary_alert(self) -> None:
|
||||
"""
|
||||
Canary alerts are generated by Thinkst's hardware or virtual canaries.
|
||||
"""
|
||||
expected_message = ('**:alert: Canary has been triggered!**\n\n'
|
||||
'On 2020-06-09 13:59:38 (UTC), `0000000testnode` was triggered '
|
||||
'from `2.2.2.2` (`attacker-ip.local`):\n\n'
|
||||
'> This is a dummy incident.')
|
||||
|
||||
self.send_and_test_stream_message(
|
||||
'canary_alert',
|
||||
'canary alert - 0000000testnode',
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
def test_canary_alert_no_reverse_dns(self) -> None:
|
||||
"""
|
||||
Canary alerts are generated by Thinkst's hardware or virtual canaries.
|
||||
"""
|
||||
expected_message = ('**:alert: Canary has been triggered!**\n\n'
|
||||
'On 2020-06-09 13:59:38 (UTC), `0000000testnode` was triggered '
|
||||
'from `2.2.2.2`:\n\n'
|
||||
'> This is a dummy incident.')
|
||||
|
||||
self.send_and_test_stream_message(
|
||||
'canary_alert_no_reverse_dns',
|
||||
'canary alert - 0000000testnode',
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
def test_canary_alert_with_specific_topic(self) -> None:
|
||||
"""
|
||||
Canary alerts are generated by Thinkst's hardware or virtual canaries.
|
||||
"""
|
||||
self.url = self.build_webhook_url(topic='foo')
|
||||
expected_message = ('**:alert: Canary `0000000testnode` has been triggered!**\n\n'
|
||||
'On 2020-06-09 13:59:38 (UTC), `0000000testnode` was triggered '
|
||||
'from `2.2.2.2` (`attacker-ip.local`):\n\n'
|
||||
'> This is a dummy incident.')
|
||||
|
||||
self.send_and_test_stream_message(
|
||||
'canary_alert',
|
||||
'foo',
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
def test_canarytoken_new(self) -> None:
|
||||
"""
|
||||
Thinkst Canarytokens are simple tripwires for detecting when resources have been accessed.
|
||||
"""
|
||||
expected_message = ('**:alert: Canarytoken has been triggered on 2020-06-09 14:04:39!**\n\n'
|
||||
'> Congrats! The newly saved webhook works \n\n'
|
||||
'[Manage this canarytoken](http://example.com/test/url/for/webhook)')
|
||||
|
||||
self.send_and_test_stream_message(
|
||||
'canarytoken_new',
|
||||
'canarytoken alert',
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
def test_canarytoken_real(self) -> None:
|
||||
"""
|
||||
Thinkst Canarytokens are simple tripwires for detecting when resources have been accessed.
|
||||
"""
|
||||
expected_message = ('**:alert: Canarytoken has been triggered on 2020-06-09 14:04:47 (UTC)!**\n\n'
|
||||
'> Canarytoken example \n\n'
|
||||
'[Manage this canarytoken](https://canarytokens.org/manage?token=foo&auth=bar)')
|
||||
|
||||
self.send_and_test_stream_message(
|
||||
'canarytoken_real',
|
||||
'canarytoken alert',
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
def test_canarytoken_with_specific_topic(self) -> None:
|
||||
"""
|
||||
Thinkst Canarytokens are simple tripwires for detecting when resources have been accessed.
|
||||
"""
|
||||
self.url = self.build_webhook_url(topic='foo')
|
||||
expected_message = ('**:alert: Canarytoken has been triggered on 2020-06-09 14:04:47 (UTC)!**\n\n'
|
||||
'> Canarytoken example \n\n'
|
||||
'[Manage this canarytoken](https://canarytokens.org/manage?token=foo&auth=bar)')
|
||||
|
||||
self.send_and_test_stream_message(
|
||||
'canarytoken_real',
|
||||
'foo',
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded"
|
||||
)
|
|
@ -0,0 +1,79 @@
|
|||
# Webhooks for external integrations.
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
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
|
||||
from zerver.models import UserProfile
|
||||
|
||||
|
||||
def is_canarytoken(message: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
There are two types of requests that Thinkst will send - depending on whether it is
|
||||
a canary or a canarytoken. Unfortunately, there isn't a great way to differentiate other
|
||||
than look at the contents.
|
||||
"""
|
||||
return 'Timestamp' not in message
|
||||
|
||||
|
||||
def canarytoken_message(message: Dict[str, Any]) -> Tuple[str, str]:
|
||||
"""
|
||||
Construct the message for a canarytoken-type request.
|
||||
"""
|
||||
topic = 'canarytoken alert'
|
||||
body = (f"**:alert: Canarytoken has been triggered on {message['time']}!**\n\n"
|
||||
f"> {message['memo']} \n\n"
|
||||
f"[Manage this canarytoken]({message['manage_url']})")
|
||||
|
||||
return (topic, body)
|
||||
|
||||
|
||||
def canary_message(message: Dict[str, Any], user_specified_topic: Optional[str]) -> Tuple[str, str]:
|
||||
"""
|
||||
Construct the message for a canary-type request.
|
||||
"""
|
||||
topic = f"canary alert - {message['CanaryName']}"
|
||||
|
||||
reverse_dns = ''
|
||||
if 'ReverseDNS' in message:
|
||||
reverse_dns = f" (`{message['ReverseDNS']}`)"
|
||||
|
||||
name = ''
|
||||
if user_specified_topic:
|
||||
name = f" `{message['CanaryName']}`"
|
||||
|
||||
body = (f"**:alert: Canary{name} has been triggered!**\n\n"
|
||||
f"On {message['Timestamp']}, `{message['CanaryName']}` was triggered "
|
||||
f"from `{message['SourceIP']}`{reverse_dns}:\n\n"
|
||||
f"> {message['Intro']}")
|
||||
|
||||
return (topic, body)
|
||||
|
||||
|
||||
@api_key_only_webhook_view('Thinkst')
|
||||
@has_request_variables
|
||||
def api_thinkst_webhook(request: HttpRequest, user_profile: UserProfile,
|
||||
message: Dict[str, Any]=REQ(argument_type='body'),
|
||||
user_specified_topic: Optional[str]=REQ('topic', default=None)) -> HttpResponse:
|
||||
"""
|
||||
Construct a response to a webhook event from a Thinkst canary or canarytoken, see
|
||||
linked documentation below for a schema:
|
||||
|
||||
https://help.canary.tools/hc/en-gb/articles/360002426577-How-do-I-configure-notifications-for-a-Generic-Webhook-
|
||||
"""
|
||||
body = None
|
||||
topic = None
|
||||
|
||||
if is_canarytoken(message):
|
||||
topic, body = canarytoken_message(message)
|
||||
else:
|
||||
topic, body = canary_message(message, user_specified_topic)
|
||||
|
||||
if user_specified_topic:
|
||||
topic = user_specified_topic
|
||||
|
||||
check_send_webhook_message(request, user_profile, topic, body)
|
||||
return json_success()
|
Loading…
Reference in New Issue