2020-06-09 17:42:17 +02:00
|
|
|
# Webhooks for external integrations.
|
|
|
|
from typing import Any, Dict, Optional, Tuple
|
|
|
|
|
|
|
|
from django.http import HttpRequest, HttpResponse
|
|
|
|
|
2020-08-20 00:32:15 +02:00
|
|
|
from zerver.decorator import webhook_view
|
2020-06-09 17:42:17 +02:00
|
|
|
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:
|
|
|
|
"""
|
2020-07-20 19:11:19 +02:00
|
|
|
Requests sent from Thinkst canaries are either from canarytokens or
|
|
|
|
canaries, which can be differentiated by the value of the `AlertType`
|
|
|
|
field.
|
2020-06-09 17:42:17 +02:00
|
|
|
"""
|
2021-02-12 08:20:45 +01:00
|
|
|
return message["AlertType"] == "CanarytokenIncident"
|
2020-06-09 17:42:17 +02:00
|
|
|
|
|
|
|
|
2020-07-20 19:11:19 +02:00
|
|
|
def canary_name(message: Dict[str, Any]) -> str:
|
2020-06-09 17:42:17 +02:00
|
|
|
"""
|
2020-07-20 19:11:19 +02:00
|
|
|
Returns the name of the canary or canarytoken.
|
2020-06-09 17:42:17 +02:00
|
|
|
"""
|
2020-07-20 19:11:19 +02:00
|
|
|
if is_canarytoken(message):
|
2021-02-12 08:20:45 +01:00
|
|
|
return message["Reminder"]
|
2020-07-20 19:11:19 +02:00
|
|
|
else:
|
2021-02-12 08:20:45 +01:00
|
|
|
return message["CanaryName"]
|
2020-06-09 17:42:17 +02:00
|
|
|
|
2020-07-20 19:11:19 +02:00
|
|
|
|
|
|
|
def canary_kind(message: Dict[str, Any]) -> str:
|
|
|
|
"""
|
|
|
|
Returns a description of the kind of request - canary or canarytoken.
|
|
|
|
"""
|
|
|
|
if is_canarytoken(message):
|
2021-02-12 08:20:45 +01:00
|
|
|
return "canarytoken"
|
2020-07-20 19:11:19 +02:00
|
|
|
else:
|
2021-02-12 08:20:45 +01:00
|
|
|
return "canary"
|
2020-06-09 17:42:17 +02:00
|
|
|
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def source_ip_and_reverse_dns(message: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
|
2020-06-09 17:42:17 +02:00
|
|
|
"""
|
2020-07-20 19:11:19 +02:00
|
|
|
Extract the source IP and reverse DNS information from a canary request.
|
2020-06-09 17:42:17 +02:00
|
|
|
"""
|
2020-07-20 19:11:19 +02:00
|
|
|
reverse_dns, source_ip = (None, None)
|
2020-06-09 17:42:17 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "SourceIP" in message:
|
|
|
|
source_ip = message["SourceIP"]
|
2020-07-20 19:11:19 +02:00
|
|
|
# `ReverseDNS` can sometimes exist and still be empty.
|
2021-02-12 08:20:45 +01:00
|
|
|
if "ReverseDNS" in message and message["ReverseDNS"]:
|
|
|
|
reverse_dns = message["ReverseDNS"]
|
2020-06-09 17:42:17 +02:00
|
|
|
|
2020-07-20 19:11:19 +02:00
|
|
|
return (source_ip, reverse_dns)
|
|
|
|
|
|
|
|
|
|
|
|
def body(message: Dict[str, Any]) -> str:
|
|
|
|
"""
|
|
|
|
Construct the response to a canary or canarytoken request.
|
|
|
|
"""
|
|
|
|
|
|
|
|
title = canary_kind(message).title()
|
|
|
|
name = canary_name(message)
|
2021-02-12 03:52:14 +01:00
|
|
|
body = f"**:alert: {title} *{name}* has been triggered!**\n\n{message['Intro']}\n\n"
|
2020-07-20 19:11:19 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "IncidentHash" in message:
|
2021-05-10 07:02:14 +02:00
|
|
|
body += f"**Incident ID:** `{message['IncidentHash']}`\n"
|
2020-07-20 19:11:19 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "Token" in message:
|
2020-07-20 19:11:19 +02:00
|
|
|
body += f"**Token:** `{message['Token']}`\n"
|
2020-06-09 17:42:17 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "Description" in message:
|
2020-07-20 19:11:19 +02:00
|
|
|
body += f"**Kind:** {message['Description']}\n"
|
2020-06-09 17:42:17 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "Timestamp" in message:
|
2020-07-20 19:11:19 +02:00
|
|
|
body += f"**Timestamp:** {message['Timestamp']}\n"
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "CanaryIP" in message:
|
2020-07-20 19:11:19 +02:00
|
|
|
body += f"**Canary IP:** `{message['CanaryIP']}`\n"
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "CanaryLocation" in message:
|
2021-05-10 07:02:14 +02:00
|
|
|
body += f"**Canary location:** {message['CanaryLocation']}\n"
|
2020-07-20 19:11:19 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "Triggered" in message:
|
|
|
|
unit = "times" if message["Triggered"] > 1 else "time"
|
2020-07-20 19:11:19 +02:00
|
|
|
body += f"**Triggered:** {message['Triggered']} {unit}\n"
|
|
|
|
|
|
|
|
source_ip, reverse_dns = source_ip_and_reverse_dns(message)
|
|
|
|
if source_ip:
|
|
|
|
body += f"**Source IP:** `{source_ip}`\n"
|
|
|
|
if reverse_dns:
|
|
|
|
body += f"**Reverse DNS:** `{reverse_dns}`\n"
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "AdditionalDetails" in message:
|
|
|
|
for detail in message["AdditionalDetails"]:
|
|
|
|
if isinstance(detail[1], str) and "*" in detail[1]:
|
2020-07-20 19:11:19 +02:00
|
|
|
# Thinkst sends passwords as a series of stars which can mess with
|
|
|
|
# formatting, so wrap these in backticks.
|
|
|
|
body += f"**{detail[0]}:** `{detail[1]}`\n"
|
|
|
|
else:
|
|
|
|
body += f"**{detail[0]}:** {detail[1]}\n"
|
|
|
|
|
|
|
|
return body
|
2020-06-09 17:42:17 +02:00
|
|
|
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
@webhook_view("Thinkst")
|
2020-06-09 17:42:17 +02:00
|
|
|
@has_request_variables
|
2020-07-20 19:11:19 +02:00
|
|
|
def api_thinkst_webhook(
|
2021-02-12 08:19:30 +01:00
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
2021-02-12 08:20:45 +01:00
|
|
|
message: Dict[str, Any] = REQ(argument_type="body"),
|
|
|
|
user_specified_topic: Optional[str] = REQ("topic", default=None),
|
2020-07-20 19:11:19 +02:00
|
|
|
) -> HttpResponse:
|
2020-06-09 17:42:17 +02:00
|
|
|
"""
|
2020-07-20 19:11:19 +02:00
|
|
|
Construct a response to a webhook event from a Thinkst canary or canarytoken.
|
|
|
|
|
|
|
|
Thinkst offers public canarytokens with canarytokens.org and with their canary
|
|
|
|
product, but the schema returned by these identically named services are
|
|
|
|
completely different - canarytokens from canarytokens.org are handled by a
|
|
|
|
different Zulip integration.
|
|
|
|
|
|
|
|
Thinkst's documentation for the schema is linked below, but in practice the JSON
|
|
|
|
received doesn't always conform.
|
2020-06-09 17:42:17 +02:00
|
|
|
|
|
|
|
https://help.canary.tools/hc/en-gb/articles/360002426577-How-do-I-configure-notifications-for-a-Generic-Webhook-
|
|
|
|
"""
|
|
|
|
|
2020-07-20 19:11:19 +02:00
|
|
|
response = body(message)
|
2020-06-09 17:42:17 +02:00
|
|
|
|
2020-07-20 19:11:19 +02:00
|
|
|
topic = None
|
2020-06-09 17:42:17 +02:00
|
|
|
if user_specified_topic:
|
|
|
|
topic = user_specified_topic
|
2020-07-20 19:11:19 +02:00
|
|
|
else:
|
|
|
|
name = canary_name(message)
|
|
|
|
kind = canary_kind(message)
|
|
|
|
|
|
|
|
topic = f"{kind} alert - {name}"
|
2020-06-09 17:42:17 +02:00
|
|
|
|
2020-07-20 19:11:19 +02:00
|
|
|
check_send_webhook_message(request, user_profile, topic, response)
|
2020-06-09 17:42:17 +02:00
|
|
|
return json_success()
|