Skip to content

Commit

Permalink
Add smtp only (#328)
Browse files Browse the repository at this point in the history
* Add smtp

* Clean up tests

* rm defaults and fix breakage in testing
  • Loading branch information
jayjb authored Dec 13, 2023
1 parent 4ec37c2 commit ca1e028
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 49 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/build_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ jobs:
sed -i'' "s/CANARY_DEV_BUILD_ID=.*/CANARY_DEV_BUILD_ID=${GITHUB_SHA:0:8}/" frontend.env
sudo docker pull thinkst/canarytokens:${GITHUB_REF##*/}
sudo docker compose -f docker-compose-letsencrypt-${GITHUB_REF##*/}.yml pull
sudo docker compose -f docker-compose-letsencrypt-${GITHUB_REF##*/}.yml down
sudo docker compose -f docker-compose-letsencrypt-${GITHUB_REF##*/}.yml up -d
sudo docker system prune -f -a
Expand All @@ -120,6 +119,5 @@ jobs:
sed -i'' "s/CANARY_DEV_BUILD_ID=.*/CANARY_DEV_BUILD_ID=${GITHUB_SHA:0:8}/" frontend.env
sudo docker pull thinkst/canarytokens:${GITHUB_REF##*/}
sudo docker compose -f docker-compose-aws-logging-letsencrypt-${GITHUB_REF##*/}.yml pull
sudo docker compose -f docker-compose-aws-logging-letsencrypt-${GITHUB_REF##*/}.yml down
sudo docker compose -f docker-compose-aws-logging-letsencrypt-${GITHUB_REF##*/}.yml up -d
sudo docker system prune -f -a
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
rev: 6.1.0
hooks:
- id: flake8
additional_dependencies: [flake8-typing-imports==1.12.0]
Expand Down
103 changes: 63 additions & 40 deletions canarytokens/channel_output_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
import textwrap
from typing import Optional
import enum
import uuid

import minify_html
import requests
import sendgrid
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from jinja2 import Template
from pydantic import EmailStr, HttpUrl, SecretStr
from python_http_client.exceptions import HTTPError
Expand Down Expand Up @@ -215,6 +219,45 @@ def mailgun_send(
return email_response, message_id


def smtp_send(
*,
email_address: EmailStr,
email_content_html: str,
email_content_text: str,
email_subject: str,
from_email: EmailStr,
from_display: str,
smtp_password: str,
smtp_username: str,
smtp_server: str,
smtp_port: str,
) -> tuple[EmailResponseStatuses, str]:
email_response = EmailResponseStatuses.ERROR
message_id = uuid.uuid4().hex
try:
fromaddr = from_email
toaddr = email_address
smtpmsg = MIMEMultipart("alternative")
smtpmsg["From"] = from_display
smtpmsg["To"] = email_address
smtpmsg["Subject"] = email_subject
part1 = MIMEText(email_content_text, "plain")
part2 = MIMEText(email_content_html, "html")
smtpmsg.attach(part1)
smtpmsg.attach(part2)

with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls()
server.login(smtp_username, smtp_password)
server.sendmail(fromaddr, toaddr, smtpmsg.as_string())
except smtplib.SMTPException as e:
log.error("A smtp error occurred: %s - %s" % (e.__class__, e))
email_response = EmailResponseStatuses.ERROR
else:
email_response = EmailResponseStatuses.SENT
return email_response, message_id


class EmailOutputChannel(OutputChannel):
CHANNEL = OUTPUT_CHANNEL_EMAIL

Expand Down Expand Up @@ -371,16 +414,17 @@ def do_send_alert(
recipient=canarydrop.alert_email_recipient,
details=alert_details,
)
email_content_html = EmailOutputChannel.format_report_html(
alert_details,
Path(
f"{self.switchboard_settings.TEMPLATES_PATH}/emails/notification.html"
),
)
if self.switchboard_settings.MAILGUN_API_KEY:
email_response_status, message_id = mailgun_send(
email_address=canarydrop.alert_email_recipient,
email_subject=self.email_subject,
email_content_html=EmailOutputChannel.format_report_html(
alert_details,
Path(
f"{self.switchboard_settings.TEMPLATES_PATH}/emails/notification.html"
),
),
email_content_html=email_content_html,
email_content_text=EmailOutputChannel.format_report_text(alert_details),
from_email=EmailStr(self.from_email),
from_display=self.from_display,
Expand All @@ -392,19 +436,25 @@ def do_send_alert(
email_response_status, message_id = sendgrid_send(
api_key=self.switchboard_settings.SENDGRID_API_KEY,
email_address=canarydrop.alert_email_recipient,
email_content_html=EmailOutputChannel.format_report_html(
alert_details,
Path(
f"{self.switchboard_settings.TEMPLATES_PATH}/emails/notification.html"
),
),
email_content_html=email_content_html,
from_email=EmailStr(self.from_email),
email_subject=self.email_subject,
from_display=self.from_display,
sandbox_mode=False,
)
elif self.switchboard_settings.SMTP_SERVER:
raise NotImplementedError("SMTP_SERVER - not supported")
email_response_status, message_id = smtp_send(
email_address=canarydrop.alert_email_recipient,
email_content_html=email_content_html,
email_content_text=EmailOutputChannel.format_report_text(alert_details),
email_subject=self.email_subject,
from_email=EmailStr(self.from_email),
from_display=self.from_display,
smtp_password=self.switchboard_settings.SMTP_PASSWORD,
smtp_username=self.switchboard_settings.SMTP_USERNAME,
smtp_server=self.switchboard_settings.SMTP_SERVER,
smtp_port=self.switchboard_settings.SMTP_PORT,
)
else:
log.error("No email settings found")

Expand Down Expand Up @@ -505,30 +555,3 @@ def check_sendgrid_mail_status(api_key: str) -> bool:
# # Mandrill errors are thrown as exceptions
# log.error('A mandrill error occurred: %s - %s' % (e.__class__, e))
# # A mandrill error occurred: <class 'mandrill.UnknownSubaccountError'> - No subaccount exists with the id 'customer-123'....

# def smtp_send(self, msg=None, canarydrop=None):
# try:
# fromaddr = msg['from_address']
# toaddr = canarydrop['alert_email_recipient']

# smtpmsg = MIMEText(msg['body'])
# smtpmsg['From'] = fromaddr
# smtpmsg['To'] = toaddr
# smtpmsg['Subject'] = msg['subject']

# if settings.DEBUG:
# pprint.pprint(message)
# else:
# server = smtplib.SMTP(settings.SMTP_SERVER, settings.SMTP_PORT)
# server.ehlo()
# server.starttls()
# server.ehlo()
# server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD)
# text = smtpmsg.as_string()
# server.sendmail(fromaddr, toaddr, text)

# log.info('Sent alert to {recipient} for token {token}'\
# .format(recipient=canarydrop['alert_email_recipient'],
# token=canarydrop.canarytoken.value()))
# except smtplib.SMTPException as e:
# log.error('A smtp error occurred: %s - %s' % (e.__class__, e))
8 changes: 7 additions & 1 deletion canarytokens/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,20 @@ class SwitchboardSettings(BaseSettings):
MAX_ALERT_FAILURES: int = 5

IPINFO_API_KEY: Optional[SecretStr] = None

# Mailgun Required Settings
MAILGUN_API_KEY: Optional[SecretStr] = None
MAILGUN_BASE_URL: Optional[HttpUrl] = HttpUrl(
"https://api.mailgun.net", scheme="https"
)
MAILGUN_DOMAIN_NAME: Optional[str]
# Sendgrid Required Settings
SENDGRID_API_KEY: Optional[SecretStr] = None
SENDGRID_SANDBOX_MODE: bool = True
# SMTP Required Settings
SMTP_USERNAME: Optional[str]
SMTP_PASSWORD: Optional[str]
SMTP_SERVER: Optional[str]
SMTP_PORT: Optional[Port] = Port(587)

SENTRY_DSN: Optional[HttpUrl] = None
SENTRY_ENVIRONMENT: Literal["prod", "staging", "dev", "ci", "local"] = "local"
Expand Down
7 changes: 3 additions & 4 deletions canarytokens/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@

g_template_dir: Optional[str]

switchboard_settings = SwitchboardSettings()


def set_template_env(template_dir):
global g_template_dir
Expand Down Expand Up @@ -265,6 +263,7 @@ def _log4_shell(matches: Match[AnyStr]) -> dict[str, dict[str, str]]:
def _grab_http_general_info(request: Request):
""""""
useragent = request.getHeader("User-Agent") or "(no user-agent specified)"
switchboard_settings = SwitchboardSettings()
src_ip = (
request.getHeader(switchboard_settings.REAL_IP_HEADER)
or request.client.host
Expand All @@ -277,7 +276,7 @@ def _grab_http_general_info(request: Request):
src_ip_chain = [o.strip() for o in src_ips.split(",")]
# TODO: 'ts_key' -> which tokens fire this?
hit_time = request.args.get("ts_key", [datetime.utcnow().strftime("%s.%f")])[0]
flatten_singletons = lambda l: l[0] if len(l) == 1 else l # noqa: E731
flatten_singletons = lambda d: d[0] if len(d) == 1 else d # noqa: E731
request_headers = {
k.decode(): flatten_singletons([s.decode() for s in v])
for k, v in request.requestHeaders.getAllRawHeaders()
Expand Down Expand Up @@ -378,7 +377,7 @@ def _parse_azure_id_trigger(
src_ip = json_data.get("ip", "127.0.0.1")

auth_details = json_data.get("auth_details", "")
if type(auth_details) == list:
if type(auth_details) is list:
out = ""
for d in auth_details:
out += "\n{}: {}".format(d["key"], d["value"])
Expand Down
32 changes: 31 additions & 1 deletion tests/units/test_channel_output_email.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import datetime
from pathlib import Path

import uuid
from pydantic import EmailStr
import pytest
from unittest.mock import patch
from twisted.logger import capturedLogs

from canarytokens import queries
Expand All @@ -12,6 +13,7 @@
EmailOutputChannel,
mailgun_send,
sendgrid_send,
smtp_send,
EmailResponseStatuses,
)
from canarytokens.models import (
Expand Down Expand Up @@ -223,6 +225,34 @@ def test_mailgun_send(
assert len(message_id) > 0


# TODO: Write more comprehensive tests for SMTP. The difficulty here is that we don't have a consistent API to use
# because different SMTP servers may handle things differently. I figure as we break and enhance, we'll add tests too
@patch("canarytokens.channel_output_email.smtplib.SMTP", autospec=True)
def test_smtp_send(
mock_SMTP,
settings: SwitchboardSettings,
):
details = _get_send_token_details()
result, message_id = smtp_send(
email_content_html=EmailOutputChannel.format_report_html(
details, Path(f"{settings.TEMPLATES_PATH}/emails/notification.html")
),
email_content_text=EmailOutputChannel.format_report_text(details),
email_address=EmailStr("tokens-testing@thinkst.com"),
from_email=settings.ALERT_EMAIL_FROM_ADDRESS,
email_subject=settings.ALERT_EMAIL_SUBJECT,
from_display=settings.ALERT_EMAIL_FROM_DISPLAY,
smtp_password="testpassword",
smtp_port=1025,
smtp_server="localhost",
smtp_username="testuser",
)
assert mock_SMTP.return_value.__enter__.return_value.sendmail.call_count == 1
assert len(message_id) == len(uuid.uuid4().hex)
assert result == EmailResponseStatuses.SENT
assert len(message_id) > 0


def _do_send_alert(
frontend_settings: FrontendSettings,
switchboard_settings: SwitchboardSettings,
Expand Down

0 comments on commit ca1e028

Please sign in to comment.