├── .github └── workflows │ └── docker_image_build.yml ├── .gitignore ├── .pylintrc ├── Dockerfile ├── Old ├── SAMbot.py ├── helper.py └── mispattruploader.py ├── README.md ├── docker-compose.yml ├── machinetag.json ├── main.py ├── misp.py └── requirements.txt /.github/workflows/docker_image_build.yml: -------------------------------------------------------------------------------- 1 | name: Building and publishing a docker image 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-docker-image: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Log in to the Container registry 19 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 20 | with: 21 | registry: ${{ env.REGISTRY }} 22 | username: ${{ github.actor }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - name: Extract metadata (tags, labels) for Docker 26 | id: meta 27 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 28 | with: 29 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 30 | 31 | - name: Build and push Docker image 32 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 33 | with: 34 | context: . 35 | push: true 36 | tags: ${{ steps.meta.outputs.tags }} 37 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # exclude json files 107 | *.json 108 | 109 | # exclude logs 110 | logs/* 111 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [defaults] 2 | disable=C0301 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image from the Docker Hub 2 | FROM python:3.8 3 | 4 | # These two environment variables prevent __pycache__/ files. 5 | ENV PYTHONUNBUFFERED 1 6 | ENV PYTHONDONTWRITEBYTECODE 1 7 | 8 | #RUN git clone https://github.com/yaleman/sam-bot /code/ 9 | 10 | WORKDIR /code 11 | 12 | COPY ./ /code/ 13 | 14 | #RUN git checkout docker 15 | 16 | RUN pip install -r requirements.txt 17 | 18 | 19 | #COPY *.py /code/ 20 | COPY config.json /code/ 21 | 22 | CMD python main.py 23 | -------------------------------------------------------------------------------- /Old/SAMbot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import time 4 | import re 5 | import requests 6 | import logging 7 | import json 8 | import pyjokes 9 | import traceback 10 | import helper 11 | import asyncio 12 | import nest_asyncio 13 | nest_asyncio.apply() 14 | import slack 15 | from pprint import pprint 16 | from logging.config import dictConfig 17 | from mispattruploader import * 18 | 19 | 20 | loop = asyncio.get_event_loop() 21 | ### Configuration loading and logging enabled. 22 | dir_path = os.path.dirname(os.path.realpath(__file__)) 23 | config_file = dir_path + '/config.json' 24 | # instantiate Slack client 25 | output_error_file = "" 26 | with open(config_file) as json_data_file: 27 | data = json.load(json_data_file) 28 | token=data["slack"]["SLACK_BOT_TOKEN"] 29 | if 'logging' in data: 30 | if 'output_file' in data['logging']: 31 | output_all_file = data['logging']['output_file'] 32 | else: 33 | exit("Please include output_file in config") 34 | if 'output_error_file' in data['logging']: 35 | output_error_file = data['logging']['output_error_file'] 36 | else: 37 | output_all_file = dir_path + "/sambot.log" 38 | 39 | 40 | logging_config = dict( 41 | version = 1, 42 | formatters = { 43 | 'f': {'format': 44 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s'} 45 | }, 46 | handlers = { 47 | 'Stream': {'class': 'logging.StreamHandler', 48 | 'formatter': 'f', 49 | 'level': 'DEBUG' 50 | }, 51 | 'file_all': { 52 | 'class': 'logging.handlers.RotatingFileHandler', 53 | 'level': 'DEBUG', 54 | 'formatter': 'f', 55 | 'filename': output_all_file, 56 | 'mode': 'a', 57 | 'maxBytes': 10485760, 58 | 'backupCount': 5, 59 | }, 60 | }, 61 | root = { 62 | 'handlers': ['Stream', 'file_all'], 63 | 'level': 'DEBUG', 64 | }, 65 | ) 66 | 67 | if output_error_file != "": 68 | logging_config['handlers']['file_error'] = { 69 | 'class': 'logging.handlers.RotatingFileHandler', 70 | 'level': 'ERROR', 71 | 'formatter': 'f', 72 | 'filename': output_error_file, 73 | 'mode': 'a', 74 | 'maxBytes': 10485760, 75 | 'backupCount': 5, 76 | } 77 | logging_config['root']['handlers'].append('file_error') 78 | 79 | dictConfig(logging_config) 80 | 81 | logger = logging.getLogger('SAMbot') 82 | try: 83 | slack_client = slack.WebClient(token=token, run_async=True) 84 | test = loop.run_until_complete(slack_client.api_call("auth.test")) 85 | logger.info("Slack client created") 86 | logger.info("Connecting to misp server") 87 | misp = misp_custom(data['misp']['url'], data['misp']['key'], data['misp']['ssl']) 88 | logger.info("Connected to misp server successfully") 89 | helperFunc = helper.TonyTheHelper(slack_client) 90 | # starterbot's user ID in Slack: value is assigned after the bot starts up 91 | starterbot_id = None 92 | slack_test = slack.RTMClient(token=token) 93 | except Exception as e: 94 | logger.error('Exception Caught! FIX YOUR CONFIG') 95 | error = traceback.format_exc() 96 | logger.error(error) 97 | exit() 98 | # constants 99 | RTM_READ_DELAY = 1 # 1 second delay between reading from RTM 100 | EXAMPLE_COMMAND = "Tell a joke" 101 | MENTION_REGEX = "^<@(|[WU].+?)>(.*)" 102 | 103 | def get_username(slack_event, channel): 104 | slack_temp = slack.WebClient(token=token, run_async=False) 105 | logger.info(channel) 106 | members = slack_temp.conversations_members(channel=channel) 107 | logger.info(members.data) 108 | print(dir(members)) 109 | if 'user' in slack_event: 110 | logger.info(slack_event['user']) 111 | for member in members.data['members']: 112 | logger.info("member: %s" %member) 113 | if member == slack_event['user']: 114 | member_profile = slack_temp.users_info(user=member) 115 | if member_profile['ok']: 116 | name = member_profile['user']['profile']['display_name_normalized'] 117 | logger.info(name) 118 | return name 119 | else: 120 | return "Unknown" 121 | else: 122 | return "Unknown" 123 | 124 | 125 | def parse_bot_commands(event): 126 | """ 127 | Parses a list of events coming from the Slack RTM API to find bot commands. 128 | If a bot command is found, this function returns a tuple of command and channel. 129 | If its not found, then this function returns None, None. 130 | """ 131 | logger.info(event) 132 | 133 | #try: 134 | #logger.debug(event) 135 | if "files" in event: 136 | for file in event["files"]: 137 | if file["mode"] == "snippet": 138 | url = file["url_private_download"] 139 | title = file["title"] 140 | if title == "Untitled": 141 | strTitle = "#Warroom" 142 | else: 143 | strTitle = "#Warroom " + title 144 | headers = {'Authorization': 'Bearer '+token} 145 | r = requests.get(url, headers=headers) 146 | content = r.content.decode("utf-8") 147 | logger.error(r.content) 148 | e_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(event['event_ts']))) 149 | e_title = e_time + " - " + strTitle 150 | username = get_username(event, event["channel"]) 151 | #await username 152 | logger.info(username) 153 | misp_response = misp.misp_send(0, content, e_title, username) 154 | return misp_response, None, event["channel"], event["user"] 155 | #except: 156 | # error = traceback.format_exc() 157 | # helperFunc.respond_channel(error, event["channel"]) 158 | 159 | 160 | # def respond(command, channel, user): 161 | # # This is where you start to implement more commands! 162 | # # Sends the response back to the channel 163 | # slack_client.api_call( 164 | # "chat.postEphemeral", 165 | # channel=channel, 166 | # text=command, 167 | # user=user 168 | # ) 169 | 170 | # def helperFunc.respond_channel(command, channel): 171 | # slack_client.api_call( 172 | # "chat.postMessage", 173 | # channel=channel, 174 | # text=command 175 | # ) 176 | 177 | def parse_direct_mention(message_text): 178 | """ 179 | Finds a direct mention (a mention that is at the beginning) in message text 180 | and returns the user ID which was mentioned. If there is no direct mention, returns None 181 | """ 182 | matches = re.search(MENTION_REGEX, message_text) 183 | # the first group contains the username, the second group contains the remaining message 184 | return (matches.group(1), matches.group(2).strip()) if matches else (None, None) 185 | 186 | def tell_a_joke(command, channel, user): 187 | """ 188 | Executes bot command if the command is known 189 | """ 190 | # Default response is help text for the user 191 | default_response = "Not sure what you mean. Try *{}*.".format(EXAMPLE_COMMAND) 192 | help_command = "Help" 193 | # Finds and executes the given command, filling in response 194 | response = None 195 | # This is where you start to implement more commands! 196 | if command.lower().startswith(EXAMPLE_COMMAND.lower()): 197 | response = pyjokes.get_joke(category='all') + " This joke has been Brought to you by pyjokes." 198 | elif command.lower().startswith(help_command.lower()): 199 | response = helperFunc.print_help() 200 | # Sends the response back to the channel 201 | helperFunc.respond_channel(response or default_response, channel) 202 | 203 | @slack.RTMClient.run_on(event='message') 204 | def main(**payload): 205 | data = payload['data'] 206 | web_client = payload['web_client'] 207 | rtm_client = payload['rtm_client'] 208 | logger.info(payload) 209 | try: 210 | if data != None: 211 | misp_response, smartass, channel, user = parse_bot_commands(data) 212 | if misp_response: 213 | helperFunc.respond(misp_response, channel, user) 214 | elif smartass: 215 | print(smartass) 216 | tell_a_joke(smartass, channel, user) 217 | time.sleep(RTM_READ_DELAY) 218 | except KeyboardInterrupt: 219 | logger.error("Bot was manually stopped with ctrl+c") 220 | except Exception as e: 221 | error = traceback.format_exc() 222 | logger.error(error) 223 | helperFunc.respond_channel("The bot has caught a fatal error. Please review the error log. Exiting now.", "#Warroom") 224 | exit() 225 | 226 | 227 | if __name__ == "__main__": 228 | if slack_client.rtm_connect(with_team_state=False, auto_reconnect=True): 229 | logger.info("SAMbot connected and running!") 230 | # # Read bot's user ID by calling Web API method `auth.test` 231 | # starterbot_id = slack_client.api_call("auth.test")["user_id"] 232 | # online = True 233 | 234 | try: 235 | slack_test.start() 236 | loop.run_forever() 237 | except KeyboardInterrupt: 238 | logger.error("Bot was manually stopped with ctrl+c") 239 | except Exception as e: 240 | error = traceback.format_exc() 241 | logger.error(error) 242 | helperFunc.respond_channel("The bot has caught a fatal error. Please review the error log. Exiting now.", "#Warroom") 243 | online = False 244 | 245 | else: 246 | logger.info("Connection failed. Exception traceback printed above.") 247 | -------------------------------------------------------------------------------- /Old/helper.py: -------------------------------------------------------------------------------- 1 | from logging.config import dictConfig 2 | import logging 3 | 4 | 5 | 6 | class TonyTheHelper: 7 | def __init__(self, slackclient): 8 | self.slack_client = slackclient 9 | self.helper_logging = logging.getLogger('TonyTheHelper') 10 | self.helper_logging.info('Connected from TonyTheHelper') 11 | 12 | def print_help(self): 13 | response = """ 14 | >Please see the accepted fields in the Github repo 15 | `https://github.com/IRATEAU/sam-bot/blob/master/README.md` 16 | """ 17 | return response 18 | 19 | def respond(self, command, channel, user): 20 | # This is where you start to implement more commands! 21 | # Sends the response back to the channel 22 | self.slack_client.chat_postEphemeral( 23 | channel=channel, 24 | text=command, 25 | user=user 26 | ) 27 | 28 | def respond_channel(self, command, channel): 29 | self.slack_client.chat_postMessage( 30 | channel=channel, 31 | text=command 32 | ) -------------------------------------------------------------------------------- /Old/mispattruploader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging, re, requests, argparse, sys, time, traceback 3 | from pymisp import MISPEvent, PyMISP, MISPObject 4 | from defang import refang 5 | from pprint import pprint 6 | from urllib.parse import urlparse 7 | 8 | class misp_custom: 9 | 10 | def __init__(self, misp_url, misp_key, misp_ssl): 11 | try: 12 | self.misp = PyMISP(misp_url, misp_key, misp_ssl)#, 'json') 13 | except Exception as err: 14 | sys.exit('Batch Job Terminated: MISP connection error - \n'+repr(err)) 15 | self.misp_logger = logging.getLogger('mispattruploader') 16 | 17 | def submit_to_misp(self, misp, misp_event, misp_objects): 18 | ''' 19 | Submit a list of MISP objects to a MISP event 20 | :misp: PyMISP API object for interfacing with MISP 21 | :misp_event: MISPEvent object 22 | :misp_objects: List of MISPObject objects. Must be a list 23 | ''' 24 | # go through round one and only add MISP objects 25 | a = [] 26 | for misp_object in misp_objects: 27 | self.misp_logger.debug(misp_object) 28 | if len(misp_object.attributes) > 0: 29 | if misp_object.name == 'network-connection': 30 | template_id = 'af16764b-f8e5-4603-9de1-de34d272f80b' 31 | else: 32 | # self.misp_logger.debug(dir(pymisp.api)) 33 | # self.misp_logger.debug(dir(self.misp)) 34 | # exit() 35 | self.misp_logger.debug(misp_object.template_uuid) 36 | object_template = self.misp.get_object_template(misp_object.template_uuid) 37 | template_id = object_template['ObjectTemplate']['id'] 38 | self.misp_logger.debug(template_id) 39 | self.misp_logger.debug(dir(misp_event)) 40 | self.misp_logger.debug(misp_event) 41 | _a = misp.add_object(event=misp_event, misp_object=misp_object) 42 | self.misp_logger.debug(_a) 43 | a.append(_a) 44 | # go through round two and add all the object references for each object 45 | b = [] 46 | for misp_object in misp_objects: 47 | for reference in misp_object.ObjectReference: 48 | _b =misp.add_object_reference(reference) 49 | b.append(_b) 50 | return a,b 51 | 52 | def check_object_length(self, misp_objects): 53 | for misp_object in misp_objects: 54 | self.misp_logger.info(misp_object.name) 55 | self.misp_logger.info(dir(misp_object)) 56 | if len(misp_object.attributes) == 0: 57 | self.misp_logger.error('failure to put in correct tags') 58 | return False 59 | return True 60 | 61 | def get_comm_and_tags(self, strInput): 62 | comment = None 63 | str_comment = "" 64 | tags = ["tlp:green"] 65 | tag_type = None 66 | for line in strInput.splitlines(): 67 | 68 | if ("comment:" in line.lower()): 69 | vals = line.split(":", 1) 70 | comment = vals[1:] 71 | elif ("tag:" in line.lower()): 72 | vals = line.split(":", 1) 73 | value = vals[1].strip().lower() 74 | if "tlp" in value: 75 | tags.remove("tlp:green") 76 | vals_str = "tlp:" 77 | vals_split = vals[1].split(":") 78 | vals_str += vals_split[1] 79 | tags.append(vals_str) 80 | elif ("type:" in line.lower()): 81 | vals = line.split(":", 1) 82 | value = vals[1].strip().lower() 83 | if value == "phish": 84 | tag_type = value 85 | elif value == "malware": 86 | tag_type = value 87 | elif value == "bec/spam": 88 | tag_type = value 89 | elif value == "dump": 90 | tag_type = value 91 | elif (value == "apt") or (value == "APT"): 92 | tag_type = value 93 | if tag_type: 94 | self.misp_logger.info('Setting tag to ir8: %s' %tag_type) 95 | tag = "ir8:" + tag_type 96 | tags.append(tag) 97 | else: 98 | tags = None 99 | if comment != None: 100 | for c in comment: 101 | str_comment += c 102 | else: 103 | str_comment = comment 104 | return str_comment, tags 105 | 106 | ####### SLACK ENTRY POINT! ###### 107 | def misp_send(self, strMISPEventID, strInput, strInfo, strUsername): 108 | # Establish communication with MISP 109 | # event = MISPEvent() 110 | # event.info = 'Test event' 111 | # event.analysis = 0 112 | # event.distribution = 3 113 | # event.threat_level_id = 2 114 | 115 | # event.add_attribute('md5', '678ff97bf16d8e1c95679c4681834c41') 116 | # # 117 | 118 | # self.misp.add_event(event) 119 | 120 | # exit() 121 | 122 | 123 | 124 | 125 | try: 126 | objects=[] 127 | #get comments and tags from string input 128 | str_comment, tags = self.get_comm_and_tags(strInput) 129 | print(tags) 130 | if tags == None: 131 | self.misp_logger.info('Irate not in Tags: %s equals None' %tags) 132 | response = None 133 | return response 134 | #setup misp objects 135 | mispobj_email = MISPObject(name="email") 136 | mispobj_file = MISPObject(name="file") 137 | mispobj_files = {} 138 | mispobj_domainip = MISPObject(name="domain-ip") 139 | url_no = 0 140 | file_no = 0 141 | mispobj_urls = {} 142 | 143 | #process input 144 | for line in strInput.splitlines(): 145 | if ("domain:" in line.lower()): #Catch domain and add to domain/IP object 146 | mispobj_domainip = MISPObject(name="domain-ip") 147 | vals = line.split(":", 1) 148 | mispobj_domainip.add_attribute("domain", value=vals[1].strip(), comment=str_comment) 149 | objects.append(mispobj_domainip) 150 | elif ("ip:" in line.lower()) or ("ip-dst:" in line.lower()) or ("ip-src:" in line.lower()): #Catch IP and add to domain/IP object 151 | if "domain:" in strInput.splitlines(): 152 | mispobj_domainip = MISPObject(name="domain-ip") 153 | vals = line.split(":", 1) 154 | mispobj_domainip.add_attribute("ip", value=vals[1].strip(), comment=str_comment) 155 | objects.append(mispobj_domainip) 156 | else: 157 | mispobj_network_connection = MISPObject(name="network-connection") 158 | vals = line.split(":", 1) 159 | if ("ip:" in line.lower()) or ("ip-dst:" in line.lower()): 160 | mispobj_network_connection.add_attribute("ip-dst", type="ip-dst", value=vals[1].strip(), comment=str_comment) 161 | else: 162 | mispobj_network_connection.add_attribute("ip-src", type="ip-src", value=vals[1].strip(), comment=str_comment) 163 | objects.append(mispobj_network_connection) 164 | 165 | elif ("source-email:" in line.lower()) or ("email-source" in line.lower()) or ("from:" in line.lower()): #Catch email and add to email object 166 | vals = line.split(":", 1) 167 | mispobj_email.add_attribute("from", value = vals[1].strip(), comment=str_comment) 168 | elif ("url:" in line.lower()) or (('kit:' in line.lower() or ('creds:' in line.lower())) and (('hxxp' in line.lower()) or ('http' in line.lower()))): #Catch URL and add to URL object 169 | vals = line.split(":", 1) 170 | url = vals[1].strip() 171 | url = refang(url) 172 | parsed = urlparse(url) 173 | mispobj_url = MISPObject(name="url") 174 | mispobj_url.add_attribute("url", value=parsed.geturl(), category="Payload delivery", comment=str_comment) 175 | if parsed.hostname: 176 | mispobj_url.add_attribute("host", value=parsed.hostname, comment=str_comment) 177 | if parsed.scheme: 178 | mispobj_url.add_attribute("scheme", value=parsed.scheme, comment=str_comment) 179 | if parsed.port: 180 | mispobj_url.add_attribute("port", value=parsed.port, comment=str_comment) 181 | mispobj_urls[url_no] = mispobj_url 182 | url_no += 1 183 | 184 | #Catch different hashes and add to file object 185 | elif ("sha1:" in line.lower()) or ("SHA1:" in line): 186 | vals = line.split(":", 1) 187 | mispobj_file.add_attribute("sha1", value=vals[1].strip(),comment=str_comment) 188 | elif ("sha256:" in line.lower()) or ("SHA256:" in line): 189 | vals = line.split(":", 1) 190 | mispobj_file.add_attribute("sha256", value=vals[1].strip(), comment=str_comment) 191 | elif ("md5:" in line.lower()) or ("MD5:" in line): 192 | vals = line.split(":", 1) 193 | mispobj_file.add_attribute("md5", value=vals[1].strip(), comment=str_comment) 194 | elif ("subject:" in line.lower()): #or ("subject:" in line): #Catch subject and add to email object 195 | self.misp_logger.info('adding subject') 196 | vals = line.split(":", 1) 197 | mispobj_email.add_attribute("subject", value = vals[1].strip(), comment=str_comment) 198 | elif ("hash|filename:" in line.lower()): #catch hash|filename pair and add to file object 199 | vals = line.split(":", 1) 200 | val = vals[1].split("|") 201 | l_hash = val[0] 202 | l_filename = val[1] 203 | l_mispobj_file = MISPObject(name="file") 204 | if len(re.findall(r"\b[a-fA-F\d]{32}\b", l_hash)) > 0: 205 | l_mispobj_file.add_attribute("md5", value=l_hash.strip(), comment=str_comment) 206 | l_mispobj_file.add_attribute("filename", value=l_filename.strip(), comment=str_comment) 207 | mispobj_files[file_no] = l_mispobj_file 208 | elif len(re.findall(r'\b[0-9a-f]{40}\b', l_hash)) > 0: 209 | l_mispobj_file.add_attribute("sha1", value=l_hash.strip(), comment=str_comment) 210 | l_mispobj_file.add_attribute("filename", value=l_filename.strip(), comment=str_comment) 211 | mispobj_files[file_no] = l_mispobj_file 212 | elif len(re.findall(r'\b[A-Fa-f0-9]{64}\b', l_hash)) > 0: 213 | l_mispobj_file.add_attribute("sha256", value=l_hash.strip(), comment=str_comment) 214 | l_mispobj_file.add_attribute("filename", value=l_filename.strip(), comment=str_comment) 215 | mispobj_files[file_no] = l_mispobj_file 216 | file_no +=1 217 | 218 | 219 | #add all misp objects to List to be processed and submitted to MISP server as one. 220 | if len(mispobj_file.attributes) > 0: 221 | objects.append(mispobj_file) 222 | if len(mispobj_email.attributes) > 0: 223 | objects.append(mispobj_email) 224 | 225 | 226 | for u_key, u_value in mispobj_urls.items(): 227 | if len(u_value.attributes) > 0: 228 | objects.append(u_value) 229 | for f_key, f_value in mispobj_files.items(): 230 | if len(f_value.attributes) > 0: 231 | objects.append(f_value) 232 | # Update timestamp and event 233 | 234 | except Exception as e: 235 | error = traceback.format_exc() 236 | response = "Error occured when converting string to misp objects:\n %s" %error 237 | self.misp_logger.error(response) 238 | return response 239 | 240 | if self.check_object_length(objects) != True: 241 | self.misp_logger.error('Input from %s did not contain accepted tags.\n Input: \n%s' %(strUsername, strInput)) 242 | return "Error in the tags you entered. Please see the guide for accepted tags." 243 | 244 | try: 245 | # self.misp_logger.error(dir(self.misp)) 246 | misp_event = MISPEvent() 247 | misp_event.info = strInfo 248 | misp_event.distribution = 0 249 | misp_event.analysis = 2 250 | misp_event.threat_level_id = 3 251 | # event.add_attribute('md5', '678ff97bf16d8e1c95679c4681834c41') 252 | #event = self.misp.new_event(info=strInfo, distribution='0', analysis='2', threat_level_id='3', published=False) 253 | #misp_event = MISPEvent() 254 | #misp_event.load(event) 255 | add = self.misp.add_event(misp_event) 256 | self.misp_logger.info("Added event %s" %add) 257 | a,b = self.submit_to_misp(self.misp, misp_event, objects) 258 | for tag in tags: 259 | self.misp.tag(misp_event.uuid, tag) 260 | #self.misp.add_internal_comment(misp_event.id, reference="Author: " + strUsername, comment=str_comment) 261 | ccc = self.misp.publish(misp_event, alert=False) 262 | self.misp_logger.info(ccc) 263 | misp_event = self.misp.get_event(misp_event) 264 | response = misp_event 265 | #for response in misp_event: 266 | if ('errors' in response and response['errors'] != None): 267 | return ("Submission error: "+repr(response['errors'])) 268 | else: 269 | if response['Event']['RelatedEvent']: 270 | e_related = "" 271 | for each in response['Event']['RelatedEvent']: 272 | e_related = e_related + each['Event']['id'] + ", " 273 | return "Created ID: " + str(response['Event']['id']) + "\nRelated Events: " + ''.join(e_related) 274 | else: 275 | return "Created ID: " + str(response['Event']['id']) 276 | 277 | except Exception as e: 278 | error = traceback.format_exc() 279 | response = "Error occured when submitting to misp:\n %s" %error 280 | self.misp_logger.error(response) 281 | return response -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sam-bot 2 | SAM Bot creates MISP events from data fed to it from Slack in a code snippet. 3 | 4 | The following fields are accepted by SAMbot and will be added to the MISP event. 5 | 6 | - type: 7 | - url: or kit: or creds: (it will also pickup any line with http or hxxp in it) 8 | - ip: 9 | - domain: 10 | - ip-dst: 11 | - ip-src: 12 | - from: or source-email: or email-source 13 | - subject: 14 | - md5: 15 | - sha1: 16 | - sha256: 17 | - tag: 18 | - hash|filename: 19 | 20 | Accepted fields for type are: 21 | 22 | - phish 23 | - malware 24 | - bec/scam 25 | - dump 26 | - apt 27 | 28 | Tags that are accepted are 29 | 30 | - TLP:white 31 | - TLP:green 32 | - TLP:amber 33 | - TLP:red 34 | 35 | ### Example 36 | ~~~shell 37 | type: malware 38 | Url: http://bad.biz/r1/asda.exe 39 | ip: 8.8.8.8 40 | ip-dst: 8.8.8.8 41 | ip-src: 1.1.1.1 42 | domain: bad.biz 43 | from: phish@avalanche.ru 44 | subject: please transfer now 45 | md5: c4c17055ea16183fbb6133b6e5cfb6f9 46 | sha1: 17a5db6350140685d219f4f69dcc0e669a4f027e 47 | sha256: 6b773f5367c1a6a108537b9ee17c95314158b1de0b5195eabb9a52eaf145b90a 48 | hash|filename: 6b773f5367c1a6a108537b9ee17c95314158b1de0b5195eabb9a52eaf145b90a|asda.exe 49 | tag: tlp:RED 50 | ~~~ 51 | 52 | 53 | ## Installation requirements 54 | 55 | #### Must use Python3 56 | 57 | Run the following: 58 | ~~~~shell 59 | pip3 install -r requirements.txt 60 | ~~~~ 61 | 62 | ### Bot Configuration: 63 | - Add MISP URL and API key to config.json file 64 | - Add Slack bot token to config.json file 65 | - Add log name/location to config.json (Optional) 66 | 67 | ### MISP requirements: 68 | Import the machinetag.json file as a new taxonomy 69 | ~~~~shell 70 | $ cd /var/www/MISP/app/files/taxonomies/ 71 | $ mkdir privatetaxonomy 72 | $ cd privatetaxonomy 73 | $ vi machinetag.json 74 | $ paste contents 75 | ~~~~ 76 | 77 | ### Taxonomies to be enabled at a minimum: 78 | the bot requires that the following taxonomies are enable to run 79 | - TLP 80 | - IR8 81 | 82 | If you don't specify a `logging` config, it'll default to putting logs in './logs/', which is the log volume for docker. 83 | 84 | config.json example 85 | ~~~~shell 86 | { 87 | "slack":{ 88 | "SLACK_BOT_OAUTH_TOKEN" : "xoxb-332250278039-yQQQom0PPoRz2QufGHlTnwg7", 89 | "SLACK_SIGNING_SECRET" : "sadfasdfasfasfasdfsadfasdfafasdfasdfasd" 90 | }, 91 | "misp" : { 92 | "url" : "https://misp.test.local", 93 | "key" : "asdfasdfsadfasfsadfsadfsdfdsafasdfasfasfasdfasdfsadf" 94 | }, 95 | "logging" : { 96 | "output_file" : "sambot.log", 97 | "output_error_file": "sambot_error.log" 98 | } 99 | } 100 | ~~~~ 101 | 102 | # Development 103 | 104 | Set up a slack app, follow instructions in the git repo here: https://github.com/slackapi/python-slack-events-api/tree/main/example 105 | 106 | ## Event Subscriptions 107 | 108 | It needs to sub to the following events: 109 | 110 | - message.channels 111 | - file_created 112 | - file_shared 113 | 114 | ## Oauth Scopes required 115 | 116 | - users:read 117 | - links:read 118 | - files:read 119 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | sambot: 5 | container_name: sambot 6 | build: 7 | context: . 8 | ports: 9 | - 3000:3000 10 | volumes: 11 | - logs:/code/logs/ 12 | environment: 13 | - TEST_MODE=1 14 | volumes: 15 | logs: 16 | -------------------------------------------------------------------------------- /machinetag.json: -------------------------------------------------------------------------------- 1 | { 2 | "predicates": [ 3 | { 4 | "colour": "#CC0033", 5 | "description": "Observed phishing content", 6 | "value": "phish" 7 | }, 8 | { 9 | "colour": "#FFC000", 10 | "description": "Observed malware", 11 | "value": "malware" 12 | }, 13 | { 14 | "colour": "#339900", 15 | "description": "Observed BEC/SCAM", 16 | "value": "bec/scam" 17 | }, 18 | { 19 | "colour": "#ffffff", 20 | "description": "Information from a data dump.", 21 | "value": "dump" 22 | }, 23 | { 24 | "colour": "#d208f4", 25 | "description": "Believed to be possible APT related indicators", 26 | "value": "apt" 27 | } 28 | ], 29 | "version": 1, 30 | "description": "Tags Defined for IR8 specific information", 31 | "namespace": "ir8", 32 | "exclusive": true 33 | } 34 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | sam-bot 3 | 4 | You need to have an Event subscription enabled that points to the flask app 5 | https://api.slack.com/apps/A012QE04RME/event-subscriptions? 6 | https://api.slack.com/events-api#subscriptions 7 | https://github.com/slackapi/python-slack-events-api 8 | https://github.com/slackapi/python-slackclient/blob/master/tutorial/03-responding-to-slack-events.md 9 | ''' 10 | 11 | import logging 12 | from logging.config import dictConfig 13 | import json 14 | import os 15 | import sys 16 | import time 17 | import traceback 18 | import threading 19 | 20 | import git 21 | import requests 22 | import flask 23 | import slack 24 | import slack.errors 25 | from slackeventsapi import SlackEventAdapter 26 | from misp import MispCustomConnector 27 | 28 | dir_path = os.path.dirname(os.path.realpath(__file__)) 29 | config_file = dir_path + '/config.json' 30 | 31 | # parse config file 32 | with open(config_file) as json_data_file: 33 | try: 34 | data = json.load(json_data_file) 35 | except json.decoder.JSONDecodeError as error: 36 | sys.exit(f"Couldn't parse config.json: {error}") 37 | 38 | if 'testing' in data: 39 | TEST_MODE = data.get('testing') 40 | else: 41 | TEST_MODE = os.getenv('TEST_MODE', False) 42 | 43 | if 'logging' in data: 44 | # default to sambot.log in log dir next to script if it's not set 45 | LOGFILE_DEFAULT = data['logging'].get('output_file', f"{dir_path}/logs/sambot.log") 46 | # default to sambot_error.log in log dir next to script if it's not set 47 | LOGFILE_ERROR = data['logging'].get('output_error_file', f"{dir_path}/logs/sambot_error.log") 48 | else: 49 | # defaults 50 | LOGFILE_DEFAULT = "./logs/sambot.log" 51 | LOGFILE_ERROR = "./logs/sambot_error.log" 52 | 53 | logging_config = dict( 54 | version = 1, 55 | formatters = { 56 | 'f': {'format': 57 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s'} 58 | }, 59 | handlers = { 60 | 'Stream': {'class': 'logging.StreamHandler', 61 | 'formatter': 'f', 62 | 'level': 'DEBUG' 63 | }, 64 | 'file_all': { 65 | 'class': 'logging.handlers.RotatingFileHandler', 66 | 'level': 'DEBUG', 67 | 'formatter': 'f', 68 | 'filename': LOGFILE_DEFAULT, 69 | 'mode': 'a', 70 | 'maxBytes': 10485760, 71 | 'backupCount': 5, 72 | }, 73 | }, 74 | root = { 75 | 'handlers': ['Stream', 'file_all'], 76 | 'level': 'DEBUG', 77 | }, 78 | ) 79 | 80 | 81 | logging_config['handlers']['file_error'] = { 82 | 'class': 'logging.handlers.RotatingFileHandler', 83 | 'level': 'ERROR', 84 | 'formatter': 'f', 85 | 'filename': LOGFILE_ERROR, 86 | 'mode': 'a', 87 | 'maxBytes': 10485760, 88 | 'backupCount': 5, 89 | } 90 | logging_config['root']['handlers'].append('file_error') 91 | 92 | dictConfig(logging_config) 93 | 94 | logger = logging.getLogger('SAMbot') 95 | 96 | # connecting to MISP 97 | try: 98 | misp_object = MispCustomConnector(misp_url=data['misp']['url'], 99 | misp_key=data['misp']['key'], 100 | misp_ssl=data.get('misp', {}).get('ssl', True), # default to using SSL 101 | ) 102 | logger.info("Connected to misp server successfully") 103 | # Who knows what kind of errors PyMISP will throw? 104 | #pylint: disable=broad-except 105 | except Exception: 106 | logger.error('Failed to connect to MISP:') 107 | logger.error(traceback.format_exc()) 108 | sys.exit(1) 109 | 110 | # config file - slack section 111 | if not data.get('slack'): 112 | logger.error("No 'slack' config section, quitting.") 113 | sys.exit(1) 114 | 115 | MISSED_SLACK_KEY = False 116 | for key in ('SLACK_BOT_OAUTH_TOKEN', 'SLACK_SIGNING_SECRET'): 117 | if key not in data.get('slack'): 118 | MISSED_SLACK_KEY = True 119 | logger.error("Couldn't find %s in config.json slack section, going to quit.", key) 120 | if MISSED_SLACK_KEY: 121 | sys.exit(1) 122 | else: 123 | slack_bot_token = data['slack']['SLACK_BOT_OAUTH_TOKEN'] 124 | slack_signing_secret = data['slack']['SLACK_SIGNING_SECRET'] 125 | 126 | 127 | slack_events_adapter = SlackEventAdapter(slack_signing_secret, '/slack/events') 128 | 129 | def get_username(prog_username, slack_client, token): 130 | """ pulls the slack username from the event """ 131 | logger.debug('Got %s as username', prog_username) 132 | user_info = slack_client.users_info(token=token, user=prog_username) 133 | if user_info.get('ok'): 134 | if user_info.get('user') is not None: 135 | user = user_info['user'] 136 | if user.get('profile') is not None: 137 | profile = user['profile'] 138 | if profile.get('display_name') is not None: 139 | username = profile['display_name'] 140 | logger.debug('Returning %s', username) 141 | return username 142 | return False 143 | 144 | def file_handler(event): 145 | """ handles files from slack client """ 146 | logger.info('got file from slack') 147 | 148 | for file_object in event.get('files'): 149 | if file_object.get('mode') == "snippet": 150 | url = file_object.get('url_private_download') 151 | title = file_object.get('title') 152 | if title == 'Untitled': 153 | event_title = '#Warroom' 154 | else: 155 | event_title = f"#Warroom {title}" 156 | headers = {'Authorization': f"Bearer {slack_bot_token}"} 157 | 158 | response = requests.get(url, headers=headers) 159 | response.raise_for_status() 160 | 161 | # TODO: this might just need to be response.text 162 | content = response.content.decode("utf-8") 163 | 164 | e_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(event['event_ts']))) 165 | e_title = f"{e_time} - {event_title}" 166 | username = get_username(event.get('user'), slack_client, slack_bot_token) 167 | # logger.info(username) 168 | # logger.info(e_title) 169 | # logger.info(content) 170 | misp_response = misp_object.misp_send(0, content, e_title, username) 171 | slack_client.chat_postEphemeral( 172 | channel=event.get('channel'), 173 | text=misp_response, 174 | user=event.get('user'), 175 | ) 176 | 177 | @slack_events_adapter.on('message') 178 | def handle_message(event_data): 179 | """ slack message handler """ 180 | logger.info('handle_message Got message from slack') 181 | logger.info(event_data) 182 | message = event_data.get('event') 183 | 184 | logger.info(f"Message type: {message.get('type')}") 185 | logger.info(f"Message text: {message.get('text')}") 186 | if message.get('files'): 187 | logger.info("Files message") 188 | file_info = message 189 | thread_object = threading.Thread(target=file_handler, args=(file_info,)) 190 | thread_object.start() 191 | #file_handler(file_info) 192 | return_value = flask.Response('', headers={'X-Slack-No-Retry': 1}), 200 193 | # elif str(message.get('type')) == 'message' and str(message.get('text')) == 'sambot git update': 194 | # logger.info(f"Git pull message from {message.get('user')} in {message.get('channel')}") 195 | 196 | # response = f"Doing a git pull now..." 197 | # slack_client.chat_postMessage(channel=message.get('channel'), text=response) 198 | 199 | # git_repo = git.cmd.Git(os.path.dirname(os.path.realpath(__file__))) 200 | # git_result = git_repo.pull() 201 | 202 | # response = f"Done!\n```{git_result}```" 203 | # slack_client.chat_postMessage(channel=message.get('channel'), text=response) 204 | 205 | # return_value = '', 200 206 | # if the incoming message contains 'hi', then respond with a 'hello message' 207 | elif message.get('subtype') is None and 'hi' in message.get('text'): 208 | logger.info(f"Hi message from {message.get('user')} in {message.get('channel')}") 209 | response = f"Hello <@{message.get('user')}>! :tada:" 210 | slack_client.chat_postMessage(channel=message.get('channel'), text=response) 211 | return_value = '', 200 212 | else: 213 | logger.info("Message fell through...") 214 | # shouldn't get here, but return a 403 if you do. 215 | return_value = 'Unhandled message type', 403 216 | return return_value 217 | 218 | @slack_events_adapter.on("error") 219 | def error_handler(err): 220 | """ slack error message handler """ 221 | logger.error("Slack error: %s", str(err)) 222 | 223 | def find_channel_id(slack_client, channel_name='_autobot'): 224 | """ returns the channel ID of the channel """ 225 | for channel in slack_client.conversations_list().get('channels'): 226 | if channel.get('name') == channel_name: 227 | logger.debug(f"found channel id for {channel_name}: {channel.get('id')}") 228 | return channel.get('id') 229 | return False 230 | 231 | if __name__ == '__main__': 232 | slack_client = slack.WebClient(slack_bot_token) 233 | 234 | if TEST_MODE: 235 | BOT_CHANNEL = find_channel_id(slack_client, '_autobot') 236 | slack_client.conversations_join(channel=BOT_CHANNEL) 237 | #slack_client.chat_postMessage(channel=BOT_CHANNEL, text="I've starting up in test mode!") 238 | logger.debug("I've started up in test mode...") 239 | 240 | for channel in slack_client.conversations_list().get('channels'): 241 | logger.debug(channel) 242 | slack_events_adapter.start(port=3000, host='0.0.0.0') 243 | -------------------------------------------------------------------------------- /misp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ MISP custom connector and general handler """ 4 | 5 | import sys 6 | import traceback 7 | import logging 8 | import re 9 | from urllib.parse import urlparse 10 | 11 | from pymisp import MISPEvent, PyMISP, MISPObject 12 | from defang import refang 13 | 14 | class MispCustomConnector: 15 | """ custom MISP connector """ 16 | def __init__(self, misp_url:str, misp_key: str, misp_ssl: bool): 17 | try: 18 | # https://pymisp.readthedocs.io/en/latest/modules.html#pymisp 19 | self.misp = PyMISP(misp_url, misp_key, misp_ssl) 20 | except Exception as err: 21 | sys.exit('Batch Job Terminated: MISP connection error - \n'+repr(err)) 22 | self.misp_logger = logging.getLogger('mispattruploader') 23 | 24 | def submit_to_misp(self, misp: PyMISP, misp_event: MISPEvent, original_misp_objects: list): 25 | ''' 26 | Submit a list of MISP objects to a MISP event 27 | :misp: PyMISP API object for interfacing with MISP 28 | :misp_event: MISPEvent object 29 | :misp_objects: List of MISPObject objects. Must be a list 30 | ''' 31 | # go through round one and only add MISP objects 32 | misp_objects = [] 33 | for misp_object in original_misp_objects: 34 | self.misp_logger.info(misp_object) 35 | if len(misp_object.attributes) > 0: 36 | if misp_object.name == 'network-connection': 37 | template_id = 'af16764b-f8e5-4603-9de1-de34d272f80b' 38 | else: 39 | # self.misp_logger.debug(dir(pymisp.api)) 40 | # self.misp_logger.debug(dir(self.misp)) 41 | # exit() 42 | self.misp_logger.info(misp_object) 43 | self.misp_logger.info(misp_object.template_uuid) 44 | object_template = self.misp.get_object_template(misp_object.template_uuid) 45 | self.misp_logger.info("MISP Object Template: " + str(object_template)) 46 | template_id = object_template['ObjectTemplate']['id'] 47 | self.misp_logger.info(template_id) 48 | # self.misp_logger.info(dir(misp_event)) 49 | self.misp_logger.info(misp_event) 50 | 51 | # add the object and get the result 52 | result = misp.add_object(event=misp_event, misp_object=misp_object) 53 | self.misp_logger.info("MISP Add Object Result" + str(result)) 54 | misp_objects.append(result) 55 | # go through round two and add all the object references for each object 56 | misp_object_references = [] 57 | for misp_object in misp_objects: 58 | self.misp_logger.info("MISP Object Result" + str(misp_object)) 59 | if 'ObjectReference' in misp_object: ## this doesnt seem to happen 60 | # TODO - Fix this. 61 | for reference in misp_object.ObjectReference: 62 | 63 | # add the reference and get the result 64 | result = misp.add_object_reference(reference) 65 | misp_object_references.append(result) 66 | return misp_objects, misp_object_references 67 | 68 | def check_object_length(self, misp_objects: list): 69 | """ check object has some attributes """ 70 | self.misp_logger.info("check_object_length called") 71 | for misp_object in misp_objects: 72 | self.misp_logger.info("got object: %s", misp_object.name) 73 | if len(misp_object.attributes) == 0: 74 | self.misp_logger.error('failed to put in correct tags') 75 | return False 76 | return True 77 | 78 | def get_comm_and_tags(self, strInput: str): 79 | """ pull comments and tags from input """ 80 | comment = None 81 | str_comment = "" 82 | tags = ["tlp:green"] 83 | tag_type = None 84 | for line in strInput.splitlines(): 85 | 86 | if "comment:" in line.lower(): 87 | vals = line.split(":", 1) 88 | comment = vals[1:] 89 | elif "tag:" in line.lower(): 90 | vals = line.split(":", 1) 91 | value = vals[1].strip().lower() 92 | if "tlp" in value: 93 | tags.remove("tlp:green") 94 | vals_str = "tlp:" 95 | vals_split = vals[1].split(":") 96 | vals_str += vals_split[1] 97 | tags.append(vals_str) 98 | elif "type:" in line.lower(): 99 | vals = line.split(":", 1) 100 | value = vals[1].strip().lower() 101 | if value in ('phish', 'malware', 'bec/spam', 'dump', 'apt', 'APT'): 102 | tag_type = value.lower() 103 | if tag_type: 104 | self.misp_logger.info('Setting tag to ir8: %s', tag_type) 105 | tag = "ir8:" + tag_type 106 | tags.append(tag) 107 | else: 108 | tags = None 109 | if comment != None: 110 | for c in comment: 111 | str_comment += c 112 | else: 113 | str_comment = comment 114 | return str_comment, tags 115 | 116 | ####### SLACK ENTRY POINT! ###### 117 | def misp_send(self, strMISPEventID, strInput, strInfo, strUsername): 118 | """ send an event to MISP """ 119 | try: 120 | objects=[] 121 | #get comments and tags from string input 122 | str_comment, tags = self.get_comm_and_tags(strInput) 123 | print(tags) 124 | if tags is None: 125 | self.misp_logger.info('Irate not in Tags: %s equals None', tags) 126 | response = None 127 | return response 128 | #setup misp objects 129 | mispobj_email = MISPObject(name="email") 130 | mispobj_file = MISPObject(name="file") 131 | mispobj_files = {} 132 | mispobj_domainip = MISPObject(name="domain-ip") 133 | url_no = 0 134 | file_no = 0 135 | mispobj_urls = {} 136 | 137 | #process input 138 | for line in strInput.splitlines(): 139 | if "domain:" in line.lower(): #Catch domain and add to domain/IP object 140 | mispobj_domainip = MISPObject(name="domain-ip") 141 | vals = line.split(":", 1) 142 | mispobj_domainip.add_attribute("domain", value=vals[1].strip(), comment=str_comment) 143 | objects.append(mispobj_domainip) 144 | elif "ip:" in line.lower() or "ip-dst:" in line.lower() or "ip-src:" in line.lower(): #Catch IP and add to domain/IP object 145 | if "domain:" in strInput.splitlines(): 146 | mispobj_domainip = MISPObject(name="domain-ip") 147 | vals = line.split(":", 1) 148 | mispobj_domainip.add_attribute("ip", value=vals[1].strip(), comment=str_comment) 149 | objects.append(mispobj_domainip) 150 | else: 151 | mispobj_network_connection = MISPObject(name="network-connection") 152 | vals = line.split(":", 1) 153 | if ("ip:" in line.lower()) or ("ip-dst:" in line.lower()): 154 | mispobj_network_connection.add_attribute("ip-dst", type="ip-dst", value=vals[1].strip(), comment=str_comment) 155 | else: 156 | mispobj_network_connection.add_attribute("ip-src", type="ip-src", value=vals[1].strip(), comment=str_comment) 157 | objects.append(mispobj_network_connection) 158 | 159 | elif "source-email:" in line.lower() or "email-source" in line.lower() or "from:" in line.lower(): #Catch email and add to email object 160 | vals = line.split(":", 1) 161 | mispobj_email.add_attribute("from", value = vals[1].strip(), comment=str_comment) 162 | elif "url:" in line.lower() or (('kit:' in line.lower() or ('creds:' in line.lower())) and (('hxxp' in line.lower()) or ('http' in line.lower()))): #Catch URL and add to URL object 163 | vals = line.split(":", 1) 164 | url = vals[1].strip() 165 | url = refang(url) 166 | parsed = urlparse(url) 167 | mispobj_url = MISPObject(name="url") 168 | mispobj_url.add_attribute("url", value=parsed.geturl(), category="Payload delivery", comment=str_comment) 169 | if parsed.hostname: 170 | mispobj_url.add_attribute("host", value=parsed.hostname, comment=str_comment) 171 | if parsed.scheme: 172 | mispobj_url.add_attribute("scheme", value=parsed.scheme, comment=str_comment) 173 | if parsed.port: 174 | mispobj_url.add_attribute("port", value=parsed.port, comment=str_comment) 175 | mispobj_urls[url_no] = mispobj_url 176 | url_no += 1 177 | 178 | #Catch different hashes and add to file object 179 | elif "sha1:" in line.lower(): 180 | vals = line.split(":", 1) 181 | mispobj_file.add_attribute("sha1", value=vals[1].strip(),comment=str_comment) 182 | elif "sha256:" in line.lower(): 183 | vals = line.split(":", 1) 184 | mispobj_file.add_attribute("sha256", value=vals[1].strip(), comment=str_comment) 185 | elif "md5:" in line.lower(): 186 | vals = line.split(":", 1) 187 | mispobj_file.add_attribute("md5", value=vals[1].strip(), comment=str_comment) 188 | elif "subject:" in line.lower(): #or ("subject:" in line): #Catch subject and add to email object 189 | vals = line.split(":", 1) 190 | self.misp_logger.info(f"adding subject: {vals[1].strip()}") 191 | mispobj_email.add_attribute("subject", value = vals[1].strip(), comment=str_comment) 192 | elif "hash|filename:" in line.lower(): #catch hash|filename pair and add to file object 193 | vals = line.split(":", 1) 194 | val = vals[1].split("|") 195 | l_hash = val[0] 196 | l_filename = val[1] 197 | l_mispobj_file = MISPObject(name="file") 198 | if len(re.findall(r"\b[a-fA-F\d]{32}\b", l_hash)) > 0: 199 | l_mispobj_file.add_attribute("md5", value=l_hash.strip(), comment=str_comment) 200 | l_mispobj_file.add_attribute("filename", value=l_filename.strip(), comment=str_comment) 201 | mispobj_files[file_no] = l_mispobj_file 202 | elif len(re.findall(r'\b[0-9a-f]{40}\b', l_hash)) > 0: 203 | l_mispobj_file.add_attribute("sha1", value=l_hash.strip(), comment=str_comment) 204 | l_mispobj_file.add_attribute("filename", value=l_filename.strip(), comment=str_comment) 205 | mispobj_files[file_no] = l_mispobj_file 206 | elif len(re.findall(r'\b[A-Fa-f0-9]{64}\b', l_hash)) > 0: 207 | l_mispobj_file.add_attribute("sha256", value=l_hash.strip(), comment=str_comment) 208 | l_mispobj_file.add_attribute("filename", value=l_filename.strip(), comment=str_comment) 209 | mispobj_files[file_no] = l_mispobj_file 210 | file_no +=1 211 | 212 | 213 | #add all misp objects to List to be processed and submitted to MISP server as one. 214 | if len(mispobj_file.attributes) > 0: 215 | objects.append(mispobj_file) 216 | if len(mispobj_email.attributes) > 0: 217 | objects.append(mispobj_email) 218 | 219 | for u_key, u_value in mispobj_urls.items(): 220 | if len(u_value.attributes) > 0: 221 | objects.append(u_value) 222 | for f_key, f_value in mispobj_files.items(): 223 | if len(f_value.attributes) > 0: 224 | objects.append(f_value) 225 | # Update timestamp and event 226 | 227 | except Exception as e: 228 | error = traceback.format_exc() 229 | response = f"Error occured when converting string to misp objects:\n{error}" 230 | self.misp_logger.error(response) 231 | return response 232 | 233 | if not self.check_object_length(objects): 234 | self.misp_logger.error('Input from %s did not contain accepted tags.\n Input: \n%s', (strUsername, strInput)) 235 | return "Error in the tags you entered. Please see the guide for accepted tags: (https://github.com/IRATEAU/sam-bot/blob/master/README.md)" 236 | 237 | try: 238 | misp_event = MISPEvent() 239 | misp_event.info = strInfo 240 | misp_event.distribution = 0 241 | misp_event.analysis = 2 242 | misp_event.threat_level_id = 3 243 | add = self.misp.add_event(misp_event) 244 | self.misp_logger.info("Added event %s", add) 245 | if objects: 246 | self.misp_logger.info("Adding objects to event...") 247 | objects, references = self.submit_to_misp(self.misp, misp_event, objects) 248 | self.misp_logger.info("References: %s", references) 249 | 250 | for tag in tags: 251 | self.misp_logger.info("Adding tag %s", tag) 252 | self.misp.tag(misp_event.uuid, tag) 253 | 254 | #self.misp.add_internal_comment(misp_event.id, reference="Author: " + strUsername, comment=str_comment) 255 | self.misp_logger.info("Publishing event...") 256 | publish_result = self.misp.publish(misp_event, alert=False) 257 | self.misp_logger.info("Publish result: %s", publish_result) 258 | 259 | if 'errors' in publish_result and publish_result.get('errors'): 260 | return_value = ("Submission error: "+repr(publish_result.get('errors'))) 261 | else: 262 | if misp_event.get('Event',{}).get('RelatedEvent'): 263 | e_related = "" 264 | for each in misp_event['Event']['RelatedEvent']: 265 | e_related = e_related + each['Event']['id'] + ", " 266 | return_value = "Created ID: " + publish_result.get('id') + "\nRelated Events: " + ''.join(e_related) 267 | else: 268 | return_value = "Created ID: " + publish_result.get('id') 269 | return return_value 270 | 271 | except Exception as e: 272 | error = traceback.format_exc() 273 | response = "Error occured when submitting to MISP:\n %s" %error 274 | self.misp_logger.error(response) 275 | return response 276 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | defang==0.5.3 2 | Flask==3.0.0 3 | GitPython==3.1.40 4 | GitPython==3.1.40 5 | nest_asyncio==1.5.8 6 | pyjokes==0.6.0 7 | pymisp==2.4.176 8 | Requests==2.31.0 9 | slackclient==2.9.4 10 | slackeventsapi==3.0.1 11 | --------------------------------------------------------------------------------