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

" + "\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', '
') + }, + room_id=app_config_citadel['citadel_room_id'], + app_name=common_data.get('app_name') + ), + title=citadel_title, + **common_data + ) + + if app_config_thehive: + formatted_title = app_config_thehive['title'].format(**common_data) + + domain_name = common_data.get('domain_name') + if domain_name: + try: + site = Site.objects.get(domain_name=domain_name) + except Site.DoesNotExist: + print(f"{timezone.now()} - Site with domain_name {domain_name} not found.") + return + + ticket_id = site.ticket_id + + send_notification( + channel="thehive", + content_template=app_config_thehive['description_template'], + subscribers_filter={'thehive': True}, + send_func=lambda content: send_thehive_alert( + title=formatted_title, + description=content, + severity=app_config_thehive['severity'], + tags=app_config_thehive['tags'], + customFields=app_config_thehive.get('customFields'), + app_name=app_name, + domain_name=site.domain_name, + observables=observables + ), + **common_data + ) + else: + if subscribers.filter(thehive=True).exists(): + send_thehive_alert( + title=formatted_title, + description=app_config_thehive['description_template'].format(**common_data), + severity=app_config_thehive['severity'], + tags=app_config_thehive['tags'], + app_name=app_name, + domain_name=None, + observables=observables, + customFields=app_config_thehive.get('customFields'), + thehive_url=thehive_url, + api_key=api_key + ) + + if app_config_email: + email_list = [subscriber.user_rec.email for subscriber in subscribers.filter(email=True)] + + if email_list: + email_subject = app_config_email['subject'].format(**common_data) + send_email_notifications(email_subject, email_body, email_list, app_name) + else: + pass + + if app_config_email: + # Récupération des abonnés ayant une adresse email + email_list = [subscriber.user_rec.email for subscriber in subscribers.filter(email=True)] + + except Exception as e: + print(f"Error sending notifications for {app_name}: {str(e)}") \ No newline at end of file diff --git a/Watcher/Watcher/data_leak/mail_template/group_template.py b/Watcher/Watcher/common/mail_template/data_leak_group_template.py similarity index 99% rename from Watcher/Watcher/data_leak/mail_template/group_template.py rename to Watcher/Watcher/common/mail_template/data_leak_group_template.py index 9fea5fa..ec8b669 100644 --- a/Watcher/Watcher/data_leak/mail_template/group_template.py +++ b/Watcher/Watcher/common/mail_template/data_leak_group_template.py @@ -1,7 +1,7 @@ from django.conf import settings -def get_group_template(keyword, alerts_number): +def get_data_leak_group_template(keyword, alerts_number): body = """\ diff --git a/Watcher/Watcher/common/mail_template/data_leak_template.py b/Watcher/Watcher/common/mail_template/data_leak_template.py new file mode 100644 index 0000000..f4ed007 --- /dev/null +++ b/Watcher/Watcher/common/mail_template/data_leak_template.py @@ -0,0 +1,160 @@ +from django.conf import settings + + +def get_data_leak_template(alert): + github_repo = "https://github.com/thalesgroup-cert/Watcher" + body = """\ + + + + + + +
+ +
+ Watcher Logo +

Data Leak Alert #""" + str(alert.pk) + """

+
+ + +
+

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"""\ + + + + + + +
+ +
+ Watcher Logo +

DNS Finder: Alert #{alert.pk}

+
+ + +
+

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"""\ + + + + + + +
+ +
+ Watcher Logo +

Website Monitoring Alert

+
+ + +
+

Dear team,

+ +

The current status of the monitored website is as follows:

