diff --git a/misp_modules/modules/__init__.py b/misp_modules/modules/__init__.py index 47ddcbf..97fdc13 100644 --- a/misp_modules/modules/__init__.py +++ b/misp_modules/modules/__init__.py @@ -1,3 +1,4 @@ from .expansion import * # noqa from .import_mod import * # noqa from .export_mod import * # noqa +from .action_mod import * # noqa diff --git a/misp_modules/modules/action_mod/__init__.py b/misp_modules/modules/action_mod/__init__.py new file mode 100644 index 0000000..d706e5c --- /dev/null +++ b/misp_modules/modules/action_mod/__init__.py @@ -0,0 +1 @@ +__all__ = ['testaction', 'mattermost'] diff --git a/misp_modules/modules/action_mod/_utils/utils.py b/misp_modules/modules/action_mod/_utils/utils.py new file mode 100644 index 0000000..3afdc17 --- /dev/null +++ b/misp_modules/modules/action_mod/_utils/utils.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +from jinja2.sandbox import SandboxedEnvironment + +default_template = """ +# Tutorial: How to use jinja2 templating + +:warning: For these examples, we consider the module received data under the MISP core format + +1. You can use the dot `.` notation or the subscript syntax `[]` to access attributes of a variable + - `{% raw %}{{ Event.info }}{% endraw %}` -> {{ Event.info }} + - `{% raw %}{{ Event['info'] }}{% endraw %}` -> {{ Event['info'] }} + +2. Jinja2 allows you to easily create list: +```{% raw %} +{% for attribute in Event.Attribute %} +- {{ attribute.value }} +{% endfor %} +{% endraw %}``` + +Gives: +{% for attribute in Event.Attribute %} +- {{ attribute.value }} +{% endfor %} + +3. Jinja2 allows you to add logic +```{% raw %} +{% if "tlp:white" in Event.Tag %} +- This Event has the TLP:WHITE tag +{% else %} +- This Event doesn't have the TLP:WHITE tag +{% endif %} +{% endraw %}``` + +Gives: +{% if "tlp:white" in Event.Tag %} +- This Event has the TLP:WHITE tag +{% else %} +- This Event doesn't have the TLP:WHITE tag +{% endif %} + +## Jinja2 allows you to modify variables by using filters + +3. The `reverse` filter +- `{% raw %}{{ Event.info | reverse }}{% endraw %}` -> {{ Event.info | reverse }} + +4. The `format` filter +- `{% raw %}{{ "%s :: %s" | format(Event.Attribute[0].type, Event.Attribute[0].value) }}{% endraw %}` -> {{ "%s :: %s" | format(Event.Attribute[0].type, Event.Attribute[0].value) }} + +5.The `groupby` filter +```{% raw %} +{% for type, attributes in Event.Attribute|groupby("type") %} +- {{ type }}{% for attribute in attributes %} + - {{ attribute.value }} + {% endfor %} +{% endfor %} +{% endraw %}``` + +Gives: +{% for type, attributes in Event.Attribute|groupby("type") %} +- {{ type }}{% for attribute in attributes %} + - {{ attribute.value }} + {% endfor %} +{% endfor %} +""" + + +def renderTemplate(data, template=default_template): + env = SandboxedEnvironment() + return env.from_string(template).render(data) \ No newline at end of file diff --git a/misp_modules/modules/action_mod/mattermost.py b/misp_modules/modules/action_mod/mattermost.py new file mode 100644 index 0000000..dbcd336 --- /dev/null +++ b/misp_modules/modules/action_mod/mattermost.py @@ -0,0 +1,97 @@ +import json +from mattermostdriver import Driver +from ._utils import utils + +misperrors = {'error': 'Error'} + +# config fields that your code expects from the site admin +moduleconfig = { + 'params': { + 'mattermost_hostname': { + 'type': 'string', + 'description': 'The Mattermost domain', + 'value': 'example.mattermost.com', + }, + 'bot_access_token': { + 'type': 'string', + 'description': 'Access token generated when you created the bot account', + }, + 'channel_id': { + 'type': 'string', + 'description': 'The channel you added the bot to', + }, + 'message_template': { + 'type': 'large_string', + 'description': 'The template to be used to generate the message to be posted', + 'value': 'The **template** will be rendered using *Jinja2*!', + }, + }, + # Blocking modules break the exection of the current of action + 'blocking': False, + # Indicates whether parts of the data passed to this module should be filtered. Filtered data can be found under the `filteredItems` key + 'support_filters': True, + # Indicates whether the data passed to this module should be compliant with the MISP core format + 'expect_misp_core_format': False, +} + + +# returns either "boolean" or "data" +# Boolean is used to simply signal that the execution has finished. +# For blocking modules the actual boolean value determines whether we break execution +returns = 'boolean' + +moduleinfo = {'version': '0.1', 'author': 'Sami Mokaddem', + 'description': 'Simplistic module to send message to a Mattermost channel.', + 'module-type': ['action']} + + +def createPost(request): + params = request['params'] + mm = Driver({ + 'url': params['mattermost_hostname'], + 'token': params['bot_access_token'], + 'scheme': 'https', + 'basepath': '/api/v4', + 'port': 443, + }) + mm.login() + + data = {} + if 'matchingData' in request: + data = request['matchingData'] + else: + data = request['data'] + + if params['message_template']: + message = utils.renderTemplate(data, params['message_template']) + else: + message = '```\n{}\n```'.format(json.dumps(data)) + + mm.posts.create_post(options={ + 'channel_id': params['channel_id'], + 'message': message + }) + return True + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + createPost(request) + r = {"data": True} + return r + + +def introspection(): + modulesetup = {} + try: + modulesetup['config'] = moduleconfig + except NameError: + pass + return modulesetup + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/action_mod/testaction.py b/misp_modules/modules/action_mod/testaction.py new file mode 100644 index 0000000..d773c4e --- /dev/null +++ b/misp_modules/modules/action_mod/testaction.py @@ -0,0 +1,59 @@ +import json +from ._utils import utils + +misperrors = {'error': 'Error'} + +# config fields that your code expects from the site admin +moduleconfig = { + 'params': { + 'foo': { + 'type': 'string', + 'description': 'blablabla', + 'value': 'xyz' + }, + 'Data extraction path': { + # Extracted data can be found under the `matchingData` key + 'type': 'hash_path', + 'description': 'Only post content extracted from this path', + 'value': 'Attribute.{n}.AttributeTag.{n}.Tag.name', + }, + }, + # Blocking modules break the exection of the current of action + 'blocking': False, + # Indicates whether parts of the data passed to this module should be extracted. Extracted data can be found under the `filteredItems` key + 'support_filters': False, + # Indicates whether the data passed to this module should be compliant with the MISP core format + 'expect_misp_core_format': False, +} + +# returns either "boolean" or "data" +# Boolean is used to simply signal that the execution has finished. +# For blocking modules the actual boolean value determines whether we break execution +returns = 'boolean' + +moduleinfo = {'version': '0.1', 'author': 'Andras Iklody', + 'description': 'This module is merely a test, always returning true. Triggers on event publishing.', + 'module-type': ['action']} + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) # noqa + success = True + r = {"data": success} + return r + + +def introspection(): + modulesetup = {} + try: + modulesetup['config'] = moduleconfig + except NameError: + pass + return modulesetup + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo