Dear team,
+ +A new data leakage alert has been detected: for the keyword:
+ +Keyword: """ + str(alert.keyword) + """
+Source: """ + str(alert.url) + """
+You can check more details here.
+ +Kind Regards,
+ Watcher
diff --git a/Watcher/Watcher/common/__init__.py b/Watcher/Watcher/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Watcher/Watcher/common/admin.py b/Watcher/Watcher/common/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Watcher/Watcher/common/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Watcher/Watcher/common/apps.py b/Watcher/Watcher/common/apps.py new file mode 100644 index 0000000..01cca2f --- /dev/null +++ b/Watcher/Watcher/common/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommonConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'common' diff --git a/Watcher/Watcher/common/core.py b/Watcher/Watcher/common/core.py new file mode 100644 index 0000000..095eed4 --- /dev/null +++ b/Watcher/Watcher/common/core.py @@ -0,0 +1,508 @@ +from .utils.send_slack_messages import send_slack_message +from .utils.send_citadel_messages import send_citadel_message +from .utils.send_email_notifications import send_email_notifications +from django.utils import timezone +from django.conf import settings +import re +from html import unescape +from site_monitoring.models import Site +from .mail_template.threats_watcher_template import get_threats_watcher_template +from .mail_template.data_leak_template import get_data_leak_template +from .mail_template.data_leak_group_template import get_data_leak_group_template +from .mail_template.dns_finder_template import get_dns_finder_template +from .mail_template.dns_finder_group_template import get_dns_finder_group_template +from .mail_template.site_monitoring_template import get_site_monitoring_template + +thehive_url = settings.THE_HIVE_URL +api_key = settings.THEHIVE_API_KEY + +from datetime import datetime +from secrets import token_hex + +def generate_ref(): + """ + Generates a unique 'sourceRef' identifier for an alert. + + :return: Unique identifier as a string. + """ + ref = datetime.now().strftime("%y%m%d") + "-" + str(token_hex(3))[:5] + return ref + + +# Configuration for Slack +APP_CONFIG_SLACK = { + 'threats_watcher': { + 'content_template': ( + "*[THREATS WATCHER - WARNING] 🔥 Trendy Threats Detected 🔥*\n\n" + "Dear team,\n\n" + "Please find the new trendy word(s) detected below:\n\n" + "{words_list}\n\n" + "Please, find more details <{details_url}|here>." + ), + 'channel': settings.SLACK_CHANNEL, + 'url_suffix': '#/', + }, + 'data_leak': { + 'content_template': ( + "*[DATA LEAK - ALERT #{alert_pk}] 🔔 Data Leak Detected: {keyword_name} 🔔*\n\n" + "Dear team,\n\n" + "New Data Leakage Alert for {keyword_name} keyword:\n\n" + "*• Keyword:* {keyword_name}\n" + "*• Source:* {url}\n\n" + "Please, find more details <{details_url}|here>." + ), + 'channel': settings.SLACK_CHANNEL, + 'url_suffix': '#data_leak', + }, + 'website_monitoring': { + 'content_template': ( + "*[SITE MONITORING - INCIDENT] 🔔 {alert_type} on {domain_name} 🔔*\n\n" + "Dear team,\n\n" + "Please find the new incident detected below:\n\n" + "*• Type of alert:* {alert_type}\n" + "*• Domain name:* {domain_name}\n" + "*• Difference Score:* {difference_score}\n" + "*• New Ip:* {new_ip}\n" + "*• Old Ip:* {old_ip}\n" + "*• New Ip Second:* {new_ip_second}\n" + "*• Old IP Second:* {old_ip_second}\n" + "*• New MX Records:* {new_mx_records}\n" + "*• Old MX Records:* {old_mx_records}\n" + "*• New Mail Server:* {new_mail_A_record_ip}\n" + "*• Old Mail Server:* {old_mail_A_record_ip}\n\n" + "Please, find more details <{details_url}|here>." + ), + 'channel': settings.SLACK_CHANNEL, + 'url_suffix': '#/website_monitoring/', + }, + 'dns_finder': { + 'content_template': ( + "*[DNS FINDER - ALERT #{alert.pk}] 🚨 Suspicious DNS Detected: {alert.dns_twisted.domain_name} 🚨*\n\n" + "Dear team,\n\n" + "New Twisted DNS found: \n\n" + "*• Twisted DNS:* {alert.dns_twisted.domain_name}\n" + "*• Corporate Keyword:* {alert.dns_twisted.keyword_monitored}\n" + "*• Corporate DNS:* {alert.dns_twisted.dns_monitored}\n" + "*• Fuzzer:* {alert.dns_twisted.fuzzer}\n\n" + "Please, find more details <{details_url}|here>." + ), + 'channel': settings.SLACK_CHANNEL, + 'url_suffix': '#/dns_finder/', + }, +} + +# Configuration for Citadel +APP_CONFIG_CITADEL = { + 'threats_watcher': { + 'content_template': ( + "
[THREATS WATCHER - WARNING] 🔥 Trendy Threats Detected 🔥
" + "Dear team,
" + "Please find the new trendy word(s) detected below:\n
" + "Please, find more details here.
" + ), + 'citadel_room_id': settings.CITADEL_ROOM_ID, + 'url_suffix': '#/', + }, + 'data_leak': { + 'content_template': ( + "[DATA LEAK - ALERT #{alert_pk}] 🔔 Data Leak Detected: {keyword_name} 🔔
" + "Dear team,
" + "New Data Leakage Alert for {keyword_name} keyword:
\n" + "Please, find more details here.
" + ), + 'citadel_room_id': settings.CITADEL_ROOM_ID, + 'url_suffix': '#data_leak', + }, + 'website_monitoring': { + 'content_template': ( + "[SITE MONITORING - INCIDENT] 🔔 {alert_type} on {domain_name} 🔔
" + "Dear team,
" + "Please find the new incident detected below:
\n" + "Please, find more details here.
" + ), + 'citadel_room_id': settings.CITADEL_ROOM_ID, + 'url_suffix': '#/website_monitoring/', + }, + 'dns_finder': { + 'content_template': ( + "[DNS FINDER - ALERT #{alert.pk}] 🚨 Suspicious DNS Detected: {alert.dns_twisted.domain_name} 🚨
" + "Dear team,
" + "New Twisted DNS found:
\n" + "Please, find more details here.
" + ), + 'citadel_room_id': settings.CITADEL_ROOM_ID, + 'url_suffix': '#/dns_finder/', + }, +} + +# Configuration for TheHive +APP_CONFIG_THEHIVE = { + 'threats_watcher': { + 'title': "Threats Watcher buzzword detected", + 'description_template': ( + "**Alert:**\n" + "**The following trendy words were detected:**\n" + "{words_list}" + ), + 'severity': 2, + 'tags': ["Threats Watcher", "Watcher", "Buzzword", "Trendy Words", "Threat Detection"] + }, + 'data_leak': { + 'title': "New Data Leakage - Alert #{alert_pk} for {keyword_name} keyword", + 'description_template': ( + "**Alert:**\n" + "**New data leak detected:**\n" + "*Keyword:* {keyword_name}\n" + "*Source:* {url}\n" + ), + 'severity': 3, + 'tags': ["Data Leak", "Watcher", "Sensitive Data", "Leak Detection"] + }, + 'website_monitoring': { + 'title': "Website Monitoring Detected - {alert_type} on {domain_name}", + 'description_template': ( + "**Alert:**\n" + "**New website monitoring incident detected:**\n" + "*Type of alert:* {alert_type}\n" + "*Domain name:* {domain_name}\n" + "*• Difference Score:* {difference_score}\n" + "*• New Ip:* {new_ip}\n" + "*• Old Ip:* {old_ip}\n" + "*• New Ip Second:* {new_ip_second}\n" + "*• Old IP Second:* {old_ip_second}\n" + "*• New MX Records:* {new_mx_records}\n" + "*• Old MX Records:* {old_mx_records}\n" + "*• New Mail Server:* {new_mail_A_record_ip}\n" + "*• Old Mail Server:* {old_mail_A_record_ip}\n" + ), + 'severity': 2, + 'tags': ["Website Monitoring", "Watcher", "Incident", "Website", "Domain Name"] + }, + 'dns_finder': { + 'title': "New Twisted DNS found - {alert.dns_twisted.domain_name}", + 'description_template': ( + "**Alert:**\n" + "**New Twisted DNS found:**\n" + "*Corporate Keyword:* {alert.dns_twisted.domain_name}\n" + "*Twisted DNS:* {alert.dns_twisted.keyword_monitored}\n" + "*Corporate DNS:* {alert.dns_twisted.dns_monitored}\n" + "*Fuzzer:* {alert.dns_twisted.fuzzer}\n" + ), + 'severity': 3, + 'tags': ["DNS Finder", "Watcher", "Twisted DNS", "Corporate Keywords", "Corporate DNS Assets"] + }, +} + + +# Configuration for Email +APP_CONFIG_EMAIL = { + 'threats_watcher': { + 'subject': "[WARNING] Threats Watcher buzzword detected", + 'template_func': get_threats_watcher_template, + }, + 'data_leak': { + 'subject': "[ALERT] Data Leak Detected", + 'template_func': get_data_leak_template, + }, + 'data_leak_group': { + 'subject': "[ALERT] Group Data Leak Detected", + 'template_func': get_data_leak_group_template, + }, + 'website_monitoring': { + 'subject': "[ALERT] Website Monitoring Detected", + 'template_func': get_site_monitoring_template, + }, + 'dns_finder': { + 'subject': "[ALERT] DNS Finder", + 'template_func': get_dns_finder_template, + }, + 'dns_finder_group': { + 'subject': "[ALERT] Group DNS Finder", + 'template_func': get_dns_finder_group_template, + }, +} + + + +def collect_observables(app_name, context_data): + """ + Collect observables specific to each app based on the context data provided. + """ + observables = [] + + if app_name == 'threats_watcher': + for word in context_data.get('email_words', []): + if re.match(r'^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', word): + observables.append({"dataType": "domain", "data": word}) + else: + observables.append({"dataType": "other", "data": word}) + + elif app_name == 'website_monitoring': + site = context_data.get('site') + alert_data = context_data.get('alert_data', {}) + if site: + observables.append({"dataType": "domain", "data": site.domain_name}) + if alert_data.get('new_ip'): + observables.append({"dataType": "ip", "data": alert_data['new_ip']}) + if alert_data.get('old_ip'): + observables.append({"dataType": "ip", "data": alert_data['old_ip']}) + if alert_data.get('new_ip_second'): + observables.append({"dataType": "ip", "data": alert_data['new_ip_second']}) + if alert_data.get('old_ip_second'): + observables.append({"dataType": "ip", "data": alert_data['old_ip_second']}) + if alert_data.get('new_mx_records'): + for mx_record in alert_data['new_mx_records']: + observables.append({"dataType": "domain", "data": mx_record}) + if alert_data.get('old_mx_records'): + for mx_record in alert_data['old_mx_records']: + observables.append({"dataType": "domain", "data": mx_record}) + if alert_data.get('new_mail_A_record_ip'): + observables.append({"dataType": "ip", "data": alert_data['new_mail_A_record_ip']}) + if alert_data.get('old_mail_A_record_ip'): + observables.append({"dataType": "ip", "data": alert_data['old_mail_A_record_ip']}) + + elif app_name == 'data_leak': + alert = context_data.get('alert') + if alert: + observables.append({"dataType": "url", "data": alert.url}) + observables.append({"dataType": "other", "data": alert.keyword.name}) + + elif app_name == 'dns_finder': + alert = context_data.get('alert') + if alert: + observables.append({"dataType": "domain", "data": alert.dns_twisted.domain_name}) + if alert.dns_twisted.keyword_monitored: + observables.append({"dataType": "other", "data": alert.dns_twisted.keyword_monitored.name}) + if alert.dns_twisted.dns_monitored: + observables.append({"dataType": "domain", "data": alert.dns_twisted.dns_monitored.domain_name}) + if alert.dns_twisted.fuzzer: + observables.append({"dataType": "other", "data": alert.dns_twisted.fuzzer}) + + return observables + +def remove_html_tags(text): + """Remove HTML tags from a text.""" + clean = re.compile('<.*?>') + return re.sub(clean, '', text) + +def send_app_specific_notifications(app_name, context_data, subscribers): + from .utils.send_thehive_alerts import send_thehive_alert + + """ + Send notifications based on the application type (Slack, Citadel, TheHive cases & alerts, Email). + Collect observables, format the notification content, and send them to respective channels. + """ + app_config_slack = APP_CONFIG_SLACK.get(app_name) + app_config_citadel = APP_CONFIG_CITADEL.get(app_name) + app_config_thehive = APP_CONFIG_THEHIVE.get(app_name) + app_config_email = APP_CONFIG_EMAIL.get(app_name) + + if not app_config_slack or not app_config_citadel or not app_config_thehive or not app_config_email: + return + + + if not subscribers.exists(): + return + + custom_field_key = settings.THE_HIVE_CUSTOM_FIELD + + observables = collect_observables(app_name, context_data) + + def send_notification(channel, content_template, subscribers_filter, send_func, **kwargs): + """Helper to format and send notification based on the channel.""" + if subscribers.filter(**subscribers_filter).exists(): + content = content_template.format(**kwargs) + send_func(content) + + try: + common_data = {} + if app_name == 'threats_watcher': + words_list = '\n'.join([remove_html_tags(unescape(word)) for word in context_data.get('email_words', [])]) + if not words_list: + return + common_data = { + 'words_list': words_list, + 'details_url': settings.WATCHER_URL + app_config_slack['url_suffix'], + 'app_name': 'threats_watcher' + } + + email_words = context_data.get('email_words', []) + email_body = get_threats_watcher_template(settings.WORDS_OCCURRENCE, email_words) + + + elif app_name == 'website_monitoring': + site = context_data.get('site') + alert_data = context_data.get('alert_data', {}) + if not site: + return + common_data = { + 'alert_type': alert_data.get('type', 'None'), + 'domain_name': site.domain_name, + 'difference_score': alert_data.get('difference_score', 'None'), + 'new_ip': alert_data.get('new_ip', 'None'), + 'old_ip': alert_data.get('old_ip', 'None'), + 'new_ip_second': alert_data.get('new_ip_second', 'None'), + 'old_ip_second': alert_data.get('old_ip_second', 'None'), + 'new_mx_records': alert_data.get('new_MX_records', 'None'), + 'old_mx_records': alert_data.get('old_MX_records', 'None'), + 'new_mail_A_record_ip': alert_data.get('new_mail_A_record_ip', 'None'), + 'old_mail_A_record_ip': alert_data.get('old_mail_A_record_ip', 'None'), + 'web_status': alert_data.get('web_status', 'N/A'), + 'timestamp': timezone.now().isoformat(), + 'details_url': settings.WATCHER_URL + app_config_slack['url_suffix'], + 'app_name': 'website_monitoring' + } + + email_words = context_data.get('alert', []) + website_url = site.domain_name + alert_id = alert_data.get('id', '-') + email_body = get_site_monitoring_template(website_url, alert_id, alert_data) + print(f"alert_data: {alert_data}") + + + elif app_name == 'data_leak': + alert = context_data.get('alert') + if not alert: + return + common_data = { + 'alert_pk': alert.pk, + 'keyword_name': alert.keyword.name, + 'url': alert.url, + 'content': alert.content[:200], + 'timestamp': alert.created_at.isoformat(), + 'details_url': settings.WATCHER_URL + app_config_slack['url_suffix'], + 'app_name': 'data_leak' + } + email_words = context_data.get('alert', []) + email_body = get_data_leak_template(alert) + + + elif app_name == 'dns_finder': + alert = context_data.get('alert') + if not alert: + print("Error: Alert object is None in context_data") + return + if not alert.dns_twisted: + print(f"Error: dns_twisted is missing in alert {alert.pk if alert.pk else 'Unknown'}") + return + if not alert.dns_twisted.domain_name: + print(f"Error: domain_name is missing in dns_twisted for alert {alert.pk if alert.pk else 'Unknown'}") + return + common_data = { + 'alert': alert, + 'details_url': settings.WATCHER_URL + app_config_slack['url_suffix'], + 'app_name': 'dns_finder' + } + email_words = context_data.get('alert', []) + email_body = get_dns_finder_template(alert) + + send_notification( + channel="slack", + content_template=app_config_slack['content_template'], + subscribers_filter={'slack': True}, + send_func=lambda content: send_slack_message(content, app_config_slack['channel'], app_name), + **common_data + ) + + citadel_title = app_config_citadel.get('title', f"[{app_name.upper()}] Incident Alert") + send_notification( + channel="citadel", + content_template=app_config_citadel['content_template'], + subscribers_filter={'citadel': True}, + send_func=lambda content: send_citadel_message( + content={ + "msgtype": "m.text", + "format": "org.matrix.custom.html", + "body": citadel_title + " - New Incident Alert", + "formatted_body": content.replace('\n', 'Dear team,
+ +A new data leakage alert has been detected: for the keyword:
+ +Keyword: """ + str(alert.keyword) + """
+Source: """ + str(alert.url) + """
+You can check more details here.
+ +Kind Regards,
+ Watcher
[""" + str(settings.EMAIL_CLASSIFICATION) + """]
+ + + """ + return body diff --git a/Watcher/Watcher/dns_finder/mail_template/default_template_cert_transparency.py b/Watcher/Watcher/common/mail_template/dns_finder_cert_transparency.py similarity index 99% rename from Watcher/Watcher/dns_finder/mail_template/default_template_cert_transparency.py rename to Watcher/Watcher/common/mail_template/dns_finder_cert_transparency.py index e60cd56..10802bc 100644 --- a/Watcher/Watcher/dns_finder/mail_template/default_template_cert_transparency.py +++ b/Watcher/Watcher/common/mail_template/dns_finder_cert_transparency.py @@ -1,7 +1,7 @@ from django.conf import settings -def get_cert_transparency_template(alert): +def get_dns_finder_cert_transparency_template(alert): body = """\ diff --git a/Watcher/Watcher/dns_finder/mail_template/group_template.py b/Watcher/Watcher/common/mail_template/dns_finder_group_template.py similarity index 99% rename from Watcher/Watcher/dns_finder/mail_template/group_template.py rename to Watcher/Watcher/common/mail_template/dns_finder_group_template.py index 0840c83..9cf7055 100644 --- a/Watcher/Watcher/dns_finder/mail_template/group_template.py +++ b/Watcher/Watcher/common/mail_template/dns_finder_group_template.py @@ -1,7 +1,7 @@ from django.conf import settings -def get_group_template(dns_monitored, alerts_number): +def get_dns_finder_group_template(dns_monitored, alerts_number): body = """\ diff --git a/Watcher/Watcher/common/mail_template/dns_finder_template.py b/Watcher/Watcher/common/mail_template/dns_finder_template.py new file mode 100644 index 0000000..70aff93 --- /dev/null +++ b/Watcher/Watcher/common/mail_template/dns_finder_template.py @@ -0,0 +1,166 @@ +from django.conf import settings + +def get_dns_finder_template(alert): + """ + Génère un email HTML pour une alerte DNS Finder. + + :param alert: Objet contenant les informations sur l'alerte. + :return: Contenu HTML de l'email. + """ + body = f"""\ + + + + + + +Dear team,
+A new Twisted DNS record has been detected:
+ +Domain Name: {alert.dns_twisted.domain_name}
+Asset Monitored: {alert.dns_twisted.dns_monitored}
+You can check more details here.
+ +Kind Regards,
+Watcher
+[{settings.EMAIL_CLASSIFICATION}]
+ + + """ + return body diff --git a/Watcher/Watcher/common/mail_template/site_monitoring_template.py b/Watcher/Watcher/common/mail_template/site_monitoring_template.py new file mode 100644 index 0000000..076f12c --- /dev/null +++ b/Watcher/Watcher/common/mail_template/site_monitoring_template.py @@ -0,0 +1,176 @@ +from django.conf import settings + + +def get_site_monitoring_template(website_status, website_url, alert_id): + """ + Génère le corps du mail HTML pour le monitoring de site avec les informations spécifiées. + + :param website_status: Le statut du site (OK, DOWN, etc.) + :param website_url: L'URL du site surveillé + :param alert_id: L'ID de l'alerte + :param ticket_id: L'ID du ticket associé + :return: Le corps du mail en HTML + """ + github_repo = "https://github.com/thalesgroup-cert/Watcher" + + alert_details = f""" +Domain name: {website_status}
+Type: {alert_id['type']}
+New IP: {alert_id['new_ip']}, {alert_id['new_ip_second']}
+Old IP: {alert_id['old_ip']}, {alert_id['old_ip_second']}
+ """ + + body = f"""\ + + + + + + +Dear team,
+ +The current status of the monitored website is as follows:
+ +You can check more details here.
+ +Kind Regards,
+ Watcher
[{settings.EMAIL_CLASSIFICATION}]
+ + + """ + return body diff --git a/Watcher/Watcher/common/mail_template/threats_watcher_template.py b/Watcher/Watcher/common/mail_template/threats_watcher_template.py new file mode 100644 index 0000000..49ef666 --- /dev/null +++ b/Watcher/Watcher/common/mail_template/threats_watcher_template.py @@ -0,0 +1,165 @@ +from django.conf import settings + + +def get_threats_watcher_template(words_occurrence, email_words): + github_repo = "https://github.com/thalesgroup-cert/Watcher" + body = """\ + + + + + + +Dear team,
+ +Please find below trendy word(s) that match at least """ + str(words_occurrence) + """ times:
+ +".join(email_words) + """ +
You can check more details here.
+ +Kind Regards,
+ Watcher
[""" + str(settings.EMAIL_CLASSIFICATION) + """]
+ + + """ + return body \ No newline at end of file diff --git a/Watcher/Watcher/common/migrations/__init__.py b/Watcher/Watcher/common/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Watcher/Watcher/common/models.py b/Watcher/Watcher/common/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/Watcher/Watcher/common/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/Watcher/Watcher/common/tests.py b/Watcher/Watcher/common/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Watcher/Watcher/common/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Watcher/Watcher/common/utils/send_citadel_messages.py b/Watcher/Watcher/common/utils/send_citadel_messages.py new file mode 100644 index 0000000..5f1c5f6 --- /dev/null +++ b/Watcher/Watcher/common/utils/send_citadel_messages.py @@ -0,0 +1,36 @@ +import requests +from django.conf import settings +from django.utils import timezone + +def send_citadel_message(content, room_id, app_name): + """ + Sends a message to the specified Citadel room. + + Args: + content (dict): The content of the message (must contain 'msgtype' and 'body'). + room_id (str): The ID of the Citadel room to send the message to. + """ + + if not settings.CITADEL_API_TOKEN or not settings.CITADEL_ROOM_ID: + print(f"{str(timezone.now())} - No configuration for Citadel, not notifications sending.") + return + + url = f"{settings.CITADEL_URL}/_matrix/client/r0/rooms/{room_id}/send/m.room.message?access_token={settings.CITADEL_API_TOKEN}" + + headers = { + 'Content-Type': 'application/json', + } + + payload = { + 'msgtype': content.get('msgtype', 'm.text'), + 'body': content.get('body', ''), + 'format': content.get('format', None), + 'formatted_body': content.get('formatted_body', None) + } + + response = requests.post(url, headers=headers, json=payload) + + if response.status_code == 200: + print(f"{str(timezone.now())} - Message sent to Citadel successfully for {app_name}.") + else: + print(f"{str(timezone.now())} - Failed to send message to Citadel: {response.status_code} - {response.text}") \ No newline at end of file diff --git a/Watcher/Watcher/common/utils/send_email_notifications.py b/Watcher/Watcher/common/utils/send_email_notifications.py new file mode 100644 index 0000000..b83e937 --- /dev/null +++ b/Watcher/Watcher/common/utils/send_email_notifications.py @@ -0,0 +1,36 @@ +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import smtplib +from django.conf import settings +from datetime import datetime + + +def send_email_notifications(subject, body, emails_to, app_name): + """ + Send e-mail notifications to the list of recipients. + + Args: + subject (str): Subject of the email. + body (str): HTML body content of the email. + emails_to (list): List of recipient emails. + app_name (str): Name of the application sending the notification. + """ + + emails_to = [str(email) for email in emails_to if isinstance(email, str) or hasattr(email, 'email')] + + try: + msg = MIMEMultipart() + msg['From'] = settings.EMAIL_FROM + msg['To'] = ','.join(emails_to) + msg['Subject'] = subject + msg.attach(MIMEText(body, 'html', _charset='utf-8')) + text = msg.as_string() + + smtp_server = smtplib.SMTP(settings.SMTP_SERVER) + smtp_server.sendmail(settings.EMAIL_FROM, emails_to, text) + smtp_server.quit() + + print(f"{datetime.now()} - Email sent successfully for {app_name}.") + + except Exception as e: + print(f"{datetime.now()} - Failed to send email: {str(e)}") diff --git a/Watcher/Watcher/common/utils/send_slack_messages.py b/Watcher/Watcher/common/utils/send_slack_messages.py new file mode 100644 index 0000000..bb9f9e1 --- /dev/null +++ b/Watcher/Watcher/common/utils/send_slack_messages.py @@ -0,0 +1,35 @@ +import requests +from django.conf import settings +from django.utils import timezone + +def send_slack_message(content, channel, app_name): + """ + Sends a message to the specified Slack channel. + + Args: + content (str): The content of the message to send. + channel (str): The Slack channel where the message will be sent. + """ + + # if not settings.SLACK_API_TOKEN or not settings.SLACK_CHANNEL: + if not settings.SLACK_API_TOKEN or not settings.SLACK_CHANNEL: + print("No configuration for Slack, not notifications sending") + return + + url = 'https://slack.com/api/chat.postMessage' + headers = { + 'Authorization': f'Bearer {settings.SLACK_API_TOKEN}', + 'Content-Type': 'application/json', + } + + payload = { + 'channel': channel, + 'text': content, + } + + response = requests.post(url, headers=headers, json=payload) + + if response.status_code == 200: + print(f"{str(timezone.now())} - Message sent to Slack successfully for {app_name}.") + else: + print(f"{str(timezone.now())} - Failed to send message to Slack: {response.status_code} - {response.text}") diff --git a/Watcher/Watcher/common/utils/send_thehive_alerts.py b/Watcher/Watcher/common/utils/send_thehive_alerts.py new file mode 100644 index 0000000..5991ad9 --- /dev/null +++ b/Watcher/Watcher/common/utils/send_thehive_alerts.py @@ -0,0 +1,111 @@ +import requests +from django.conf import settings +from django.utils import timezone +from common.utils.update_thehive import ( + handle_alert_or_case, + create_new_alert +) +from site_monitoring.models import Site +from common.core import generate_ref + + +def get_ticket_id_for_domain(domain_name): + """ + Retrieve the ticket_id for a monitored domain from the Watcher database. + + :param domain_name: The domain name for which the ticket_id is being fetched. + :return: ticket_id if domain is found, otherwise None. + :rtype: str or None + """ + try: + domain = Site.objects.get(domain_name=domain_name) + return domain.ticket_id + except Site.DoesNotExist: + print(f"{timezone.now()} - Domain {domain_name} not found in Watcher database.") + return None + except Exception as e: + print(f"{timezone.now()} - Error while retrieving ticket_id for domain {domain_name}: {e}") + return None + + +def post_to_thehive(url, data, headers, proxies): + """ + Send a POST request to TheHive API and handle errors. + + :param url: The URL for TheHive API. + :param data: The data to send in the POST request. + :param headers: Headers to include in the request. + :param proxies: Proxies to use for the request. + :return: Response from TheHive as a JSON object if successful, None if failed. + :rtype: dict or None + """ + try: + response = requests.post(url, headers=headers, json=data, verify=False, proxies=proxies) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return None + + +def send_thehive_alert(title, description, severity, tags, app_name, domain_name, observables=None, customFields=None, thehive_url=None, api_key=None): + """ + Send or update an alert in TheHive based on the application and ticket_id. + + :param title: The title of the alert. + :param description: The description of the alert. + :param severity: The severity level of the alert (integer). + :param tags: A list of tags associated with the alert. + :param app_name: The application triggering the alert (e.g., 'website_monitoring'). + :param domain_name: The domain name related to the alert (used for ticket_id lookup). + :param observables: Any observables (default is None). + :param customFields: Custom fields for the alert (default is None). + :param thehive_url: The URL of TheHive instance (default uses settings). + :param api_key: The API key for authenticating with TheHive (default uses settings). + :return: None + :rtype: None + """ + thehive_url = thehive_url or settings.THE_HIVE_URL + api_key = api_key or settings.THEHIVE_API_KEY + + ticket_id = None + + if app_name == 'website_monitoring' and domain_name: + ticket_id = get_ticket_id_for_domain(domain_name) + + if ticket_id is None: + ticket_id = generate_ref() + + if app_name == 'website_monitoring' and not ticket_id: + return + + # Handle the alert for 'website_monitoring' if ticket_id is found + if app_name == 'website_monitoring': + handle_alert_or_case( + ticket_id=ticket_id, + title=title, + description=description, + severity=severity, + tags=tags, + app_name=app_name, + observables=observables, + customFields=customFields, + comment=f"New alert created by {app_name} on {timezone.now()}: New information reported and initial context added.", + thehive_url=thehive_url, + api_key=api_key + ) + + # Create a new alert if the application is not 'website_monitoring' + else: + create_new_alert( + ticket_id=ticket_id, + title=title, + description=description, + severity=severity, + tags=tags, + app_name=app_name, + observables=observables, + customFields=customFields, + comment=f"New alert created by {app_name} on {timezone.now()}: New information reported and initial context added.", + thehive_url=thehive_url, + api_key=api_key + ) diff --git a/Watcher/Watcher/common/utils/update_thehive.py b/Watcher/Watcher/common/utils/update_thehive.py new file mode 100644 index 0000000..3e1ef87 --- /dev/null +++ b/Watcher/Watcher/common/utils/update_thehive.py @@ -0,0 +1,238 @@ +import requests +from django.utils import timezone +from common.core import generate_ref +from django.conf import settings + + +def search_thehive_for_ticket_id(watcher_id, thehive_url, api_key, item_type=None): + """ + Search TheHive for an item (alert or case) based on customFields.
- Data Leak: Alert - """ - body += "#" + str( - alert.pk) + """ -- |
-
-
|
-
-
|
-
[""" - body += str( - settings.EMAIL_CLASSIFICATION) + """]
- DNS Finder: Alert - """ - body += "#" + str( - alert.pk) + """ -- |
-
-
|
-
-
|
-
[""" - body += str( - settings.EMAIL_CLASSIFICATION) + """]