Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add email exposed key handling #609

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 113 additions & 91 deletions canarytokens/channel_output_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
from canarytokens.models import (
AnyTokenHit,
AnyTokenExposedHit,
Memo,
TokenExposedHit,
readable_token_type_names,
TokenAlertDetails,
TokenExposedDetails,
token_types_with_article_an,
TokenTypes,
)
Expand Down Expand Up @@ -71,7 +74,7 @@ def __init__(
status: EmailResponseStatuses,
canarydrop: Canarydrop,
message_id: str,
alert_details: TokenAlertDetails,
alert_details: Union[TokenAlertDetails, TokenExposedDetails],
max_alert_failures: int,
):
self.status = status
Expand All @@ -92,9 +95,14 @@ def handle_ignored(self):
self.canarydrop.disable_alert_email()

def handle_sent(self):
time = (
self.alert_details.time
if isinstance(self.alert_details, TokenAlertDetails)
else self.alert_details.exposed_time
)
queries.remove_mail_from_to_send_status(
token=self.alert_details.token,
time=self.alert_details.time,
time=time,
)
queries.put_mail_on_sent_queue(
mail_key=self.message_id,
Expand Down Expand Up @@ -283,6 +291,29 @@ def __init__(
name=name,
)

@staticmethod
def format_token_exposed_html(
details: TokenExposedDetails,
template_path: Path,
):
"""Returns a string containing an incident report in HTML,
suitable for emailing"""
readable_type = readable_token_type_names[details.token_type]
BasicDetails = details.dict()
BasicDetails["readable_type"] = readable_type
BasicDetails["token_type"] = details.token_type.value
BasicDetails["memo"] = details.memo
BasicDetails["key_id"] = details.key_id
BasicDetails["time_ymd"] = details.time_ymd
BasicDetails["time_hm"] = details.time_hm

rendered_html = Template(template_path.read_text()).render(
BasicDetails=BasicDetails,
ManageLink=details.manage_url,
HistoryLink=details.history_url,
)
return minify_html.minify(rendered_html)

@staticmethod
def format_report_html(
details: TokenAlertDetails,
Expand Down Expand Up @@ -376,6 +407,38 @@ def format_report_text(details: TokenAlertDetails, body_length: int = 999999999)
).strip()
return body

@staticmethod
def format_token_exposed_text(
details: TokenExposedDetails, body_length: int = 999999999
):
"""Returns a string containing an incident report in text,
suitable for emailing"""
if body_length <= 140:
thinkst-daniel marked this conversation as resolved.
Show resolved Hide resolved
body = f"Canarytoken exposed on the internet @ {details.exposed_time}: "
capacity = 140 - len(body)
body += details.memo[:capacity]
else:
body = textwrap.dedent(
f"""
One of your Canarytokens was exposed on the internet.
Location: {details.public_location or "unknown"}
Time : {details.exposed_time}
Memo : {details.memo}
Manage your settings for this Canarydrop:
{details.manage_url}
"""
).strip()
return body

@staticmethod
def format_token_exposed_intro(details: TokenExposedDetails):
article = "An" if details.token_type in token_types_with_article_an else "A"
readable_type = readable_token_type_names[details.token_type]
intro = (
f"{article} {readable_type} Canarytoken has been exposed on the internet."
)
return intro

@staticmethod
def format_report_intro(details: TokenAlertDetails):
details.channel
Expand Down Expand Up @@ -421,28 +484,56 @@ def do_send_alert(
canarydrop: Canarydrop,
token_hit: Union[AnyTokenHit, AnyTokenExposedHit],
):
alert_details = input_channel.gather_alert_details(
canarydrop=canarydrop,
host=self.switchboard_settings.PUBLIC_DOMAIN,
protocol=self.switchboard_scheme,
)
#
if isinstance(token_hit, TokenExposedHit):
details = TokenExposedDetails(
token_type=token_hit.token_type,
token=canarydrop.canarytoken.value(),
key_id=canarydrop.aws_access_key_id,
memo=Memo(canarydrop.memo),
public_location=token_hit.public_location,
exposed_time=token_hit.time_of_hit,
manage_url=canarydrop.build_manage_url(
self.switchboard_scheme, self.hostname
),
public_domain=self.hostname,
)
else:
details = input_channel.gather_alert_details(
canarydrop=canarydrop,
host=self.switchboard_settings.PUBLIC_DOMAIN,
protocol=self.switchboard_scheme,
)

queries.add_mail_to_send_status(
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"
),
details=details,
)

if isinstance(details, TokenExposedDetails):
email_content_html = EmailOutputChannel.format_token_exposed_html(
details,
Path(
f"{self.switchboard_settings.TEMPLATES_PATH}/emails/notification_token_exposed.html"
),
)
email_content_text = EmailOutputChannel.format_token_exposed_text(details)
email_subject = "Canarytoken Exposed"
else:
email_content_html = EmailOutputChannel.format_report_html(
details,
Path(
f"{self.switchboard_settings.TEMPLATES_PATH}/emails/notification.html"
),
)
email_content_text = EmailOutputChannel.format_report_text(details)
email_subject = self.email_subject

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_subject=email_subject,
email_content_html=email_content_html,
email_content_text=EmailOutputChannel.format_report_text(alert_details),
email_content_text=email_content_text,
from_email=EmailStr(self.from_email),
from_display=self.from_display,
api_key=self.switchboard_settings.MAILGUN_API_KEY,
Expand All @@ -455,16 +546,16 @@ def do_send_alert(
email_address=canarydrop.alert_email_recipient,
email_content_html=email_content_html,
from_email=EmailStr(self.from_email),
email_subject=self.email_subject,
email_subject=email_subject,
from_display=self.from_display,
sandbox_mode=False,
)
elif self.switchboard_settings.SMTP_SERVER:
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,
email_content_text=email_content_text,
email_subject=email_subject,
from_email=EmailStr(self.from_email),
from_display=self.from_display,
smtp_password=self.switchboard_settings.SMTP_PASSWORD,
Expand All @@ -480,11 +571,11 @@ def do_send_alert(
status=email_response_status,
canarydrop=canarydrop,
message_id=message_id,
alert_details=alert_details,
alert_details=details,
max_alert_failures=self.switchboard_settings.MAX_ALERT_FAILURES,
)
)
return alert_details
return details

