diff --git a/.env b/.env index c48ff44..7289d5a 100644 --- a/.env +++ b/.env @@ -28,8 +28,13 @@ ALLOWED_HOST= CSRF_TRUSTED_ORIGINS= # DJANGO EMAIL Configuration -EMAIL_FROM=from@from.com -SMTP_SERVER=localhost +EMAIL_FROM= +SMTP_SERVER= +EMAIL_PORT=25 +EMAIL_USE_TLS=False +EMAIL_USE_SSL=False +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= WATCHER_URL=https://example.watcher.local WATCHER_LOGO=https://raw.githubusercontent.com/thalesgroup-cert/Watcher/master/Watcher/static/Watcher-logo-simple.png EMAIL_SUBJECT_TAG_SITE_MONITORING=INCIDENT diff --git a/Watcher/Watcher/common/core.py b/Watcher/Watcher/common/core.py index f701e81..4312688 100644 --- a/Watcher/Watcher/common/core.py +++ b/Watcher/Watcher/common/core.py @@ -6,18 +6,14 @@ import re from html import unescape from site_monitoring.models import Site +from datetime import datetime +from secrets import token_hex 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 +from .mail_template.dns_finder_template import get_dns_finder_template +from .mail_template.dns_finder_cert_transparency import get_dns_finder_cert_transparency_template -thehive_url = settings.THE_HIVE_URL -api_key = settings.THE_HIVE_API_KEY - -from datetime import datetime -from secrets import token_hex def generate_ref(): """ @@ -228,10 +224,6 @@ def generate_ref(): '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, @@ -240,9 +232,9 @@ def generate_ref(): '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, + 'dns_finder_cert_transparency': { + 'subject': "[ALERT] DNS Finder", + 'template_func': get_dns_finder_cert_transparency_template, }, } @@ -304,11 +296,13 @@ def collect_observables(app_name, context_data): 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 @@ -324,14 +318,14 @@ def send_app_specific_notifications(app_name, context_data, subscribers): 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) + thehive_url = settings.THE_HIVE_URL + api_key = settings.THE_HIVE_API_KEY + 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(): @@ -412,13 +406,21 @@ def send_notification(channel, content_template, subscribers_filter, send_func, 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 + + source = context_data.get('source') + if source == 'print_callback': + email_body = get_dns_finder_cert_transparency_template(alert) + elif source == 'check_dnstwist': + email_body = get_dns_finder_template(alert) + else: + print(f"Warning: Unknown source '{source}' for alert.") + email_body = "Alert with no specific model defined." + 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", @@ -501,7 +503,6 @@ def send_notification(channel, content_template, subscribers_filter, send_func, 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: diff --git a/Watcher/Watcher/common/mail_template/data_leak_group_template.py b/Watcher/Watcher/common/mail_template/data_leak_group_template.py index ec8b669..5094655 100644 --- a/Watcher/Watcher/common/mail_template/data_leak_group_template.py +++ b/Watcher/Watcher/common/mail_template/data_leak_group_template.py @@ -1,188 +1,158 @@ from django.conf import settings - def get_data_leak_group_template(keyword, alerts_number): - body = """\ - - - - - - - - - - - - -
-

Data Leak: Alerts

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

-
-

Dear team,

-

- """ - body += str(alerts_number) + """ New Data Leakage Alerts for - - """ - body += str(keyword) + """ keyword:

Details here.

-

- Best Regards, -

-

Watcher
-
-

-

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

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


- - - """ - return body + body = f"""\ + + + + + + +
+ +
+ Watcher Logo +

Data Leak: Alerts

+
+ + +
+

Dear team,

+ +
+

{alerts_number} New Data Leakage Alerts for {keyword} keyword

+
+ +

You can check more details here.

+ +

Best Regards,

+

Watcher

+
+ + + +
+ +

[{settings.EMAIL_CLASSIFICATION}]

+ + + """ + return body \ No newline at end of file diff --git a/Watcher/Watcher/common/mail_template/dns_finder_cert_transparency.py b/Watcher/Watcher/common/mail_template/dns_finder_cert_transparency.py index 10802bc..2c7055f 100644 --- a/Watcher/Watcher/common/mail_template/dns_finder_cert_transparency.py +++ b/Watcher/Watcher/common/mail_template/dns_finder_cert_transparency.py @@ -1,194 +1,160 @@ from django.conf import settings - def get_dns_finder_cert_transparency_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.keyword_monitored) + """

Details here.

-

- Best Regards, -

-

Watcher
-
-

-

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

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


- - - """ - return body + body = f"""\ + + + + + + +
+ +
+ Watcher Logo +

DNS Finder: Alert #{alert.pk}

