Skip to content

Commit

Permalink
[Handlers] SES, SNS, Twilio, Stored Procedure, SMTP (#284)
Browse files Browse the repository at this point in the history
* Adds SES, SNS, Stored Procedure, Twilio, and SMTP handlers, contributed by Roman Dobrik
  • Loading branch information
sfc-gh-gbutzi authored Aug 26, 2019
1 parent bc30168 commit 3e712b5
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 16 deletions.
6 changes: 6 additions & 0 deletions src/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ ignore_missing_imports = True

[mypy-googleapiclient.*]
ignore_missing_imports = True

[mypy-twilio.*]
ignore_missing_imports = True

[mypy-flask.*]
ignore_missing_imports = True
83 changes: 83 additions & 0 deletions src/runners/handlers/ses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import boto3

from botocore.exceptions import ClientError
from runners.helpers import log
from runners.helpers.dbconfig import REGION


def handle(alert, type='ses', recipient_email=None, sender_email=None, text=None, html=None, subject=None, cc=None, bcc=None, reply_to=None, charset="UTF-8"):

# check if recipient email is not empty
if recipient_email is None:
log.error(f'Cannot identify recipient email')
return None

if text is None:
log.error(f'SES Message is empty')
return None

if cc is None:
ccs = []
else:
ccs = cc.split(",")

if bcc is None:
bccs = []
else:
bccs = bcc.split(",")

if reply_to is None:
replyTo = []
else:
replyTo = reply_to.split(",")

destination = {
'ToAddresses': [
recipient_email
],
'CcAddresses': ccs,
'BccAddresses': bccs
}

body = {
'Text': {
'Charset': charset,
'Data': text,
},
}

if html is not None:
body.update(Html={
'Charset': charset,
'Data': html,
})

message = {
'Body': body,
'Subject': {
'Charset': charset,
'Data': subject,
},
}

log.debug(f'SES message for recipient with email {recipient_email}', message)

client = boto3.client('ses', region_name=REGION)

# Try to send the email.
try:
# Provide the contents of the email.
response = client.send_email(
Destination=destination,
Message=message,
Source=sender_email,
ReplyToAddresses=replyTo
)
# Display an error if something goes wrong.
except ClientError as e:
log.error(f'Failed to send email {e}')
return None
else:
log.debug("SES Email sent!")

return response
57 changes: 42 additions & 15 deletions src/runners/handlers/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@
def message_template(vars):
payload = None

# remove handlers data, it might contain JSON incompatible strucutres
vars['alert'].pop('HANDLERS')

# if we have Slack user data, send it to template
if 'user' in vars:
params = {'alert': vars['alert'], 'properties': vars['properties'], 'user': vars['user']}
else:
params = {'alert': vars['alert'], 'properties': vars['properties']}

log.debug(f"Javascript template parameters", params)
try:
# retrieve Slack message structure from javascript UDF
rows = db.connect_and_fetchall(
"select " + vars['template'] + "(" + db.value_to_sql(params) + ")")
"select " + vars['template'] + "(parse_json('" + json.dumps(params) + "'))")
row = rows[1]

if len(row) > 0:
Expand All @@ -33,10 +37,11 @@ def message_template(vars):
log.error(f"Error loading javascript template", e)
raise

log.debug(f"Template payload", payload)
return payload


