From: allfro Date: Mon, 8 Apr 2013 01:23:45 +0000 (-0400) Subject: Added TDS support with Canari Plume (WSGI transform runner) X-Git-Url: https://git.nothing2do.fr/?a=commitdiff_plain;h=83e200d03bc602921ef57a702794120466836655;p=get-hack-src.git Added TDS support with Canari Plume (WSGI transform runner) --- diff --git a/src/canari/resources/__init__.py b/src/canari/resources/__init__.py index 3cc3b81..e16b34d 100644 --- a/src/canari/resources/__init__.py +++ b/src/canari/resources/__init__.py @@ -13,5 +13,6 @@ __status__ = 'Development' __all__ = [ 'images', 'etc', + 'tds', 'template' ] \ No newline at end of file diff --git a/src/canari/resources/etc/canari.conf b/src/canari/resources/etc/canari.conf index a7e7146..496a32c 100644 --- a/src/canari/resources/etc/canari.conf +++ b/src/canari/resources/etc/canari.conf @@ -3,4 +3,8 @@ configs = # Additional exec paths (comma separated) -path = ${PATH},/usr/local/bin,/opt/local/bin \ No newline at end of file +path = ${PATH},/usr/local/bin,/opt/local/bin + +[remote] +# Specify any transforms that your WSGI container will host. This is for Plume ONLY. +modules = \ No newline at end of file diff --git a/src/canari/resources/tds/__init__.py b/src/canari/resources/tds/__init__.py new file mode 100644 index 0000000..3f733cb --- /dev/null +++ b/src/canari/resources/tds/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +__author__ = 'Nadeem Douba' +__copyright__ = 'Copyright 2012, Canari Project' +__credits__ = [] + +__license__ = 'GPL' +__version__ = '0.1' +__maintainer__ = 'Nadeem Douba' +__email__ = 'ndouba@gmail.com' +__status__ = 'Development' \ No newline at end of file diff --git a/src/canari/resources/tds/plume.py b/src/canari/resources/tds/plume.py new file mode 100644 index 0000000..982496d --- /dev/null +++ b/src/canari/resources/tds/plume.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python + +# Builtin imports +from xml.etree.cElementTree import fromstring +from ConfigParser import NoSectionError +from cStringIO import StringIO +from urlparse import urljoin +import importlib +import hashlib +import sys +import os +import re + +# Third-party imports +from flask import Flask, Response, request + +# Canari imports +import canari.config as _config +from canari.commands.common import get_transform_version +from canari.maltego.message import (Message, MaltegoMessage, MaltegoTransformRequestMessage, + MaltegoTransformResponseMessage, MaltegoTransformExceptionMessage, MaltegoException) +from canari.resource import image_resources + + +__author__ = 'Nadeem Douba' +__copyright__ = 'Copyright 2012, Canari Project' +__credits__ = [] + +__license__ = 'GPL' +__version__ = '0.1' +__maintainer__ = 'Nadeem Douba' +__email__ = 'ndouba@gmail.com' +__status__ = 'Development' + + +class Canari(Flask): + def __init__(self, import_name): + super(Canari, self).__init__(import_name) + self.transforms = {} + self._initialize() + + def _copy_images(self, pkg): + if pkg.endswith('.transforms'): + pkg = pkg.replace('.transforms', '') + for i in image_resources(pkg): + dname = 'static/%s' % hashlib.md5(i).hexdigest() + if not os.path.exists(dname): + with file(i, mode='rb') as src: + with open(dname, mode="wb") as dst: + dst.write(src.read()) + + def _initialize(self): + # Flask application container reload hack. + reload(_config) + + packages = None + + # Read modules that are to be loaded at runtime + try: + packages = _config.config['remote/modules'] + except NoSectionError: + sys.stderr.write('Exiting... You did not specify a [remote] section and a "modules" option in your canari.conf file!') + exit(-1) + + # Is packages not blank + if not packages: + sys.stderr.write('Exiting... You did not specify any transform modules to load in your canari.conf file!') + exit(-1) + elif isinstance(packages, basestring): + packages = [packages] + + # Create the static directory for static file loading + if not os.path.exists('static'): + os.mkdir('static', 0755) + + # Iterate through the list of packages to load + for p in packages: + # Copy all the image resource files in case they are used as entity icons + self._copy_images(p) + + if not p.endswith('.transforms'): + p = ('%s.transforms' % p) + + sys.stderr.write('Loading transform package %s\n' % repr(p)) + + # Load our first transform package + m = importlib.import_module(p) + + for t in m.__all__: + t = ('%s.%s' % (p, t)) + + # Let's import our transforms one by one + m2 = importlib.import_module(t) + if not hasattr(m2, 'dotransform'): + continue + + # Should the transform be publicly available? + if hasattr(m2.dotransform, 'remote') and m2.dotransform.remote: + sys.stderr.write('Loading transform %s at /%s...\n' % (repr(t), t)) + # Does it conform to V2 of the Canari transform signature standard? + if get_transform_version(m2.dotransform) == 2: + sys.stderr.write('Plume does not support V2 Canari transforms (%s). Please update to V3. ' + 'See http://www.canariproject.com/plume for details.\n' % repr(t)) + exit(-1) + # Does the transform need to be executed as root? If so, is this running in mod_wsgi? Yes = Bad! + elif os.name == 'posix' and hasattr(m2.dotransform, 'privileged') and\ + os.geteuid() and __name__.startswith('_mod_wsgi_'): + sys.stderr.write('Warning, mod_wsgi does not allow applications to run with root privileges. ' + 'Transform %s ignored...\n' % repr(t)) + continue + # So everything is good, let's register our transform with the global transform registry. + if hasattr(m2.dotransform, 'inputs'): + inputs = [e[1]('').type for e in m2.dotransform.inputs] + inputs = inputs + [i.split('.')[-1] for i in inputs] + self.transforms[t] = (m2.dotransform, inputs) + else: + self.transforms[t] = (m2.dotransform, []) + + +# Create our Flask app. +app = Canari(__name__) + +def croak(error_msg): + """Throw an exception in the Maltego GUI containing error_msg.""" + s = StringIO() + Message( + MaltegoMessage( + MaltegoTransformExceptionMessage(exceptions=MaltegoException(error_msg) + ) + ) + ).write(file=s) + return s.getvalue() + + +def message(m): + """Write a MaltegoMessage to stdout and exit successfully""" + v = None + if isinstance(m, basestring): + # Let's make sure that we're not spewing out local file system information ;) + for url in re.findall("\s*(file://[^\s<]+)\s*(?im)", m): + path = 'static/%s' % hashlib.md5(url[7:]).hexdigest() + new_url = urljoin(request.host_url, path) + m.replace(url, new_url, 1) + v = m + else: + sio = StringIO() + # Let's make sure that we're not spewing out local file system information ;) + for e in m.entities: + if e.iconurl is not None: + e.iconurl = e.iconurl.strip() + if e.iconurl.startswith('file://'): + path = 'static/%s' % hashlib.md5(e.iconurl[7:]).hexdigest() + new_url = urljoin(request.host_url, path) + e.iconurl = new_url + + Message(MaltegoMessage(m)).write(sio) + v = sio.getvalue() + # Get rid of those nasty unicode 32 characters + return Response(re.sub(r'(&#\d{5};){2}', r'', v), status=200, mimetype='text/html') + + +def dotransform(t): + try: + # Get the body of the request + request_str = request.data + + # Let's get an XML object tree + xml = fromstring(request_str).find('MaltegoTransformRequestMessage') + + # Get the entity being passed in. + e = xml.find('Entities/Entity') + etype = e.get('Type', '') + + if t[1] and etype not in t[1]: + return Response(status=404) + + # Initialize Maltego Request values to pass into transform + value = e.find('Value').text or '' + fields = dict([(f.get('Name', ''), f.text) for f in xml.findall('Entities/Entity/AdditionalFields/Field')]) + params = dict([(f.get('Name', ''), f.text) for f in xml.findall('TransformFields/Field')]) + limits = xml.find('Limits').attrib + + # Initialize a private copy of the config to pass into the transform + config = _config.CanariConfigParser() + for k, i in params.items(): + if '.' in k: + config[k.replace('.', '/', 1)] = i + else: + config['default/%s' % k] = i + # The private config variables CANNOT override the server's settings. This is for security? + config._sections.update(_config.config._sections) + + # Execute it! + msg = t[0]( + MaltegoTransformRequestMessage(value, fields, params, limits), + request_str if hasattr(t[0], 'cmd') and callable(t[0].cmd) else MaltegoTransformResponseMessage(), + config + ) + + # Let's serialize the return response and clean up whatever mess was left behind + if isinstance(msg, MaltegoTransformResponseMessage) or isinstance(msg, basestring): + return message(msg) + else: + raise MaltegoException('Could not resolve message type returned by transform.') + + # Unless we croaked somewhere, then we need to fix things up here... + except MaltegoException, me: + return croak(str(me)) + except Exception, e: + return croak(str(e)) + + +# This is where the TDS will ask: "Are you a transform?" and we say "200 - Yes I am!" or "404 - PFO" +@app.route('/', methods=['GET']) +def transform_checker(transform): + if transform not in app.transforms: + return Response(status=404) + return Response(status=200) + + +# This is where we process a transform request. +@app.route('/', methods=['POST']) +def transform_runner(transform): + if transform not in app.transforms: + return Response(status=400) + return dotransform(app.transforms[transform]) + + +# Finally, if you want to run Flask standalone for debugging, just type python plume.py and you're off to the races! +if __name__ == '__main__': + app.run(debug=True) diff --git a/src/canari/resources/tds/plume.wsgi b/src/canari/resources/tds/plume.wsgi new file mode 100644 index 0000000..7c1405b --- /dev/null +++ b/src/canari/resources/tds/plume.wsgi @@ -0,0 +1,27 @@ +#!/usr/bin/python + +import tempfile +import sys +import os + +__author__ = 'Nadeem Douba' +__copyright__ = 'Copyright 2012, Canari Project' +__credits__ = [] + +__license__ = 'GPL' +__version__ = '0.1' +__maintainer__ = 'Nadeem Douba' +__email__ = 'ndouba@gmail.com' +__status__ = 'Development' + + +# Import modules relative to where this script is if necessary +my_location = os.path.dirname(__file__) +os.chdir(my_location) +sys.path.append(my_location) + +# Use temporary directory as Python Egg Cache +os.environ['PYTHON_EGG_CACHE'] = tempfile.gettempdir() + +# Import our Flask app +from plume import app as application