mirror of https://github.com/MISP/MISP
212 lines
7.1 KiB
Python
212 lines
7.1 KiB
Python
|
#!/usr/bin/env python3
|
||
|
# -*- coding: utf-8 -*-
|
||
|
#
|
||
|
'''
|
||
|
### MISP to Slack ####
|
||
|
ZMQ client to post events, attributes or sighting updates from a MISP instance to a slack channel.
|
||
|
|
||
|
This tool is part of the MISP core project and released under the GNU Affero
|
||
|
General Public License v3.0
|
||
|
|
||
|
Copyright (C) 2020 Christophe Vandeplas
|
||
|
|
||
|
For instructions on creating your BOT, please read: https://api.slack.com/bot-users
|
||
|
Your bot will need the permissions:
|
||
|
- channels:join
|
||
|
- chat:write
|
||
|
- users:write
|
||
|
|
||
|
WARNING WARNING - THIS SCRIPT DOES NOT MAGICALLY RESPECT ACLs
|
||
|
MAKE SURE YOU SET THE RIGHT FILTERS IN THE SETTINGS
|
||
|
'''
|
||
|
|
||
|
|
||
|
import argparse
|
||
|
import sys
|
||
|
import time
|
||
|
import zmq
|
||
|
import json
|
||
|
try:
|
||
|
import slack
|
||
|
except ImportError:
|
||
|
exit("Missing slackclient dependency. Please 'pip3 install slackclient'")
|
||
|
try:
|
||
|
from slackbot_settings import channel_name, slack_token, misp_url, misp_is_public, allowed_distributions, allowed_sharing_groups, max_value_len, include_attr, include_obj
|
||
|
except ImportError:
|
||
|
exit("Missing slackbot_settings.py. Please create from 'slackbot_settings.py.sample'")
|
||
|
|
||
|
|
||
|
def sanitize_value(s):
|
||
|
# very dirty cleanup
|
||
|
s = s.replace('http', 'hxxp')
|
||
|
s = s.replace('.', '[.]')
|
||
|
s = s.replace('@', '[AT]')
|
||
|
# truncate long strings
|
||
|
return (s[:max_value_len] + '..') if len(s) > max_value_len else s
|
||
|
|
||
|
|
||
|
def gen_attrs_text(attrs):
|
||
|
attrs_text_lst = []
|
||
|
type_value_mapping = {}
|
||
|
for a in attrs:
|
||
|
try:
|
||
|
type_value_mapping[a['type']].add(sanitize_value(a['value']))
|
||
|
except Exception:
|
||
|
type_value_mapping[a['type']] = set()
|
||
|
type_value_mapping[a['type']].add(sanitize_value(a['value']))
|
||
|
for k, v in type_value_mapping.items():
|
||
|
attrs_text_lst.append(f"- *{k}*: {','.join(v)}")
|
||
|
attrs_text = '\n'.join(attrs_text_lst)
|
||
|
return attrs_text
|
||
|
|
||
|
|
||
|
def publish_event(e):
|
||
|
cnt_attr = len(e.get('Attribute') or '')
|
||
|
cnt_obj = len(e.get('Object') or '')
|
||
|
cnt_tags = len(e.get('Tag') or '')
|
||
|
url = misp_url + '/events/view/' + e['id']
|
||
|
zmq_message_short = f"New MISP event '{e['info']}' with {cnt_attr} attributes, {cnt_obj} objects and {cnt_tags} tags."
|
||
|
|
||
|
image_url = 'https://raw.githubusercontent.com/MISP/MISP/2.4/docs/img/misp.png'
|
||
|
if misp_is_public:
|
||
|
image_url = f"{misp_url}/img/orgs/{e['Orgc']['name']}.png"
|
||
|
|
||
|
zmq_message_blocks = [
|
||
|
{
|
||
|
"type": "section",
|
||
|
"text": {
|
||
|
"type": "mrkdwn",
|
||
|
"text": f"*A new MISP <{url}|event> has been published:*\n"
|
||
|
f"Title: {e['info']}\n"
|
||
|
f"Date: {e['date']}\n"
|
||
|
f"Threat Level: {e['threat_level_id']}\n"
|
||
|
f"Contains {cnt_attr} attributes, {cnt_obj} objects and {cnt_tags} tags\n"
|
||
|
f"Full event: <{url}|{url}>"
|
||
|
},
|
||
|
"accessory": {
|
||
|
"type": "image",
|
||
|
"image_url": image_url,
|
||
|
"alt_text": "MISP or org logo"
|
||
|
}
|
||
|
}
|
||
|
]
|
||
|
|
||
|
if 'Tag' in e:
|
||
|
tag_block = {
|
||
|
"type": "actions",
|
||
|
"elements": [
|
||
|
]
|
||
|
}
|
||
|
tags = set([t['name'] for t in e['Tag']])
|
||
|
for a in e['Attribute']:
|
||
|
if 'Tag' in a:
|
||
|
for t in a['Tag']:
|
||
|
tags.add(t['name'])
|
||
|
for o in e['Object']:
|
||
|
for a in o['Attribute']:
|
||
|
if 'Tag' in a:
|
||
|
for t in a['Tag']:
|
||
|
tags.add(t['name'])
|
||
|
|
||
|
tags = sorted(tags)
|
||
|
for t in tags:
|
||
|
t = t.replace('misp-galaxy:', '').replace('mitre-', '')
|
||
|
tag_block['elements'].append({
|
||
|
"type": "button",
|
||
|
"text": {
|
||
|
"type": "plain_text",
|
||
|
"text": t
|
||
|
},
|
||
|
"value": "#"
|
||
|
})
|
||
|
zmq_message_blocks.append(tag_block)
|
||
|
|
||
|
# List attributes
|
||
|
if include_attr:
|
||
|
zmq_message_blocks.append({"type": "divider"})
|
||
|
attrs_text = gen_attrs_text(e['Attribute'])
|
||
|
if attrs_text:
|
||
|
zmq_message_blocks.append(
|
||
|
{
|
||
|
"type": "section",
|
||
|
"text": {
|
||
|
"type": "mrkdwn",
|
||
|
"text": f"*Attributes:*\n{attrs_text}"
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
# List Objects
|
||
|
if include_obj:
|
||
|
zmq_message_blocks.append({"type": "divider"})
|
||
|
for o in e['Object']:
|
||
|
attrs_text = gen_attrs_text(o['Attribute'])
|
||
|
if attrs_text:
|
||
|
# print(json.dumps(o, indent=2))
|
||
|
zmq_message_blocks.append(
|
||
|
{
|
||
|
"type": "section",
|
||
|
"text": {
|
||
|
"type": "mrkdwn",
|
||
|
"text": f"*{o['name'].capitalize()} object:*\n{attrs_text}"
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
|
||
|
# Send the message
|
||
|
client = slack.WebClient(token=slack_token)
|
||
|
client.users_setPresence(presence='auto')
|
||
|
channel = client.channels_join(name=channel_name)
|
||
|
client.chat_postMessage(
|
||
|
channel=channel['channel']['id'],
|
||
|
text=zmq_message_short,
|
||
|
blocks=zmq_message_blocks
|
||
|
)
|
||
|
|
||
|
|
||
|
parser = argparse.ArgumentParser(description='MISP to Slack bot - ZMQ client to gather events, attributes and sighting updates from a MISP instance')
|
||
|
parser.add_argument("-p", "--port", default="50000", help='set TCP port of the MISP ZMQ (default: 50000)')
|
||
|
parser.add_argument("-r", "--host", default="127.0.0.1", help='set host of the MISP ZMQ (default: 127.0.0.1)')
|
||
|
parser.add_argument("-t", "--sleep", default=0.1, help='sleep time (default: 0.1)', type=int)
|
||
|
args = parser.parse_args()
|
||
|
|
||
|
port = args.port
|
||
|
host = args.host
|
||
|
context = zmq.Context()
|
||
|
socket = context.socket(zmq.SUB)
|
||
|
socket.connect("tcp://%s:%s" % (host, port))
|
||
|
socket.setsockopt(zmq.SUBSCRIBE, b'')
|
||
|
|
||
|
poller = zmq.Poller()
|
||
|
poller.register(socket, zmq.POLLIN)
|
||
|
|
||
|
while True:
|
||
|
socks = dict(poller.poll(timeout=None))
|
||
|
if socket in socks and socks[socket] == zmq.POLLIN:
|
||
|
message = socket.recv()
|
||
|
topic, s, m = message.decode('utf-8').partition(" ")
|
||
|
|
||
|
try:
|
||
|
m_json = json.loads(m)
|
||
|
except Exception:
|
||
|
sys.stderr.write(f'Ignoring non-json message: {m}')
|
||
|
time.sleep(args.sleep)
|
||
|
continue
|
||
|
|
||
|
if 'status' in m_json:
|
||
|
pass
|
||
|
elif 'Event' in m_json:
|
||
|
# print(m_json)
|
||
|
e = m_json['Event']
|
||
|
if '*' in allowed_distributions or \
|
||
|
(e['distribution'] in allowed_distributions and (
|
||
|
e['distribution'] != '5' or (
|
||
|
'*' in allowed_sharing_groups or e['sharing_group_id'] in allowed_sharing_groups)
|
||
|
)):
|
||
|
print(f"Publishing event {e['id']} on slack")
|
||
|
publish_event(e)
|
||
|
else:
|
||
|
print(f"Ignoring event {e['id']} as it has a filtered distribution.")
|
||
|
else:
|
||
|
print(f'Non supported message: {m}')
|
||
|
time.sleep(args.sleep)
|