Skip to content
This repository has been archived by the owner on Jul 9, 2022. It is now read-only.

feat: add telegram/mattermost notif + auto relaunch #61

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ RUN pip install -r /requirements.txt
RUN mkdir /app
COPY src /app/

CMD python3 /app/ransomwatch.py
ENTRYPOINT ["python3", "/app/ransomwatch.py"]
# CMD ["--delay", "3600"]

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ In `config_vol/`, please copy `config.sample.yaml` to `config.yaml`, and add the
* Notification destinations. RansomWatch currently supports notifying via.the following:
* Slack: Follow [these](https://api.slack.com/messaging/webhooks) instructions to add a new app to your Slack workspace and add the webhook URL to the config.
* Discord: Follow [these](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) instructions to add a new app to your Discord server and add the webhook URL to the config.
* Telegram: Follow [these](https://core.telegram.org/bots) instructions to create a new Telegram bot and get the apikey for the config.
* Mattermost: Follow [these](https://docs.mattermost.com/developer/webhooks-incoming.html) instructions to add a new incoming webhook to your Mattermost server and add the webhook URL to the config.

Additionally, there are a few environment variables you may need to set:

Expand Down
16 changes: 14 additions & 2 deletions config_vol/config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,24 @@ sites:
notifications:
dest1:
# type of notification
# valid options: slack, discord
# valid options: slack, discord, telegram, mattermost
type: slack

# url for the webhook (slack, discord)
# url for the webhook (slack, discord, mattermost)
url: https://hooks.slack.com/services/something/goes/here

# telegram bot ApiKey
telegram_apikey: xxxx

# telegram chatid (could be obtained in https://api.telegram.org/bot<YOUR_APIKEY>/getUpdates)
telegram_chatid: xxxx

# mattermost username
mattermost_username: Ransomwatch

# mattermost channel
mattermost_channel: Ransomwatch

# should new victim notifications be sent to this webhook?
new_victims: true

Expand Down
8 changes: 4 additions & 4 deletions src/notifications/discord.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def _post_webhook(body: Dict, url: str) -> bool:

return True

def send_new_victim_notification(url: str, victim: Victim) -> bool:
def send_new_victim_notification(victim: Victim, url: str) -> bool:
published_ts = datetime.strftime(victim.published, '%b %d, %Y') if victim.published is not None else "N/A"

body = {
Expand Down Expand Up @@ -64,7 +64,7 @@ def send_new_victim_notification(url: str, victim: Victim) -> bool:

return DiscordNotification._post_webhook(body, url)

def send_victim_removed_notification(url: str, victim: Victim) -> bool:
def send_victim_removed_notification(victim: Victim, url: str) -> bool:
published_ts = datetime.strftime(victim.published, '%b %d, %Y') if victim.published is not None else "N/A"

body = {
Expand Down Expand Up @@ -100,7 +100,7 @@ def send_victim_removed_notification(url: str, victim: Victim) -> bool:

return DiscordNotification._post_webhook(body, url)

def send_site_down_notification(url: str, site: Site) -> bool:
def send_site_down_notification(site: Site, url: str) -> bool:
last_up_ts = datetime.strftime(site.last_up, '%b %d, %Y at %H:%M:%S UTC') if site.last_up is not None else "N/A"

body = {
Expand Down Expand Up @@ -128,7 +128,7 @@ def send_site_down_notification(url: str, site: Site) -> bool:

return DiscordNotification._post_webhook(body, url)

def send_error_notification(url: str, context: str, error: str, fatal: bool = False) -> bool:
def send_error_notification(context: str, error: str, fatal: bool = False, url: str = None) -> bool:
body = {
"embeds": [
{
Expand Down
44 changes: 35 additions & 9 deletions src/notifications/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from db.models import Site, Victim
from .slack import SlackNotification
from .discord import DiscordNotification
from .telegram import TelegramNotification
from .mattermost import MattermostNotification

class NotificationManager():
def send_new_victim_notification(victim: Victim):
Expand All @@ -13,11 +15,17 @@ def send_new_victim_notification(victim: Victim):
continue

if params["type"] == "slack":
if not SlackNotification.send_new_victim_notification(params["url"], victim):
if not SlackNotification.send_new_victim_notification(victim, url=params["url"]):
logging.error(f"Failed to send new victim notification to Slack workspace \"{dest}\"")
elif params["type"] == "discord":
if not DiscordNotification.send_new_victim_notification(params["url"], victim):
if not DiscordNotification.send_new_victim_notification(victim, url=params["url"]):
logging.error(f"Failed to send new victim notification to Discord guild \"{dest}\"")
elif params["type"] == "telegram":
if not TelegramNotification.send_new_victim_notification(victim, apikey=params["telegram_apikey"], chatid=params["telegram_chatid"]):
logging.error(f"Failed to send new victim notification to Telegram chat \"{dest}\"")
elif params["type"] == "mattermost":
if not MattermostNotification.send_new_victim_notification(victim, url=params["url"], username=params['mattermost_username'], channel=params['mattermost_channel']):
logging.error(f"Failed to send new victim notification to Mattermost chat \"{dest}\"")
else:
logging.error(f"Attempted to send a new victim notification to an unsupported notification type: {params['type']}")

Expand All @@ -28,11 +36,17 @@ def send_victim_removed_notification(victim: Victim):
continue

if params["type"] == "slack":
if not SlackNotification.send_victim_removed_notification(params["url"], victim):
if not SlackNotification.send_victim_removed_notification(victim, url=params["url"]):
logging.error(f"Failed to send removed victim notification to Slack workspace \"{dest}\"")
elif params["type"] == "discord":
if not DiscordNotification.send_victim_removed_notification(params["url"], victim):
if not DiscordNotification.send_victim_removed_notification(victim, url=params["url"]):
logging.error(f"Failed to send removed victim notification to Discord guild \"{dest}\"")
elif params["type"] == "telegram":
if not TelegramNotification.send_victim_removed_notification(victim, apikey=params["telegram_apikey"], chatid=params["telegram_chatid"]):
logging.error(f"Failed to send removed victim notification to Telegram chat \"{dest}\"")
elif params["type"] == "mattermost":
if not MattermostNotification.send_victim_removed_notification(victim, url=params["url"], username=params['mattermost_username'], channel=params['mattermost_channel']):
logging.error(f"Failed to send removed victim notification to Mattermost chat \"{dest}\"")
else:
logging.error(f"Attempted to send a removed victim notification to an unsupported notification type: {params['type']}")

Expand All @@ -43,11 +57,17 @@ def send_site_down_notification(site: Site):
continue

if params["type"] == "slack":
if not SlackNotification.send_site_down_notification(params["url"], site):
if not SlackNotification.send_site_down_notification(site, url=params["url"]):
logging.error(f"Failed to send site down notification to Slack workspace \"{dest}\"")
elif params["type"] == "discord":
if not DiscordNotification.send_site_down_notification(params["url"], site):
if not DiscordNotification.send_site_down_notification(site, url=params["url"]):
logging.error(f"Failed to send site down notification to Discord guild \"{dest}\"")
elif params["type"] == "telegram":
if not TelegramNotification.send_site_down_notification(site, apikey=params["telegram_apikey"], chatid=params["telegram_chatid"]):
logging.error(f"Failed to send site down notification to Telegram chat \"{dest}\"")
elif params["type"] == "mattermost":
if not MattermostNotification.send_site_down_notification(site, url=params["url"], username=params['mattermost_username'], channel=params['mattermost_channel']):
logging.error(f"Failed to send site down notification to Mattermost chat \"{dest}\"")
else:
logging.error(f"Attempted to send a site down notification to an unsupported notification type: {params['type']}")

Expand All @@ -58,10 +78,16 @@ def send_error_notification(context: str, error: str, fatal: bool = False):
continue

if params["type"] == "slack":
if not SlackNotification.send_error_notification(params["url"], context, error, fatal):
if not SlackNotification.send_error_notification(context, error, fatal, url=params["url"]):
logging.error(f"Failed to send error notification to Slack workspace \"{dest}\"")
elif params["type"] == "discord":
if not DiscordNotification.send_error_notification(params["url"], context, error, fatal):
if not DiscordNotification.send_error_notification(context, error, fatal, url=params["url"]):
logging.error(f"Failed to send error notification to Discord guild \"{dest}\"")
elif params["type"] == "telegram":
if not TelegramNotification.send_error_notification(context, error, fatal, apikey=params["telegram_apikey"], chatid=params["telegram_chatid"]):
logging.error(f"Failed to send error notification to Telegram chat \"{dest}\"")
elif params["type"] == "mattermost":
if not MattermostNotification.send_error_notification(context, error, fatal, url=params["url"], username=params['mattermost_username'], channel=params['mattermost_channel']):
logging.error(f"Failed to send error notification to Mattermost chat \"{dest}\"")
else:
logging.error(f"Attempted to send a site down notification to an unsupported notification type: {params['type']}")
logging.error(f"Attempted to send error notification to an unsupported notification type: {params['type']}")
78 changes: 78 additions & 0 deletions src/notifications/mattermost.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from datetime import datetime
import logging
import requests

from db.models import Site, Victim
from .source import NotificationSource

class MattermostNotification(NotificationSource):
def _post_webhook(body: dict, url: str) -> bool:
r = requests.post(url, json=body)
if r.status_code != 200:
logging.error(
f"Error sending Mattermost notification ({r.status_code}): {r.content.decode()}")
return False

return True

def send_new_victim_notification(victim: Victim, url: str, channel: str, username: str) -> bool:
published_ts = datetime.strftime(victim.published, '%b %d, %Y') if victim.published is not None else "N/A"

body = f":bell: **New Victim Posted**\n" \
f"**Actor:** {victim.site.actor}\n" \
f"**Organization:** {victim.name}\n" \
f"**Published Date:** {published_ts}\n" \
f"**First Seen:** " + datetime.strftime(victim.first_seen, '%b %d, %Y at %H:%M:%S UTC') + "\n" \
f"**Leak Site:** [Link]({victim.site.url})\n"
if victim.url is not None:
body += f"**Victim Page:** [Link]({victim.url})\n"
else:
body += f"**Victim Page:** *no link available*\n"

payload={
"channel": channel,
"username": username,
"text": body
}
return MattermostNotification._post_webhook(payload, url)

def send_victim_removed_notification(victim: Victim, url: str, channel: str, username: str) -> bool:
published_ts = datetime.strftime(victim.published, '%b %d, %Y') if victim.published is not None else "N/A"

body = f":no_bell: **New Victim Posted**\n" \
f"**Actor:** {victim.site.actor}\n" \
f"**Organization:** {victim.name}\n" \
f"**Published Date:** {published_ts}\n" \
f"**Last Seen:** " + datetime.strftime(victim.last_seen, '%b %d, %Y at %H:%M:%S UTC') + "\n" \
f"**Leak Site:** [Link]({victim.site.url})\n"

payload={
"channel": channel,
"username": username,
"text": body
}
return MattermostNotification._post_webhook(payload, url)

def send_site_down_notification(site: Site, url: str, channel: str, username: str) -> bool:
last_up_ts = datetime.strftime(site.last_up, '%b %d, %Y at %H:%M:%S UTC') if site.last_up is not None else "N/A"

body = f":construction: **Site Down**\n" \
f"**Actor:** {site.actor}\n" \
f"**Last Up:** {last_up_ts}\n" \
f"**Leak Site:** [Link]({site.url})\n"
payload={
"channel": channel,
"username": username,
"text": body
}
return MattermostNotification._post_webhook(payload, url)

def send_error_notification(context: str, error: str, fatal: bool = False, url: str = None, channel: str = None, username: str = None) -> bool:
body = f":warning: **{'Fatal ' if fatal else ''}Error**\n" \
f"**An error occurred:** {context}\n\n```{error}```\nFor more details, please check the app container logs\n\n_If you think this is a bug, please [open an issue](https://github.com/captainGeech42/ransomwatch/issues) on GitHub_"
payload={
"channel": channel,
"username": username,
"text": body
}
return MattermostNotification._post_webhook(payload, url)
8 changes: 4 additions & 4 deletions src/notifications/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def _post_webhook(body: Dict, url: str) -> bool:

return True

def send_new_victim_notification(url: str, victim: Victim) -> bool:
def send_new_victim_notification(victim: Victim, url: str) -> bool:
published_ts = datetime.strftime(victim.published, '%b %d, %Y') if victim.published is not None else "N/A"

body = {
Expand Down Expand Up @@ -72,7 +72,7 @@ def send_new_victim_notification(url: str, victim: Victim) -> bool:

return SlackNotification._post_webhook(body, url)

def send_victim_removed_notification(url: str, victim: Victim) -> bool:
def send_victim_removed_notification(victim: Victim, url: str) -> bool:
published_ts = datetime.strftime(victim.published, '%b %d, %Y') if victim.published is not None else "N/A"

body = {
Expand Down Expand Up @@ -124,7 +124,7 @@ def send_victim_removed_notification(url: str, victim: Victim) -> bool:

return SlackNotification._post_webhook(body, url)

def send_site_down_notification(url: str, site: Site) -> bool:
def send_site_down_notification(site: Site, url: str) -> bool:
last_up_ts = datetime.strftime(site.last_up, '%b %d, %Y at %H:%M:%S UTC') if site.last_up is not None else "N/A"

body = {
Expand Down Expand Up @@ -168,7 +168,7 @@ def send_site_down_notification(url: str, site: Site) -> bool:

return SlackNotification._post_webhook(body, url)

def send_error_notification(url: str, context: str, error: str, fatal: bool = False) -> bool:
def send_error_notification(context: str, error: str, fatal: bool = False, url: str = None) -> bool:
body = {
"attachments": [
{
Expand Down
8 changes: 4 additions & 4 deletions src/notifications/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
# webhook/url only at this point, email or something in the future
# would require reworking config loading into something fancier
class NotificationSource():
def send_new_victim_notification(url: str, victim: Victim) -> bool:
def send_new_victim_notification(victim: Victim, **kwargs) -> bool:
raise Exception("Function implementation not found")

def send_victim_removed_notification(url: str, victim: Victim) -> bool:
def send_victim_removed_notification(victim: Victim, **kwargs) -> bool:
raise Exception("Function implementation not found")

def send_site_down_notification(url: str, info: str) -> bool:
def send_site_down_notification(info: str, **kwargs) -> bool:
raise Exception("Function implementation not found")

def send_error_notification(url: str, context: str, error: str, fatal: bool = False) -> bool:
def send_error_notification(context: str, error: str, fatal: bool = False, **kwargs) -> bool:
raise Exception("Function implementation not found")
Loading