Skip to content

Commit

Permalink
[lambda][output] updating slack output format to support large messag…
Browse files Browse the repository at this point in the history
…e and be beautiful
  • Loading branch information
ryandeivert committed May 5, 2017
1 parent 37330d1 commit 71ee325
Showing 1 changed file with 124 additions and 12 deletions.
136 changes: 124 additions & 12 deletions stream_alert/alert_processor/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
'''
import cgi
import json
import logging
import os
Expand All @@ -23,8 +24,6 @@

import boto3

from botocore.exceptions import ClientError

from stream_alert.alert_processor.output_base import StreamOutputBase, OutputProperty

logging.basicConfig()
Expand Down Expand Up @@ -226,6 +225,8 @@ def dispatch(self, **kwargs):
class SlackOutput(StreamOutputBase):
"""SlackOutput handles all alert dispatching for Slack"""
__service__ = 'slack'
# Slack recommends no messages larger than 4000 bytes. This does not account for unicode
MAX_MESSAGE_SIZE = 4000

def get_user_defined_properties(self):
"""Get properties that must be asssigned by the user when configuring a new Slack
Expand All @@ -252,24 +253,135 @@ def get_user_defined_properties(self):
cred_requirement=True))
])

@staticmethod
def _format_message(rule_name, alert):
@classmethod
def _format_message(cls, rule_name, alert):
"""Format the message to be sent to slack.
Args:
rule_name [string]: The name of the rule that triggered the alert
alert: Alert relevant to the triggered rule
Returns:
[string] formatted message string to send to Slack. The message will look like:
```StreamAlert Rule Triggered
Rule Name: rule_name
Rule Description: rule_description
{JSON DUMP OF ALERT}```
[string] formatted message with attachments to send to Slack.
The message will look like:
StreamAlert Rule Triggered: rule_name
Rule Description:
This will be the docstring from the rule, sent as the rule_description
Record (Part 1 of 2):
...
"""
# Convert the alert we have to a nicely formatted string for slack
alert_text = '\n'.join(cls._json_to_slack_mrkdwn(alert, 0))
# Slack requires escaping the characters: '&', '>' and '<' and cgi does just that
alert_text = cgi.escape(alert_text)
messages = []
index = cls.MAX_MESSAGE_SIZE
while alert_text != '':
if len(alert_text) <= index:
messages.append(alert_text)
break

# Find the closest line break prior to this index
while index > 1 and alert_text[index] != '\n':
index -= 1

# Append the message part up until this index, and move to the next chunk
messages.append(alert_text[:index])
alert_text = alert_text[index+1:]

index = cls.MAX_MESSAGE_SIZE

header_text = '*StreamAlert Rule Triggered: {}*'.format(rule_name)
full_message = {
'text': header_text,
'mrkdwn': True,
'attachments': []
}

for index, message in enumerate(messages):
title = 'Record:'
if len(messages) > 1:
title = 'Record (Part {} of {}):'.format(index+1, len(messages))
rule_desc = ''
# Only print the rule description on the first attachment
if index == 0:
rule_desc = alert['metadata']['rule_description'] or DEFAULT_RULE_DESCRIPTION
rule_desc = '*Rule Description:*\n{}\n'.format(rule_desc)

# Add this attachemnt to the full message array of attachments
full_message['attachments'].append({
'fallback': header_text,
'color': '#b22222',
'pretext': rule_desc,
'title': title,
'text': message,
'mrkdwn_in': ['text', 'pretext']
})

# Return the dumped json payload to be sent to slack
return json.dumps(full_message)

@classmethod
def _json_to_slack_mrkdwn(cls, json_values, indent_count):
"""Translate a json object into a more human-readable blob of text
This will handle recursion of all nested maps and lists within the object
Args:
values [object]: variant to be translated (could be json map, list, etc)
tab_indent_count [integer]: Number of tabs to prefix each line with
Returns:
[list] list of strings that have been properly tabbed and formatted for printing
"""
rule_desc = alert['metadata']['rule_description'] or DEFAULT_RULE_DESCRIPTION
message = '```StreamAlert Rule Triggered\nRule Name: {}\nRule Description: {}\n{}```'
return message.format(rule_name, rule_desc, json.dumps(alert['record'], indent=4))
tab = '\t'
all_lines = []
if isinstance(json_values, dict):
all_lines = cls._json_map_to_text(json_values, tab, indent_count)
elif isinstance(json_values, list):
all_lines = cls._json_list_to_text(json_values, tab, indent_count)
else:
all_lines.append('{}'.format(json_values))

return all_lines

@classmethod
def _json_map_to_text(cls, json_values, tab, indent_count):
all_lines = []
for key, value in json_values.iteritems():
if isinstance(value, (dict, list)) and value:
all_lines.append('{}*{}:*'.format(tab*indent_count, key))
all_lines.extend(cls._json_to_slack_mrkdwn(value, indent_count+1))
else:
new_lines = cls._json_to_slack_mrkdwn(value, indent_count+1)
if len(new_lines) == 1:
all_lines.append('{}*{}:* {}'.format(tab*indent_count, key, new_lines[0]))
elif new_lines:
all_lines.append('{}*{}:*'.format(tab*indent_count, key))
all_lines.extend(new_lines)
else:
all_lines.append('{}*{}:* {}'.format(tab*indent_count, key, value))

return all_lines

@classmethod
def _json_list_to_text(cls, json_values, tab, indent_count):
all_lines = []
for index, value in enumerate(json_values):
if isinstance(value, (dict, list)) and value:
all_lines.append('{}*[{}]*'.format(tab*indent_count, index+1))
all_lines.extend(cls._json_to_slack_mrkdwn(value, indent_count+1))
else:
new_lines = cls._json_to_slack_mrkdwn(value, indent_count+1)
if len(new_lines) == 1:
all_lines.append('{}*[{}]* {}'.format(tab*indent_count, index+1, new_lines[0]))
elif new_lines:
all_lines.append('{}*[{}]*'.format(tab*indent_count, index+1))
all_lines.extend(new_lines)
else:
all_lines.append('{}*[{}]* {}'.format(tab*indent_count, index+1, value))

return all_lines

def dispatch(self, **kwargs):
"""Send alert text to Slack
Expand Down

0 comments on commit 71ee325

Please sign in to comment.