+ +
+ {alert_details} +
+ +

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 = """\ + + + + + + +
+ +
+ Threats Watcher Logo +

Threats Watcher

+
+ + +
+

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.. + + :param watcher_id: The ID to search for. + :param thehive_url: The URL of TheHive instance. + :param api_key: The API key for authenticating with TheHive. + :param item_type: The type of item to search for ("alert" or "case"). + :return: The type and the first result if found, else None, None. + """ + headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'} + proxies = {"http": None, "https": None} + + query = { + "query": [ + {"_name": f"list{item_type.capitalize()}"}, + {"_name": "filter", "_and": [ + {"_field": f"customFields.{settings.THE_HIVE_CUSTOM_FIELD}.string", "_value": watcher_id} + ]} + ] + } if item_type else None + + if not query: + return None, None + + url = f"{thehive_url}/api/v1/query" + try: + response = requests.post(url, headers=headers, json=query, verify=False, proxies=proxies) + response.raise_for_status() + results = response.json() + + if results: + return item_type, results[0] + except requests.exceptions.RequestException as e: + print(f"{timezone.now()} - Error searching for {item_type} with {settings.THE_HIVE_CUSTOM_FIELD} {watcher_id}: {e}") + + return None, None + + +def add_observables_to_item(item_type, item_id, observables_data, thehive_url, api_key): + """ + Add observables to an existing item (alert or case) in TheHive. + + :param item_type: The type of item ("alert" or "case"). + :param item_id: The ID of the item to add observables to. + :param observables_data: A list of observables to add. + :param thehive_url: The URL of TheHive instance. + :param api_key: The API key for authenticating with TheHive. + :return: None + """ + url = f"{thehive_url}/api/v1/{item_type}/{item_id}/observable" + headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'} + proxies = {"http": None, "https": None} + + added_observables = [] + + for observable in observables_data: + try: + # Send a POST request to add each observable to the item + response = requests.post(url, headers=headers, json=observable, verify=False, proxies=proxies) + response.raise_for_status() + added_observables.append(observable['data']) # Add the observable data to the success list + except requests.exceptions.RequestException as e: + print(f"{timezone.now()} - Error while adding observable {observable['data']}: {e}") + + +def add_comment_to_item(item_type, item_id, comment, thehive_url, api_key): + """ + Add a comment to an existing item (alert or case) in TheHive. + + :param item_type: The type of item ("alert" or "case"). + :param item_id: The ID of the item to add a comment to. + :param comment: The comment to add to the item. + :param thehive_url: The URL of TheHive instance. + :param api_key: The API key for authenticating with TheHive. + :return: None + """ + url = f"{thehive_url}/api/v1/{item_type}/{item_id}/comment" + headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'} + proxies = {"http": None, "https": None} + data = {"message": comment} + + try: + response = requests.post(url, headers=headers, json=data, verify=False, proxies=proxies) + response.raise_for_status() + except requests.exceptions.RequestException as e: + print(f"{timezone.now()} - Error while adding comment: {e}") + + +def create_observables(observables): + """ + Format observables for TheHive API. + + :param observables: A list of raw observables. + :return: A list of formatted observables ready for TheHive. + :rtype: list + """ + observables_data = [] + for obs in observables: + observable_data = { + "dataType": obs['dataType'], + "data": obs['data'], + "message": f"Observable added: {obs['data']} on {timezone.now()}", + "ioc": True, + "sighted": True, + "tlp": 2 + } + observables_data.append(observable_data) + return observables_data + + +def update_existing_alert_case(item_type, existing_item, observables, comment, thehive_url, api_key): + """ + Update an existing alert or case by adding observables and a comment. + + :param item_type: The type of item ("alert" or "case"). + :param existing_item: The existing item to update. + :param observables: The observables to add to the item. + :param comment: The comment to add to the item. + :param thehive_url: The URL of TheHive instance. + :param api_key: The API key for authenticating with TheHive. + :return: None + """ + item_id = existing_item["_id"] + + if observables: + observables_data = create_observables(observables) + print(f"{timezone.now()} - Adding observables to {item_type} with ID {item_id}...") + add_observables_to_item(item_type, item_id, observables_data, thehive_url, api_key) + + if comment: + print(f"{timezone.now()} - Adding comment to {item_type} with ID {item_id}...") + add_comment_to_item(item_type, item_id, comment, thehive_url, api_key) + + +def create_new_alert(ticket_id, title, description, severity, tags, app_name, observables, customFields, comment, thehive_url, api_key): + """ + Create a new alert in TheHive with the provided details. + + :param ticket_id: The ticket ID for the new alert. + :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. + :param observables: A list of observables to associate with the alert. + :param customFields: Custom fields to include in the alert. + :param comment: The comment to add to the alert. + :param thehive_url: The URL of TheHive instance. + :param api_key: The API key for authenticating with TheHive. + :return: The created alert if successful, None otherwise. + :rtype: dict or None + """ + if ticket_id is None: + ticket_id = generate_ref() + + alert_data = { + "title": title, + "description": description, + "severity": severity, + "tags": tags, + "type": app_name, + "source": "watcher", + "sourceRef": ticket_id, + "customFields": customFields or { + settings.THE_HIVE_CUSTOM_FIELD: {"string": ticket_id}, + "email-sender": {"string": settings.THE_HIVE_EMAIL_SENDER} + } + } + + headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'} + proxies = {"http": None, "https": None} + + url = f"{thehive_url}/api/v1/alert" + try: + response = requests.post(url, headers=headers, json=alert_data, verify=False, proxies=proxies) + response.raise_for_status() + alert = response.json() + alert_id = alert.get('_id') + print(f"{timezone.now()} - Alert created successfully with ID: {alert_id}") + + if observables: + observables_data = create_observables(observables) + add_observables_to_item("alert", alert_id, observables_data, thehive_url, api_key) + + if comment: + add_comment_to_item("alert", alert_id, comment, thehive_url, api_key) + + return alert + + except requests.exceptions.RequestException as e: + print(f"{timezone.now()} - Failed to create alert: {e}") + return None + + +def handle_alert_or_case(ticket_id, observables, comment, title, description, severity, tags, app_name, customFields, thehive_url, api_key): + """ + Handle the creation or updating of alerts and cases in TheHive. + + :param ticket_id: The ID of the ticket (can be used to search for existing alerts/cases). + :param observables: A list of observables to add to the item. + :param comment: A comment to add to the item. + :param title: The title for the alert. + :param description: The description for the alert. + :param severity: The severity of the alert (integer). + :param tags: A list of tags for the alert. + :param app_name: The name of the application triggering the alert. + :param customFields: Custom fields to be included in the alert. + :param thehive_url: The URL of TheHive. + :param api_key: The API key for authentication. + :return: None + """ + if app_name != 'website_monitoring': + print(f"{timezone.now()} - Unsupported application: {app_name}.") + return + + # Search in TheHive by customFields. + alert_type, alert_item = search_thehive_for_ticket_id(ticket_id, thehive_url, api_key, item_type="alert") + case_type, case_item = search_thehive_for_ticket_id(ticket_id, thehive_url, api_key, item_type="case") + + if case_item: + print(f"{timezone.now()} - Case found for {settings.THE_HIVE_CUSTOM_FIELD} {ticket_id}. Updating...") + update_existing_alert_case("case", case_item, observables, comment, thehive_url, api_key) + elif alert_item: + print(f"{timezone.now()} - Alert found for {settings.THE_HIVE_CUSTOM_FIELD} {ticket_id}. Updating...") + update_existing_alert_case("alert", alert_item, observables, comment, thehive_url, api_key) + 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=comment, thehive_url=thehive_url, api_key=api_key + ) \ No newline at end of file diff --git a/Watcher/Watcher/common/views.py b/Watcher/Watcher/common/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/Watcher/Watcher/common/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/Watcher/Watcher/data_leak/admin.py b/Watcher/Watcher/data_leak/admin.py index c438565..0cb12fc 100644 --- a/Watcher/Watcher/data_leak/admin.py +++ b/Watcher/Watcher/data_leak/admin.py @@ -16,9 +16,18 @@ def __new__(cls, *args, **kwargs): @admin.register(Subscriber) class Subscriber(admin.ModelAdmin): - list_display = ['user_rec', 'created_at'] - list_filter = ['created_at'] - search_fields = ['user_rec'] + list_display = ('user_rec', 'created_at', 'email', 'thehive', 'slack', 'citadel') + list_filter = ('email', 'thehive', 'slack', 'citadel') + search_fields = ('user_rec__username',) + fieldsets = ( + (None, { + 'fields': ('user_rec', 'created_at') + }), + ('Notification Channels', { + 'fields': ('email', 'thehive', 'slack', 'citadel'), + 'description': "Select the notification channels for this subscriber." + }), + ) class AlertResource(resources.ModelResource): diff --git a/Watcher/Watcher/data_leak/core.py b/Watcher/Watcher/data_leak/core.py index c6ebb4e..58a4ff4 100644 --- a/Watcher/Watcher/data_leak/core.py +++ b/Watcher/Watcher/data_leak/core.py @@ -8,12 +8,9 @@ import tzlocal from django.conf import settings from django.db.models.functions import Length -from .mail_template.default_template import get_template -from .mail_template.group_template import get_group_template -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText from json.decoder import JSONDecodeError -import smtplib +from common.core import send_app_specific_notifications +from django.db.models import Q def start_scheduler(): @@ -26,7 +23,6 @@ def start_scheduler(): scheduler.add_job(main_data_leak, 'cron', day_of_week='mon-sun', minute='*/5', id='week_job', max_instances=10, replace_existing=True) - scheduler.add_job(cleanup, 'cron', day_of_week='mon-sun', hour='*/2', id='clean', replace_existing=True) scheduler.start() @@ -233,12 +229,8 @@ def check_keywords(keywords): for result in results: print(str(timezone.now()) + " - Create alert for: ", keyword, "url: ", result) alert = Alert.objects.create(keyword=Keyword.objects.get(name=keyword), url=result) - # limiting the number of specific email per alert - if len(results) < 6: - send_email(alert) - # if there is too many alerts, we send a group email - if len(results) >= 6: - send_group_email(keyword, len(results)) + send_data_leak_notifications(alert) + # now we check Pastebin for new pastes result = check_pastebin(keywords) @@ -248,75 +240,25 @@ def check_keywords(keywords): print(str(timezone.now()) + " - Create alert for: ", keyword, "url: ", url) alert = Alert.objects.create(keyword=Keyword.objects.get(name=keyword), url=url, content=paste_content_hits[url]) - send_email(alert) + send_data_leak_notifications(alert) -def send_email(alert): +def send_data_leak_notifications(alert): """ - Send e-mail alert. - - :param alert: Alert object. + Sends notifications to Slack, Citadel, or TheHive based on data_leak. + + :param alert: Alert Object. """ - emails_to = list() - # Get all subscribers email - for subscriber in Subscriber.objects.all(): - emails_to.append(subscriber.user_rec.email) - - # If there is at least one subscriber - if len(emails_to) > 0: - try: - msg = MIMEMultipart() - msg['From'] = settings.EMAIL_FROM - msg['To'] = ','.join(emails_to) - msg['Subject'] = str("[ALERT #" + str(alert.pk) + "] Data Leak") - body = get_template(alert) - 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() - - except Exception as e: - # Print any error messages to stdout - print(str(timezone.now()) + " - Email Error : ", e) - finally: - for email in emails_to: - print(str(timezone.now()) + " - Email sent to ", email) - else: - print(str(timezone.now()) + " - No subscriber, no email sent.") + subscribers = Subscriber.objects.filter( + (Q(slack=True) | Q(citadel=True) | Q(thehive=True) | Q(email=True)) + ) + if not subscribers.exists(): + print(f"{timezone.now()} - No subscribers for Data Leak, no message sent.") + return -def send_group_email(keyword, alerts_number): - """ - Send group e-mail for a specific keyword. + context_data = { + 'alert': alert + } - :param keyword: Matched Keyword. - :param alerts_number: Number of alerts. - """ - emails_to = list() - # Get all subscribers email - for subscriber in Subscriber.objects.all(): - emails_to.append(subscriber.user_rec.email) - - # If there is at least one subscriber - if len(emails_to) > 0: - try: - msg = MIMEMultipart() - msg['From'] = settings.EMAIL_FROM - msg['To'] = ','.join(emails_to) - msg['Subject'] = str("[" + str(alerts_number) + " ALERTS] Data Leak") - body = get_group_template(keyword, alerts_number) - 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() - - except Exception as e: - # Print any error messages to stdout - print(str(timezone.now()) + " - Email Error : ", e) - finally: - for email in emails_to: - print(str(timezone.now()) + " - Email sent to ", email) - else: - print(str(timezone.now()) + " - No subscriber, no email sent.") + send_app_specific_notifications('data_leak', context_data, subscribers) \ No newline at end of file diff --git a/Watcher/Watcher/data_leak/mail_template/default_template.py b/Watcher/Watcher/data_leak/mail_template/default_template.py deleted file mode 100644 index b2d2893..0000000 --- a/Watcher/Watcher/data_leak/mail_template/default_template.py +++ /dev/null @@ -1,194 +0,0 @@ -from django.conf import settings - - -def get_template(alert): - body = """\ - - - - - - - - - - - - -
-