+
+ + +
+

Dear team,

+

A new Twisted DNS found:

+ +
+

Domain Name: {alert.dns_twisted.domain_name}

+

Keyword Monitored: {alert.dns_twisted.keyword_monitored}

+
+ +

You can check more details here.

+ +

Best Regards,

+

Watcher

+
+ + + +
+ +

[{settings.EMAIL_CLASSIFICATION}]

+ + + """ + return body \ No newline at end of file diff --git a/Watcher/Watcher/common/mail_template/dns_finder_group_template.py b/Watcher/Watcher/common/mail_template/dns_finder_group_template.py index 9cf7055..c6d99a8 100644 --- a/Watcher/Watcher/common/mail_template/dns_finder_group_template.py +++ b/Watcher/Watcher/common/mail_template/dns_finder_group_template.py @@ -1,188 +1,154 @@ from django.conf import settings - def get_dns_finder_group_template(dns_monitored, alerts_number): - body = """\ - - - - - - - - - - - - -
-

DNS Finder: Alerts

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

-
-

Dear team,

-

- """ - body += str(alerts_number) + """ New DNS Twisted Alerts for - - """ - body += str(dns_monitored.domain_name) + """ asset:

Details here.

-

- Best Regards, -

-

Watcher
-
-

-

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

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


- - - """ - return body + body = f"""\ + + + + + + +
+ +
+ Watcher Logo +

Group DNS Finder: Alerts

+
+ + +
+

Dear team,

+

{alerts_number} New DNS Twisted Alerts for {dns_monitored.domain_name} asset:

+

You can check more details here.

+ +

Kind Regards,

+

Watcher

+
+ + + +
+ +

[{settings.EMAIL_CLASSIFICATION}]

