Skip to content

Commit

Permalink
Merge pull request MISP#107 from raw-data/master
Browse files Browse the repository at this point in the history
multi-threaded suricata search
  • Loading branch information
adulau authored Jun 28, 2017
2 parents 4a21727 + 73b66af commit b846460
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 0 deletions.
24 changes: 24 additions & 0 deletions examples/suricata_search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Description
Get all attributes, from a MISP (https://github.com/MISP) instance, that can be converted into Suricata rules, given a *parameter* and a *term* to search

**requires**
* PyMISP (https://github.com/CIRCL/PyMISP/)
* python 2.7 or python3 (suggested)


# Usage
* **suricata_search.py -p tags -s 'APT' -o misp_ids.rules -t 5**
- search for 'APT' tag
- use 5 threads while generating IDS rules
- dump results to misp_ids.rules

* **suricata_search.py -p tags -s 'APT' -o misp_ids.rules -ne 411 357 343**
- same as above, but skip events ID 411,357 and 343

* **suricata_search.py -p tags -s 'circl:incident-classification="malware", tlp:green' -o misp_ids.rules**
- search for multiple tags 'circl:incident-classification="malware", tlp:green'

* **suricata_search.py -p categories -s 'Artifacts dropped' -t 20 -o artifacts_dropped.rules**
- search for category 'Artifacts dropped'
- use 20 threads while generating IDS rules
- dump results to artifacts_dropped.rules
209 changes: 209 additions & 0 deletions examples/suricata_search/suricata_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import argparse
import os
import queue
import sys
from threading import Thread, enumerate
from keys import misp_url, misp_key, misp_verifycert

try:
from pymisp import PyMISP
except ImportError as err:
sys.stderr.write("ERROR: {}\n".format(err))
sys.stderr.write("\t[try] with pip install pymisp\n")
sys.stderr.write("\t[try] with pip3 install pymisp\n")
sys.exit(1)


HEADER = """
#This part might still contain bugs, use and your own risk and report any issues.
#
# MISP export of IDS rules - optimized for suricata
#
# These NIDS rules contain some variables that need to exist in your configuration.
# Make sure you have set:
#
# $HOME_NET - Your internal network range
# $EXTERNAL_NET - The network considered as outside
# $SMTP_SERVERS - All your internal SMTP servers
# $HTTP_PORTS - The ports used to contain HTTP traffic (not required with suricata export)
#
"""

# queue for events matching searched term/s
IDS_EVENTS = queue.Queue()

# queue for downloaded Suricata rules
DOWNLOADED_RULES = queue.Queue()

# Default number of threads to use
THREAD = 4

try:
input = raw_input
except NameError:
pass


def init():
""" init connection to MISP """
return PyMISP(misp_url, misp_key, misp_verifycert, 'json')


def search(misp, quiet, noevent, **kwargs):
""" Start search in MISP """

result = misp.search(**kwargs)

# fetch all events matching **kwargs
track_events = 0
skip_events = list()
for event in result['response']:
event_id = event["Event"].get("id")
track_events += 1

to_ids = False
for attribute in event["Event"]["Attribute"]:
to_ids_event = attribute["to_ids"]
if to_ids_event:
to_ids = True
break

# if there is at least one eligible event to_ids, add event_id
if to_ids:
# check if the event_id is not blacklisted by the user
if isinstance(noevent, list):
if event_id not in noevent[0]:
to_ids_event = (event_id, misp)
IDS_EVENTS.put(to_ids_event)
else:
skip_events.append(event_id)
else:
to_ids_event = (event_id, misp)
IDS_EVENTS.put(to_ids_event)

if not quiet:
print ("\t[i] matching events: {}".format(track_events))
if len(skip_events) > 0:
print ("\t[i] skipped {0} events -> {1}".format(len(skip_events),skip_events))
print ("\t[i] events selected for IDS export: {}".format(IDS_EVENTS.qsize()))


def collect_rules(thread):
""" Dispatch tasks to Suricata_processor worker """

for x in range(int(thread)):
th = Thread(target=suricata_processor, args=(IDS_EVENTS, ))
th.start()

for x in enumerate():
if x.name == "MainThread":
continue
x.join()


def suricata_processor(ids_events):
""" Trigger misp.download_suricata_rule_event """

while not ids_events.empty():
event_id, misp = ids_events.get()
ids_rules = misp.download_suricata_rule_event(event_id).text

for r in ids_rules.split("\n"):
# skip header
if not r.startswith("#"):
if len(r) > 0: DOWNLOADED_RULES.put(r)


def return_rules(output, quiet):
""" Return downloaded rules to user """

rules = set()
while not DOWNLOADED_RULES.empty():
rules.add(DOWNLOADED_RULES.get())

if output is None:

if not quiet:
print ("[+] Displaying rules")

print (HEADER)
for r in rules: print (r)
print ("#")

else:

if not quiet:
print ("[+] Writing rules to {}".format(output))
print ("[+] Generated {} rules".format(len(rules)))

with open(output, 'w') as f:
f.write(HEADER)
f.write("\n".join(r for r in rules))
f.write("\n"+"#")


def format_request(param, term, misp, quiet, output, thread, noevent):
""" Format request and start search """

kwargs = {param: term}

print ("[+] Searching for: {}".format(kwargs))
search(misp, quiet, noevent, **kwargs)

# collect Suricata rules
collect_rules(thread)


if __name__ == "__main__":

parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
description='Get all attributes that can be converted into Suricata rules, given a parameter and a term to '
'search.',
epilog='''
EXAMPLES:
suricata_search.py -p tags -s 'APT' -o misp_ids.rules -t 5
suricata_search.py -p tags -s 'APT' -o misp_ids.rules -ne 411 357 343
suricata_search.py -p tags -s 'tlp:green, OSINT' -o misp_ids.rules
suricata_search.py -p tags -s 'circl:incident-classification="malware", tlp:green' -o misp_ids.rules
suricata_search.py -p categories -s 'Artifacts dropped' -t 20 -o artifacts_dropped.rules
''')
parser.add_argument("-p", "--param", required=True, help="Parameter to search (e.g. categories, tags, org, etc.).")
parser.add_argument("-s", "--search", required=True, help="Term/s to search.")
parser.add_argument("-q", "--quiet", action='store_true', help="No status messages")
parser.add_argument("-t", "--thread", required=False, help="Number of threads to use", default=THREAD)
parser.add_argument("-ne", "--noevent", nargs='*', required=False, dest='noevent', action='append',
help="Event/s ID to exclude during the search")
parser.add_argument("-o", "--output", help="Output file",required=False)

args = parser.parse_args()

if args.output is not None and os.path.exists(args.output):
try:
check = input("[!] Output file {} exists, do you want to continue [Y/n]? ".format(args.output))
if check not in ["Y","y"]:
exit(0)
except KeyboardInterrupt:
sys.exit(0)

if not args.quiet:
print ("[i] Connecting to MISP instance: {}".format(misp_url))

print ("[i] Note: duplicated IDS rules will be removed")

# Based on # of terms, format request
if "," in args.search:
for term in args.search.split(","):
term = term.strip()
misp = init()
format_request(args.param, term, misp, args.quiet, args.output, args.thread, args.noevent)
else:
if not args.quiet:
misp = init()
format_request(args.param, args.search, misp, args.quiet, args.output, args.thread, args.noevent)

# return collected rules
return_rules(args.output, args.quiet)

0 comments on commit b846460

Please sign in to comment.