diff --git a/.travis.yml b/.travis.yml
index ec9b16f..c70bed3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,12 +9,12 @@ python:
- "3.4"
- "3.5"
- "3.5-dev"
+ - "3.6"
+ - "3.6-dev"
- "nightly"
install:
- - pip install -U nose
- - pip install coveralls
- - pip install codecov
+ - pip install -U nose codecov pytest
- pip install -U -r REQUIREMENTS
- pip install .
@@ -35,4 +35,3 @@ script:
after_success:
- coverage combine .coverage*
- codecov
- - coveralls
diff --git a/README.md b/README.md
index ed364ff..7d0c17f 100644
--- a/README.md
+++ b/README.md
@@ -26,19 +26,26 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/
* [EUPI](misp_modules/modules/expansion/eupi.py) - a hover and expansion module to get information about an URL from the [Phishing Initiative project](https://phishing-initiative.eu/?lang=en).
* [GeoIP](misp_modules/modules/expansion/geoip_country.py) - a hover and expansion module to get GeoIP information from geolite/maxmind.
* [IPASN](misp_modules/modules/expansion/ipasn.py) - a hover and expansion to get the BGP ASN of an IP address.
+* [iprep](misp-modules/modules/expansion/iprep.py) - an expansion module to get IP reputation from packetmail.net.
* [passivetotal](misp_modules/modules/expansion/passivetotal.py) - a [passivetotal](https://www.passivetotal.org/) module that queries a number of different PassiveTotal datasets.
+* [shodan](misp_modules/modules/expansion/shodan.py) - a minimal [shodan](https://www.shodan.io/) expansion module.
* [sourcecache](misp_modules/modules/expansion/sourcecache.py) - a module to cache a specific link from a MISP instance.
+* [threatminer](misp_modules/modules/expansion/threatminer.py) - an expansion module to expand from [ThreatMiner](https://www.threatminer.org/).
* [countrycode](misp_modules/modules/expansion/countrycode.py) - a hover module to tell you what country a URL belongs to.
* [virustotal](misp_modules/modules/expansion/virustotal.py) - an expansion module to pull known resolutions and malware samples related with an IP/Domain from virusTotal (this modules require a VirusTotal private API key)
+* [wikidata](misp_modules/modules/expansion/wiki.py) - a [wikidata](https://www.wikidata.org) expansion module.
+* [xforce](misp_modules/modules/expansion/xforceexchange.py) - an IBM X-Force Exchange expansion module.
### Export modules
* [CEF](misp_modules/modules/export_mod/cef_export.py) module to export Common Event Format (CEF).
+* [Lite Export](/misp-modules/blob/master/misp_modules/modules/export_mod/liteexport.py) module to export a lite event.
### Import modules
* [Cuckoo JSON](misp_modules/modules/import_mod/cuckooimport.py) Cuckoo JSON import.
* [OCR](misp_modules/modules/import_mod/ocr.py) Optical Character Recognition (OCR) module for MISP to import attributes from images, scan or faxes.
+* [OpenIOC](misp_modules/modules/import_mod/openiocimport.py) OpenIOC import based on PyMISP library.
* [stiximport](misp_modules/modules/import_mod/stiximport.py) - An import module to process STIX xml/json.
* [Email Import](misp_modules/modules/import_mod/email_import.py) Email import module for MISP to import basic metadata.
* [VMRay](misp_modules/modules/import_mod/vmray_import.py) - An import module to process VMRay export.
@@ -53,7 +60,7 @@ cd misp-modules
sudo pip3 install -I -r REQUIREMENTS
sudo pip3 install -I .
sudo vi /etc/rc.local, add this line: `sudo -u www-data misp-modules -s &`
-/usr/local/bin/misp-modules #to start the modules
+misp-modules #to start the modules
~~~~
## How to add your own MISP modules?
@@ -352,6 +359,23 @@ Recommended Plugin.Import_ocr_enabled true Enable or disable the ocr
In this same menu set any other plugin settings that are required for testing.
+## Install misp-module on an offline instance.
+First, you need to grab all necessery packages for example like this :
+
+Use pip wheel to create an archive
+~~~
+mkdir misp-modules-offline
+pip3 wheel -r REQUIREMENTS shodan --wheel-dir=./misp-modules-offline
+tar -cjvf misp-module-bundeled.tar.bz2 ./misp-modules-offline/*
+~~~
+On offline machine :
+~~~
+mkdir misp-modules-bundle
+tar xvf misp-module-bundeled.tar.bz2 -C misp-modules-bundle
+cd misp-modules-bundle
+ls -1|while read line; do sudo pip3 install --force-reinstall --ignore-installed --upgrade --no-index --no-deps ${line};done
+~~~
+Next you can follow standard install procedure.
## How to contribute your own module?
diff --git a/REQUIREMENTS b/REQUIREMENTS
index 6cda15a..0665aac 100644
--- a/REQUIREMENTS
+++ b/REQUIREMENTS
@@ -13,9 +13,10 @@ ipasn-redis
asnhistory
git+https://github.com/Rafiot/uwhoisd.git@testing#egg=uwhois&subdirectory=client
git+https://github.com/MISP/MISP-STIX-Converter.git#egg=misp_stix_converter
-git+https://github.com/CIRCL/PyMISP.git#egg=pymisp
+git+https://github.com/MISP/PyMISP.git#egg=pymisp
pillow
pytesseract
SPARQLWrapper
domaintools_api
pygeoip
+bs4
diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py
index 914cb1c..de3c5c3 100644
--- a/misp_modules/modules/expansion/__init__.py
+++ b/misp_modules/modules/expansion/__init__.py
@@ -2,4 +2,4 @@ from . import _vmray
__all__ = ['vmray_submit', 'asn_history', 'circl_passivedns', 'circl_passivessl',
'countrycode', 'cve', 'dns', 'domaintools', 'eupi', 'ipasn', 'passivetotal', 'sourcecache',
- 'virustotal', 'whois', 'shodan', 'reversedns', 'geoip_country', 'wiki']
+ 'virustotal', 'whois', 'shodan', 'reversedns', 'geoip_country', 'wiki', 'iprep', 'threatminer']
diff --git a/misp_modules/modules/expansion/iprep.py b/misp_modules/modules/expansion/iprep.py
new file mode 100755
index 0000000..6073052
--- /dev/null
+++ b/misp_modules/modules/expansion/iprep.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+import json
+import requests
+
+
+misperrors = {'error': 'Error'}
+mispattributes = {'input': ['ip-src', 'ip-dst'], 'output': ['text']}
+moduleinfo = {'version': '1.0', 'author': 'Keith Faber',
+ 'description': 'Query IPRep Data for IP Address',
+ 'module-type': ['expansion']}
+
+moduleconfig = ['apikey']
+
+
+def handler(q=False):
+ if q is False:
+ return False
+ request = json.loads(q)
+ if request.get('ip-src'):
+ toquery = request['ip-src']
+ elif request.get('ip-dst'):
+ toquery = request['ip-dst']
+ else:
+ misperrors['error'] = "Unsupported attributes type"
+ return misperrors
+
+ if not request.get('config') and not request['config'].get('apikey'):
+ misperrors['error'] = 'IPRep api key is missing'
+ return misperrors
+
+ err, rep = parse_iprep(toquery, request['config'].get('apikey'))
+ if len(err) > 0:
+ misperrors['error'] = ','.join(err)
+ return misperrors
+ return {'results': rep}
+
+
+def parse_iprep(ip, api):
+ meta_fields = ['origin', 'Query_Time', 'created_on', 'IP_Lookup_History', 'IPs_in_collection', '_id', 'disclaimer',
+ 'MaxMind_Free_GeoIP', 'Unique_Lookups', 'query_result']
+ rep = []
+ err = []
+ full_text = ''
+ url = 'https://www.packetmail.net/iprep.php/%s' % ip
+ try:
+ data = requests.get(url, params={'apikey': api}).json()
+ except:
+ return ['Error pulling data'], rep
+ # print '%s' % data
+ for name, val in data.items():
+ if name not in meta_fields:
+ try:
+ context = val['context']
+ if type(context) is list:
+ if context[0].get('alert'):
+ context = ','.join([hit['alert']['signature'] for hit in context])
+ elif context[0].get('signature'):
+ context = ','.join([hit['signature'] for hit in context])
+ elif context[0].get('target_port') and context[0].get('protocol'):
+ context = ','.join(
+ ['Port Attacked: %s %s' % (hit['target_port'], hit['protocol']) for hit in context])
+ elif context[0].get('phishing_kit') and context[0].get('url'):
+ context = ','.join(['%s (%s)' % (hit['phishing_kit'], hit['url']) for hit in context])
+ else:
+ context = ';'.join(['%s: %s' % (k, v) for k, v in context[0].items()])
+
+ if val.get('special_note'):
+ context += '; ' + val['special_note']
+
+ misp_val = context
+ full_text += '\n%s' % context
+ misp_comment = 'IPRep Source %s: %s' % (name, val['last_seen'])
+ rep.append({'types': mispattributes['output'], 'categories':['External analysis'], 'values': misp_val, 'comment': misp_comment})
+ except:
+ err.append('Error parsing source: %s' % name)
+
+ rep.append({'types': ['freetext'], 'values': full_text , 'comment': 'Free text import of IPRep'})
+ return err, rep
+
+
+def introspection():
+ return mispattributes
+
+
+def version():
+ moduleinfo['config'] = moduleconfig
+ return moduleinfo
diff --git a/misp_modules/modules/expansion/threatminer.py b/misp_modules/modules/expansion/threatminer.py
new file mode 100755
index 0000000..1a4be50
--- /dev/null
+++ b/misp_modules/modules/expansion/threatminer.py
@@ -0,0 +1,173 @@
+import json
+import requests
+from requests import HTTPError
+import base64
+
+misperrors = {'error': 'Error'}
+mispattributes = {'input': ['hostname', 'domain', 'ip-src', 'ip-dst', 'md5', 'sha1', 'sha256', 'sha512'],
+ 'output': ['domain', 'ip-src', 'ip-dst', 'text', 'md5', 'sha1', 'sha256', 'sha512', 'ssdeep',
+ 'authentihash', 'filename', 'whois-registrant-email', 'url', 'link']
+ }
+
+# possible module-types: 'expansion', 'hover' or both
+moduleinfo = {'version': '1', 'author': 'KX499',
+ 'description': 'Get information from ThreatMiner',
+ 'module-type': ['expansion']}
+
+desc = '{}: Threatminer - {}'
+
+
+def handler(q=False):
+ if q is False:
+ return False
+
+ q = json.loads(q)
+
+ r = {'results': []}
+
+ if 'ip-src' in q:
+ r['results'] += get_ip(q['ip-src'])
+ if 'ip-dst' in q:
+ r['results'] += get_ip(q['ip-dst'])
+ if 'domain' in q:
+ r['results'] += get_domain(q['domain'])
+ if 'hostname' in q:
+ r['results'] += get_domain(q['hostname'])
+ if 'md5' in q:
+ r['results'] += get_hash(q['md5'])
+ if 'sha1' in q:
+ r['results'] += get_hash(q['sha1'])
+ if 'sha256' in q:
+ r['results'] += get_hash(q['sha256'])
+ if 'sha512' in q:
+ r['results'] += get_hash(q['sha512'])
+
+ uniq = []
+ for res in r['results']:
+ if res not in uniq:
+ uniq.append(res)
+ r['results'] = uniq
+ return r
+
+
+def get_domain(q):
+ ret = []
+ for flag in [1, 2, 3, 4, 5, 6]:
+ req = requests.get('https://www.threatminer.org/domain.php', params={'q': q, 'api': 'True', 'rt': flag})
+ if not req.status_code == 200:
+ continue
+ results = req.json().get('results')
+ if not results:
+ continue
+
+ for result in results:
+ if flag == 1: #whois
+ emails = result.get('whois', {}).get('emails')
+ if not emails:
+ continue
+ for em_type, email in emails.items():
+ ret.append({'types': ['whois-registrant-email'], 'values': [email], 'comment': desc.format(q, 'whois')})
+ if flag == 2: #pdns
+ ip = result.get('ip')
+ if ip:
+ ret.append({'types': ['ip-src', 'ip-dst'], 'values': [ip], 'comment': desc.format(q, 'pdns')})
+ if flag == 3: #uri
+ uri = result.get('uri')
+ if uri:
+ ret.append({'types': ['url'], 'values': [uri], 'comment': desc.format(q, 'uri')})
+ if flag == 4: #samples
+ if type(result) is str:
+ ret.append({'types': ['sha256'], 'values': [result], 'comment': desc.format(q, 'samples')})
+ if flag == 5: #subdomains
+ if type(result) is str:
+ ret.append({'types': ['domain'], 'values': [result], 'comment': desc.format(q, 'subdomain')})
+ if flag == 6: #reports
+ link = result.get('URL')
+ if link:
+ ret.append({'types': ['url'], 'values': [link], 'comment': desc.format(q, 'report')})
+
+ return ret
+
+
+def get_ip(q):
+ ret = []
+ for flag in [1, 2, 3, 4, 5, 6]:
+ req = requests.get('https://www.threatminer.org/host.php', params={'q': q, 'api': 'True', 'rt': flag})
+ if not req.status_code == 200:
+ continue
+ results = req.json().get('results')
+ if not results:
+ continue
+
+ for result in results:
+ if flag == 1: #whois
+ emails = result.get('whois', {}).get('emails')
+ if not emails:
+ continue
+ for em_type, email in emails.items():
+ ret.append({'types': ['whois-registrant-email'], 'values': [email], 'comment': desc.format(q, 'whois')})
+ if flag == 2: #pdns
+ ip = result.get('ip')
+ if ip:
+ ret.append({'types': ['ip-src', 'ip-dst'], 'values': [ip], 'comment': desc.format(q, 'pdns')})
+ if flag == 3: #uri
+ uri = result.get('uri')
+ if uri:
+ ret.append({'types': ['url'], 'values': [uri], 'comment': desc.format(q, 'uri')})
+ if flag == 4: #samples
+ if type(result) is str:
+ ret.append({'types': ['sha256'], 'values': [result], 'comment': desc.format(q, 'samples')})
+ if flag == 5: #ssl
+ if type(result) is str:
+ ret.append({'types': ['x509-fingerprint-sha1'], 'values': [result], 'comment': desc.format(q, 'ssl')})
+ if flag == 6: #reports
+ link = result.get('URL')
+ if link:
+ ret.append({'types': ['url'], 'values': [link], 'comment': desc.format(q, 'report')})
+
+ return ret
+
+
+def get_hash(q):
+ ret = []
+ for flag in [1, 3, 6, 7]:
+ req = requests.get('https://www.threatminer.org/sample.php', params={'q': q, 'api': 'True', 'rt': flag})
+ if not req.status_code == 200:
+ continue
+ results = req.json().get('results')
+ if not results:
+ continue
+
+ for result in results:
+ if flag == 1: #meta (filename)
+ name = result.get('file_name')
+ if name:
+ ret.append({'types': ['filename'], 'values': [name], 'comment': desc.format(q, 'file')})
+ if flag == 3: #network
+ domains = result.get('domains')
+ for dom in domains:
+ if dom.get('domain'):
+ ret.append({'types': ['domain'], 'values': [dom['domain']], 'comment': desc.format(q, 'network')})
+
+ hosts = result.get('hosts')
+ for h in hosts:
+ if type(h) is str:
+ ret.append({'types': ['ip-src', 'ip-dst'], 'values': [h], 'comment': desc.format(q, 'network')})
+ if flag == 6: #detections
+ detections = result.get('av_detections')
+ for d in detections:
+ if d.get('detection'):
+ ret.append({'types': ['text'], 'values': [d['detection']], 'comment': desc.format(q, 'detection')})
+ if flag == 7: #report
+ if type(result) is str:
+ ret.append({'types': ['sha256'], 'values': [result], 'comment': desc.format(q, 'report')})
+
+ return ret
+
+
+def introspection():
+ return mispattributes
+
+
+def version():
+ return moduleinfo
diff --git a/misp_modules/modules/expansion/virustotal.py b/misp_modules/modules/expansion/virustotal.py
index c0c7715..44199a1 100755
--- a/misp_modules/modules/expansion/virustotal.py
+++ b/misp_modules/modules/expansion/virustotal.py
@@ -1,23 +1,24 @@
import json
import requests
-import hashlib
-import re
+from requests import HTTPError
import base64
-import os
misperrors = {'error': 'Error'}
-mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst"],
- 'output':['domain', "ip-src", "ip-dst", "text"]
+mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sha1", "sha256", "sha512"],
+ 'output': ['domain', "ip-src", "ip-dst", "text", "md5", "sha1", "sha256", "sha512", "ssdeep",
+ "authentihash", "filename"]
}
# possible module-types: 'expansion', 'hover' or both
-moduleinfo = {'version': '', 'author': 'Hannah Ward',
+moduleinfo = {'version': '2', 'author': 'Hannah Ward',
'description': 'Get information from virustotal',
'module-type': ['expansion']}
# config fields that your code expects from the site admin
moduleconfig = ["apikey", "event_limit"]
-limit = 5 #Default
+limit = 5 # Default
+comment = '%s: Enriched via VT'
+
def handler(q=False):
global limit
@@ -30,124 +31,172 @@ def handler(q=False):
limit = int(q["config"].get("event_limit", 5))
r = {"results": []}
-
+
if "ip-src" in q:
- r["results"] += getIP(q["ip-src"], key)
+ r["results"] += getIP(q["ip-src"], key)
if "ip-dst" in q:
- r["results"] += getIP(q["ip-dst"], key)
+ r["results"] += getIP(q["ip-dst"], key)
if "domain" in q:
- r["results"] += getDomain(q["domain"], key)
+ r["results"] += getDomain(q["domain"], key)
if 'hostname' in q:
- r["results"] += getDomain(q['hostname'], key)
+ r["results"] += getDomain(q['hostname'], key)
+ if 'md5' in q:
+ r["results"] += getHash(q['md5'], key)
+ if 'sha1' in q:
+ r["results"] += getHash(q['sha1'], key)
+ if 'sha256' in q:
+ r["results"] += getHash(q['sha256'], key)
+ if 'sha512' in q:
+ r["results"] += getHash(q['sha512'], key)
uniq = []
for res in r["results"]:
- if res not in uniq:
- uniq.append(res)
+ if res not in uniq:
+ uniq.append(res)
r["results"] = uniq
return r
-def getIP(ip, key, do_not_recurse = False):
+
+def getHash(hash, key, do_not_recurse=False):
+ req = requests.get("https://www.virustotal.com/vtapi/v2/file/report",
+ params={"allinfo": 1, "apikey": key, 'resource': hash})
+ try:
+ req.raise_for_status()
+ req = req.json()
+ except HTTPError as e:
+ misperrors['error'] = str(e)
+ return misperrors
+
+ if req["response_code"] == 0:
+ # Nothing found
+ return []
+
+ return getMoreInfo(req, key)
+
+
+def getIP(ip, key, do_not_recurse=False):
global limit
toReturn = []
- req = requests.get("https://www.virustotal.com/vtapi/v2/ip-address/report",
- params = {"ip":ip, "apikey":key}
- ).json()
+ req = requests.get("https://www.virustotal.com/vtapi/v2/ip-address/report",
+ params={"ip": ip, "apikey": key})
+ try:
+ req.raise_for_status()
+ req = req.json()
+ except HTTPError as e:
+ misperrors['error'] = str(e)
+ return misperrors
+
if req["response_code"] == 0:
- #Nothing found
- return []
-
+ # Nothing found
+ return []
+
if "resolutions" in req:
- for res in req["resolutions"][:limit]:
- toReturn.append( {"types":["domain"], "values":[res["hostname"]]})
- #Pivot from here to find all domain info
- if not do_not_recurse:
- toReturn += getDomain(res["hostname"], key, True)
+ for res in req["resolutions"][:limit]:
+ toReturn.append({"types": ["domain"], "values": [res["hostname"]], "comment": comment % ip})
+ # Pivot from here to find all domain info
+ if not do_not_recurse:
+ toReturn += getDomain(res["hostname"], key, True)
toReturn += getMoreInfo(req, key)
return toReturn
-
+
+
def getDomain(domain, key, do_not_recurse=False):
global limit
toReturn = []
- req = requests.get("https://www.virustotal.com/vtapi/v2/domain/report",
- params = {"domain":domain, "apikey":key}
- ).json()
+ req = requests.get("https://www.virustotal.com/vtapi/v2/domain/report",
+ params={"domain": domain, "apikey": key})
+ try:
+ req.raise_for_status()
+ req = req.json()
+ except HTTPError as e:
+ misperrors['error'] = str(e)
+ return misperrors
+
if req["response_code"] == 0:
- #Nothing found
- return []
-
+ # Nothing found
+ return []
+
if "resolutions" in req:
- for res in req["resolutions"][:limit]:
- toReturn.append( {"types":["ip-dst", "ip-src"], "values":[res["ip_address"]]})
- #Pivot from here to find all info on IPs
- if not do_not_recurse:
- toReturn += getIP(res["ip_address"], key, True)
+ for res in req["resolutions"][:limit]:
+ toReturn.append({"types": ["ip-dst", "ip-src"], "values": [res["ip_address"]], "comment": comment % domain})
+ # Pivot from here to find all info on IPs
+ if not do_not_recurse:
+ toReturn += getIP(res["ip_address"], key, True)
+ if "subdomains" in req:
+ for subd in req["subdomains"]:
+ toReturn.append({"types": ["domain"], "values": [subd], "comment": comment % domain})
toReturn += getMoreInfo(req, key)
return toReturn
+
def findAll(data, keys):
- a = []
- if isinstance(data, dict):
- for key in data.keys():
- if key in keys:
- a.append(data[key])
- else:
- if isinstance(data[key], (dict, list)):
- a += findAll(data[key], keys)
- if isinstance(data, list):
- for i in data:
- a += findAll(i, keys)
-
- return a
+ a = []
+ if isinstance(data, dict):
+ for key in data.keys():
+ if key in keys:
+ a.append(data[key])
+ else:
+ if isinstance(data[key], (dict, list)):
+ a += findAll(data[key], keys)
+ if isinstance(data, list):
+ for i in data:
+ a += findAll(i, keys)
+
+ return a
+
def getMoreInfo(req, key):
global limit
r = []
- #Get all hashes first
+ # Get all hashes first
hashes = []
hashes = findAll(req, ["md5", "sha1", "sha256", "sha512"])
- r.append({"types":["md5", "sha1", "sha256", "sha512"], "values":hashes})
+ r.append({"types": ["md5", "sha1", "sha256", "sha512"], "values": hashes})
for hsh in hashes[:limit]:
- #Search VT for some juicy info
- data = requests.get("http://www.virustotal.com/vtapi/v2/file/report",
- params={"allinfo":1, "apikey":key, "resource":hsh}
- ).json()
+ # Search VT for some juicy info
+ try:
+ data = requests.get("http://www.virustotal.com/vtapi/v2/file/report",
+ params={"allinfo": 1, "apikey": key, "resource": hsh}
+ ).json()
+ except:
+ continue
- # Go through each key and check if it exists
- if "submission_names" in data:
- r.append({'types':["filename"], "values":data["submission_names"]})
+ # Go through each key and check if it exists
+ if "submission_names" in data:
+ r.append({'types': ["filename"], "values": data["submission_names"], "comment": comment % hsh})
- if "ssdeep" in data:
- r.append({'types':["ssdeep"], "values":[data["ssdeep"]]})
+ if "ssdeep" in data:
+ r.append({'types': ["ssdeep"], "values": [data["ssdeep"]], "comment": comment % hsh})
- if "authentihash" in data:
- r.append({"types":["authentihash"], "values":[data["authentihash"]]})
+ if "authentihash" in data:
+ r.append({"types": ["authentihash"], "values": [data["authentihash"]], "comment": comment % hsh})
- if "ITW_urls" in data:
- r.append({"types":["url"], "values":data["ITW_urls"]})
+ if "ITW_urls" in data:
+ r.append({"types": ["url"], "values": data["ITW_urls"], "comment": comment % hsh})
- #Get the malware sample
- sample = requests.get("https://www.virustotal.com/vtapi/v2/file/download",
- params = {"hash":hsh, "apikey":key})
-
- malsample = sample.content
+ # Get the malware sample
+ sample = requests.get("https://www.virustotal.com/vtapi/v2/file/download",
+ params={"hash": hsh, "apikey": key})
+
+ malsample = sample.content
+
+ # It is possible for VT to not give us any submission names
+ if "submission_names" in data:
+ r.append({"types": ["malware-sample"],
+ "categories": ["Payload delivery"],
+ "values": data["submission_names"],
+ "data": str(base64.b64encode(malsample), 'utf-8')
+ }
+ )
- # It is possible for VT to not give us any submission names
- if "submission_names" in data:
- r.append({"types":["malware-sample"],
- "categories":["Payload delivery"],
- "values":data["submission_names"],
- "data": str(base64.b64encode(malsample), 'utf-8')
- }
- )
-
return r
+
def introspection():
return mispattributes
+
def version():
moduleinfo['config'] = moduleconfig
return moduleinfo
-
diff --git a/misp_modules/modules/expansion/xforceexchange.py b/misp_modules/modules/expansion/xforceexchange.py
new file mode 100644
index 0000000..459c69a
--- /dev/null
+++ b/misp_modules/modules/expansion/xforceexchange.py
@@ -0,0 +1,101 @@
+import requests
+import json
+import sys
+
+BASEurl = "https://api.xforce.ibmcloud.com/"
+
+extensions = {"ip1": "ipr/%s",
+ "ip2": "ipr/malware/%s",
+ "url": "url/%s",
+ "hash": "malware/%s",
+ "vuln": "/vulnerabilities/search/%s",
+ "dns": "resolve/%s"}
+
+sys.path.append('./')
+
+misperrors = {'error': 'Error'}
+mispattributes = {'input': ['ip-src','ip-dst' 'vulnerability', 'md5', 'sha1', 'sha256'],
+ 'output': ['ip-src', 'ip-dst', 'text', 'domain']}
+
+# possible module-types: 'expansion', 'hover' or both
+moduleinfo = {'version': '1', 'author': 'Joerg Stephan (@johest)',
+ 'description': 'IBM X-Force Exchange expansion module',
+ 'module-type': ['expansion', 'hover']}
+
+# config fields that your code expects from the site admin
+moduleconfig = ["apikey", "event_limit"]
+limit = 5000 #Default
+
+
+
+def MyHeader(key=False):
+ global limit
+ if key is False:
+ return None
+
+ return {"Authorization": "Basic %s " % key,
+ "Accept": "application/json",
+ 'User-Agent': 'Mozilla 5.0'}
+
+def handler(q=False):
+ global limit
+ if q is False:
+ return False
+
+ q = json.loads(q)
+
+ key = q["config"]["apikey"]
+ limit = int(q["config"].get("event_limit", 5))
+
+ r = {"results": []}
+
+ if "ip-src" in q:
+ r["results"] += apicall("dns", q["ip-src"], key)
+ if "ip-dst" in q:
+ r["results"] += apicall("dns", q["ip-dst"], key)
+ if "md5" in q:
+ r["results"] += apicall("hash", q["md5"], key)
+ if "sha1" in q:
+ r["results"] += apicall("hash", q["sha1"], key)
+ if "sha256" in q:
+ r["results"] += apicall("hash", q["sha256"], key)
+ if 'vulnerability' in q:
+ r["results"] += apicall("vuln", q["vulnerability"], key)
+ if "domain" in q:
+ r["results"] += apicall("dns", q["domain"], key)
+
+ uniq = []
+ for res in r["results"]:
+ if res not in uniq:
+ uniq.append(res)
+ r["results"] = uniq
+ return r
+
+def apicall(indicator_type, indicator, key=False):
+ try:
+ myURL = BASEurl + (extensions[str(indicator_type)])%indicator
+ jsondata = requests.get(myURL, headers=MyHeader(key)).json()
+ except:
+ jsondata = None
+ redata = []
+ #print(jsondata)
+ if not jsondata is None:
+ if indicator_type is "hash":
+ if "malware" in jsondata:
+ lopointer = jsondata["malware"]
+ redata.append({"type": "text", "values": lopointer["risk"]})
+ if indicator_type is "dns":
+ if "records" in str(jsondata):
+ lopointer = jsondata["Passive"]["records"]
+ for dataset in lopointer:
+ redata.append({"type":"domain", "values": dataset["value"]})
+
+ return redata
+
+def introspection():
+ return mispattributes
+
+
+def version():
+ moduleinfo['config'] = moduleconfig
+ return moduleinfo
diff --git a/misp_modules/modules/export_mod/__init__.py b/misp_modules/modules/export_mod/__init__.py
index 69f1c00..ee457cf 100644
--- a/misp_modules/modules/export_mod/__init__.py
+++ b/misp_modules/modules/export_mod/__init__.py
@@ -1 +1 @@
-__all__ = ['testexport','cef_export']
+__all__ = ['testexport','cef_export','liteexport']
diff --git a/misp_modules/modules/export_mod/liteexport.py b/misp_modules/modules/export_mod/liteexport.py
new file mode 100755
index 0000000..5d47489
--- /dev/null
+++ b/misp_modules/modules/export_mod/liteexport.py
@@ -0,0 +1,89 @@
+import json
+import base64
+
+misperrors = {'error': 'Error'}
+
+moduleinfo = {'version': '1',
+ 'author': 'TM',
+ 'description': 'export lite',
+ 'module-type': ['export']}
+
+moduleconfig = ["indent_json_export"]
+
+mispattributes = {}
+outputFileExtension = "json"
+responseType = "application/json"
+
+def handler(q=False):
+ if q is False:
+ return False
+
+ request = json.loads(q)
+
+ config = {}
+ if "config" in request:
+ config = request["config"]
+ else:
+ config = {"indent_json_export" : None}
+
+ if config['indent_json_export'] is not None:
+ try:
+ config['indent_json_export'] = int(config['indent_json_export'])
+ except:
+ config['indent_json_export'] = None
+
+ if 'data' not in request:
+ return False
+
+ #~ Misp json structur
+ liteEvent = {'Event':{}}
+
+ for evt in request['data']:
+ rawEvent = evt['Event']
+ liteEvent['Event']['info'] = rawEvent['info']
+ liteEvent['Event']['Attribute'] = []
+
+ attrs = evt['Attribute']
+ for attr in attrs:
+ if 'Internal reference' not in attr['category']:
+ liteAttr = {}
+ liteAttr['category'] = attr['category']
+ liteAttr['type'] = attr['type']
+ liteAttr['value'] = attr['value']
+ liteEvent['Event']['Attribute'].append(liteAttr)
+
+ return {'response' : [],
+ 'data' : str(base64.b64encode(
+ bytes(
+ json.dumps(liteEvent, indent=config['indent_json_export']),
+ 'utf-8')),
+ 'utf-8')
+ }
+
+def introspection():
+ modulesetup = {}
+ try:
+ responseType
+ modulesetup['responseType'] = responseType
+ except NameError:
+ pass
+ try:
+ userConfig
+ modulesetup['userConfig'] = userConfig
+ except NameError:
+ pass
+ try:
+ outputFileExtension
+ modulesetup['outputFileExtension'] = outputFileExtension
+ except NameError:
+ pass
+ try:
+ inputSource
+ modulesetup['inputSource'] = inputSource
+ except NameError:
+ pass
+ return modulesetup
+
+def version():
+ moduleinfo['config'] = moduleconfig
+ return moduleinfo
diff --git a/misp_modules/modules/import_mod/__init__.py b/misp_modules/modules/import_mod/__init__.py
index 82505d9..6beeaa2 100644
--- a/misp_modules/modules/import_mod/__init__.py
+++ b/misp_modules/modules/import_mod/__init__.py
@@ -1,3 +1,4 @@
from . import _vmray
-__all__ = ['vmray_import', 'testimport', 'ocr', 'stiximport', 'cuckooimport', 'email_import']
+__all__ = ['vmray_import', 'testimport', 'ocr', 'stiximport', 'cuckooimport',
+ 'email_import', 'mispjson', 'openiocimport']
diff --git a/misp_modules/modules/import_mod/mispjson.py b/misp_modules/modules/import_mod/mispjson.py
new file mode 100755
index 0000000..f9d52ec
--- /dev/null
+++ b/misp_modules/modules/import_mod/mispjson.py
@@ -0,0 +1,61 @@
+import json
+import base64
+
+misperrors = {'error': 'Error'}
+userConfig = { };
+
+inputSource = ['file']
+
+moduleinfo = {'version': '0.1', 'author': 'Richard van den Berg',
+ 'description': 'MISP JSON format import module for merging MISP events',
+ 'module-type': ['import']}
+
+moduleconfig = []
+
+
+def handler(q=False):
+ if q is False:
+ return False
+ r = {'results': []}
+ request = json.loads(q)
+ try:
+ mfile = base64.b64decode(request["data"]).decode('utf-8')
+ misp = json.loads(mfile)
+ event = misp['response'][0]['Event']
+ for a in event["Attribute"]:
+ tmp = {}
+ tmp["values"] = a["value"]
+ tmp["categories"] = a["category"]
+ tmp["types"] = a["type"]
+ tmp["to_ids"] = a["to_ids"]
+ tmp["comment"] = a["comment"]
+ if a.get("data"):
+ tmp["data"] = a["data"]
+ r['results'].append(tmp)
+ except:
+ pass
+ return r
+
+def introspection():
+ modulesetup = {}
+ try:
+ userConfig
+ modulesetup['userConfig'] = userConfig
+ except NameError:
+ pass
+ try:
+ inputSource
+ modulesetup['inputSource'] = inputSource
+ except NameError:
+ pass
+ return modulesetup
+
+
+def version():
+ moduleinfo['config'] = moduleconfig
+ return moduleinfo
+
+if __name__ == '__main__':
+ x = open('test.json', 'r')
+ r = handler(q=x.read())
+ print(json.dumps(r))
diff --git a/misp_modules/modules/import_mod/openiocimport.py b/misp_modules/modules/import_mod/openiocimport.py
new file mode 100755
index 0000000..d5e39a0
--- /dev/null
+++ b/misp_modules/modules/import_mod/openiocimport.py
@@ -0,0 +1,96 @@
+import json
+import base64
+
+from pymisp.tools import openioc
+
+misperrors = {'error': 'Error'}
+userConfig = {
+ 'not save ioc': {
+ 'type': 'Boolean',
+ 'message': 'If you check this box, IOC file will not save as an attachment in MISP'
+ },
+ 'default tag': {
+ 'type': 'String',
+ 'message': 'Add tags spaced by a comma (tlp:white,misp:threat-level="no-risk")',
+ 'validation' : '0'
+ }
+ }
+
+inputSource = ['file']
+
+moduleinfo = {'version': '0.1', 'author': 'Raphaƫl Vinot',
+ 'description': 'Import OpenIOC package',
+ 'module-type': ['import']}
+
+moduleconfig = []
+
+
+def handler(q=False):
+ # Just in case we have no data
+ if q is False:
+ return False
+
+ # The return value
+ r = {'results': []}
+
+ # Load up that JSON
+ q = json.loads(q)
+
+ # It's b64 encoded, so decode that stuff
+ package = base64.b64decode(q.get("data")).decode('utf-8')
+
+ # If something really weird happened
+ if not package:
+ return json.dumps({"success": 0})
+
+ pkg = openioc.load_openioc(package)
+
+ if q.get('config'):
+ if q['config'].get('not save ioc') == "0":
+ addFile = {
+ "values": [q.get('filename')],
+ "types": ['attachment'],
+ "categories": ['Support Tool'],
+ "data" : q.get('data'),
+ }
+ # add tag
+ if q['config'].get('default tag') is not None:
+ addFile["tags"] = q['config']['default tag'].split(",")
+ # add file as attachment
+ r["results"].append(addFile)
+
+
+ # return all attributes
+ for attrib in pkg.attributes:
+ toAppend = {
+ "values": [attrib.value],
+ "types": [attrib.type],
+ "categories": [attrib.category],
+ "comment":attrib.comment
+ }
+ # add tag
+ if q.get('config') and q['config'].get('default tag') is not None:
+ toAppend["tags"] = q['config']['default tag'].split(",")
+
+ r["results"].append(toAppend)
+ return r
+
+
+def introspection():
+ modulesetup = {}
+ try:
+ userConfig
+ modulesetup['userConfig'] = userConfig
+ except NameError:
+ pass
+ try:
+ inputSource
+ modulesetup['inputSource'] = inputSource
+ except NameError:
+ pass
+ return modulesetup
+
+
+def version():
+ moduleinfo['config'] = moduleconfig
+ return moduleinfo
diff --git a/tests/openioc.xml b/tests/openioc.xml
new file mode 100644
index 0000000..dc7858c
--- /dev/null
+++ b/tests/openioc.xml
@@ -0,0 +1,91 @@
+
+
+ STUXNET VIRUS (METHODOLOGY)
+ Generic indicator for the stuxnet virus. When loaded, stuxnet spawns lsass.exe in a suspended state. The malware then maps in its own executable section and fixes up the CONTEXT to point to the newly mapped in section. This is a common task performed by malware and allows the malware to execute under the pretense of a known and trusted process.
+ methodology
+ Mandiant
+ 0001-01-01T00:00:00
+
+
+
+
+
+ .stub
+
+
+
+ mdmcpq3.PNF
+
+
+
+ mdmeric3.PNF
+
+
+
+ oem6C.PNF
+
+
+
+ oem7A.PNF
+
+
+
+
+ fs_rec.sys
+
+
+
+ mrxsmb.sys
+
+
+
+ sr.sys
+
+
+
+ fastfat.sys
+
+
+
+
+
+ mrxcls.sys
+
+
+
+ Realtek Semiconductor Corp
+
+
+
+
+
+ mrxnet.sys
+
+
+
+ Realtek Semiconductor Corp
+
+
+
+
+
+ HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\MRxCls\ImagePath
+
+
+
+ mrxcls.sys
+
+
+
+
+
+ HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\MRxNet\ImagePath
+
+
+
+ mrxnet.sys
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/test.py b/tests/test.py
index 7b7ce8e..b9e4842 100644
--- a/tests/test.py
+++ b/tests/test.py
@@ -42,6 +42,20 @@ class TestModules(unittest.TestCase):
print(response.json())
response.connection.close()
+ def test_openioc(self):
+ with open("tests/openioc.xml", "rb") as f:
+ content = base64.b64encode(f.read())
+ data = json.dumps({"module": "openiocimport",
+ "data": content.decode(),
+ })
+ response = requests.post(self.url + "query", data=data).json()
+ print(response)
+
+ print("OpenIOC :: {}".format(response))
+ values = [x["values"][0] for x in response["results"]]
+ assert("mrxcls.sys" in values)
+ assert("mdmcpq3.PNF" in values)
+
def test_stix(self):
with open("tests/stix.xml", "rb") as f:
content = base64.b64encode(f.read())
@@ -58,7 +72,7 @@ class TestModules(unittest.TestCase):
assert("eu-society.com" in values)
def test_email_headers(self):
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": None,
"guess_zip_attachment_passwords": None,
"extract_urls": None}
@@ -106,7 +120,7 @@ class TestModules(unittest.TestCase):
self.assertIn("", values)
def test_email_attachment_basic(self):
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": None,
"guess_zip_attachment_passwords": None,
"extract_urls": None}
@@ -129,9 +143,8 @@ class TestModules(unittest.TestCase):
attch_data = base64.b64decode(i["data"])
self.assertEqual(attch_data, b'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
-
def test_email_attachment_unpack(self):
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": None,
"extract_urls": None}
@@ -163,7 +176,7 @@ class TestModules(unittest.TestCase):
def test_email_dont_unpack_compressed_doc_attachments(self):
"""Ensures that compressed
"""
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": None,
"extract_urls": None}
@@ -193,9 +206,8 @@ class TestModules(unittest.TestCase):
self.assertEqual(filesum.hexdigest(),
'098da5381a90d4a51e6b844c18a0fecf2e364813c2f8b317cfdc51c21f2506a5')
-
def test_email_attachment_unpack_with_password(self):
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
@@ -222,9 +234,8 @@ class TestModules(unittest.TestCase):
self.assertEqual(attch_data,
b'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
-
def test_email_attachment_password_in_body(self):
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
@@ -247,7 +258,7 @@ class TestModules(unittest.TestCase):
'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
def test_email_attachment_password_in_body_quotes(self):
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
@@ -275,7 +286,7 @@ class TestModules(unittest.TestCase):
'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
def test_email_attachment_password_in_html_body(self):
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
@@ -305,7 +316,7 @@ class TestModules(unittest.TestCase):
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
- #print(response.json())
+ # print(response.json())
values = [x["values"] for x in response.json()["results"]]
self.assertIn('EICAR.com', values)
for i in response.json()['results']:
@@ -356,7 +367,7 @@ class TestModules(unittest.TestCase):
response = requests.post(self.url + "query", data=data)
def test_email_attachment_password_in_subject(self):
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
@@ -385,9 +396,8 @@ class TestModules(unittest.TestCase):
self.assertEqual(attch_data,
'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
-
def test_email_extract_html_body_urls(self):
- query = {"module":"email_import"}
+ query = {"module": "email_import"}
query["config"] = {"unzip_attachments": None,
"guess_zip_attachment_passwords": None,
"extract_urls": "true"}
@@ -415,12 +425,12 @@ without modifying core components. The API is available via a simple REST API wh
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
- #print(response.json())
+ # print(response.json())
values = [x["values"] for x in response.json()["results"]]
self.assertIn("https://github.com/MISP/MISP", values)
self.assertIn("https://www.circl.lu/assets/files/misp-training/3.1-MISP-modules.pdf", values)
- #def test_domaintools(self):
+ # def test_domaintools(self):
# query = {'config': {'username': 'test_user', 'api_key': 'test_key'}, 'module': 'domaintools', 'domain': 'domaintools.com'}
# try:
# response = requests.post(self.url + "query", data=json.dumps(query)).json()
@@ -429,33 +439,34 @@ without modifying core components. The API is available via a simple REST API wh
# response = requests.post(self.url + "query", data=json.dumps(query)).json()
# print(response)
+
def decode_email(message):
message64 = base64.b64encode(message.as_bytes()).decode()
return message64
def get_base_email():
- headers = {"Received":"via dmail-2008.19 for +INBOX; Tue, 3 Feb 2009 19:29:12 -0600 (CST)",
- "Received":"from abc.luxsci.com ([10.10.10.10]) by xyz.luxsci.com (8.13.7/8.13.7) with ESMTP id n141TCa7022588 for ; Tue, 3 Feb 2009 19:29:12 -0600",
- "Received":"from [192.168.0.3] (verizon.net [44.44.44.44]) (user=test@sender.com mech=PLAIN bits=2) by abc.luxsci.com (8.13.7/8.13.7) with ESMTP id n141SAfo021855 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=NOT) for ; Tue, 3 Feb 2009 19:28:10 -0600",
- "X-Received":"by 192.168.0.45 with SMTP id q4mr156123401yw1g.911.1912342394963; Tue, 3 Feb 2009 19:32:15 -0600 (PST)",
- "Message-ID":"<4988EF2D.40804@example.com>",
- "Date":"Tue, 03 Feb 2009 20:28:13 -0500",
- "From":'"Innocent Person" ',
- "User-Agent":'Thunderbird 2.0.0.19 (Windows/20081209)',
- "Sender":'"Malicious MailAgent" ',
- "References":"",
- "In-Reply-To":"",
- "Accept-Language":'en-US',
- "X-Mailer":'mlx 5.1.7',
+ headers = {"Received": "via dmail-2008.19 for +INBOX; Tue, 3 Feb 2009 19:29:12 -0600 (CST)",
+ "Received": "from abc.luxsci.com ([10.10.10.10]) by xyz.luxsci.com (8.13.7/8.13.7) with ESMTP id n141TCa7022588 for ; Tue, 3 Feb 2009 19:29:12 -0600",
+ "Received": "from [192.168.0.3] (verizon.net [44.44.44.44]) (user=test@sender.com mech=PLAIN bits=2) by abc.luxsci.com (8.13.7/8.13.7) with ESMTP id n141SAfo021855 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=NOT) for ; Tue, 3 Feb 2009 19:28:10 -0600",
+ "X-Received": "by 192.168.0.45 with SMTP id q4mr156123401yw1g.911.1912342394963; Tue, 3 Feb 2009 19:32:15 -0600 (PST)",
+ "Message-ID": "<4988EF2D.40804@example.com>",
+ "Date": "Tue, 03 Feb 2009 20:28:13 -0500",
+ "From": '"Innocent Person" ',
+ "User-Agent": 'Thunderbird 2.0.0.19 (Windows/20081209)',
+ "Sender": '"Malicious MailAgent" ',
+ "References": "",
+ "In-Reply-To": "",
+ "Accept-Language": 'en-US',
+ "X-Mailer": 'mlx 5.1.7',
"Return-Path": "evil_spoofer@example.com",
- "Thread-Topic":'This is a thread.',
- "Thread-Index":'AQHSR8Us3H3SoaY1oUy9AAwZfMF922bnA9GAgAAi9s4AAGvxAA==',
- "Content-Language":'en-US',
- "To":'"Testy Testerson" ',
- "Cc":'"Second Person" , "Other Friend" , "Last One" ',
- "Subject":'Example Message',
- "MIME-Version":'1.0'}
+ "Thread-Topic": 'This is a thread.',
+ "Thread-Index": 'AQHSR8Us3H3SoaY1oUy9AAwZfMF922bnA9GAgAAi9s4AAGvxAA==',
+ "Content-Language": 'en-US',
+ "To": '"Testy Testerson" ',
+ "Cc": '"Second Person" , "Other Friend" , "Last One" ',
+ "Subject": 'Example Message',
+ "MIME-Version": '1.0'}
msg = MIMEMultipart()
for key, val in headers.items():
msg.add_header(key, val)