+ + + """ + return body \ No newline at end of file diff --git a/Watcher/Watcher/common/utils/send_citadel_messages.py b/Watcher/Watcher/common/utils/send_citadel_messages.py index 5f1c5f6..2dc973d 100644 --- a/Watcher/Watcher/common/utils/send_citadel_messages.py +++ b/Watcher/Watcher/common/utils/send_citadel_messages.py @@ -12,7 +12,7 @@ def send_citadel_message(content, room_id, app_name): """ if not settings.CITADEL_API_TOKEN or not settings.CITADEL_ROOM_ID: - print(f"{str(timezone.now())} - No configuration for Citadel, not notifications sending.") + print(f"{str(timezone.now())} - No configuration for Citadel, notifications disabled. Configure it in the '.env' file.") return url = f"{settings.CITADEL_URL}/_matrix/client/r0/rooms/{room_id}/send/m.room.message?access_token={settings.CITADEL_API_TOKEN}" diff --git a/Watcher/Watcher/common/utils/send_email_notifications.py b/Watcher/Watcher/common/utils/send_email_notifications.py index b83e937..dd60c98 100644 --- a/Watcher/Watcher/common/utils/send_email_notifications.py +++ b/Watcher/Watcher/common/utils/send_email_notifications.py @@ -1,36 +1,41 @@ -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -import smtplib -from django.conf import settings +from django.core.mail import EmailMessage from datetime import datetime - +from django.conf import settings +from django.utils import timezone def send_email_notifications(subject, body, emails_to, app_name): """ - Send e-mail notifications to the list of recipients. - + Sends email notifications using Django EmailMessage. + 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. + subject (str): The subject of the email. + body (str): The HTML content of the email. + emails_to (list): List of recipients. + app_name (str): The name of the sending application. """ - emails_to = [str(email) for email in emails_to if isinstance(email, str) or hasattr(email, 'email')] + if not settings.SMTP_SERVER or not settings.EMAIL_FROM: + print(f"{str(timezone.now())} - No configuration for Email, notifications disabled. Configure it in the '.env' file.") + return + + # Filter valid email addresses + emails_to = [email if isinstance(email, str) else getattr(email, 'email', None) for email in emails_to] + emails_to = [email for email in emails_to if email] + + if not emails_to: + print(f"{datetime.now()} - No valid recipients for {app_name}.") + return 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}.") - + # Create the email + email = EmailMessage( + subject=f"{subject}", + body=body, + from_email=settings.EMAIL_FROM, + to=emails_to, + ) + email.content_subtype = "html" + email.send(fail_silently=False) + print(f"{datetime.now()} - Email successfully sent for {app_name}.") except Exception as e: - print(f"{datetime.now()} - Failed to send email: {str(e)}") + print(f"{datetime.now()} - Failed to send email for {app_name}: {e}") diff --git a/Watcher/Watcher/common/utils/send_slack_messages.py b/Watcher/Watcher/common/utils/send_slack_messages.py index bb9f9e1..43ac99f 100644 --- a/Watcher/Watcher/common/utils/send_slack_messages.py +++ b/Watcher/Watcher/common/utils/send_slack_messages.py @@ -11,9 +11,8 @@ def send_slack_message(content, channel, app_name): 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") + print(f"{str(timezone.now())} - No configuration for Slack, notifications disabled. Configure it in the '.env' file.") return url = 'https://slack.com/api/chat.postMessage' diff --git a/Watcher/Watcher/common/utils/send_thehive_alerts.py b/Watcher/Watcher/common/utils/send_thehive_alerts.py index 6db8b9a..263f2f6 100644 --- a/Watcher/Watcher/common/utils/send_thehive_alerts.py +++ b/Watcher/Watcher/common/utils/send_thehive_alerts.py @@ -64,6 +64,11 @@ def send_thehive_alert(title, description, severity, tags, app_name, domain_name :return: None :rtype: None """ + + if not settings.THE_HIVE_API_KEY or not settings.THE_HIVE_URL: + print(f"{str(timezone.now())} - No configuration for TheHive, notifications disabled. Configure it in the '.env' file.") + return + thehive_url = thehive_url or settings.THE_HIVE_URL api_key = api_key or settings.THE_HIVE_API_KEY diff --git a/Watcher/Watcher/data_leak/core.py b/Watcher/Watcher/data_leak/core.py index 58a4ff4..9c131cc 100644 --- a/Watcher/Watcher/data_leak/core.py +++ b/Watcher/Watcher/data_leak/core.py @@ -11,6 +11,8 @@ from json.decoder import JSONDecodeError from common.core import send_app_specific_notifications from django.db.models import Q +from django.core.mail import EmailMessage +from common.mail_template.data_leak_group_template import get_data_leak_group_template def start_scheduler(): @@ -229,8 +231,12 @@ 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) - send_data_leak_notifications(alert) - + # limiting the number of specific email per alert + if len(results) < 6: + send_data_leak_notifications(alert) + # if there is too many alerts, we send a group email + if len(results) >= 6: + send_group_email(keyword, len(results)) # now we check Pastebin for new pastes result = check_pastebin(keywords) @@ -261,4 +267,37 @@ def send_data_leak_notifications(alert): 'alert': alert } - send_app_specific_notifications('data_leak', context_data, subscribers) \ No newline at end of file + send_app_specific_notifications('data_leak', context_data, subscribers) + + +def send_group_email(keyword, alerts_number): + """ + Send group e-mail for a specific keyword. + + :param keyword: Matched Keyword. + :param alerts_number: Number of alerts. + """ + emails_to = [subscriber.user_rec.email for subscriber in Subscriber.objects.all()] + + if emails_to: + try: + # Construire l'email + subject = f"[{alerts_number} ALERTS] Data Leak" + body = get_data_leak_group_template(keyword, alerts_number) + + email = EmailMessage( + subject=subject, + body=body, + from_email=settings.EMAIL_FROM, + to=emails_to, + ) + email.content_subtype = "html" + + email.send(fail_silently=False) + + for email_address in emails_to: + print(f"{timezone.now()} - Email sent to {email_address}") + except Exception as e: + print(f"{timezone.now()} - Email Error: {e}") + else: + print(f"{timezone.now()} - No subscriber, no email sent.") \ No newline at end of file diff --git a/Watcher/Watcher/dns_finder/core.py b/Watcher/Watcher/dns_finder/core.py index 9c3bf84..414d8db 100644 --- a/Watcher/Watcher/dns_finder/core.py +++ b/Watcher/Watcher/dns_finder/core.py @@ -11,6 +11,8 @@ import certstream from common.core import send_app_specific_notifications from django.db.models import Q +from django.core.mail import EmailMessage +from common.mail_template.dns_finder_group_template import get_dns_finder_group_template def start_scheduler(): @@ -27,7 +29,7 @@ def start_scheduler(): id='main_certificate_transparency', max_instances=2, replace_existing=True) - + scheduler.start() @@ -59,6 +61,8 @@ 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) + alert.source = 'print_callback' + alert.save() send_dns_finder_notifications(alert) @@ -131,11 +135,16 @@ def check_dnstwist(dns_monitored): dns_monitored=dns_monitored, fuzzer=twisted_website_dict['fuzzer']) alert = Alert.objects.create(dns_twisted=dns_twisted) + alert.source = 'check_dnstwist' + alert.save() alerts_list.append(alert) - for alert in alerts_list: - send_dns_finder_notifications(alert) - + # Send email alerts + if len(alerts_list) < 6: + for alert in alerts_list: + send_dns_finder_notifications(alert) + if len(alerts_list) >= 6: + send_group_email(dns_monitored, len(alerts_list)) except ValueError: print('Decoding JSON has failed') @@ -160,8 +169,49 @@ def send_dns_finder_notifications(alert): print(f"Error: Invalid alert object or missing domain_name in dns_twisted for alert: {alert}") return + source = None + if hasattr(alert, 'source'): + source = alert.source + context_data = { - 'alert': alert, + 'alert': alert, + 'source': source } - send_app_specific_notifications('dns_finder', context_data, subscribers) \ No newline at end of file + send_app_specific_notifications('dns_finder', context_data, subscribers) + + +def send_group_email(dns_monitored, alerts_number): + """ + Send group e-mail for a specific dns_monitored. + + :param dns_monitored: DnsMonitored Object. + :param alerts_number: Number of alerts. + """ + # Collecter les emails des abonnés + emails_to = [subscriber.user_rec.email for subscriber in Subscriber.objects.all()] + + if emails_to: # S'il y a au moins un abonné + try: + # Construire l'email + subject = f"[{alerts_number} ALERTS] DNS Finder" + body = get_dns_finder_group_template(dns_monitored, alerts_number) + + email = EmailMessage( + subject=subject, + body=body, + from_email=settings.EMAIL_FROM, + to=emails_to, + ) + email.content_subtype = "html" + + # Envoyer l'email + email.send(fail_silently=False) + + # Confirmation des emails envoyés + for email_address in emails_to: + print(f"{timezone.now()} - Email sent to {email_address}") + except Exception as e: + print(f"{timezone.now()} - Email Error: {e}") + else: + print(f"{timezone.now()} - No subscriber, no email sent.") \ No newline at end of file diff --git a/Watcher/Watcher/site_monitoring/core.py b/Watcher/Watcher/site_monitoring/core.py index 3fc5784..5a3f247 100644 --- a/Watcher/Watcher/site_monitoring/core.py +++ b/Watcher/Watcher/site_monitoring/core.py @@ -18,12 +18,8 @@ from dns import resolver from dns.exception import DNSException import shadow_useragent -import time -import random from common.core import send_app_specific_notifications from django.db.models import Q -from common.core import generate_ref -import string try: shadow_useragent = shadow_useragent.ShadowUserAgent() @@ -43,17 +39,10 @@ def start_scheduler(): scheduler.add_job(monitoring_check, 'cron', day_of_week='mon-sun', minute='*/1', id='weekend_job', max_instances=10, replace_existing=True) - + scheduler.start() -def generate_unique_suffix(length=3): - """ - Génère un suffixe unique constitué de lettres et chiffres aléatoires. - """ - return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) - - def monitoring_init(site): """ Init the monitoring for a specific website. diff --git a/Watcher/Watcher/watcher/settings.py b/Watcher/Watcher/watcher/settings.py index 2b15d46..148a90e 100755 --- a/Watcher/Watcher/watcher/settings.py +++ b/Watcher/Watcher/watcher/settings.py @@ -82,13 +82,13 @@ # Email Configuration EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -SMTP_SERVER = os.environ.get('SMTP_SERVER', 'localhost') -EMAIL_PORT = 465 -EMAIL_USE_SSL = True -EMAIL_USE_TLS = False -EMAIL_HOST_USER = os.environ.get('EMAIL_USER', 'your-email@example.com') # Votre email (ex: exemple@gmail.com) -EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD', 'your-password') # Le mot de passe de votre email -EMAIL_FROM = os.environ.get('EMAIL_FROM', 'from@from.com') +SMTP_SERVER = os.environ.get('SMTP_SERVER', '') +EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25)) +EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'False') == 'True' +EMAIL_USE_SSL = os.environ.get('EMAIL_USE_SSL', 'False') == 'True' +EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '') +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '') +EMAIL_FROM = os.environ.get('EMAIL_FROM', '') EMAIL_SUBJECT_TAG_SITE_MONITORING = os.environ.get('EMAIL_SUBJECT_TAG_SITE_MONITORING', 'INCIDENT') # Display at the end of the email notification EMAIL_CLASSIFICATION = os.environ.get('EMAIL_CLASSIFICATION', 'Internal') @@ -135,7 +135,7 @@ # Slack configuration SLACK_API_TOKEN = os.environ.get('SLACK_API_TOKEN', '') -SLACK_CHANNEL = os.environ.get('SLACK_CHANNEL', '#watcher') +SLACK_CHANNEL = os.environ.get('SLACK_CHANNEL', '') # Citadel configuration CITADEL_API_TOKEN = os.environ.get('CITADEL_API_TOKEN', '')