def handle(alert, recipient_email=None, channel=None, template=None, message=None):
def handle(alert, recipient_email=None, channel=None, template=None, message=None, file_content=None, file_type=None, file_name=None):
if 'SLACK_API_TOKEN' not in os.environ:
log.info(f"No SLACK_API_TOKEN in env, skipping handler.")
return None
Expand Down Expand Up @@ -103,21 +108,43 @@ def handle(alert, recipient_email=None, channel=None, template=None, message=Non
if message is not None:
text = message

response = sc.api_call(
"chat.postMessage",
channel=channel,
text=text,
blocks=blocks,
attachments=attachments
)
response = None

log.debug(f'Slack response', response)
if file_content is not None:
if template is not None:
response = sc.api_call(
"chat.postMessage",
channel=channel,
text=text,
blocks=blocks,
attachments=attachments
)

if response['ok'] is False:
log.error(f"Slack handler error", response['error'])
return None
file_descriptor = sc.api_call("files.upload", content=file_content, title=text, channels=channel, filetype=file_type, filename=file_name)

if file_descriptor['ok'] is True:
file = file_descriptor["file"]
file_url = file["url_private"]
else:
log.error(f"Slack file upload error", file_descriptor['error'])

else:
response = sc.api_call(
"chat.postMessage",
channel=channel,
text=text,
blocks=blocks,
attachments=attachments
)

if response is not None:
log.debug(f'Slack response', response)

if response['ok'] is False:
log.error(f"Slack handler error", response['error'])
return None

if 'message' in response:
del response['message']
if 'message' in response:
del response['message']

return response
38 changes: 38 additions & 0 deletions src/runners/handlers/sms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os

from twilio.rest import Client

from runners.helpers import log
from runners.helpers import vault


def handle(alert, type='sms', recipient_phone=None, sender_phone=None, message=None):

if not os.environ.get('TWILIO_API_SID'):
log.info(f"No TWILIO_API_SID in env, skipping handler.")
return None

twilio_sid = os.environ["TWILIO_API_SID"]

twilio_token = vault.decrypt_if_encrypted(os.environ['TWILIO_API_TOKEN'])

# check if phone is not empty if yes notification will be delivered to twilio
if recipient_phone is None:
log.error(f'Cannot identify assignee phone number')
return None

if message is None:
log.error(f'SMS Message is empty')
return None

log.debug(f'Twilio message for recipient with phone number {recipient_phone}', message)

client = Client(twilio_sid, twilio_token)

response = client.messages.create(
body=message,
from_=sender_phone,
to=recipient_phone
)

return response
Empty file added src/runners/handlers/smtp.py
Empty file.
55 changes: 55 additions & 0 deletions src/runners/handlers/sns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import json
import boto3

from botocore.exceptions import ClientError
from runners.helpers import log
from runners.helpers.dbconfig import REGION


def handle(alert, type='sns', topic=None, target=None, recipient_phone=None, subject=None, message_structure=None, message=None):

# check if phone is nit empty if yes notification will be delivered to twilio
if recipient_phone is None and topic is None and target is None:
log.error(f'Cannot identify recipient')
return None

if message is None:
log.error(f'SNS Message is empty')
return None

log.debug(f'SNS message ', message)

client = boto3.client('sns', region_name=REGION)

params = {}

if message_structure is not None:
params['MessageStructure'] = message_structure
if message_structure == 'json':
message = json.dumps(message)

if topic is not None:
params['TopicArn'] = topic
if target is not None:
params['TargetArn'] = target
if recipient_phone is not None:
params['PhoneNumber'] = recipient_phone
if subject is not None:
params['Subject'] = subject

log.debug(f"SNS message", message)

params['Message'] = message

# Try to send the message.
try:
# Provide the contents of the message.
response = client.publish(**params)
# Display an error if something goes wrong.
except ClientError as e:
log.error(f'Failed to send message {e}')
return None
else:
log.debug("SNS message sent!")

return response
54 changes: 54 additions & 0 deletions src/runners/handlers/sp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from runners.helpers import log
from runners.helpers import db


def call_procedure(procedure, parameters):
payload = None

try:
# call stored procedure
if parameters is not None and len(parameters) > 0:
params = "("
for i in range(len(parameters)):
params = params + "%s"
if i < len(parameters) - 1:
params = params + ","
params = params + ")"
else:
params = "()"

sql = "call " + procedure + params

log.debug(f"Procedure call sql {sql}")
connection = db.connect()
cur = connection.cursor()
cur.execute(sql, tuple(parameters))
rows = cur.fetchall()

if len(rows) > 0:
row = rows[0]

if len(row) > 0:
log.debug(f"Stored procedure {procedure} response", ''.join(row[0]))
payload = ''.join(row[0])

cur.close()
except Exception as e:
log.error(f"Error executing stored procedure", e)
raise

return payload


def handle(alert, procedure=None, parameters=None):
log.debug(f"Procedure name {procedure}")
log.debug(f"Procedure parameters {parameters}")
if procedure is not None:
# call Snowflake stored procedure
try:
result = call_procedure(procedure, parameters)
return result
except Exception:
return None
else:
return None
3 changes: 2 additions & 1 deletion src/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'azure-storage-common==1.4.0',
'google-api-python-client==1.7.10',
'pyTenable==0.3.22',
'boto3'
'boto3',
'twilio==6.29.4'
],
)

0 comments on commit 3e712b5

Please sign in to comment.