diff --git a/examples/addtag.py b/examples/addtag.py
index 7535c17..5cecf28 100644
--- a/examples/addtag.py
+++ b/examples/addtag.py
@@ -31,6 +31,6 @@ if __name__ == '__main__':
attribute = temp
break
- misp.add_tag(attribute, args.tag, True)
+ misp.add_tag(attribute, args.tag, attribute=True)
else:
misp.add_tag(event['Event'], args.tag)
diff --git a/examples/situational-awareness/attribute_treemap.py b/examples/situational-awareness/attribute_treemap.py
index 33ab6b5..d0b0ed4 100755
--- a/examples/situational-awareness/attribute_treemap.py
+++ b/examples/situational-awareness/attribute_treemap.py
@@ -5,7 +5,7 @@ from pymisp import PyMISP
from keys import misp_url, misp_key, misp_verifycert
import argparse
import tools
-
+import pygal_tools
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Take a sample of events (based on last.py of searchall.py) and create a treemap epresenting the distribution of attributes in this sample.')
@@ -26,6 +26,6 @@ if __name__ == '__main__':
attributes = tools.attributesListBuild(events)
temp = tools.getNbAttributePerEventCategoryType(attributes)
temp = temp.groupby(level=['category', 'type']).sum()
- tools.createTreemap(temp, 'Attributes Distribution', 'attribute_treemap.svg', 'attribute_table.html')
+ pygal_tools.createTreemap(temp, 'Attributes Distribution', 'attribute_treemap.svg', 'attribute_table.html')
else:
print ('There is no event answering the research criteria')
diff --git a/examples/situational-awareness/bokeh_tools.py b/examples/situational-awareness/bokeh_tools.py
new file mode 100644
index 0000000..84613d8
--- /dev/null
+++ b/examples/situational-awareness/bokeh_tools.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from bokeh.plotting import figure, output_file, show, ColumnDataSource
+from bokeh.models import HoverTool
+import date_tools
+
+
+def tagsDistributionScatterPlot(NbTags, dates, plotname='Tags Distribution Plot'):
+
+ output_file(plotname + ".html")
+
+ counts = {}
+ glyphs = {}
+ desc = {}
+ hover = HoverTool()
+ plot = figure(plot_width=800, plot_height=800, x_axis_type="datetime", x_axis_label='Date', y_axis_label='Number of tags', tools=[hover])
+
+ for name in NbTags.keys():
+ desc[name] = []
+ for date in dates[name]:
+ desc[name].append(date_tools.datetimeToString(date, "%Y-%m-%d"))
+ counts[name] = plot.circle(dates[name], NbTags[name], legend="Number of events with y tags", source=ColumnDataSource(
+ data=dict(
+ desc=desc[name]
+ )
+ ))
+ glyphs[name] = counts[name].glyph
+ glyphs[name].size = int(name) * 2
+ hover.tooltips = [("date", "@desc")]
+ if int(name) != 0:
+ glyphs[name].fill_alpha = 1/int(name)
+ show(plot)
diff --git a/examples/situational-awareness/date_tools.py b/examples/situational-awareness/date_tools.py
new file mode 100644
index 0000000..43b0634
--- /dev/null
+++ b/examples/situational-awareness/date_tools.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from datetime import datetime
+from datetime import timedelta
+from dateutil.parser import parse
+
+
+class DateError(Exception):
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
+
+
+# ############### Date Tools ################
+
+def dateInRange(datetimeTested, begin=None, end=None):
+ if begin is None:
+ begin = datetime(1970, 1, 1)
+ if end is None:
+ end = datetime.now()
+ return begin <= datetimeTested <= end
+
+
+def toDatetime(date):
+ return parse(date)
+
+
+def datetimeToString(datetime, formatstring):
+ return datetime.strftime(formatstring)
+
+
+def checkDateConsistancy(begindate, enddate, lastdate):
+ if begindate is not None and enddate is not None:
+ if begindate > enddate:
+ raise DateError('begindate ({}) cannot be after enddate ({})'.format(begindate, enddate))
+
+ if enddate is not None:
+ if toDatetime(enddate) < lastdate:
+ raise DateError('enddate ({}) cannot be before lastdate ({})'.format(enddate, lastdate))
+
+ if begindate is not None:
+ if toDatetime(begindate) > datetime.now():
+ raise DateError('begindate ({}) cannot be after today ({})'.format(begindate, datetime.now().date()))
+
+
+def setBegindate(begindate, lastdate):
+ return max(begindate, lastdate)
+
+
+def setEnddate(enddate):
+ return min(enddate, datetime.now())
+
+
+def getLastdate(last):
+ return (datetime.now() - timedelta(days=int(last))).replace(hour=0, minute=0, second=0, microsecond=0)
+
+
+def getNDaysBefore(date, days):
+ return (date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
+
+
+def getToday():
+ return (datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0)
+
+
+def days_between(date_1, date_2):
+ return abs((date_2 - date_1).days)
diff --git a/examples/situational-awareness/pygal_tools.py b/examples/situational-awareness/pygal_tools.py
new file mode 100644
index 0000000..57379bc
--- /dev/null
+++ b/examples/situational-awareness/pygal_tools.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import pygal
+from pygal.style import Style
+import pandas
+import random
+
+
+def createTable(colors, categ_types_hash, tablename='attribute_table.html'):
+ with open(tablename, 'w') as target:
+ target.write('\n\n
\n\n\n')
+ for categ_name, types in categ_types_hash.items():
+ table = pygal.Treemap(pretty_print=True)
+ target.write('\n {}
\n'.format(colors[categ_name], categ_name))
+ for d in types:
+ table.add(d['label'], d['value'])
+ target.write(table.render_table(transpose=True))
+ target.write('\n\n')
+
+
+def createTreemap(data, title, treename='attribute_treemap.svg', tablename='attribute_table.html'):
+ labels_categ = data.index.labels[0]
+ labels_types = data.index.labels[1]
+ names_categ = data.index.levels[0]
+ names_types = data.index.levels[1]
+ categ_types_hash = {}
+ for categ_id, type_val, total in zip(labels_categ, labels_types, data):
+ if not categ_types_hash.get(names_categ[categ_id]):
+ categ_types_hash[names_categ[categ_id]] = []
+ dict_to_print = {'label': names_types[type_val], 'value': total}
+ categ_types_hash[names_categ[categ_id]].append(dict_to_print)
+
+ colors = {categ: "#%06X" % random.randint(0, 0xFFFFFF) for categ in categ_types_hash.keys()}
+ style = Style(background='transparent',
+ plot_background='#FFFFFF',
+ foreground='#111111',
+ foreground_strong='#111111',
+ foreground_subtle='#111111',
+ opacity='.6',
+ opacity_hover='.9',
+ transition='400ms ease-in',
+ colors=tuple(colors.values()))
+
+ treemap = pygal.Treemap(pretty_print=True, legend_at_bottom=True, style=style)
+ treemap.title = title
+ treemap.print_values = True
+ treemap.print_labels = True
+
+ for categ_name, types in categ_types_hash.items():
+ treemap.add(categ_name, types)
+
+ createTable(colors, categ_types_hash)
+ treemap.render_to_file(treename)
diff --git a/examples/situational-awareness/tag_scatter.py b/examples/situational-awareness/tag_scatter.py
new file mode 100644
index 0000000..68a27de
--- /dev/null
+++ b/examples/situational-awareness/tag_scatter.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from pymisp import PyMISP
+from keys import misp_url, misp_key, misp_verifycert
+import argparse
+import numpy
+import tools
+import date_tools
+import bokeh_tools
+
+import time
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Show the evolution of trend of tags.')
+ parser.add_argument("-d", "--days", type=int, required=True, help='')
+ parser.add_argument("-s", "--begindate", required=True, help='format yyyy-mm-dd')
+ parser.add_argument("-e", "--enddate", required=True, help='format yyyy-mm-dd')
+
+ args = parser.parse_args()
+
+ misp = PyMISP(misp_url, misp_key, misp_verifycert)
+
+ result = misp.search(date_from=args.begindate, date_to=args.enddate, metadata=False)
+
+ # Getting data
+
+ if 'response' in result:
+ events = tools.eventsListBuildFromArray(result)
+ NbTags = []
+ dates = []
+ enddate = date_tools.toDatetime(args.enddate)
+ begindate = date_tools.toDatetime(args.begindate)
+
+ for i in range(round(date_tools.days_between(enddate, begindate)/args.days)):
+ begindate = date_tools.getNDaysBefore(enddate, args.days)
+ eventstemp = tools.selectInRange(events, begindate, enddate)
+ if eventstemp is not None:
+ for event in eventstemp.iterrows():
+ if 'Tag' in event[1]:
+ dates.append(enddate)
+ if isinstance(event[1]['Tag'], list):
+ NbTags.append(len(event[1]['Tag']))
+ else:
+ NbTags.append(0)
+ enddate = begindate
+
+ # Prepare plot
+
+ NbTagsPlot = {}
+ datesPlot = {}
+
+ for i in range(len(NbTags)):
+ if NbTags[i] == -1:
+ continue
+ count = 1
+ for j in range(i+1, len(NbTags)):
+ if NbTags[i] == NbTags[j] and dates[i] == dates[j]:
+ count = count + 1
+ NbTags[j] = -1
+ if str(count) in NbTagsPlot:
+ NbTagsPlot[str(count)].append(NbTags[i])
+ datesPlot[str(count)].append(dates[i])
+ else:
+ NbTagsPlot[str(count)] = [NbTags[i]]
+ datesPlot[str(count)] = [dates[i]]
+ NbTags[i] = -1
+
+ # Plot
+
+ bokeh_tools.tagsDistributionScatterPlot(NbTagsPlot, datesPlot)
diff --git a/examples/situational-awareness/tag_search.py b/examples/situational-awareness/tag_search.py
index 20d422d..989a404 100644
--- a/examples/situational-awareness/tag_search.py
+++ b/examples/situational-awareness/tag_search.py
@@ -6,6 +6,7 @@ from keys import misp_url, misp_key, misp_verifycert
from datetime import datetime
import argparse
import tools
+import date_tools
def init(url, key):
@@ -29,17 +30,17 @@ if __name__ == '__main__':
args.days = 7
result = misp.search(last='{}d'.format(args.days), metadata=True)
- tools.checkDateConsistancy(args.begindate, args.enddate, tools.getLastdate(args.days))
+ date_tools.checkDateConsistancy(args.begindate, args.enddate, date_tools.getLastdate(args.days))
if args.begindate is None:
- args.begindate = tools.getLastdate(args.days)
+ args.begindate = date_tools.getLastdate(args.days)
else:
- args.begindate = tools.setBegindate(tools.toDatetime(args.begindate), tools.getLastdate(args.days))
+ args.begindate = date_tools.setBegindate(date_tools.toDatetime(args.begindate), tools.getLastdate(args.days))
if args.enddate is None:
args.enddate = datetime.now()
else:
- args.enddate = tools.setEnddate(tools.toDatetime(args.enddate))
+ args.enddate = date_tools.setEnddate(date_tools.toDatetime(args.enddate))
if 'response' in result:
events = tools.selectInRange(tools.eventsListBuildFromArray(result), begin=args.begindate, end=args.enddate)
diff --git a/examples/situational-awareness/tags_count.py b/examples/situational-awareness/tags_count.py
index c58ca5b..acddc23 100644
--- a/examples/situational-awareness/tags_count.py
+++ b/examples/situational-awareness/tags_count.py
@@ -6,6 +6,7 @@ from keys import misp_url, misp_key, misp_verifycert
from datetime import datetime
import argparse
import tools
+import date_tools
def init(url, key):
@@ -28,17 +29,17 @@ if __name__ == '__main__':
args.days = 7
result = misp.search(last='{}d'.format(args.days), metadata=True)
- tools.checkDateConsistancy(args.begindate, args.enddate, tools.getLastdate(args.days))
+ date_tools.checkDateConsistancy(args.begindate, args.enddate, date_tools.getLastdate(args.days))
if args.begindate is None:
- args.begindate = tools.getLastdate(args.days)
+ args.begindate = date_tools.getLastdate(args.days)
else:
- args.begindate = tools.setBegindate(tools.toDatetime(args.begindate), tools.getLastdate(args.days))
+ args.begindate = date_tools.setBegindate(date_tools.toDatetime(args.begindate), date_tools.getLastdate(args.days))
if args.enddate is None:
args.enddate = datetime.now()
else:
- args.enddate = tools.setEnddate(tools.toDatetime(args.enddate))
+ args.enddate = date_tools.setEnddate(date_tools.toDatetime(args.enddate))
if 'response' in result:
events = tools.selectInRange(tools.eventsListBuildFromArray(result), begin=args.begindate, end=args.enddate)
diff --git a/examples/situational-awareness/tags_to_graphs.py b/examples/situational-awareness/tags_to_graphs.py
index 76464a4..f153961 100644
--- a/examples/situational-awareness/tags_to_graphs.py
+++ b/examples/situational-awareness/tags_to_graphs.py
@@ -5,6 +5,8 @@ from pymisp import PyMISP
from keys import misp_url, misp_key, misp_verifycert
import argparse
import tools
+import date_tools
+import bokeh_tools
def formattingDataframe(dataframe, dates, NanValue):
@@ -54,12 +56,12 @@ if __name__ == '__main__':
events = tools.eventsListBuildFromArray(result)
result = []
dates = []
- enddate = tools.getToday()
+ enddate = date_tools.getToday()
colourDict = {}
faketag = False
for i in range(split):
- begindate = tools.getNDaysBefore(enddate, size)
+ begindate = date_tools.getNDaysBefore(enddate, size)
dates.append(str(enddate.date()))
eventstemp = tools.selectInRange(events, begin=begindate, end=enddate)
if eventstemp is not None:
diff --git a/examples/situational-awareness/test_attribute_treemap.html b/examples/situational-awareness/test_attribute_treemap.html
index 0bc9c72..60f2833 100644
--- a/examples/situational-awareness/test_attribute_treemap.html
+++ b/examples/situational-awareness/test_attribute_treemap.html
@@ -6,7 +6,6 @@
height: 746px;
margin-top: 100px;
}
-
#treemap
{
width: 1000px;
diff --git a/examples/situational-awareness/tools.py b/examples/situational-awareness/tools.py
index 694eb2b..5ef7cf4 100644
--- a/examples/situational-awareness/tools.py
+++ b/examples/situational-awareness/tools.py
@@ -2,13 +2,9 @@
# -*- coding: utf-8 -*-
from json import JSONDecoder
-import random
import pygal
from pygal.style import Style
import pandas
-from datetime import datetime
-from datetime import timedelta
-from dateutil.parser import parse
import numpy
from scipy import stats
from pytaxonomies import Taxonomies
@@ -16,67 +12,25 @@ import re
import matplotlib.pyplot as plt
from matplotlib import pylab
import os
-
-
-class DateError(Exception):
- def __init__(self, value):
- self.value = value
-
- def __str__(self):
- return repr(self.value)
-
-
-# ############### Date Tools ################
-
-def dateInRange(datetimeTested, begin=None, end=None):
- if begin is None:
- begin = datetime(1970, 1, 1)
- if end is None:
- end = datetime.now()
- return begin <= datetimeTested <= end
-
-
-def toDatetime(date):
- return parse(date)
-
-
-def checkDateConsistancy(begindate, enddate, lastdate):
- if begindate is not None and enddate is not None:
- if begindate > enddate:
- raise DateError('begindate ({}) cannot be after enddate ({})'.format(begindate, enddate))
-
- if enddate is not None:
- if toDatetime(enddate) < lastdate:
- raise DateError('enddate ({}) cannot be before lastdate ({})'.format(enddate, lastdate))
-
- if begindate is not None:
- if toDatetime(begindate) > datetime.now():
- raise DateError('begindate ({}) cannot be after today ({})'.format(begindate, datetime.now().date()))
-
-
-def setBegindate(begindate, lastdate):
- return max(begindate, lastdate)
-
-
-def setEnddate(enddate):
- return min(enddate, datetime.now())
-
-
-def getLastdate(last):
- return (datetime.now() - timedelta(days=int(last))).replace(hour=0, minute=0, second=0, microsecond=0)
-
-
-def getNDaysBefore(date, days):
- return (date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
-
-
-def getToday():
- return (datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0)
-
+import date_tools
+from dateutil.parser import parse
# ############### Tools ################
+def selectInRange(Events, begin=None, end=None):
+ inRange = []
+ for i, Event in Events.iterrows():
+ if date_tools.dateInRange(parse(Event['date']), begin, end):
+ inRange.append(Event.tolist())
+ inRange = pandas.DataFrame(inRange)
+ temp = Events.columns.tolist()
+ if inRange.empty:
+ return None
+ inRange.columns = temp
+ return inRange
+
+
def getTaxonomies(dataframe):
taxonomies = Taxonomies()
taxonomies = list(taxonomies.keys())
@@ -233,19 +187,6 @@ def tagsListBuild(Events):
return Tags
-def selectInRange(Events, begin=None, end=None):
- inRange = []
- for i, Event in Events.iterrows():
- if dateInRange(parse(Event['date']), begin, end):
- inRange.append(Event.tolist())
- inRange = pandas.DataFrame(inRange)
- temp = Events.columns.tolist()
- if inRange.empty:
- return None
- inRange.columns = temp
- return inRange
-
-
def isTagIn(dataframe, tag):
temp = dataframe[dataframe['name'].str.contains(tag)].index.tolist()
index = []
@@ -277,56 +218,10 @@ def getNbAttributePerEventCategoryType(attributes):
def getNbOccurenceTags(Tags):
return Tags.groupby('name').count()['id']
+
# ############### Charts ################
-def createTable(colors, categ_types_hash, tablename='attribute_table.html'):
- with open(tablename, 'w') as target:
- target.write('\n\n\n\n\n')
- for categ_name, types in categ_types_hash.items():
- table = pygal.Treemap(pretty_print=True)
- target.write('\n {}
\n'.format(colors[categ_name], categ_name))
- for d in types:
- table.add(d['label'], d['value'])
- target.write(table.render_table(transpose=True))
- target.write('\n\n')
-
-
-def createTreemap(data, title, treename='attribute_treemap.svg', tablename='attribute_table.html'):
- labels_categ = data.index.labels[0]
- labels_types = data.index.labels[1]
- names_categ = data.index.levels[0]
- names_types = data.index.levels[1]
- categ_types_hash = {}
- for categ_id, type_val, total in zip(labels_categ, labels_types, data):
- if not categ_types_hash.get(names_categ[categ_id]):
- categ_types_hash[names_categ[categ_id]] = []
- dict_to_print = {'label': names_types[type_val], 'value': total}
- categ_types_hash[names_categ[categ_id]].append(dict_to_print)
-
- colors = {categ: "#%06X" % random.randint(0, 0xFFFFFF) for categ in categ_types_hash.keys()}
- style = Style(background='transparent',
- plot_background='#FFFFFF',
- foreground='#111111',
- foreground_strong='#111111',
- foreground_subtle='#111111',
- opacity='.6',
- opacity_hover='.9',
- transition='400ms ease-in',
- colors=tuple(colors.values()))
-
- treemap = pygal.Treemap(pretty_print=True, legend_at_bottom=True, style=style)
- treemap.title = title
- treemap.print_values = True
- treemap.print_labels = True
-
- for categ_name, types in categ_types_hash.items():
- treemap.add(categ_name, types)
-
- createTable(colors, categ_types_hash)
- treemap.render_to_file(treename)
-
-
def tagsToLineChart(dataframe, title, dates, colourDict):
style = createTagsPlotStyle(dataframe, colourDict)
line_chart = pygal.Line(x_label_rotation=20, style=style, show_legend=False)
diff --git a/examples/yara_dump.py b/examples/yara_dump.py
new file mode 100755
index 0000000..0e7875f
--- /dev/null
+++ b/examples/yara_dump.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+'''
+YARA dumper for MISP
+ by Christophe Vandeplas
+'''
+
+import keys
+from pymisp import PyMISP
+import yara
+import re
+
+
+def dirty_cleanup(value):
+ changed = False
+ substitutions = (('”', '"'),
+ ('“', '"'),
+ ('″', '"'),
+ ('`', "'"),
+ ('\r', '')
+ # ('$ ', '$'), # this breaks rules
+ # ('\t\t', '\n'), # this breaks rules
+ )
+ for substitution in substitutions:
+ if substitution[0] in value:
+ changed = True
+ value = value.replace(substitution[0], substitution[1])
+ return value, changed
+
+
+misp = PyMISP(keys.misp_url, keys.misp_key, keys.misp_verify, 'json')
+result = misp.search(controller='attributes', type_attribute='yara')
+
+attr_cnt = 0
+attr_cnt_invalid = 0
+attr_cnt_duplicate = 0
+attr_cnt_changed = 0
+yara_rules = []
+yara_rule_names = []
+if 'response' in result and 'Attribute' in result['response']:
+ for attribute in result['response']['Attribute']:
+ value = attribute['value']
+ event_id = attribute['event_id']
+ attribute_id = attribute['id']
+
+ value = re.sub('^[ \t]*rule ', 'rule misp_e{}_'.format(event_id), value, flags=re.MULTILINE)
+ value, changed = dirty_cleanup(value)
+ if changed:
+ attr_cnt_changed += 1
+ if 'global rule' in value: # refuse any global rules as they might disable everything
+ continue
+
+ # compile the yara rule to confirm it's validity
+ # if valid, ignore duplicate rules
+ try:
+ attr_cnt += 1
+ yara.compile(source=value)
+ yara_rules.append(value)
+ # print("Rule e{} a{} OK".format(event_id, attribute_id))
+ except yara.SyntaxError as e:
+ attr_cnt_invalid += 1
+ # print("Rule e{} a{} NOK - {}".format(event_id, attribute_id, e))
+ except yara.Error as e:
+ attr_cnt_invalid += 1
+ print(e)
+ import traceback
+ print(traceback.format_exc())
+
+# remove duplicates - process the full yara rule list and process errors to eliminate duplicate rule names
+all_yara_rules = '\n'.join(yara_rules)
+while True:
+ try:
+ yara.compile(source=all_yara_rules)
+ except yara.SyntaxError as e:
+ if 'duplicated identifier' in e.args[0]:
+ duplicate_rule_names = re.findall('duplicated identifier "(.*)"', e.args[0])
+ for item in duplicate_rule_names:
+ all_yara_rules = all_yara_rules.replace('rule {}'.format(item), 'rule duplicate_{}'.format(item), 1)
+ attr_cnt_duplicate += 1
+ continue
+ else:
+ # This should never happen as all rules were processed before separately. So logically we should only have duplicates.
+ exit("ERROR SyntaxError in rules: {}".format(e.args))
+ break
+
+# save to a file
+fname = 'misp.yara'
+with open(fname, 'w') as f_out:
+ f_out.write(all_yara_rules)
+
+print("")
+print("MISP attributes with YARA rules: total={} valid={} invalid={} duplicate={} changed={}.".format(attr_cnt, attr_cnt - attr_cnt_invalid, attr_cnt_invalid, attr_cnt_duplicate, attr_cnt_changed))
+print("Valid YARA rule file save to file '{}'. Invalid rules/attributes were ignored.".format(fname))
diff --git a/pymisp/__init__.py b/pymisp/__init__.py
index bd4ec96..b5250fa 100644
--- a/pymisp/__init__.py
+++ b/pymisp/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '2.4.62'
+__version__ = '2.4.65'
from .exceptions import PyMISPError, NewEventError, NewAttributeError, MissingDependency, NoURL, NoKey
from .api import PyMISP
diff --git a/pymisp/api.py b/pymisp/api.py
index 71ec608..7465138 100644
--- a/pymisp/api.py
+++ b/pymisp/api.py
@@ -33,9 +33,11 @@ from .mispevent import MISPEvent, MISPAttribute, EncodeUpdate
# Least dirty way to support python 2 and 3
try:
basestring
+ unicode
warnings.warn("You're using python 2, it is strongly recommended to use python >=3.4")
except NameError:
basestring = str
+ unicode = str
class distributions(object):
@@ -100,18 +102,19 @@ class PyMISP(object):
try:
# Make sure the MISP instance is working and the URL is valid
- response = self.get_version()
- misp_version = response['version'].split('.')
pymisp_version = __version__.split('.')
- for a, b in zip(misp_version, pymisp_version):
- if a == b:
- continue
- elif a < b:
- warnings.warn("Remote MISP instance (v{}) older than PyMISP (v{}). You should update your MISP instance, or install an older PyMISP version.".format(response['version'], __version__))
- else: # a > b
- # NOTE: That can happen and should not be blocking
- warnings.warn("Remote MISP instance (v{}) newer than PyMISP (v{}). Please check if a newer version of PyMISP is available.".format(response['version'], __version__))
- continue
+ response = self.get_recommended_api_version()
+ if not response.get('version'):
+ warnings.warn("Unable to check the recommended PyMISP version (MISP <2.4.60), please upgrade.")
+ else:
+ recommended_pymisp_version = response['version'].split('.')
+ for a, b in zip(pymisp_version, recommended_pymisp_version):
+ if a == b:
+ continue
+ elif a > b:
+ warnings.warn("The version of PyMISP recommended by the MISP instance ({}) is older than the one you're using now ({}). Please upgrade the MISP instance or use an older PyMISP version.".format(response['version'], __version__))
+ else: # a < b
+ warnings.warn("The version of PyMISP recommended by the MISP instance ({}) is newer than the one you're using now ({}). Please upgrade PyMISP.".format(response['version'], __version__))
except Exception as e:
raise PyMISPError('Unable to connect to MISP ({}). Please make sure the API key and the URL are correct (http/https is required): {}'.format(self.root_url, e))
@@ -174,7 +177,7 @@ class PyMISP(object):
for e in errors:
if not e:
continue
- if isinstance(e, str):
+ if isinstance(e, basestring):
messages.append(e)
continue
for type_e, msgs in e.items():
@@ -349,23 +352,35 @@ class PyMISP(object):
if e.published:
return {'error': 'Already published'}
e.publish()
- return self.update(event)
+ return self.update(e)
def change_threat_level(self, event, threat_level_id):
e = self._make_mispevent(event)
e.threat_level_id = threat_level_id
- return self.update(event)
+ return self.update(e)
def change_sharing_group(self, event, sharing_group_id):
e = self._make_mispevent(event)
e.distribution = 4 # Needs to be 'Sharing group'
e.sharing_group_id = sharing_group_id
- return self.update(event)
+ return self.update(e)
def new_event(self, distribution=None, threat_level_id=None, analysis=None, info=None, date=None, published=False, orgc_id=None, org_id=None, sharing_group_id=None):
misp_event = self._prepare_full_event(distribution, threat_level_id, analysis, info, date, published, orgc_id, org_id, sharing_group_id)
return self.add_event(json.dumps(misp_event, cls=EncodeUpdate))
+ def tag(self, uuid, tag):
+ session = self.__prepare_session()
+ path = '/tags/attachTagToObject/{}/{}/'.format(uuid, tag)
+ response = session.post(urljoin(self.root_url, path))
+ return self._check_response(response)
+
+ def untag(self, uuid, tag):
+ session = self.__prepare_session()
+ path = '/tags/removeTagFromObject/{}/{}/'.format(uuid, tag)
+ response = session.post(urljoin(self.root_url, path))
+ return self._check_response(response)
+
def add_tag(self, event, tag, attribute=False):
# FIXME: this is dirty, this function needs to be deprecated with something tagging a UUID
session = self.__prepare_session()
@@ -373,6 +388,9 @@ class PyMISP(object):
to_post = {'request': {'Attribute': {'id': event['id'], 'tag': tag}}}
path = 'attributes/addTag'
else:
+ # Allow for backwards-compat with old style
+ if "Event" in event:
+ event = event["Event"]
to_post = {'request': {'Event': {'id': event['id'], 'tag': tag}}}
path = 'events/addTag'
response = session.post(urljoin(self.root_url, path), data=json.dumps(to_post))
@@ -411,7 +429,7 @@ class PyMISP(object):
e = MISPEvent(self.describe_types)
e.load(event)
e.attributes += attributes
- response = self.update(event)
+ response = self.update(e)
return response
def add_named_attribute(self, event, type_value, value, category=None, to_ids=False, comment=None, distribution=None, proposal=False, **kwargs):
@@ -464,7 +482,7 @@ class PyMISP(object):
# It's a file handle - we can read it
fileData = attachment.read()
- elif isinstance(attachment, str):
+ elif isinstance(attachment, basestring):
# It can either be the b64 encoded data or a file path
if os.path.exists(attachment):
# It's a path!
@@ -1042,6 +1060,13 @@ class PyMISP(object):
else:
return {'error': 'Impossible to retrieve the version of the master branch.'}
+ def get_recommended_api_version(self):
+ """Returns the recommended API version from the server"""
+ session = self.__prepare_session()
+ url = urljoin(self.root_url, 'servers/getPyMISPVersion.json')
+ response = session.get(url)
+ return self._check_response(response)
+
def get_version(self):
"""Returns the version of the instance."""
session = self.__prepare_session()
@@ -1060,10 +1085,10 @@ class PyMISP(object):
# ############## Export Attributes in text ####################################
- def get_all_attributes_txt(self, type_attr):
- """Get all attributes from a specific type as plain text. Only published and IDS flagged attributes are exported."""
+ def get_all_attributes_txt(self, type_attr, tags=False, eventId=False, allowNonIDS=False, date_from=False, date_to=False, last=False, enforceWarninglist=False, allowNotPublished=False):
+ """Get all attributes from a specific type as plain text. Only published and IDS flagged attributes are exported, except if stated otherwise."""
session = self.__prepare_session('txt')
- url = urljoin(self.root_url, 'attributes/text/download/%s' % type_attr)
+ url = urljoin(self.root_url, 'attributes/text/download/%s/%s/%s/%s/%s/%s/%s/%s/%s' % (type_attr, tags, eventId, allowNonIDS, date_from, date_to, last, enforceWarninglist, allowNotPublished))
response = session.get(url)
return response
@@ -1110,13 +1135,18 @@ class PyMISP(object):
response = session.post(url)
return self._check_response(response)
- def sighting_per_json(self, json_file):
+ def set_sightings(self, sightings):
+ if isinstance(sightings, dict):
+ sightings = json.dumps(sightings)
session = self.__prepare_session()
+ url = urljoin(self.root_url, 'sightings/add/')
+ response = session.post(url, data=sightings)
+ return self._check_response(response)
+
+ def sighting_per_json(self, json_file):
with open(json_file) as f:
jdata = json.load(f)
- url = urljoin(self.root_url, 'sightings/add/')
- response = session.post(url, data=json.dumps(jdata))
- return self._check_response(response)
+ return self.set_sightings(jdata)
# ############## Sharing Groups ##################
diff --git a/pymisp/data/describeTypes.json b/pymisp/data/describeTypes.json
index 820341e..823f641 100644
--- a/pymisp/data/describeTypes.json
+++ b/pymisp/data/describeTypes.json
@@ -340,6 +340,190 @@
"x509-fingerprint-sha1": {
"default_category": "Network activity",
"to_ids": 1
+ },
+ "dns-soa-email": {
+ "default_category": "Attribution",
+ "to_ids": 0
+ },
+ "size-in-bytes": {
+ "default_category": "Other",
+ "to_ids": 0
+ },
+ "counter": {
+ "default_category": "Other",
+ "to_ids": 0
+ },
+ "datetime": {
+ "default_category": "Other",
+ "to_ids": 0
+ },
+ "cpe": {
+ "default_category": "Other",
+ "to_ids": 0
+ },
+ "port": {
+ "default_category": "Network activity",
+ "to_ids": 0
+ },
+ "ip-dst|port": {
+ "default_category": "Network activity",
+ "to_ids": 1
+ },
+ "ip-src|port": {
+ "default_category": "Network activity",
+ "to_ids": 1
+ },
+ "hostname|port": {
+ "default_category": "Network activity",
+ "to_ids": 1
+ },
+ "email-dst-display-name": {
+ "default_category": "Payload delivery",
+ "to_ids": 0
+ },
+ "email-src-display-name": {
+ "default_category": "Payload delivery",
+ "to_ids": 0
+ },
+ "email-header": {
+ "default_category": "Payload delivery",
+ "to_ids": 0
+ },
+ "email-reply-to": {
+ "default_category": "Payload delivery",
+ "to_ids": 0
+ },
+ "email-x-mailer": {
+ "default_category": "Payload delivery",
+ "to_ids": 0
+ },
+ "email-mime-boundary": {
+ "default_category": "Payload delivery",
+ "to_ids": 0
+ },
+ "email-thread-index": {
+ "default_category": "Payload delivery",
+ "to_ids": 0
+ },
+ "email-message-id": {
+ "default_category": "",
+ "to_ids": 0
+ },
+ "github-username": {
+ "default_category": "Social network",
+ "to_ids": 0
+ },
+ "github-repository": {
+ "default_category": "Social network",
+ "to_ids": 0
+ },
+ "github-organisation": {
+ "default_category": "Social network",
+ "to_ids": 0
+ },
+ "jabber-id": {
+ "default_category": "Social network",
+ "to_ids": 0
+ },
+ "twitter-id": {
+ "default_category": "Social network",
+ "to_ids": 0
+ },
+ "first-name": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "middle-name": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "last-name": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "date-of-birth": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "place-of-birth": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "gender": {
+ "default_category": "",
+ "to_ids": 0
+ },
+ "passport-number": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "passport-country": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "passport-expiration": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "redress-number": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "nationality": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "visa-number": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "issue-date-of-the-visa": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "primary-residence": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "country-of-residence": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "special-service-request": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "frequent-flyer-number": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "travel-details": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "payment-details": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "place-port-of-original-embarkation": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "place-port-of-clearance": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "place-port-of-onward-foreign-destination": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "passenger-name-record-locator-number": {
+ "default_category": "Person",
+ "to_ids": 0
+ },
+ "mobile-application-id": {
+ "default_category": "Payload delivery",
+ "to_ids": 1
}
},
"types": [
@@ -427,7 +611,53 @@
"whois-registrant-name",
"whois-registrar",
"whois-creation-date",
- "x509-fingerprint-sha1"
+ "x509-fingerprint-sha1",
+ "dns-soa-email",
+ "size-in-bytes",
+ "counter",
+ "datetime",
+ "cpe",
+ "port",
+ "ip-dst|port",
+ "ip-src|port",
+ "hostname|port",
+ "email-dst-display-name",
+ "email-src-display-name",
+ "email-header",
+ "email-reply-to",
+ "email-x-mailer",
+ "email-mime-boundary",
+ "email-thread-index",
+ "email-message-id",
+ "github-username",
+ "github-repository",
+ "github-organisation",
+ "jabber-id",
+ "twitter-id",
+ "first-name",
+ "middle-name",
+ "last-name",
+ "date-of-birth",
+ "place-of-birth",
+ "gender",
+ "passport-number",
+ "passport-country",
+ "passport-expiration",
+ "redress-number",
+ "nationality",
+ "visa-number",
+ "issue-date-of-the-visa",
+ "primary-residence",
+ "country-of-residence",
+ "special-service-request",
+ "frequent-flyer-number",
+ "travel-details",
+ "payment-details",
+ "place-port-of-original-embarkation",
+ "place-port-of-clearance",
+ "place-port-of-onward-foreign-destination",
+ "passenger-name-record-locator-number",
+ "mobile-application-id"
],
"categories": [
"Internal reference",
@@ -442,6 +672,9 @@
"Attribution",
"External analysis",
"Financial fraud",
+ "Support Tool",
+ "Social network",
+ "Person",
"Other"
],
"category_type_mappings": {
@@ -497,6 +730,8 @@
"filename|pehash",
"ip-src",
"ip-dst",
+ "ip-dst|port",
+ "ip-src|port",
"hostname",
"domain",
"email-src",
@@ -517,7 +752,19 @@
"text",
"vulnerability",
"x509-fingerprint-sha1",
- "other"
+ "other",
+ "ip-dst|port",
+ "ip-src|port",
+ "hostname|port",
+ "email-dst-display-name",
+ "email-src-display-name",
+ "email-header",
+ "email-reply-to",
+ "email-x-mailer",
+ "email-mime-boundary",
+ "email-thread-index",
+ "email-message-id",
+ "mobile-application-id"
],
"Artifacts dropped": [
"md5",
@@ -602,6 +849,7 @@
"comment",
"text",
"x509-fingerprint-sha1",
+ "mobile-application-id",
"other"
],
"Persistence mechanism": [
@@ -615,6 +863,8 @@
"Network activity": [
"ip-src",
"ip-dst",
+ "ip-dst|port",
+ "ip-src|port",
"hostname",
"domain",
"domain|ip",
@@ -662,6 +912,8 @@
"filename|sha256",
"ip-src",
"ip-dst",
+ "ip-dst|port",
+ "ip-src|port",
"hostname",
"domain",
"domain|ip",
@@ -681,6 +933,7 @@
"comment",
"text",
"x509-fingerprint-sha1",
+ "github-repository",
"other"
],
"Financial fraud": [
@@ -696,7 +949,60 @@
"text",
"other"
],
+ "Support Tool": [
+ "link",
+ "text",
+ "attachment",
+ "comment",
+ "text",
+ "other"
+ ],
+ "Social network": [
+ "github-username",
+ "github-repository",
+ "github-organisation",
+ "jabber-id",
+ "twitter-id",
+ "email-src",
+ "email-dst",
+ "comment",
+ "text",
+ "other"
+ ],
+ "Person": [
+ "first-name",
+ "middle-name",
+ "last-name",
+ "date-of-birth",
+ "place-of-birth",
+ "gender",
+ "passport-number",
+ "passport-country",
+ "passport-expiration",
+ "redress-number",
+ "nationality",
+ "visa-number",
+ "issue-date-of-the-visa",
+ "primary-residence",
+ "country-of-residence",
+ "special-service-request",
+ "frequent-flyer-number",
+ "travel-details",
+ "payment-details",
+ "place-port-of-original-embarkation",
+ "place-port-of-clearance",
+ "place-port-of-onward-foreign-destination",
+ "passenger-name-record-locator-number",
+ "comment",
+ "text",
+ "other"
+ ],
"Other": [
+ "size-in-bytes",
+ "counter",
+ "datetime",
+ "cpe",
+ "port",
"comment",
"text",
"other"
diff --git a/pymisp/mispevent.py b/pymisp/mispevent.py
index db52ced..6d5a472 100644
--- a/pymisp/mispevent.py
+++ b/pymisp/mispevent.py
@@ -101,6 +101,9 @@ class MISPAttribute(object):
def delete(self):
self.deleted = True
+ def add_tag(self, tag):
+ self.Tag.append({'name': tag})
+
def verify(self, gpg_uid):
if not has_pyme:
raise Exception('pyme is required, please install: pip install --pre pyme3. You will also need libgpg-error-dev and libgpgme11-dev.')
@@ -116,7 +119,7 @@ class MISPAttribute(object):
def set_all_values(self, **kwargs):
if kwargs.get('type') and kwargs.get('category'):
if kwargs['type'] not in self.category_type_mapping[kwargs['category']]:
- raise NewAttributeError('{} and {} is an invalid combinaison, type for this category has to be in {}'.format(self.type, self.category, (', '.join(self.category_type_mapping[kwargs['category']]))))
+ raise NewAttributeError('{} and {} is an invalid combinaison, type for this category has to be in {}'.format(kwargs.get('type'), kwargs.get('category'), (', '.join(self.category_type_mapping[kwargs['category']]))))
# Required
if kwargs.get('type'):
self.type = kwargs['type']
@@ -174,7 +177,7 @@ class MISPAttribute(object):
if kwargs.get('sig'):
self.sig = kwargs['sig']
if kwargs.get('Tag'):
- self.Tag = kwargs['Tag']
+ self.Tag = [t for t in kwargs['Tag'] if t]
# If the user wants to disable correlation, let them. Defaults to False.
self.disable_correlation = kwargs.get("disable_correlation", False)
@@ -214,6 +217,8 @@ class MISPAttribute(object):
to_return = {'type': self.type, 'category': self.category, 'to_ids': self.to_ids,
'distribution': self.distribution, 'value': self.value,
'comment': self.comment, 'disable_correlation': self.disable_correlation}
+ if self.uuid:
+ to_return['uuid'] = self.uuid
if self.sig:
to_return['sig'] = self.sig
if self.sharing_group_id:
@@ -231,9 +236,8 @@ class MISPAttribute(object):
to_return = self._json()
if self.id:
to_return['id'] = self.id
- if self.uuid:
- to_return['uuid'] = self.uuid
if self.timestamp:
+ # Should never be set on an update, MISP will automatically set it to now
to_return['timestamp'] = int(time.mktime(self.timestamp.timetuple()))
if self.deleted is not None:
to_return['deleted'] = self.deleted
@@ -436,6 +440,8 @@ class MISPEvent(object):
if self.analysis not in [0, 1, 2]:
raise NewEventError('{} is invalid, the analysis has to be in 0, 1, 2'.format(self.analysis))
if kwargs.get('published') is not None:
+ self.unpublish()
+ if kwargs.get("published") == True:
self.publish()
if kwargs.get('date'):
self.set_date(kwargs['date'])
@@ -481,7 +487,7 @@ class MISPEvent(object):
if kwargs.get('Galaxy'):
self.Galaxy = kwargs['Galaxy']
if kwargs.get('Tag'):
- self.Tag = kwargs['Tag']
+ self.Tag = [t for t in kwargs['Tag'] if t]
if kwargs.get('sig'):
self.sig = kwargs['sig']
if kwargs.get('global_sig'):
@@ -542,6 +548,7 @@ class MISPEvent(object):
if self.publish_timestamp:
to_return['Event']['publish_timestamp'] = int(time.mktime(self.publish_timestamp.timetuple()))
if self.timestamp:
+ # Should never be set on an update, MISP will automatically set it to now
to_return['Event']['timestamp'] = int(time.mktime(self.timestamp.timetuple()))
to_return['Event'] = _int_to_str(to_return['Event'])
if self.attributes:
@@ -549,6 +556,19 @@ class MISPEvent(object):
jsonschema.validate(to_return, self.json_schema)
return to_return
+ def add_tag(self, tag):
+ self.Tag.append({'name': tag})
+
+ def add_attribute_tag(self, tag, attribute_identifier):
+ attribute = None
+ for a in self.attributes:
+ if a.id == attribute_identifier or a.uuid == attribute_identifier or attribute_identifier in a.value:
+ a.add_tag(tag)
+ attribute = a
+ if not attribute:
+ raise Exception('No attribute with identifier {} found.'.format(attribute_identifier))
+ return attribute
+
def publish(self):
self.published = True
diff --git a/tests/test_offline.py b/tests/test_offline.py
index 298485c..834f7af 100644
--- a/tests/test_offline.py
+++ b/tests/test_offline.py
@@ -38,7 +38,8 @@ class TestOffline(unittest.TestCase):
def initURI(self, m):
m.register_uri('GET', self.domain + 'events/1', json=self.auth_error_msg, status_code=403)
- m.register_uri('GET', self.domain + 'servers/getVersion.json', json={"version": "2.4.56"})
+ m.register_uri('GET', self.domain + 'servers/getVersion.json', json={"version": "2.4.62"})
+ m.register_uri('GET', self.domain + 'servers/getPyMISPVersion.json', json={"version": "2.4.62"})
m.register_uri('GET', self.domain + 'sharing_groups.json', json=self.sharing_groups)
m.register_uri('GET', self.domain + 'attributes/describeTypes.json', json=self.types)
m.register_uri('GET', self.domain + 'events/2', json=self.event)
@@ -97,7 +98,7 @@ class TestOffline(unittest.TestCase):
api_version = pymisp.get_api_version()
self.assertEqual(api_version, {'version': pm.__version__})
server_version = pymisp.get_version()
- self.assertEqual(server_version, {"version": "2.4.56"})
+ self.assertEqual(server_version, {"version": "2.4.62"})
def test_getSharingGroups(self, m):
self.initURI(m)