def handle_email_response(self, email_response: EmailResponse):
return email_response.handle()
Expand All @@ -503,72 +594,3 @@ def check_sendgrid_mail_status(api_key: str) -> bool:
bool: returns True if mail was processed. False otherwise.
"""
return
# sg = sendgrid.SendGridAPIClient(api_key=api_key)
# mail_key, alert_details = queries.pop_mail_off_sent_queue()
# # Check using: resp = sg.client.messages._(mail_key).get()
# if False:
# # delivery was not made.
# queries.put_mail_on_sent_queue(mail_key=mail_key, details=alert_details)
# return mail_key is not None

# def get_basic_details(self,):

# vars = { 'Description' : self.data['description'],
# 'Channel' : self.data['channel'],
# 'Time' : self.data['time'],
# 'Canarytoken' : self.data['canarytoken']
# }

# if 'src_ip' in self.data:
# vars['src_ip'] = self.data['src_ip']
# vars['SourceIP'] = self.data['src_ip']

# if 'useragent' in self.data:
# vars['User-Agent'] = self.data['useragent']

# if 'tokentype' in self.data:
# vars['TokenType'] = self.data['tokentype']

# if 'referer' in self.data:
# vars['Referer'] = self.data['referer']

# if 'location' in self.data:
# try:
# vars['Location'] = self.data['location'].decode('utf-8')
# except Exception:
# vars['Location'] = self.data['location']

# if 'log4_shell_computer_name' in self.data:
# vars['Log4JComputerName'] = self.data['log4_shell_computer_name']

# return vars

# def mandrill_send(self, msg=None, canarydrop=None):
# try:
# mandrill_client = mandrill.Mandrill(settings.MANDRILL_API_KEY)
# message = {
# 'auto_html': None,
# 'auto_text': None,
# 'from_email': msg['from_address'],
# 'from_name': msg['from_display'],
# 'text': msg['body'],
# 'html':self.format_report_html(),
# 'subject': msg['subject'],
# 'to': [{'email': canarydrop['alert_email_recipient'],
# 'name': '',
# 'type': 'to'}],
# }
# if settings.DEBUG:
# pprint.pprint(message)
# else:
# result = mandrill_client.messages.send(message=message,
# async=False,
# ip_pool='Main Pool')
# log.info('Sent alert to {recipient} for token {token}'\
# .format(recipient=canarydrop['alert_email_recipient'],
# token=canarydrop.canarytoken.value()))

# except mandrill.Error, e:
# # 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'....
27 changes: 22 additions & 5 deletions canarytokens/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,12 +587,18 @@ def can_send_alert(canarydrop: cand.Canarydrop, alert_limit: int):
# raise Exception("Imgur response was unexpected: {resp}".format(resp=resp))
# return resp["data"][imgur_id]
def add_mail_to_send_status(
recipient: EmailStr, details: models.TokenAlertDetails
recipient: EmailStr,
details: Union[models.TokenAlertDetails, models.TokenExposedDetails],
) -> int:
data = {"recipient": recipient, **details.json_safe_dict()}
mail_to_send = json.dumps(data)
time = (
details.time
if isinstance(details, models.TokenAlertDetails)
else details.exposed_time
)
return DB.get_db().set(
f"{KEY_MAIL_TO_SEND}:{details.token}:{details.time.timestamp()}", mail_to_send
f"{KEY_MAIL_TO_SEND}:{details.token}:{time.timestamp()}", mail_to_send
)


Expand All @@ -610,17 +616,28 @@ def get_all_mails_in_send_status(

def remove_mail_from_to_send_status(
token: str, time: datetime.datetime
) -> tuple[Optional[list[EmailStr]], Optional[models.TokenAlertDetails]]:
) -> tuple[
Optional[list[EmailStr]],
Optional[Union[models.TokenAlertDetails, models.TokenExposedDetails]],
]:
item = DB.get_db().getdel(f"{KEY_MAIL_TO_SEND}:{token}:{time.timestamp()}")
if item is None:
log.info(f"No mail at key: {KEY_MAIL_TO_SEND}:{token}:{time.timestamp()}")
return None, None

data = json.loads(item)
recipient = EmailStr(data.pop("recipient"))
return recipient, models.TokenAlertDetails(**data)
details = (
models.TokenExposedDetails(**data)
if "public_location" in data
else models.TokenAlertDetails(**data)
)
return recipient, details


def put_mail_on_sent_queue(mail_key: str, details: models.TokenAlertDetails) -> int:
def put_mail_on_sent_queue(
mail_key: str, details: Union[models.TokenAlertDetails, models.TokenExposedDetails]
) -> int:
sent_mail = json.dumps({"mail_key": mail_key, **details.json_safe_dict()})
return DB.get_db().lpush(KEY_SENT_MAIL_QUEUE, sent_mail)

Expand Down
Loading
Loading