Data Leak: Alert - """ - body += "#" + str( - alert.pk) + """ -

-
- - - - - - -
- - - - - - - - - - - - - - -

-
-

Dear team,

-

New Data Leakage Alert for - - """ - body += str( - alert.keyword) + """ keyword:

Source: - """ - body += str( - alert.url) + """

Details here.

-

- Best Regards, -

-

Watcher
-
-

-

-
-
- - - - - - - - - - - - -

[""" - body += str( - settings.EMAIL_CLASSIFICATION) + """]


- - - """ - return body diff --git a/Watcher/Watcher/data_leak/migrations/0011_alter_subscriber_options_subscriber_citadel_and_more.py b/Watcher/Watcher/data_leak/migrations/0011_alter_subscriber_options_subscriber_citadel_and_more.py new file mode 100644 index 0000000..8bdf836 --- /dev/null +++ b/Watcher/Watcher/data_leak/migrations/0011_alter_subscriber_options_subscriber_citadel_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.0.9 on 2024-11-29 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('data_leak', '0010_auto_20210517_0956'), + ] + + operations = [ + migrations.AlterModelOptions( + name='subscriber', + options={'verbose_name_plural': 'subscribers'}, + ), + migrations.AddField( + model_name='subscriber', + name='citadel', + field=models.BooleanField(default=False, verbose_name='Citadel'), + ), + migrations.AddField( + model_name='subscriber', + name='email', + field=models.BooleanField(default=False, verbose_name='E-mail'), + ), + migrations.AddField( + model_name='subscriber', + name='slack', + field=models.BooleanField(default=False, verbose_name='Slack'), + ), + migrations.AddField( + model_name='subscriber', + name='thehive', + field=models.BooleanField(default=False, verbose_name='TheHive'), + ), + ] diff --git a/Watcher/Watcher/data_leak/models.py b/Watcher/Watcher/data_leak/models.py index 884702c..4181f65 100644 --- a/Watcher/Watcher/data_leak/models.py +++ b/Watcher/Watcher/data_leak/models.py @@ -49,7 +49,18 @@ def __str__(self): class Subscriber(models.Model): """ - List of the email alert subscriber(s). + List of the alert subscriber(s). """ user_rec = models.ForeignKey(User, on_delete=models.CASCADE, related_name='data_leak') created_at = models.DateTimeField(default=timezone.now) + + email = models.BooleanField(default=False, verbose_name="E-mail") + thehive = models.BooleanField(default=False, verbose_name="TheHive") + slack = models.BooleanField(default=False, verbose_name="Slack") + citadel = models.BooleanField(default=False, verbose_name="Citadel") + + class Meta: + verbose_name_plural = 'subscribers' + + def __str__(self): + return f'{self.user_rec.username} - {self.created_at}' diff --git a/Watcher/Watcher/dns_finder/admin.py b/Watcher/Watcher/dns_finder/admin.py index 99f6e1e..4172a0b 100644 --- a/Watcher/Watcher/dns_finder/admin.py +++ b/Watcher/Watcher/dns_finder/admin.py @@ -99,6 +99,15 @@ def has_add_permission(self, request): @admin.register(Subscriber) class Subscriber(admin.ModelAdmin): - list_display = ['user_rec', 'created_at'] - list_filter = ['created_at'] - search_fields = ['user_rec'] + list_display = ('user_rec', 'created_at', 'email', 'thehive', 'slack', 'citadel') + list_filter = ('email', 'thehive', 'slack', 'citadel') + search_fields = ('user_rec__username',) + fieldsets = ( + (None, { + 'fields': ('user_rec', 'created_at') + }), + ('Notification Channels', { + 'fields': ('email', 'thehive', 'slack', 'citadel'), + 'description': "Select the notification channels for this subscriber." + }), + ) diff --git a/Watcher/Watcher/dns_finder/api.py b/Watcher/Watcher/dns_finder/api.py index 38852e2..217447e 100644 --- a/Watcher/Watcher/dns_finder/api.py +++ b/Watcher/Watcher/dns_finder/api.py @@ -1,6 +1,6 @@ from .models import DnsMonitored, DnsTwisted, Alert, KeywordMonitored from rest_framework import viewsets, permissions -from .serializers import AlertSerializer, DnsMonitoredSerializer, DnsTwistedSerializer, ThehiveSerializer, \ +from .serializers import AlertSerializer, DnsMonitoredSerializer, DnsTwistedSerializer, \ MISPSerializer, KeywordMonitoredSerializer @@ -53,14 +53,6 @@ def has_permission(self, request, view): return has_permission -# Thehive Viewset -class ThehiveViewSet(viewsets.ModelViewSet): - permission_classes = [ - ExportPermission - ] - serializer_class = ThehiveSerializer - - # MISP Viewset class MISPViewSet(viewsets.ModelViewSet): permission_classes = [ diff --git a/Watcher/Watcher/dns_finder/core.py b/Watcher/Watcher/dns_finder/core.py index b858d1a..9c3bf84 100644 --- a/Watcher/Watcher/dns_finder/core.py +++ b/Watcher/Watcher/dns_finder/core.py @@ -5,16 +5,12 @@ import json from django.utils import timezone from django.conf import settings -from .mail_template.default_template import get_template -from .mail_template.default_template_cert_transparency import get_cert_transparency_template -from .mail_template.group_template import get_group_template from apscheduler.schedulers.background import BackgroundScheduler import tzlocal from .models import Alert, DnsMonitored, DnsTwisted, Subscriber, KeywordMonitored -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -import smtplib import certstream +from common.core import send_app_specific_notifications +from django.db.models import Q def start_scheduler(): @@ -31,6 +27,7 @@ def start_scheduler(): id='main_certificate_transparency', max_instances=2, replace_existing=True) + scheduler.start() @@ -62,7 +59,7 @@ def print_callback(message, context): print(str(timezone.now()) + " - " + "Keyword", keyword_monitored.name, "detected in :", domain) dns_twisted = DnsTwisted.objects.create(domain_name=domain, keyword_monitored=keyword_monitored) alert = Alert.objects.create(dns_twisted=dns_twisted) - send_email_cert_transparency(alert) + send_dns_finder_notifications(alert) def main_certificate_transparency(): @@ -135,119 +132,36 @@ def check_dnstwist(dns_monitored): fuzzer=twisted_website_dict['fuzzer']) alert = Alert.objects.create(dns_twisted=dns_twisted) alerts_list.append(alert) - # Send email alerts - if len(alerts_list) < 6: - for alert in alerts_list: - send_email(alert) - if len(alerts_list) >= 6: - send_group_email(dns_monitored, len(alerts_list)) + + for alert in alerts_list: + send_dns_finder_notifications(alert) + except ValueError: print('Decoding JSON has failed') print(str(timezone.now()) + " - " + "dnstwist: Successfully processed: ", dns_monitored.domain_name) -def send_email(alert): +def send_dns_finder_notifications(alert): """ - Send e-mail alert. - + Sends notifications to Slack, Citadel, or TheHive based on dns_finder. + :param alert: Alert Object. """ - emails_to = list() - # Get all subscribers email - for subscriber in Subscriber.objects.all(): - emails_to.append(subscriber.user_rec.email) + subscribers = Subscriber.objects.filter( + (Q(slack=True) | Q(citadel=True) | Q(thehive=True) | Q(email=True)) + ) - # If there is at least one subscriber - if len(emails_to) > 0: - try: - msg = MIMEMultipart() - msg['From'] = settings.EMAIL_FROM - msg['To'] = ','.join(emails_to) - msg['Subject'] = str("[ALERT #" + str(alert.pk) + "] DNS Finder") - body = get_template(alert) - 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() - - except Exception as e: - # Print any error messages to stdout - print(str(timezone.now()) + " - Email Error : ", e) - finally: - for email in emails_to: - print(str(timezone.now()) + " - Email sent to ", email) - else: - print(str(timezone.now()) + " - No subscriber, no email sent.") - - -def send_group_email(dns_monitored, alerts_number): - """ - Send group e-mail for a specific dns_monitored. + if not subscribers.exists(): + print(f"{timezone.now()} - No subscribers for DNS Finder, no message sent.") + return - :param dns_monitored: DnsMonitored Object. - :param alerts_number: Number of alerts. - """ - emails_to = list() - # Get all subscribers email - for subscriber in Subscriber.objects.all(): - emails_to.append(subscriber.user_rec.email) + if not alert or not alert.dns_twisted or not alert.dns_twisted.domain_name: + print(f"Error: Invalid alert object or missing domain_name in dns_twisted for alert: {alert}") + return - # If there is at least one subscriber - if len(emails_to) > 0: - try: - msg = MIMEMultipart() - msg['From'] = settings.EMAIL_FROM - msg['To'] = ','.join(emails_to) - msg['Subject'] = str("[" + str(alerts_number) + " ALERTS] DNS Finder") - body = get_group_template(dns_monitored, alerts_number) - 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() - - except Exception as e: - # Print any error messages to stdout - print(str(timezone.now()) + " - Email Error : ", e) - finally: - for email in emails_to: - print(str(timezone.now()) + " - Email sent to ", email) - else: - print(str(timezone.now()) + " - No subscriber, no email sent.") - - -def send_email_cert_transparency(alert): - """ - Send e-mail alert. - - :param alert: Alert Object. - """ - emails_to = list() - # Get all subscribers email - for subscriber in Subscriber.objects.all(): - emails_to.append(subscriber.user_rec.email) + context_data = { + 'alert': alert, + } - # If there is at least one subscriber - if len(emails_to) > 0: - try: - msg = MIMEMultipart() - msg['From'] = settings.EMAIL_FROM - msg['To'] = ','.join(emails_to) - msg['Subject'] = str("[ALERT #" + str(alert.pk) + "] DNS Finder") - body = get_cert_transparency_template(alert) - 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() - - except Exception as e: - # Print any error messages to stdout - print(str(timezone.now()) + " - Email Error : ", e) - finally: - for email in emails_to: - print(str(timezone.now()) + " - Email sent to ", email) - else: - print(str(timezone.now()) + " - No subscriber, no email sent.") + send_app_specific_notifications('dns_finder', context_data, subscribers) \ No newline at end of file diff --git a/Watcher/Watcher/dns_finder/mail_template/default_template.py b/Watcher/Watcher/dns_finder/mail_template/default_template.py deleted file mode 100644 index c4546bc..0000000 --- a/Watcher/Watcher/dns_finder/mail_template/default_template.py +++ /dev/null @@ -1,194 +0,0 @@ -from django.conf import settings - - -def get_template(alert): - body = """\ - - - - - - - - - - - - -
-

DNS Finder: Alert - """ - body += "#" + str( - alert.pk) + """ -

-
- - - - - - -
- - - - - - - - - - - - - - -

-
-

Dear team,

-

New Twisted DNS found: - - """ - body += str( - alert.dns_twisted.domain_name) + """

Asset: - """ - body += str( - alert.dns_twisted.dns_monitored) + """

Details here.

-

- Best Regards, -

-

Watcher
-
-

-

-
-
- - - - - - - - - - - - -

[""" - body += str( - settings.EMAIL_CLASSIFICATION) + """]


- - - """ - return body diff --git a/Watcher/Watcher/dns_finder/migrations/0008_alter_subscriber_options_and_more.py b/Watcher/Watcher/dns_finder/migrations/0008_alter_subscriber_options_and_more.py new file mode 100644 index 0000000..2d91fd0 --- /dev/null +++ b/Watcher/Watcher/dns_finder/migrations/0008_alter_subscriber_options_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.9 on 2024-11-29 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dns_finder', '0007_auto_20210517_0956'), + ] + + operations = [ + migrations.AlterModelOptions( + name='subscriber', + options={'verbose_name_plural': 'subscribers'}, + ), + migrations.RemoveField( + model_name='dnstwisted', + name='the_hive_case_id', + ), + migrations.AddField( + model_name='subscriber', + name='citadel', + field=models.BooleanField(default=False, verbose_name='Citadel'), + ), + migrations.AddField( + model_name='subscriber', + name='email', + field=models.BooleanField(default=False, verbose_name='E-mail'), + ), + migrations.AddField( + model_name='subscriber', + name='slack', + field=models.BooleanField(default=False, verbose_name='Slack'), + ), + migrations.AddField( + model_name='subscriber', + name='thehive', + field=models.BooleanField(default=False, verbose_name='TheHive'), + ), + ] diff --git a/Watcher/Watcher/dns_finder/models.py b/Watcher/Watcher/dns_finder/models.py index fffef52..802a8b0 100644 --- a/Watcher/Watcher/dns_finder/models.py +++ b/Watcher/Watcher/dns_finder/models.py @@ -43,7 +43,6 @@ class DnsTwisted(models.Model): dns_monitored = models.ForeignKey(DnsMonitored, on_delete=models.CASCADE, blank=True, null=True) keyword_monitored = models.ForeignKey(KeywordMonitored, on_delete=models.CASCADE, blank=True, null=True) fuzzer = models.CharField(max_length=100, blank=True, null=True) - the_hive_case_id = models.CharField(max_length=100, unique=True, blank=True, null=True) misp_event_id = models.IntegerField(unique=True, blank=True, null=True) created_at = models.DateTimeField(default=timezone.now) @@ -70,7 +69,18 @@ class Meta: class Subscriber(models.Model): """ - List of the email alert subscriber(s). + List of the alert subscriber(s). """ user_rec = models.ForeignKey(User, on_delete=models.CASCADE, related_name='dns_finder') created_at = models.DateTimeField(default=timezone.now) + + email = models.BooleanField(default=False, verbose_name="E-mail") + thehive = models.BooleanField(default=False, verbose_name="TheHive") + slack = models.BooleanField(default=False, verbose_name="Slack") + citadel = models.BooleanField(default=False, verbose_name="Citadel") + + class Meta: + verbose_name_plural = 'subscribers' + + def __str__(self): + return f'{self.user_rec.username} - {self.created_at}' diff --git a/Watcher/Watcher/dns_finder/serializers.py b/Watcher/Watcher/dns_finder/serializers.py index b1fed74..a8384fc 100644 --- a/Watcher/Watcher/dns_finder/serializers.py +++ b/Watcher/Watcher/dns_finder/serializers.py @@ -7,9 +7,6 @@ import requests from rest_framework.exceptions import NotFound, AuthenticationFailed -from thehive4py.api import TheHiveApi -from thehive4py.models import Case -from site_monitoring.thehive import create_observables, update_observables from pymisp import ExpandedPyMISP, MISPEvent from site_monitoring.misp import create_misp_tags, create_attributes, update_attributes @@ -124,91 +121,4 @@ def data(self): # just return success dictionary. you can change this to your need, but i dont think output should be user data after password change alert_id = self.validated_data['id'] alert = Alert.objects.get(pk=alert_id) - return {'id': alert_id, 'misp_event_id': DnsTwisted.objects.get(pk=alert.dns_twisted.pk).misp_event_id} - - -# Thehive Serializer -class ThehiveSerializer(serializers.Serializer): - id = serializers.IntegerField() - - def save(self): - alert_id = self.validated_data['id'] - alert = Alert.objects.get(pk=alert_id) - - dns_twisted = DnsTwisted.objects.get(pk=alert.dns_twisted.pk) - - # Getting IOCs related to the new twisted domain - if Site.objects.filter(domain_name=dns_twisted.domain_name): - already_in_monitoring = True - site = Site.objects.get(domain_name=dns_twisted.domain_name) - # Save the case id in database - DnsTwisted.objects.filter(pk=dns_twisted.pk).update(the_hive_case_id=site.the_hive_case_id) - else: - already_in_monitoring = False - site = Site.objects.create(domain_name=dns_twisted.domain_name, rtir=-999999999) - monitoring_init(site) - - site = Site.objects.get(pk=site.pk) - - # We now hav the IOCs related to the domain, we can remove it from monitoring - if not already_in_monitoring: - Site.objects.filter(pk=site.pk).delete() - - if site.the_hive_case_id is None: - site.the_hive_case_id = dns_twisted.the_hive_case_id - - # Test The Hive instance connection - try: - requests.get(settings.THE_HIVE_URL) - except requests.exceptions.SSLError as e: - print(str(timezone.now()) + " - ", e) - raise AuthenticationFailed("SSL Error: " + settings.THE_HIVE_URL) - except requests.exceptions.RequestException as e: - print(str(timezone.now()) + " - ", e) - raise NotFound("Not Found: " + settings.THE_HIVE_URL) - - hive_api = TheHiveApi(settings.THE_HIVE_URL, settings.THE_HIVE_KEY, cert=True) - - if site.the_hive_case_id is not None: - # If the case already exist, then we update IOCs - update_observables(hive_api, site) - else: - # If the case does not exist, then we create it - - # Prepare the case - case = Case(title='Suspicious domain name ' + site.domain_name, - owner=settings.THE_HIVE_CASE_ASSIGNEE, - severity=2, - tlp=2, - pap=2, - flag=False, - tags=['Watcher', 'Impersonation', 'Malicious Domain', 'Typosquatting'], - description='Suspicious domain name ' + site.domain_name) - - # Create the case - print(str(timezone.now()) + " - " + 'Create Case') - print('-----------------------------') - response = hive_api.create_case(case) - - if response.status_code == 201: - print(str(timezone.now()) + " - " + "OK") - case_id = response.json()['id'] - - # Save the case id in database - DnsTwisted.objects.filter(pk=dns_twisted.pk).update(the_hive_case_id=case_id) - if Site.objects.filter(domain_name=dns_twisted.domain_name): - Site.objects.filter(pk=site.pk).update(the_hive_case_id=case_id) - - # Create all IOCs observables - create_observables(hive_api, case_id, site) - else: - print(str(timezone.now()) + " - " + 'ko: {}/{}'.format(response.status_code, response.text)) - data = {'detail': response.json()['type'] + ": " + response.json()['message']} - raise serializers.ValidationError(data) - - @property - def data(self): - # just return success dictionary. you can change this to your need, but i dont think output should be user data after password change - alert_id = self.validated_data['id'] - alert = Alert.objects.get(pk=alert_id) - return {'id': alert_id, 'the_hive_case_id': DnsTwisted.objects.get(pk=alert.dns_twisted.pk).the_hive_case_id} + return {'id': alert_id, 'misp_event_id': DnsTwisted.objects.get(pk=alert.dns_twisted.pk).misp_event_id} \ No newline at end of file diff --git a/Watcher/Watcher/dns_finder/urls.py b/Watcher/Watcher/dns_finder/urls.py index 443e372..eaa044f 100644 --- a/Watcher/Watcher/dns_finder/urls.py +++ b/Watcher/Watcher/dns_finder/urls.py @@ -1,5 +1,5 @@ from rest_framework import routers -from .api import DnsMonitoredViewSet, DnsTwistedViewSet, AlertViewSet, ThehiveViewSet, MISPViewSet, KeywordMonitoredViewSet +from .api import DnsMonitoredViewSet, DnsTwistedViewSet, AlertViewSet, MISPViewSet, KeywordMonitoredViewSet from .core import start_scheduler @@ -8,7 +8,6 @@ router.register('api/dns_finder/keyword_monitored', KeywordMonitoredViewSet, 'keyword_monitored') router.register('api/dns_finder/dns_twisted', DnsTwistedViewSet, 'dns_twisted') router.register('api/dns_finder/alert', AlertViewSet, 'alert') -router.register('api/dns_finder/thehive', ThehiveViewSet, 'thehive') router.register('api/dns_finder/misp', MISPViewSet, 'misp') urlpatterns = router.urls diff --git a/Watcher/Watcher/frontend/src/actions/DnsFinder.js b/Watcher/Watcher/frontend/src/actions/DnsFinder.js index f008e8a..5efd902 100644 --- a/Watcher/Watcher/frontend/src/actions/DnsFinder.js +++ b/Watcher/Watcher/frontend/src/actions/DnsFinder.js @@ -7,7 +7,6 @@ import { ADD_DNS_MONITORED, PATCH_DNS_MONITORED, UPDATE_DNS_FINDER_ALERT, - EXPORT_THE_HIVE_DNS_FINDER, EXPORT_MISP_DNS_FINDER, GET_KEYWORD_MONITORED, DELETE_KEYWORD_MONITORED, @@ -173,22 +172,6 @@ export const updateAlertStatus = (id, status) => (dispatch, getState) => { ); }; -// EXPORT TO THE HIVE -export const exportToTheHive = (site) => (dispatch, getState) => { - axios - .post(`/api/dns_finder/thehive/`, site, tokenConfig(getState)) - .then(res => { - dispatch(createMessage({add: `Twisted DNS Exported to Thehive`})); - dispatch({ - type: EXPORT_THE_HIVE_DNS_FINDER, - payload: res.data - }); - }) - .catch(err => - dispatch(returnErrors(err.response.data, err.response.status)) - ); -}; - // EXPORT TO MISP export const exportToMISP = (site) => (dispatch, getState) => { axios diff --git a/Watcher/Watcher/frontend/src/actions/SiteMonitoring.js b/Watcher/Watcher/frontend/src/actions/SiteMonitoring.js index 32a9737..8b522f5 100755 --- a/Watcher/Watcher/frontend/src/actions/SiteMonitoring.js +++ b/Watcher/Watcher/frontend/src/actions/SiteMonitoring.js @@ -7,7 +7,6 @@ import { ADD_SITE, PATCH_SITE, UPDATE_SITE_ALERT, - EXPORT_THE_HIVE, EXPORT_MISP } from "./types"; import {createMessage, returnErrors} from "./messages"; @@ -107,22 +106,6 @@ export const updateSiteAlertStatus = (id, status) => (dispatch, getState) => { ); }; -// EXPORT TO THE HIVE -export const exportToTheHive = (site) => (dispatch, getState) => { - axios - .post(`/api/site_monitoring/thehive/`, site, tokenConfig(getState)) - .then(res => { - dispatch(createMessage({add: `Website Exported to Thehive`})); - dispatch({ - type: EXPORT_THE_HIVE, - payload: res.data - }); - }) - .catch(err => - dispatch(returnErrors(err.response.data, err.response.status)) - ); -}; - // EXPORT TO MISP export const exportToMISP = (site) => (dispatch, getState) => { axios diff --git a/Watcher/Watcher/frontend/src/components/DnsFinder/Alerts.js b/Watcher/Watcher/frontend/src/components/DnsFinder/Alerts.js index e4cfefa..ef2ac27 100755 --- a/Watcher/Watcher/frontend/src/components/DnsFinder/Alerts.js +++ b/Watcher/Watcher/frontend/src/components/DnsFinder/Alerts.js @@ -1,7 +1,7 @@ import React, {Component, Fragment} from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; -import {getAlerts, updateAlertStatus, exportToTheHive, exportToMISP} from "../../actions/DnsFinder"; +import {getAlerts, updateAlertStatus, exportToMISP} from "../../actions/DnsFinder"; import {addSite, getSites} from "../../actions/SiteMonitoring"; import Modal from "react-bootstrap/Modal"; import Button from "react-bootstrap/Button"; @@ -23,7 +23,6 @@ export class Alerts extends Component { id: 0, exportLoading: false, exportLoadingMISPTh: false, - theHiveCaseId: null, mispEventId: null, domainName: "" }; @@ -40,7 +39,6 @@ export class Alerts extends Component { alerts: PropTypes.array.isRequired, getAlerts: PropTypes.func.isRequired, updateAlertStatus: PropTypes.func.isRequired, - exportToTheHive: PropTypes.func.isRequired, exportToMISP: PropTypes.func.isRequired, auth: PropTypes.object.isRequired, error: PropTypes.object.isRequired, @@ -267,12 +265,11 @@ export class Alerts extends Component { return back; }; - displayExportModal = (id, domainName, theHiveCaseId, mispEventId) => { + displayExportModal = (id, domainName, mispEventId) => { this.setState({ showExportModal: true, id: id, domainName: domainName, - theHiveCaseId: theHiveCaseId, mispEventId: mispEventId }); }; @@ -285,21 +282,6 @@ export class Alerts extends Component { }); }; - let onSubmitTheHive; - onSubmitTheHive = e => { - e.preventDefault(); - const id = this.state.id; - const site = {id}; - - this.props.exportToTheHive(site); - this.setState({ - domainName: "", - id: 0, - exportLoadingMISPTh: id - }); - handleClose(); - }; - let onSubmitMisp; onSubmitMisp = e => { e.preventDefault(); @@ -315,14 +297,6 @@ export class Alerts extends Component { handleClose(); }; - const theHiveExportButton = ( - ); - const theHiveUpdateButton = ( - ); const mispExportButton = ( - {this.state.theHiveCaseId ? theHiveUpdateButton : theHiveExportButton} -
{this.state.mispEventId ? mispUpdateButton : mispExportButton}
@@ -411,7 +379,7 @@ export class Alerts extends Component { ); - const theHiveUpdateButton = ( - ); const mispExportButton = ( - {this.state.theHiveCaseId ? theHiveUpdateButton : theHiveExportButton} -
{this.state.mispEventId ? mispUpdateButton : mispExportButton}
@@ -500,7 +467,7 @@ export class SuspiciousSites extends Component {