├── templates ├── errors │ └── 404.html └── home.html ├── libs ├── __init__.py ├── events │ ├── __init__.py │ └── event_consumers.py ├── Singleton.py ├── SecurityDecorators.py ├── LoggingHelpers.py ├── ConsoleColors.py └── ConfigManager.py ├── tasks ├── helpers │ ├── __init__.py │ └── mq.py ├── __init__.py ├── notifiers.py └── message_tasks.py ├── .bowerrc ├── mixins ├── __init__.py └── celery_task_mixin.py ├── static ├── ico │ ├── favicon.ico │ ├── apple-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── ms-icon-70x70.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── apple-icon-precomposed.png │ ├── browserconfig.xml │ └── manifest.json ├── img │ └── bf-logo.png ├── fonts │ ├── SourceCodePro-Medium.eot │ ├── SourceCodePro-Regular.ttf.woff │ ├── SourceCodePro-Regular.ttf.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ ├── glyphicons-halflings-regular.woff2 │ └── glyphicons-halflings-regular.svg ├── ng-app │ ├── watch.html │ ├── home.html │ ├── base.js │ └── report.html └── css │ └── selftest.css ├── requirements.txt ├── handlers ├── HomePageHandler.py ├── ErrorHandlers.py ├── CheckHandler.py ├── __init__.py └── BaseHandlers.py ├── README.md ├── bower.json ├── .gitignore ├── app.cfg ├── selftest.py └── LICENSE /templates/errors/404.html: -------------------------------------------------------------------------------- 1 | 404 -------------------------------------------------------------------------------- /libs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tasks/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from mq import create_mq_url -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "static/lib" 3 | } 4 | -------------------------------------------------------------------------------- /mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from mixins.celery_task_mixin import CeleryTaskMixin -------------------------------------------------------------------------------- /libs/events/__init__.py: -------------------------------------------------------------------------------- 1 | TASK_EVENTS = 'task_list' 2 | TASK_ROUTING_KEY = '' -------------------------------------------------------------------------------- /static/ico/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/favicon.ico -------------------------------------------------------------------------------- /static/img/bf-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/img/bf-logo.png -------------------------------------------------------------------------------- /static/ico/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon.png -------------------------------------------------------------------------------- /static/ico/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/favicon-16x16.png -------------------------------------------------------------------------------- /static/ico/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/favicon-32x32.png -------------------------------------------------------------------------------- /static/ico/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/favicon-96x96.png -------------------------------------------------------------------------------- /static/ico/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/ms-icon-70x70.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | emailprotectionslib>=0.8.2 2 | tornado 3 | requests 4 | celery 5 | tornado 6 | tornado-celery 7 | furl 8 | pika -------------------------------------------------------------------------------- /static/ico/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-57x57.png -------------------------------------------------------------------------------- /static/ico/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-60x60.png -------------------------------------------------------------------------------- /static/ico/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-72x72.png -------------------------------------------------------------------------------- /static/ico/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-76x76.png -------------------------------------------------------------------------------- /static/ico/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/ms-icon-144x144.png -------------------------------------------------------------------------------- /static/ico/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/ms-icon-150x150.png -------------------------------------------------------------------------------- /static/ico/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/ms-icon-310x310.png -------------------------------------------------------------------------------- /static/ico/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/android-icon-36x36.png -------------------------------------------------------------------------------- /static/ico/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/android-icon-48x48.png -------------------------------------------------------------------------------- /static/ico/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/android-icon-72x72.png -------------------------------------------------------------------------------- /static/ico/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/android-icon-96x96.png -------------------------------------------------------------------------------- /static/ico/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-114x114.png -------------------------------------------------------------------------------- /static/ico/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-120x120.png -------------------------------------------------------------------------------- /static/ico/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-144x144.png -------------------------------------------------------------------------------- /static/ico/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-152x152.png -------------------------------------------------------------------------------- /static/ico/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-180x180.png -------------------------------------------------------------------------------- /static/fonts/SourceCodePro-Medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/fonts/SourceCodePro-Medium.eot -------------------------------------------------------------------------------- /static/ico/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/android-icon-144x144.png -------------------------------------------------------------------------------- /static/ico/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/android-icon-192x192.png -------------------------------------------------------------------------------- /static/ico/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/ico/apple-icon-precomposed.png -------------------------------------------------------------------------------- /static/fonts/SourceCodePro-Regular.ttf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/fonts/SourceCodePro-Regular.ttf.woff -------------------------------------------------------------------------------- /static/fonts/SourceCodePro-Regular.ttf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/fonts/SourceCodePro-Regular.ttf.woff2 -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BishopFox/SpoofcheckSelfTest/HEAD/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /handlers/HomePageHandler.py: -------------------------------------------------------------------------------- 1 | from handlers.BaseHandlers import BaseHandler 2 | 3 | 4 | class HomePageHandler(BaseHandler): 5 | 6 | def get(self, *args, **kwargs): 7 | self.render('home.html') 8 | -------------------------------------------------------------------------------- /static/ico/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /static/ng-app/watch.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 |

Checking {{domain}} DNS records...

11 |
12 |
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpoofCheck Self Test 2 | Web application that lets you test if your domain is vulnerable to email spoofing. 3 | 4 | ## NOTE: This application is still in development. 5 | 6 | # Installation: 7 | This application is designed to be used as a service. However, if you want to host this yourself, do the following: 8 | 9 | git clone https://github.com/bishopfox/spoofcheckselftest.git 10 | cd spoofcheckselftest 11 | pip install -r requirements.txt 12 | 13 | # Running: 14 | ./selftest.py -s 15 | 16 | Then visit http://localhost:8888 in a web browser -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SpoofcheckSelfTest", 3 | "homepage": "https://github.com/BishopFox/SpoofcheckSelfTest", 4 | "authors": [ 5 | "Alex DeFreese " 6 | ], 7 | "description": "", 8 | "main": "", 9 | "license": "MIT", 10 | "private": true, 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "angular": "^1.6.2", 20 | "angular-websocket": "^2.0.0", 21 | "angular-route": "^1.6.2", 22 | "angular-animate": "^1.6.2", 23 | "bootstrap": "^3.3.7", 24 | "angular-recaptcha": "^4.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | from tornado.options import options 3 | 4 | from tasks.helpers import create_mq_url 5 | 6 | queue_conf = { 7 | 'CELERY_TASK_SERIALIZER': 'json', 8 | 'CELERY_ACCEPT_CONTENT': ['json'], 9 | 'CELERY_RESULT_SERIALIZER': 'json', 10 | 'CELERY_TASK_RESULT_EXPIRES': 3600 11 | } 12 | 13 | selftest_task_queue = Celery( 14 | 'selftest_task_queue', 15 | backend='rpc', 16 | broker=create_mq_url(options.mq_hostname, options.mq_port, 17 | username=options.mq_username, 18 | password=options.mq_password), 19 | include=[ 20 | "tasks.message_tasks", 21 | "tasks.notifiers", 22 | ]) 23 | selftest_task_queue.conf.update(**queue_conf) -------------------------------------------------------------------------------- /static/ico/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /tasks/notifiers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @author: moloch 4 | Copyright 2016 5 | """ 6 | import json 7 | import logging 8 | 9 | from celery.utils.log import get_task_logger 10 | 11 | from libs.events import TASK_EVENTS, TASK_ROUTING_KEY 12 | from tasks import selftest_task_queue 13 | from tasks.helpers.mq import mq_send_once 14 | 15 | 16 | LOGGER = get_task_logger(__name__) 17 | 18 | 19 | @selftest_task_queue.task 20 | def task_complete_notify(result, task_id): 21 | """ Notifies consumers that a task has been completed """ 22 | logging.debug('Task result is: %r', result) 23 | logging.critical('Sending complete notification for id: %s', task_id) 24 | msg = json.dumps({'task_id': task_id}) 25 | if mq_send_once(TASK_EVENTS, TASK_ROUTING_KEY, msg): 26 | logging.info('Confirmed delivery of task complete notification') 27 | else: 28 | logging.warning('Could not confirm task complete notification delivery') 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | .DS_Store 62 | 63 | static/lib -------------------------------------------------------------------------------- /handlers/ErrorHandlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: moloch 4 | 5 | Copyright 2013 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ''' 19 | 20 | from handlers.BaseHandlers import BaseHandler 21 | 22 | 23 | class ForbiddenHandler(BaseHandler): 24 | 25 | def get(self, *args, **kwargs): 26 | # TODO: Return error code 27 | pass 28 | 29 | 30 | class NotFoundHandler(BaseHandler): 31 | 32 | def get(self, *args, **kwargs): 33 | self.render('errors/404.html') -------------------------------------------------------------------------------- /static/ng-app/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 |

Check your email address or domain for email protections

13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
-------------------------------------------------------------------------------- /mixins/celery_task_mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @author: moloch 4 | Copyright 2016 5 | """ 6 | 7 | import logging 8 | 9 | from uuid import uuid4 10 | 11 | from tornado.gen import coroutine, Return, Task 12 | 13 | from tasks.notifiers import task_complete_notify 14 | 15 | 16 | class CeleryTaskMixin(object): 17 | 18 | @property 19 | def task_event_consumer(self): 20 | return self.settings["task_event_consumer"] 21 | 22 | @coroutine 23 | def execute_task(self, task, *args, **kwargs): 24 | """ Allows tasks to be executed using coroutines """ 25 | result = yield Task(self._execute_task, task, *args, **kwargs) 26 | raise Return(result.result) 27 | 28 | def _execute_task(self, task, *args, **kwargs): 29 | """ Not thread safe, only call this once per-notification """ 30 | logging.critical('task = %r', task) 31 | logging.critical('args = %r', args) 32 | logging.critical('kwargs = %r', kwargs) 33 | 34 | self._callback = kwargs['callback'] 35 | del kwargs['callback'] 36 | 37 | task_id = str(uuid4()) 38 | self.task_event_consumer.add_event_listener(self, task_id) 39 | self._task_result = task.apply_async(args, kwargs, 40 | task_id=task_id, 41 | link=task_complete_notify.s(task_id)) 42 | 43 | def on_task_completed(self): 44 | self._callback(self._task_result) 45 | -------------------------------------------------------------------------------- /libs/Singleton.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: moloch 4 | 5 | Copyright 2013 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ''' 19 | 20 | 21 | from threading import Lock 22 | 23 | 24 | class Singleton(object): 25 | ''' Thread safe singleton ''' 26 | 27 | def __init__(self, decorated): 28 | self._decorated = decorated 29 | self._instance_lock = Lock() 30 | 31 | def instance(self): 32 | ''' 33 | Returns the singleton instance. Upon its first call, it creates a 34 | new instance of the decorated class and calls its `__init__` method. 35 | On all subsequent calls, the already created instance is returned. 36 | ''' 37 | if not hasattr(self, "_instance"): 38 | with self._instance_lock: 39 | if not hasattr(self, "_instance"): 40 | self._instance = self._decorated() 41 | return self._instance 42 | 43 | def __call__(self): 44 | raise TypeError( 45 | 'Singletons must be accessed through the `instance` method.' 46 | ) 47 | -------------------------------------------------------------------------------- /libs/SecurityDecorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: moloch 4 | 5 | Copyright 2013 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ''' 19 | 20 | import logging 21 | import functools 22 | 23 | 24 | def csp(src, policy): 25 | ''' Decorator for easy CSP management ''' 26 | 27 | def func(method): 28 | @functools.wraps(method) 29 | def wrapper(self, *args, **kwargs): 30 | self.add_content_policy(src, policy) 31 | return wrapper 32 | return func 33 | 34 | 35 | 36 | def restrict_ip_address(method): 37 | ''' Only allows access to ip addresses in a provided list ''' 38 | 39 | @functools.wraps(method) 40 | def wrapper(self, *args, **kwargs): 41 | if self.request.remote_ip in self.application.settings['admin_ips']: 42 | return method(self, *args, **kwargs) 43 | else: 44 | self.redirect(self.application.settings['forbidden_url']) 45 | return wrapper 46 | 47 | 48 | def restrict_origin(method): 49 | ''' Check the origin header / prevent CSRF+WebSocket ''' 50 | 51 | @functools.wraps(method) 52 | def wrapper(self, *args, **kwargs): 53 | if self.request.headers['Origin'] == self.config.origin: 54 | return method(self, *args, **kwargs) 55 | return wrapper -------------------------------------------------------------------------------- /tasks/helpers/mq.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @author: moloch 4 | Copyright 2016 5 | """ 6 | 7 | import pika 8 | 9 | from tornado.options import options 10 | from furl import furl 11 | 12 | 13 | def create_mq_url(hostname, port, username=None, password=None): 14 | """ Creates a well formed amqp:// connection URI """ 15 | mq_url = furl() 16 | mq_url.scheme = "amqp" 17 | mq_url.host = hostname 18 | mq_url.port = port 19 | mq_url.username = username 20 | mq_url.password = password 21 | return str(mq_url) 22 | 23 | 24 | def mq_connect(): 25 | """ Connects to the MQ and returns the connection """ 26 | _url = create_mq_url(options.mq_hostname, options.mq_port, 27 | username=options.mq_username, 28 | password=options.mq_password) 29 | parameters = pika.URLParameters(_url) 30 | connection = pika.BlockingConnection(parameters) 31 | return connection 32 | 33 | 34 | def mq_send(channel, exchange, routing_key, message): 35 | """ 36 | Sends an MQ message on a channel to an exchange returns confirmation 37 | """ 38 | return channel.basic_publish(exchange, routing_key, message, 39 | pika.BasicProperties(content_type='text/plain', 40 | delivery_mode=1)) 41 | 42 | 43 | def mq_send_once(exchange, routing_key, message): 44 | """ 45 | Publishes a single rabbit mq message, and returns the 46 | delivery confirmation. 47 | 48 | NOTE: If you're wondering the reason for using this over 49 | `libs.events.event_producers` those producers are dependent 50 | on the Tornado ioloop, which we don't have here, nor do we 51 | want here. 52 | """ 53 | connection = mq_connect() 54 | confirmation = mq_send(connection.channel(), exchange, routing_key, message) 55 | connection.close() 56 | return confirmation 57 | -------------------------------------------------------------------------------- /libs/LoggingHelpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: moloch 4 | 5 | Copyright 2013 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ''' 19 | 20 | 21 | import logging 22 | 23 | from libs.Singleton import Singleton 24 | from collections import deque 25 | 26 | 27 | @Singleton 28 | class ObservableLoggingHandler(logging.StreamHandler): 29 | ''' 30 | An observable logging class, just shuffles logging messages 31 | from the main logger to the observers. A small history is 32 | stored in volatile memory. 33 | ''' 34 | 35 | max_history_size = 100 36 | _observers = [] 37 | _history = deque() 38 | 39 | def add_observer(self, observer): 40 | ''' Add new observer and send them any history ''' 41 | if observer not in self._observers: 42 | self._observers.append(observer) 43 | observer.update(list(self._history)) 44 | 45 | def remove_observer(self, observer): 46 | ''' Remove ref to an observer ''' 47 | if observer in self._observers: 48 | self._observers.remove(observer) 49 | 50 | def emit(self, record): 51 | ''' 52 | Overloaded method, gets called when logging messages are sent 53 | ''' 54 | msg = self.format(record) 55 | for observer in self._observers: 56 | observer.update([msg]) 57 | if self.max_history_size < len(self._history): 58 | self._history.popleft() 59 | self._history.append(msg) 60 | -------------------------------------------------------------------------------- /app.cfg: -------------------------------------------------------------------------------- 1 | ############################################### 2 | # Configuration File 3 | ############################################### 4 | 5 | [Server] 6 | # Web server listen port 7 | port = 8888 8 | 9 | # Debug mode provides stack traces, etc 10 | debug = True 11 | 12 | # Bootstrap type 'production' / 'developement' 13 | bootstrap = developement 14 | 15 | [Logging] 16 | # Sets the log level for the console / terminal 17 | console_level = debug 18 | 19 | # Enable/disable file logs 20 | file_logs = True 21 | 22 | # Set logging level for the file output (if save_logs is enabled) 23 | file_logs_level = debug 24 | 25 | # Path to output file for logs (if save_logs is enabled) 26 | file_logs_filename = app.log 27 | 28 | [Ssl] 29 | # Enable/disable SSL server 30 | # You'll need to manually setup an HTTP/302 to HTTPS if you want it 31 | use_ssl = False 32 | 33 | # Generate a self signed certificate/key like so: 34 | # openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout bar.key -out foo.crt 35 | 36 | # Certificate file path 37 | certificate_file = foo.crt 38 | # Key file path 39 | key_file = bar.key 40 | 41 | [Memcached] 42 | # Memcached settings 43 | host = 127.0.0.1 44 | port = 11211 45 | 46 | [Sessions] 47 | # Session settings (stored in memcached) 48 | max_age = 1800 49 | regeneration_interval = 1800 50 | 51 | [Security] 52 | # Comma separated list of ips that can access the admin interface 53 | # For example: admin_ips = 127.0.0.1,192.168.0.25,10.34.0.2 54 | admin_ips = 127.0.0.1 55 | 56 | # If x-headers is set to 'True', the app will honor the real IP address 57 | # provided by a load balancer in the X-Real-Ip or X-Forwarded-For header. 58 | # NOTE: This can affect the IP address security restrictions. 59 | x-headers = False 60 | 61 | [Database] 62 | # Supports 'mysql', 'postgresql', or 'sqlite'; defaults to mysql 63 | dialect = sqlite 64 | 65 | # This is the database name we're connecting to 66 | name = app 67 | 68 | # These are not used for sqlite 69 | # Leave blank or set to 'RUNTIME' if you want to prompt for creds when the application starts 70 | # this keeps sensitive db credentials out of plain-text files, and is recommended for production. 71 | host = localhost 72 | user = dbuser 73 | password = dbpassword 74 | -------------------------------------------------------------------------------- /libs/ConsoleColors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: moloch 4 | 5 | Copyright 2013 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ''' 19 | 20 | import platform 21 | 22 | if platform.system().lower() in ['linux', 'darwin']: 23 | 24 | # === Text Colors === 25 | W = "\033[0m" # default/white 26 | BLA = "\033[30m" # black 27 | R = "\033[31m" # red 28 | G = "\033[32m" # green 29 | O = "\033[33m" # orange 30 | BLU = "\033[34m" # blue 31 | P = "\033[35m" # purple 32 | C = "\033[36m" # cyan 33 | GR = "\033[37m" # gray 34 | 35 | # === Styles === 36 | bold = "\033[1m" 37 | underline = "\033[4m" 38 | blink = "\033[5m" 39 | reverse = "\033[7m" 40 | concealed = "\033[8m" 41 | 42 | # === Background Colors === 43 | bkgd_black = "\033[40m" 44 | bkgd_red = "\033[41m" 45 | bkgd_green = "\033[42m" 46 | bkgd_yellow = "\033[43m" 47 | bkgd_blue = "\033[44m" 48 | bkgd_magenta = "\033[45m" 49 | bkgd_cyan = "\033[46m" 50 | bkgd_white = "\033[47m" 51 | 52 | else: 53 | 54 | # Sets all colors to blank strings 55 | # === Text Colors === 56 | W = "" 57 | BLA = "" 58 | R = "" 59 | G = "" 60 | O = "" 61 | BLU = "" 62 | P = "" 63 | C = "" 64 | GR = "" 65 | 66 | # === Styles === 67 | bold = "" 68 | underline = "" 69 | blink = "" 70 | reverse = "" 71 | concealed = "" 72 | 73 | # === Background Colors === 74 | bkgd_black = "" 75 | bkgd_red = "" 76 | bkgd_green = "" 77 | bkgd_yellow = "" 78 | bkgd_blue = "" 79 | bkgd_magenta = "" 80 | bkgd_cyan = "" 81 | bkgd_white = "" 82 | 83 | # === Macros === 84 | INFO = bold + C + "[*] " + W 85 | WARN = bold + R + "[!] " + W 86 | PROMPT = bold + P + "[?] " + W 87 | -------------------------------------------------------------------------------- /static/css/selftest.css: -------------------------------------------------------------------------------- 1 | @font-face{ 2 | font-family: 'Source Code Pro'; 3 | font-weight: 500; 4 | font-style: normal; 5 | font-stretch: normal; 6 | src: url('/static/fonts/SourceCodePro-Regular.ttf.woff2') format('woff2'), 7 | url('/static/fonts/SourceCodePro-Regular.ttf.woff') format('woff'); 8 | } 9 | 10 | #header { 11 | background: #2c2f34; 12 | border-bottom: 4px solid #d24126; 13 | color: #fff; 14 | } 15 | 16 | h2 { 17 | border-top: 2px solid rgb(209, 65, 38); 18 | margin: 1em 0 .5em 0; 19 | padding-top: 5px; 20 | text-transform: uppercase; 21 | } 22 | 23 | .content { 24 | position: relative; 25 | margin-top: 60px; 26 | padding-top: 50px; 27 | padding-bottom: 100px; 28 | } 29 | 30 | .img-center { 31 | margin: 0 auto; 32 | } 33 | 34 | .bf-logo-lg { 35 | padding: 1em; 36 | } 37 | 38 | .bf-logo-sm { 39 | width: 200px; 40 | height: auto; 41 | padding: 10px; 42 | } 43 | 44 | .domain-check-form { 45 | padding-top: 40px; 46 | } 47 | 48 | .headline { 49 | 50 | } 51 | 52 | li.dmarc { 53 | padding-top: 7px; 54 | } 55 | 56 | .code { 57 | font-family: source-code-pro, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; 58 | } 59 | 60 | .highlight { 61 | color: rgb(209, 65, 38); 62 | } 63 | 64 | .good { 65 | color: rgb(0, 159, 44); 66 | } 67 | 68 | .bad { 69 | color: rgb(255, 0, 0); 70 | } 71 | 72 | .slide { 73 | position: absolute; 74 | left: 0; 75 | top: 0; 76 | width: 100%; 77 | height: 100%; 78 | } 79 | 80 | .slide.ng-enter, 81 | .slide.ng-leave { 82 | -webkit-transition: all 1s ease; 83 | transition: all 1s ease; 84 | } 85 | .slide.ng-enter { 86 | left: 100%; 87 | } 88 | .slide.ng-enter-active { 89 | left: 0; 90 | } 91 | .slide.ng-leave { 92 | left: 0; 93 | } 94 | .slide.ng-leave-active { 95 | left: -100%; 96 | } 97 | 98 | .other-vulnerable-recommendations { 99 | padding-top: .5em; 100 | } 101 | 102 | p { 103 | margin-bottom: 5px; 104 | } 105 | 106 | ul.recommendations { 107 | 108 | } 109 | ul.recommendations li { 110 | padding-bottom: 2px; 111 | } 112 | 113 | .new-scan-button { 114 | float: left; 115 | 116 | } 117 | 118 | ul { 119 | list-style: disc; 120 | } 121 | 122 | .captcha { 123 | margin-left: auto; 124 | margin-right: auto; 125 | width: 304px; 126 | padding-top: 1em; 127 | } -------------------------------------------------------------------------------- /handlers/CheckHandler.py: -------------------------------------------------------------------------------- 1 | from handlers.BaseHandlers import BaseWebSocketHandler 2 | import logging 3 | import json 4 | import re 5 | 6 | from tornado.gen import coroutine 7 | 8 | from mixins.celery_task_mixin import CeleryTaskMixin 9 | 10 | from tasks.message_tasks import * 11 | 12 | 13 | class MonitorSocketHandler(BaseWebSocketHandler, CeleryTaskMixin): 14 | 15 | def open(self): 16 | logging.debug("[WebSocket] Opened a Monitor Websocket") 17 | self._setup_opcodes() 18 | 19 | def _setup_opcodes(self): 20 | self.opcodes = { 21 | "check": self.check_domain 22 | } 23 | 24 | @coroutine 25 | def on_message(self, message): 26 | ''' Routes response to the correct opcode ''' 27 | try: 28 | message = json.loads(message) 29 | if 'opcode' in message and message['opcode'] in self.opcodes: 30 | yield self.opcodes[message['opcode']](message) 31 | else: 32 | raise Exception("Malformed message") 33 | except Exception as error: 34 | self.send_error('Error', str(error)) 35 | logging.exception( 36 | '[WebSocket]: Exception while routing json message') 37 | 38 | 39 | # Opcodes 40 | @coroutine 41 | def check_domain(self, message): 42 | try: 43 | domain = message["domain"] 44 | 45 | groups = re.search("@([\w.]+)", domain) 46 | 47 | if groups is not None: 48 | domain = groups.group(1) 49 | 50 | recaptcha_solution = message["captchaResponse"] 51 | 52 | ip_address = self.request.remote_ip 53 | 54 | solution_correct = yield self.execute_task(check_recaptcha_solution, **{ 55 | "user_solution": recaptcha_solution, 56 | "ip_address": ip_address 57 | }) 58 | 59 | logging.debug(solution_correct) 60 | 61 | if not solution_correct: 62 | output = { 63 | "opcode": "fail", 64 | "reason": "Failed Recaptcha" 65 | } 66 | 67 | self.write_message(output) 68 | 69 | else: 70 | logging.info("Checking domain " + domain) 71 | output = yield self.execute_task(email_spoofing_analysis, **{ 72 | "domain": domain 73 | }) 74 | 75 | self.write_message(output) 76 | except Exception as error: 77 | logging.exception(error) 78 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spoofcheck Self Test 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: moloch 4 | 5 | Copyright 2013 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ''' 19 | 20 | import logging 21 | 22 | from os import urandom 23 | from tornado import netutil 24 | from tornado.web import Application, StaticFileHandler 25 | from tornado.httpserver import HTTPServer 26 | from tornado.ioloop import IOLoop 27 | from libs.ConfigManager import ConfigManager 28 | from handlers.ErrorHandlers import * 29 | from handlers.HomePageHandler import * 30 | from handlers.CheckHandler import * 31 | 32 | from libs.events.event_consumers import TaskEventConsumer 33 | 34 | # Config 35 | config = ConfigManager.instance() 36 | 37 | 38 | def start_server(): 39 | ''' Main entry point for the application ''' 40 | 41 | task_event_consumer = TaskEventConsumer() 42 | 43 | # Application setup 44 | app = Application([ 45 | 46 | # Static Handlers - Serves static CSS, JS and images 47 | (r'/static/(.*\.(css|js|png|jpg|jpeg|svg|ttf|html|json))', 48 | StaticFileHandler, {'path': 'static/'}), 49 | 50 | # Home page serving SPA app 51 | (r'/', HomePageHandler), 52 | 53 | # Monitor Socket 54 | (r'/connect/monitor', MonitorSocketHandler), 55 | 56 | # Error Handlers - 57 | (r'/403', ForbiddenHandler), 58 | 59 | # Catch all 404 page 60 | (r'(.*)', NotFoundHandler), 61 | ], 62 | 63 | # Randomly generated secret key 64 | cookie_secret=urandom(32).encode('hex'), 65 | 66 | # Request that does not pass @authorized will be 67 | # redirected here 68 | forbidden_url='/403', 69 | 70 | # Requests that does not pass @authenticated will be 71 | # redirected here 72 | login_url='/login', 73 | 74 | # Template directory 75 | template_path='templates/', 76 | 77 | # Debug mode 78 | debug=config.debug, 79 | 80 | task_event_consumer=task_event_consumer 81 | ) 82 | 83 | sockets = netutil.bind_sockets(config.listen_port) 84 | if config.use_ssl: 85 | server = HTTPServer(app, 86 | ssl_options={ 87 | "certfile": config.certfile, 88 | "keyfile": config.keyfile, 89 | }, 90 | xheaders=config.x_headers) 91 | else: 92 | server = HTTPServer(app, xheaders=config.x_headers) 93 | server.add_sockets(sockets) 94 | io_loop = IOLoop.instance() 95 | try: 96 | io_loop.add_callback(task_event_consumer.connect) 97 | io_loop.start() 98 | except KeyboardInterrupt: 99 | logging.warn("Keyboard interrupt, shutdown everything!") 100 | except: 101 | logging.exception("Main I/O Loop threw an excetion!") 102 | finally: 103 | io_loop.stop() 104 | -------------------------------------------------------------------------------- /tasks/message_tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @author: lunarca 4 | Copyright 2017 5 | """ 6 | 7 | import os 8 | import requests 9 | import logging 10 | 11 | from tasks import selftest_task_queue 12 | 13 | from emailprotectionslib import spf as spflib 14 | from emailprotectionslib import dmarc as dmarclib 15 | 16 | 17 | @selftest_task_queue.task 18 | def email_spoofing_analysis(domain): 19 | spf_record = spflib.SpfRecord.from_domain(domain) 20 | dmarc_record = dmarclib.DmarcRecord.from_domain(domain) 21 | 22 | spf_existence = spf_record.record is not None 23 | spf_strong = spf_record.is_record_strong() if spf_existence else False 24 | 25 | dmarc_existence = dmarc_record.record is not None 26 | dmarc_policy = "" 27 | dmarc_strong = False 28 | dmarc_aggregate_reports = False 29 | dmarc_forensic_reports = False 30 | 31 | org_domain = None 32 | org_record_record = None 33 | org_sp = None 34 | org_policy = None 35 | org_aggregate_reports = None 36 | org_forensic_reports = None 37 | 38 | is_subdomain = False 39 | 40 | if dmarc_record is not None: 41 | dmarc_strong = dmarc_record.is_record_strong() 42 | dmarc_policy = dmarc_record.policy 43 | dmarc_aggregate_reports = dmarc_record.rua is not None and dmarc_record.rua != "" 44 | dmarc_forensic_reports = dmarc_record.ruf is not None and dmarc_record.ruf != "" 45 | 46 | if not dmarc_existence: 47 | try: 48 | org_domain = dmarc_record.get_org_domain() 49 | org_record = dmarc_record.get_org_record() 50 | org_record_record = org_record.record 51 | org_sp = org_record.subdomain_policy 52 | org_policy = org_record.policy 53 | org_aggregate_reports = dmarc_record.rua is not None and org_record.rua != "" 54 | org_forensic_reports = dmarc_record.ruf is not None and org_record.ruf != "" 55 | is_subdomain = True 56 | except dmarclib.OrgDomainException: 57 | org_domain = None 58 | org_record_record = None 59 | org_sp = None 60 | org_policy = None 61 | org_aggregate_reports = None 62 | org_forensic_reports = None 63 | 64 | domain_vulnerable = not (dmarc_strong and (spf_strong or org_domain is not None)) 65 | 66 | output = { 67 | 'opcode': "test", 68 | 'message': { 69 | 'vulnerable': domain_vulnerable, 70 | 'isSubdomain': is_subdomain, 71 | 'spf': { 72 | 'existence': spf_existence, 73 | 'strongConfiguration': spf_strong, 74 | 'record': spf_record.record if spf_existence else None, 75 | }, 76 | 'dmarc': { 77 | 'existence': dmarc_existence, 78 | 'policy': dmarc_policy, 79 | 'aggregateReports': dmarc_aggregate_reports, 80 | 'forensicReports': dmarc_forensic_reports, 81 | 'record': dmarc_record.record if dmarc_existence else None, 82 | 'orgRecord': { 83 | 'existence': org_record_record is not None, 84 | 'domain': org_domain, 85 | 'record': org_record_record, 86 | 'sp': org_sp, 87 | 'policy': org_policy, 88 | 'rua': org_aggregate_reports, 89 | 'ruf': org_forensic_reports, 90 | }, 91 | }, 92 | 93 | }, 94 | } 95 | 96 | return output 97 | 98 | 99 | @selftest_task_queue.task 100 | def check_recaptcha_solution(user_solution, ip_address): 101 | payload = { 102 | "secret": os.environ["RECAPTCHA_SECRET_KEY"], 103 | "response": user_solution, 104 | "remoteip": ip_address, 105 | } 106 | 107 | recaptcha_response = requests.post("https://www.google.com/recaptcha/api/siteverify", data=payload) 108 | 109 | logging.debug("[RECAPTCHA Response] " + recaptcha_response.text) 110 | return recaptcha_response.json()["success"] 111 | -------------------------------------------------------------------------------- /selftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Copyright 2012 Root the Box 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ---------------------------------------------------------------------------- 17 | 18 | This file is the main starting point for the application, based on the 19 | command line arguments it calls various components setup/start/etc. 20 | 21 | ''' 22 | 23 | import os 24 | import sys 25 | 26 | from tornado.options import options, define 27 | 28 | from datetime import datetime 29 | from libs.ConsoleColors import * 30 | 31 | __version__ = 'v0.0.1' 32 | current_time = lambda: str(datetime.now()).split(' ')[1].split('.')[0] 33 | 34 | 35 | def serve(): 36 | ''' Starts the application ''' 37 | from handlers import start_server 38 | 39 | print(INFO + '%s : Starting application ...' % current_time()) 40 | start_server() 41 | 42 | 43 | def start_worker(): 44 | from tasks import selftest_task_queue 45 | from tasks.helpers import create_mq_url 46 | from celery.bin import worker 47 | 48 | worker = worker.worker(app=selftest_task_queue) 49 | worker_options = { 50 | 'broker': create_mq_url(options.mq_hostname, options.mq_port, 51 | username=options.mq_username, 52 | password=options.mq_password), 53 | 'loglevel': options.mq_loglevel, 54 | 'traceback': options.debug, 55 | } 56 | worker.run(**worker_options) 57 | 58 | 59 | def main(): 60 | ''' Call functions in the correct order based on CLI params ''' 61 | fpath = os.path.abspath(__file__) 62 | fdir = os.path.dirname(fpath) 63 | if fdir != os.getcwd(): 64 | print(INFO + "Switching CWD to %s" % fdir) 65 | os.chdir(fdir) 66 | if options.api: 67 | serve() 68 | elif options.celery: 69 | start_worker() 70 | else: 71 | print(INFO + "Failed to start server") 72 | 73 | define("config", 74 | default="app.cfg", 75 | help="path to config file", 76 | type=str, 77 | callback=lambda path: options.parse_config_file(path, final=False)) 78 | 79 | # RabbitMQ host 80 | define("mq_hostname", 81 | group="mq", 82 | default=os.environ.get("SPOOFCHECK_MQ_HOST", "127.0.0.1"), 83 | help="the mq host") 84 | 85 | define("mq_port", 86 | group="mq", 87 | default="5672", 88 | help="the mq port", 89 | type=str) 90 | 91 | define("mq_username", 92 | group="mq", 93 | default=os.environ.get("SPOOFCHECK_MQ_USERNAME", "guest"), 94 | help="the mq username", 95 | type=str) 96 | 97 | define("mq_password", 98 | group="mq", 99 | default=os.environ.get("SPOOFCHECK_MQ_PASSWORD", "guest"), 100 | help="the mq password", 101 | type=str) 102 | 103 | define("mq_loglevel", 104 | group="mq", 105 | default=os.environ.get("SPOOFCHECK_MQ_LOGLEVEL", "INFO"), 106 | help="the mq log level") 107 | 108 | define("debug", 109 | default=bool(os.environ.get("SPOOFCHECK_DEBUG", False)), 110 | help="start server in debugging mode", 111 | group="debug", 112 | type=bool) 113 | 114 | define("celery", 115 | default=False, 116 | help="Start Celery task server", 117 | type=bool) 118 | 119 | define("api", 120 | default=False, 121 | help="Start Server", 122 | type=bool) 123 | 124 | 125 | # Main 126 | if __name__ == '__main__': 127 | try: 128 | options.parse_command_line() 129 | except IOError as error: 130 | print(WARN + str(error)) 131 | sys.exit() 132 | main() 133 | 134 | -------------------------------------------------------------------------------- /static/ng-app/base.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('spoofcheck', ['ngRoute', 'ngAnimate', 'ngWebSocket', 'vcRecaptcha']); 2 | 3 | app.config( ['$routeProvider', '$locationProvider', 'vcRecaptchaServiceProvider', 4 | function($routeProvider, $locationProvider, vcRecaptchaServiceProvider) { 5 | $routeProvider.when( 6 | '/', { 7 | templateUrl: '/static/ng-app/home.html', 8 | controller: 'HomeController' 9 | } 10 | ).when( 11 | '/watch', { 12 | templateUrl: '/static/ng-app/watch.html', 13 | controller: 'WatchController' 14 | } 15 | ).when( 16 | '/report', { 17 | templateUrl: '/static/ng-app/report.html', 18 | controller: 'ReportController' 19 | } 20 | ).otherwise({ 21 | redirectTo: '/' 22 | }) 23 | ; 24 | 25 | // TODO: Fix to pull from config file, or modify in production 26 | vcRecaptchaServiceProvider.setSiteKey("6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"); 27 | } 28 | 29 | ]) 30 | .constant('EVENTS', { 31 | output: 'output', 32 | sent: 'sent' 33 | }) 34 | ; 35 | 36 | app 37 | .factory('MonitorWebSocket', ['$websocket', 'EVENTS', '$rootScope', 38 | function($websocket, EVENTS, $rootScope) { 39 | console.log("Activated websocket"); 40 | var ws = $websocket("ws://" + location.host + "/connect/monitor"); 41 | var output = []; 42 | 43 | ws.onMessage(function(message) { 44 | var data = JSON.parse(message.data); 45 | console.log(message); 46 | 47 | // Routing 48 | if (data.opcode != "Error") { 49 | output.push(data.message); 50 | $rootScope.$broadcast(EVENTS.output); 51 | console.log(output); 52 | } 53 | 54 | }); 55 | 56 | return { 57 | output: output, 58 | checkDomain: function(domain, captchaResponse) { 59 | var data = JSON.stringify( 60 | { 61 | opcode: 'check', 62 | domain: domain, 63 | captchaResponse: captchaResponse 64 | } 65 | ); 66 | 67 | console.log(data); 68 | ws.send(data); 69 | } 70 | }; 71 | 72 | 73 | } 74 | ]) 75 | ; 76 | 77 | app 78 | .controller('HomeController', ['$scope', '$rootScope', 'MonitorWebSocket', '$location', 'EVENTS', 79 | function($scope, $rootScope, MonitorWebSocket, $location, EVENTS) { 80 | $rootScope.domain = undefined; 81 | 82 | $scope.response = null; 83 | $scope.widgetId = null; 84 | 85 | $scope.setResponse = function (response) { 86 | console.info('Response available: %s', response); 87 | $scope.captchaResponse = response; 88 | }; 89 | $scope.setWidgetId = function (widgetId) { 90 | console.info('Created widget ID: %s', widgetId); 91 | $scope.widgetId = widgetId; 92 | }; 93 | 94 | $scope.checkDomain = function(domain) { 95 | console.log("Firing checkDomain with " + domain); 96 | var parsedDomain = domain; 97 | var emailRegex = /@([\w.]+)/; 98 | var emailRegexOut = domain.match(emailRegex); 99 | if (emailRegexOut != null) { 100 | parsedDomain = emailRegexOut[1]; 101 | } 102 | 103 | $rootScope.domain = parsedDomain; 104 | $rootScope.ws = MonitorWebSocket; 105 | 106 | $rootScope.$on(EVENTS.output, function(evt, data) { 107 | console.log(evt); 108 | $location.path("/report").replace(); 109 | }); 110 | 111 | $rootScope.ws.checkDomain(domain, $scope.captchaResponse); 112 | 113 | $location.path("/watch"); 114 | $rootScope.$broadcast(EVENTS.sent); 115 | } 116 | } 117 | ]) 118 | 119 | .controller('WatchController', ['$scope', '$rootScope', '$location', 'EVENTS', 120 | function($scope, $rootScope, $location, EVENTS) { 121 | console.log("Loading watch controller"); 122 | if ($rootScope.ws === undefined || $rootScope.domain === undefined) { 123 | $location.path("/"); 124 | } 125 | 126 | 127 | } 128 | ]) 129 | 130 | 131 | .controller('ReportController', ["$scope", '$rootScope', "$location", 132 | function($scope, $rootScope, $location) { 133 | if ($rootScope.ws === undefined || $rootScope.domain === undefined) { 134 | $location.path("/"); 135 | } else { 136 | $scope.message = $rootScope.ws.output[$rootScope.ws.output.length - 1]; 137 | } 138 | } 139 | ]) 140 | ; -------------------------------------------------------------------------------- /handlers/BaseHandlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on Mar 15, 2012 4 | 5 | @author: moloch 6 | 7 | Copyright 2012 Root the Box 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | ---------------------------------------------------------------------------- 21 | 22 | This file contains the base handlers, all other handlers should inherit 23 | from these base classes. 24 | 25 | ''' 26 | 27 | 28 | import traceback 29 | 30 | from libs.ConfigManager import ConfigManager 31 | from libs.SecurityDecorators import * 32 | from tornado.web import RequestHandler 33 | from tornado.ioloop import IOLoop 34 | from tornado.websocket import WebSocketHandler 35 | 36 | 37 | class BaseHandler(RequestHandler): 38 | ''' User handlers extend this class ''' 39 | 40 | io_loop = IOLoop.instance() 41 | csp = { 42 | "default-src": ["'self'"], 43 | "script-src": [], 44 | "connect-src": [], 45 | "frame-src": [], 46 | "img-src": [], 47 | "media-src": [], 48 | "font-src": [], 49 | "object-src": [], 50 | "style-src": [], 51 | } 52 | 53 | def initialize(self): 54 | ''' Setup sessions, etc ''' 55 | self.config = ConfigManager.instance() 56 | 57 | 58 | def set_default_headers(self): 59 | ''' Set security HTTP headers ''' 60 | self.set_header("Server", "'; DROP TABLE server_types;--") 61 | self.add_header("X-Frame-Options", "DENY") 62 | self.add_header("X-XSS-Protection", "1; mode=block") 63 | self.add_header("X-Content-Type-Options", "nosniff") 64 | self._refresh_csp() 65 | 66 | def _refresh_csp(self): 67 | ''' Rebuild the Content-Security-Policy header ''' 68 | _csp = '' 69 | for src, policies in self.csp.iteritems(): 70 | if len(policies): 71 | _csp += "%s: %s;" % (src, " ".join(policies)) 72 | self.set_header("Content-Security-Policy", _csp) 73 | 74 | def append_content_policy(self, src, policy): 75 | if src in self.csp: 76 | self.csp[src].append(policy) 77 | self._refresh_csp() 78 | else: 79 | raise ValueError("Invalid content source") 80 | 81 | def write_error(self, status_code, **kwargs): 82 | ''' Write our custom error pages ''' 83 | if not self.config.debug: 84 | trace = "".join(traceback.format_exception(*kwargs["exc_info"])) 85 | logging.error("Request from %s resulted in an error code %d:\n%s" % ( 86 | self.request.remote_ip, status_code, trace 87 | )) 88 | if status_code in [403]: 89 | # This should only get called when the _xsrf check fails, 90 | # all other '403' cases we just send a redirect to /403 91 | self.redirect('/403') 92 | else: 93 | # Never tell the user we got a 500 94 | self.render('public/404.html') 95 | else: 96 | # If debug mode is enabled, just call Tornado's write_error() 97 | super(BaseHandler, self).write_error(status_code, **kwargs) 98 | 99 | def get(self, *args, **kwargs): 100 | ''' Placeholder, incase child class does not impl this method ''' 101 | self.render("public/404.html") 102 | 103 | def post(self, *args, **kwargs): 104 | ''' Placeholder, incase child class does not impl this method ''' 105 | self.render("public/404.html") 106 | 107 | def put(self, *args, **kwargs): 108 | ''' Log odd behavior, this should never get legitimately called ''' 109 | logging.warn( 110 | "%s attempted to use PUT method" % self.request.remote_ip 111 | ) 112 | 113 | def delete(self, *args, **kwargs): 114 | ''' Log odd behavior, this should never get legitimately called ''' 115 | logging.warn( 116 | "%s attempted to use DELETE method" % self.request.remote_ip 117 | ) 118 | 119 | def head(self, *args, **kwargs): 120 | ''' Ignore it ''' 121 | logging.warn( 122 | "%s attempted to use HEAD method" % self.request.remote_ip 123 | ) 124 | 125 | def options(self, *args, **kwargs): 126 | ''' Log odd behavior, this should never get legitimately called ''' 127 | logging.warn( 128 | "%s attempted to use OPTIONS method" % self.request.remote_ip 129 | ) 130 | 131 | def on_finish(self, *args, **kwargs): 132 | ''' Called after a response is sent to the client ''' 133 | pass 134 | 135 | 136 | class BaseWebSocketHandler(WebSocketHandler): 137 | ''' Handles websocket connections ''' 138 | 139 | def initialize(self): 140 | ''' Setup sessions, etc ''' 141 | self.config = ConfigManager.instance() 142 | 143 | def open(self): 144 | pass 145 | 146 | def on_message(self, message): 147 | pass 148 | 149 | def on_close(self): 150 | pass -------------------------------------------------------------------------------- /libs/ConfigManager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: moloch 4 | 5 | Copyright 2012 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ''' 19 | 20 | 21 | import os 22 | import sys 23 | import urllib 24 | import socket 25 | import getpass 26 | import logging 27 | import ConfigParser 28 | 29 | from libs.ConsoleColors import * 30 | from libs.Singleton import Singleton 31 | 32 | 33 | logging_levels = { 34 | 'notset': logging.NOTSET, 35 | 'debug': logging.DEBUG, 36 | 'info': logging.INFO, 37 | 'information': logging.INFO, 38 | 'warn': logging.WARN, 39 | 'warning': logging.WARN, 40 | } 41 | 42 | 43 | @Singleton 44 | class ConfigManager(object): 45 | ''' Central class which handles any user-controlled settings ''' 46 | 47 | def __init__(self, cfg_file='app.cfg'): 48 | self.filename = cfg_file 49 | if os.path.exists(cfg_file) and os.path.isfile(cfg_file): 50 | self.conf = os.path.abspath(cfg_file) 51 | else: 52 | sys.stderr.write(WARN+"No configuration file found at: %s." % self.conf) 53 | os._exit(1) 54 | self.refresh() 55 | self.__logging__() 56 | 57 | def __logging__(self): 58 | ''' Load network configurations ''' 59 | level = self.config.get("Logging", 'console_level').lower() 60 | logger = logging.getLogger() 61 | logger.setLevel(logging_levels.get(level, logging.NOTSET)) 62 | if self.config.getboolean("Logging", 'file_logs'): 63 | self._file_logger(logger) 64 | 65 | def _file_logger(self, logger): 66 | ''' Configure File Logger ''' 67 | file_handler = logging.FileHandler('%s' % self.logfilename) 68 | logger.addHandler(file_handler) 69 | file_format = logging.Formatter('[%(levelname)s] %(asctime)s - %(message)s') 70 | file_handler.setFormatter(file_format) 71 | flevel = self.config.get("Logging", 'file_logs_level').lower() 72 | file_handler.setLevel(logging_levels.get(flevel, logging.NOTSET)) 73 | 74 | def refresh(self): 75 | ''' Refresh config file settings ''' 76 | self.config = ConfigParser.SafeConfigParser() 77 | self.config_fp = open(self.conf, 'r') 78 | self.config.readfp(self.config_fp) 79 | 80 | def save(self): 81 | ''' Write current config to file ''' 82 | self.config_fp.close() 83 | fp = open(self.conf, 'w') 84 | self.config.write(fp) 85 | fp.close() 86 | self.refresh() 87 | 88 | @property 89 | def logfilename(self): 90 | return self.config.get("Logging", 'file_logs_filename') 91 | 92 | @property 93 | def listen_port(self): 94 | ''' Web app listen port, only read once ''' 95 | lport = self.config.getint("Server", 'port') 96 | if not 0 < lport < 65535: 97 | logging.fatal("Listen port not in valid range: %d" % lport) 98 | os._exit(1) 99 | return lport 100 | 101 | @property 102 | def bootstrap(self): 103 | return self.config.get("Server", 'bootstrap') 104 | 105 | @property 106 | def log_filename(self): 107 | return self.config.get("Logging", 'log_filename') 108 | 109 | @property 110 | def debug(self): 111 | ''' Debug mode ''' 112 | return self.config.getboolean("Server", 'debug') 113 | 114 | @debug.setter 115 | def debug(self, value): 116 | assert isinstance(value, bool) 117 | self.config.set("Server", 'debug', str(value)) 118 | 119 | @property 120 | def domain(self): 121 | ''' Automatically resolve domain, or use manual setting ''' 122 | _domain = self.config.get("Server", 'domain').strip() 123 | if _domain.lower() == 'auto': 124 | try: 125 | _domain = socket.gethostbyname(socket.gethostname()) 126 | # On some Linux systems the hostname resolves to ~127.0.0.1 127 | # per /etc/hosts, so fallback and try to get the fqdn if we can. 128 | if _domain.startswith('127.'): 129 | _domain = socket.gethostbyname(socket.getfqdn()) 130 | except: 131 | logging.warn("Failed to automatically resolve domain, please set manually") 132 | _domain = 'localhost' 133 | logging.debug("Domain was automatically configured to '%s'" % _domain) 134 | if _domain == 'localhost' or _domain.startswith('127.') or _domain == '::1': 135 | logging.warn("Possible misconfiguration 'domain' is set to 'localhost'") 136 | return _domain 137 | 138 | @property 139 | def origin(self): 140 | http = 'https://' if self.use_ssl else 'http://' 141 | return "%s%s:%d" % (http, self.domain, self.listen_port) 142 | 143 | @property 144 | def memcached(self): 145 | ''' Memached settings, cannot be changed from webui ''' 146 | host = self.config.get("Memcached", 'host') 147 | port = self.config.getint("Memcached", 'port') 148 | if not 0 < port < 65535: 149 | logging.fatal("Memcached port not in valid range: %d" % port) 150 | os._exit(1) 151 | return "%s:%d" % (host, port) 152 | 153 | @property 154 | def session_duration(self): 155 | ''' Max session age in seconds ''' 156 | return abs(self.config.getint("Sessions", 'max_age')) 157 | 158 | @property 159 | def admin_ips(self): 160 | ''' Whitelist admin ip address, this may be bypassed if x-headers is enabled ''' 161 | ips = self.config.get("Security", 'admin_ips') 162 | ips = ips.replace(" ", "").split(',') 163 | ips.append('127.0.0.1') 164 | ips.append('::1') 165 | return tuple(set(ips)) 166 | 167 | @property 168 | def x_headers(self): 169 | ''' Enable/disable HTTP X-Headers ''' 170 | xheaders = self.config.getboolean("Security", 'x-headers') 171 | if xheaders: 172 | logging.warn("X-Headers is enabled, this may affect IP security restrictions") 173 | return xheaders 174 | 175 | @property 176 | def use_ssl(self): 177 | ''' Enable/disabled SSL server ''' 178 | return self.config.getboolean("Ssl", 'use_ssl') 179 | 180 | @property 181 | def certfile(self): 182 | ''' SSL Certificate file path ''' 183 | cert = os.path.abspath(self.config.get("Ssl", 'certificate_file')) 184 | if not os.path.exists(cert): 185 | logging.fatal("SSL misconfiguration, certificate file '%s' not found." % cert) 186 | os._exit(1) 187 | return cert 188 | 189 | @property 190 | def keyfile(self): 191 | ''' SSL Key file path ''' 192 | key = os.path.abspath(self.config.get("Ssl", 'key_file')) 193 | if not os.path.exists(key): 194 | logging.fatal("SSL misconfiguration, key file '%s' not found." % key) 195 | os._exit(1) 196 | return key 197 | 198 | @property 199 | def db_connection(self): 200 | ''' Construct the database connection string ''' 201 | dialect = self.config.get("Database", 'dialect').lower().strip() 202 | dialects = { 203 | 'sqlite': self._sqlite, 204 | 'postgresql': self._postgresql, 205 | 'mysql': self._mysql, 206 | } 207 | _db = dialects.get(dialect, self._sqlite)() 208 | self._test_connection(_db) 209 | return _db 210 | 211 | def _postgresql(self): 212 | ''' 213 | Configure to use postgresql, there is not built-in support for postgresql 214 | so make sure we can import the 3rd party python lib 'pypostgresql' 215 | ''' 216 | logging.debug("Configured to use Postgresql for a database") 217 | try: 218 | import pypostgresql 219 | except ImportError: 220 | print(WARN+"You must install 'pypostgresql' to use a postgresql database.") 221 | os._exit(1) 222 | db_host, db_name, db_user, db_password = self._db_credentials() 223 | return 'postgresql+pypostgresql://%s:%s@%s/%s' % ( 224 | db_user, db_password, db_host, db_name, 225 | ) 226 | 227 | def _sqlite(self): 228 | ''' SQLite connection string, always save db file to cwd, or in-memory ''' 229 | logging.debug("Configured to use SQLite for a database") 230 | db_name = os.path.basename(self.config.get("Database", 'name')) 231 | if not len(db_name): 232 | db_name = 'app' 233 | return ('sqlite:///%s.db' % db_name) if db_name != ':memory:' else 'sqlite://' 234 | 235 | def _mysql(self): 236 | ''' Configure db_connection for MySQL ''' 237 | logging.debug("Configured to use MySQL for a database") 238 | db_server, db_name, db_user, db_password = self._db_credentials() 239 | return 'mysql://%s:%s@%s/%s' % ( 240 | db_user, db_password, db_server, db_name 241 | ) 242 | 243 | def _test_connection(self, connection_string): 244 | ''' Test the connection string to see if we can connect to the database''' 245 | engine = create_engine(connection_string) 246 | try: 247 | connection = engine.connect() 248 | connection.close() 249 | except: 250 | if self.debug: 251 | logging.exception("Database connection failed") 252 | logging.critical("Failed to connect to database, check .cfg") 253 | os._exit(1) 254 | 255 | def _db_credentials(self): 256 | ''' Pull db creds and return them url encoded ''' 257 | host = self.config.get("Database", 'host') 258 | name = self.config.get("Database", 'name') 259 | user = self.config.get("Database", 'user') 260 | password = self.config.get("Database", 'password') 261 | if user == '' or user == 'RUNTIME': 262 | user = raw_input(PROMPT+"Database User: ") 263 | if password == '' or password == 'RUNTIME': 264 | sys.stdout.write(PROMPT+"Database password: ") 265 | sys.stdout.flush() 266 | password = getpass.getpass() 267 | db_host = urllib.quote(host) 268 | db_name = urllib.quote(name) 269 | db_user = urllib.quote(user) 270 | db_password = urllib.quote_plus(password) 271 | return db_host, db_name, db_user, db_password 272 | 273 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /static/ng-app/report.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 |

{{ domain }}
is vulnerable to email spoofing

15 |

{{ domain }}
is not vulnerable to email spoofing

16 |
17 |
18 | 19 |
20 |
21 |

Analysis

22 |
    23 |
  • SPF 24 |
      25 |
    • {{ domain }} has an SPF record.
    • 26 |
    • {{ domain }} has no SPF record.
    • 27 | 28 |
    • The SPF record for {{domain}} has a strong defensive configuration.
    • 29 |
    • The SPF record for {{domain}} has a weak configuration.
    • 30 |
    • Record: 31 | {{ message.spf.record}}
    • 32 |
    33 |
  • 34 |
  • DMARC 35 |
      36 |
    • {{ domain }} has a DMARC record.
    • 37 |
    • {{ domain }} is a subdomain of {{message.dmarc.orgRecord.domain}}.
    • 38 |
    • {{ message.dmarc.orgRecord.domain }} has a DMARC record.
    • 39 |
    • {{ domain }} has no DMARC record.
    • 40 | 41 |
    • 42 | The DMARC record for {{ domain }} is configured with a policy of {{message.dmarc.policy}}. 43 |
    • 44 |
    • 45 | The DMARC record for {{domain}} is configured with a policy of none. 46 | A DMARC policy of none still delivers emails that fail SPF and DKIM alignment. 47 |
    • 48 | 49 |
    • 50 | The DMARC record for {{domain}} is configured to send aggregate reports. 51 |
    • 52 |
    • 53 | The DMARC record for {{domain}} is configured to send forensic reports. 54 |
    • 55 | 56 |
    • 57 | The DMARC record for {{message.dmarc.orgRecord.domain}} is configured with a subdomain policy of none. 58 | A DMARC subdomain policy of none still delivers emails for subdomains that fail SPF and DKIM alignment. 59 |
    • 60 | 61 |
    • 62 | The DMARC record for {{message.dmarc.orgRecord.domain}} is configured with a subdomain policy of {{message.dmarc.orgRecord.sp}}. 63 | A DMARC subdomain policy of message.dmarc.orgRecord.sp will prevent email spoofing from subdomains of {{message.dmarc.orgRecord.domain}}. 64 |
    • 65 | 66 |
    • 67 | The DMARC record for {{message.dmarc.orgRecord.domain}} has no specified subdomain policy. Without one, the subdomain alignment configuration defaults to the basic DMARC policy. 68 |
    • 69 | 70 |
    • 71 | The DMARC record for {{ message.dmarc.orgRecord.domain }} is configured with a policy of {{message.dmarc.orgRecord.policy}}. A policy of {{message.dmarc.orgRecord.p}} will prevent email spoofing from {{domain}}. 72 |
    • 73 | 74 |
    • 75 | The DMARC record for {{ message.dmarc.orgRecord.domain }} is configured with a policy of {{message.dmarc.orgRecord.policy}}. A policy of {{message.dmarc.orgRecord.p}} still delivers emails for subdomains that fail SPF and DKIM alignment. 76 |
    • 77 | 78 |
    • Record: 79 | {{ message.dmarc.record }} 80 |
    • 81 |
    • Organizational Record: 82 | {{ message.dmarc.orgRecord.record }} 83 |
    • 84 | 85 |
    86 |
  • 87 |
88 |
89 |
90 | 91 |
92 |
93 |

Recommendations

94 |
95 |

To avoid the risk of email spoofing from {{domain}}, 96 | Bishop Fox recommends the following:

97 | 98 | 99 |
    100 | 101 |
  • 102 | Begin implementing an SPF record for {{domain}}. SPF records are DNS 103 | TXT records on the root {{domain}} domain 104 | that define all of the IP addresses permitted to send emails from {{domain}}. 105 | Additional information on setting up SPF records can be found at the OpenSPF project's 106 | SPF Record Syntax page. 107 |
  • 108 | 109 |
  • 110 | Modify the SPF record for {{domain}} to contain a 111 | reject all mechanism.This is performed by adding the string -all 112 | or ~all to the SPF record for {{domain}}. 113 | Additional information on setting up SPF records can be found at the OpenSPF project's 114 | SPF Record Syntax page. 115 |
  • 116 | 117 |
  • 118 | Begin implementing a DMARC record for {{domain}}. DMARC records are 119 | DNS TXT records, located at the _dmarc.{{domain}} 120 | subdomain, that instruct receiving mail servers how to handle emails that fail SPF and DKIM 121 | alignment. For DMARC to function, {{domain}} needs to have both SPF and 122 | DKIM configured. Additional information about setting up DMARC records can be found from the 123 | Google Apps DMARC setup guide. 124 |
  • 125 | 126 |
  • 127 | A DMARC policy of none allows spoofed emails to be delivered. 128 | Begin implementing a DMARC policy of quarantine or 129 | reject. As implementing strict DMARC policies may interfere with the delivery of email from 130 | {{domain}} email addresses, Bishop Fox recommends 131 | setting up and monitoring aggregate report 132 | notifications for legitimate emails before beginning to implement a stricter policy. If no 133 | legitimate emails are reported, set the DMARC policy to quarantine 134 | and set the pct field to a low percentage. This process is described 135 | in more detail in the Google Apps 136 | DMARC setup guide. 137 |
  • 138 | 139 |
  • 140 | A DMARC subdomain policy of none allows spoofed emails from subdomains 141 | to be delivered. Begin implementing a DMARC subdomain policy of quarantine or 142 | reject, or remove the subdomain policy and implement a DMARC policy of 143 | quarantine or reject. As implementing strict 144 | DMARC policies may interfere with the delivery of email from 145 | {{message.dmarc.orgRecord.domain}} email addresses, Bishop Fox recommends 146 | setting up and monitoring aggregate report 147 | notifications for legitimate emails before beginning to implement a stricter policy. If no 148 | legitimate emails are reported, set the DMARC policy to quarantine 149 | and set the pct field to a low percentage. This process is described 150 | in more detail in the Google Apps 151 | DMARC setup guide. 152 |
  • 153 | 154 |
155 |
156 | 157 |
158 |

To manage the risk of email spoofing from domains other than {{domain}}, 159 | Bishop Fox recommends the following:

160 |
    161 | 162 |
  • 163 | Configure the {{domain}} email server to quarantine emails that fail 164 | SPF alignment on the From field. Nearly 41% of the Alexa top million domains are configured 165 | with SPF records, but only 1.8% of those domains are configured with a strict DMARC record. 166 |
  • 167 |
168 |
169 | 170 |
171 |
172 |
173 | -------------------------------------------------------------------------------- /libs/events/event_consumers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @author: moloch 4 | Copyright 2015 5 | Adapted from docs example: 6 | https://pika.readthedocs.org/en/0.10.0/examples/tornado_consumer.html 7 | """ 8 | 9 | from pika import adapters 10 | 11 | import json 12 | import pika 13 | import logging 14 | 15 | from uuid import uuid4 16 | 17 | from tornado.options import options 18 | from tornado.ioloop import IOLoop 19 | from libs.events import TASK_EVENTS, TASK_ROUTING_KEY 20 | 21 | LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | class BaseEventConsumer(object): 25 | 26 | """ 27 | This is a consumer that will handle unexpected interactions 28 | with RabbitMQ such as channel and connection closures. 29 | 30 | If RabbitMQ closes the connection, it will reopen it. You should 31 | look at the output, as there are limited reasons why the connection may 32 | be closed, which usually are tied to permission related issues or 33 | socket timeouts. 34 | If the channel is closed, it will indicate a problem with one of the 35 | commands that were issued and that should surface in the output as well. 36 | """ 37 | 38 | EXCHANGE = 'base_events' 39 | EXCHANGE_TYPE = 'fanout' 40 | QUEUE = 'text' 41 | ROUTING_KEY = '' 42 | 43 | def __init__(self): 44 | """ 45 | Create a new instance of the consumer class, passing in the AMQP 46 | URL used to connect to RabbitMQ. 47 | :param str amqp_url: The AMQP url to connect with 48 | """ 49 | LOGGER.info("======= [EventConsumer: %s] =======", self.EXCHANGE) 50 | self._channel = None 51 | self._closing = False 52 | self._consumer_tag = None 53 | self._connection = None 54 | self.io_loop = IOLoop.instance() 55 | 56 | 57 | def add_event_listener(self, listener): 58 | """ Add an event listener based on team uuid """ 59 | raise NotImplementedError() 60 | 61 | def remove_event_listener(self, listener): 62 | """ Remove an event listener """ 63 | raise NotImplementedError() 64 | 65 | def on_mq_message(self, unused_channel, basic_deliver, properties, body): 66 | """ 67 | Invoked by pika when a message is delivered from RabbitMQ. The 68 | channel is passed for your convenience. The basic_deliver object that 69 | is passed in carries the exchange, routing key, delivery tag and 70 | a redelivered flag for the message. The properties passed in is an 71 | instance of BasicProperties with the message properties and the body 72 | is the message that was sent. 73 | :param pika.channel.Channel unused_channel: The channel object 74 | :param pika.Spec.Basic.Deliver: basic_deliver method 75 | :param pika.Spec.BasicProperties: properties 76 | :param str|unicode body: The message body 77 | """ 78 | raise NotImplementedError() 79 | 80 | @property 81 | def _url(self): 82 | """ MQ connection URL """ 83 | return "amqp://%s:%s@%s:%s" % ( 84 | options.mq_username, options.mq_password, options.mq_hostname, 85 | options.mq_port) 86 | 87 | def connect(self, *args, **kwargs): 88 | """ 89 | This method connects to RabbitMQ, returning the connection handle. 90 | When the connection is established, the on_connection_open method 91 | will be invoked by pika. 92 | :rtype: pika.SelectConnection 93 | """ 94 | LOGGER.debug('Connecting to %s', self._url) 95 | LOGGER.debug('Got arguments: %r %r', args, kwargs) 96 | conn = adapters.TornadoConnection(pika.URLParameters(self._url), 97 | self.on_connection_open) 98 | self._connection = conn 99 | return conn 100 | 101 | def close_connection(self): 102 | """This method closes the connection to RabbitMQ.""" 103 | LOGGER.debug('Closing connection') 104 | self._connection.close() 105 | 106 | def add_on_connection_close_callback(self): 107 | """ 108 | This method adds an on close callback that will be invoked by pika 109 | when RabbitMQ closes the connection to the publisher unexpectedly. 110 | """ 111 | LOGGER.debug('Adding connection close callback') 112 | self._connection.add_on_close_callback(self.on_connection_closed) 113 | 114 | def on_connection_closed(self, connection, reply_code, reply_text): 115 | """ 116 | This method is invoked by pika when the connection to RabbitMQ is 117 | closed unexpectedly. Since it is unexpected, we will reconnect to 118 | RabbitMQ if it disconnects. 119 | :param pika.connection.Connection connection: The closed connection obj 120 | :param int reply_code: The server provided reply_code if given 121 | :param str reply_text: The server provided reply_text if given 122 | """ 123 | self._channel = None 124 | LOGGER.warning('Connection closed reopening in 5 seconds: (%s) %s', 125 | reply_code, reply_text) 126 | self._connection.add_timeout(5, self.reconnect) 127 | 128 | def on_connection_open(self, unused_connection): 129 | """ 130 | This method is called by pika once the connection to RabbitMQ has 131 | been established. It passes the handle to the connection object in 132 | case we need it, but in this case, we'll just mark it unused. 133 | :type unused_connection: pika.SelectConnection 134 | """ 135 | LOGGER.debug('Connection opened') 136 | self.add_on_connection_close_callback() 137 | self.open_channel() 138 | 139 | def reconnect(self): 140 | """ 141 | Will be invoked by the IOLoop timer if the connection is 142 | closed. See the on_connection_closed method. 143 | """ 144 | if not self._closing: 145 | self._connection = self.connect() 146 | 147 | def add_on_channel_close_callback(self): 148 | """ 149 | This method tells pika to call the on_channel_closed method if 150 | RabbitMQ unexpectedly closes the channel. 151 | """ 152 | LOGGER.debug('Adding channel close callback') 153 | self._channel.add_on_close_callback(self.on_channel_closed) 154 | 155 | def on_channel_closed(self, channel, reply_code, reply_text): 156 | """ 157 | Invoked by pika when RabbitMQ unexpectedly closes the channel. 158 | Channels are usually closed if you attempt to do something that 159 | violates the protocol, such as re-declare an exchange or queue with 160 | different parameters. In this case, we'll close the connection 161 | to shutdown the object. 162 | :param pika.channel.Channel: The closed channel 163 | :param int reply_code: The numeric reason the channel was closed 164 | :param str reply_text: The text reason the channel was closed 165 | """ 166 | LOGGER.warning('Channel %i was closed: (%s) %s', 167 | channel, reply_code, reply_text) 168 | self._connection.close() 169 | 170 | def on_channel_open(self, channel): 171 | """ 172 | This method is invoked by pika when the channel has been opened. 173 | The channel object is passed in so we can make use of it. 174 | Since the channel is now open, we'll declare the exchange to use. 175 | :param pika.channel.Channel channel: The channel object 176 | """ 177 | LOGGER.debug('Channel opened') 178 | self._channel = channel 179 | self.add_on_channel_close_callback() 180 | self.setup_exchange(self.EXCHANGE) 181 | 182 | def setup_exchange(self, exchange_name): 183 | """ 184 | Setup the exchange on RabbitMQ by invoking the Exchange.Declare RPC 185 | command. When it is complete, the on_exchange_declareok method will 186 | be invoked by pika. 187 | :param str|unicode exchange_name: The name of the exchange to declare 188 | """ 189 | LOGGER.info('Declaring exchange %s', exchange_name) 190 | self._channel.exchange_declare(self.on_exchange_declareok, 191 | exchange_name, 192 | self.EXCHANGE_TYPE) 193 | 194 | def on_exchange_declareok(self, unused_frame): 195 | """ 196 | Invoked by pika when RabbitMQ has finished the Exchange.Declare RPC 197 | command. 198 | :param pika.Frame.Method unused_frame: Exchange.DeclareOk response 199 | frame 200 | """ 201 | LOGGER.debug('Exchange declared') 202 | self.setup_queue(self.QUEUE) 203 | 204 | def setup_queue(self, queue_name): 205 | """ 206 | Setup the queue on RabbitMQ by invoking the Queue.Declare RPC 207 | command. When it is complete, the on_queue_declareok method will 208 | be invoked by pika. 209 | :param str|unicode queue_name: The name of the queue to declare. 210 | """ 211 | LOGGER.debug('Declaring queue %s', queue_name) 212 | self._channel.queue_declare(self.on_queue_declareok, queue_name) 213 | 214 | def on_queue_declareok(self, method_frame): 215 | """ 216 | Method invoked by pika when the Queue.Declare RPC call made in 217 | setup_queue has completed. In this method we will bind the queue 218 | and exchange together with the routing key by issuing the Queue.Bind 219 | RPC command. When this command is complete, the on_bindok method will 220 | be invoked by pika. 221 | :param pika.frame.Method method_frame: The Queue.DeclareOk frame 222 | """ 223 | LOGGER.debug('Binding %s to %s with %s', 224 | self.EXCHANGE, self.QUEUE, self.ROUTING_KEY) 225 | self._channel.queue_bind(self.on_bindok, self.QUEUE, 226 | self.EXCHANGE, self.ROUTING_KEY) 227 | 228 | def add_on_cancel_callback(self): 229 | """ 230 | Add a callback that will be invoked if RabbitMQ cancels the consumer 231 | for some reason. If RabbitMQ does cancel the consumer, 232 | on_consumer_cancelled will be invoked by pika. 233 | """ 234 | LOGGER.debug('Adding consumer cancellation callback') 235 | self._channel.add_on_cancel_callback(self.on_consumer_cancelled) 236 | 237 | def on_consumer_cancelled(self, method_frame): 238 | """ 239 | Invoked by pika when RabbitMQ sends a Basic.Cancel for a consumer 240 | receiving messages. 241 | :param pika.frame.Method method_frame: The Basic.Cancel frame 242 | """ 243 | LOGGER.debug('Consumer was cancelled remotely, shutting down: %r', 244 | method_frame) 245 | if self._channel: 246 | self._channel.close() 247 | 248 | def acknowledge_message(self, delivery_tag): 249 | """ 250 | Acknowledge the message delivery from RabbitMQ by sending a 251 | Basic.Ack RPC method for the delivery tag. 252 | :param int delivery_tag: The delivery tag from the Basic.Deliver frame 253 | """ 254 | LOGGER.debug('Acknowledging message %s', delivery_tag) 255 | self._channel.basic_ack(delivery_tag) 256 | 257 | def on_cancelok(self, unused_frame): 258 | """ 259 | This method is invoked by pika when RabbitMQ acknowledges the 260 | cancellation of a consumer. At this point we will close the channel. 261 | This will invoke the on_channel_closed method once the channel has been 262 | closed, which will in-turn close the connection. 263 | :param pika.frame.Method unused_frame: The Basic.CancelOk frame 264 | """ 265 | LOGGER.debug('RabbitMQ acknowledged the cancellation of the consumer') 266 | self.close_channel() 267 | 268 | def stop_consuming(self): 269 | """Tell RabbitMQ that you would like to stop consuming by sending the 270 | Basic.Cancel RPC command. 271 | """ 272 | if self._channel: 273 | LOGGER.info('Sending a Basic.Cancel RPC command to RabbitMQ') 274 | self._channel.basic_cancel(self.on_cancelok, self._consumer_tag) 275 | 276 | def start_consuming(self): 277 | """ 278 | This method sets up the consumer by first calling 279 | add_on_cancel_callback so that the object is notified if RabbitMQ 280 | cancels the consumer. It then issues the Basic.Consume RPC command 281 | which returns the consumer tag that is used to uniquely identify the 282 | consumer with RabbitMQ. We keep the value to use it when we want to 283 | cancel consuming. The on_mq_message method is passed in as a callback 284 | pika will invoke when a message is fully received. 285 | """ 286 | LOGGER.debug('Issuing consumer related RPC commands') 287 | self.add_on_cancel_callback() 288 | self._consumer_tag = self._channel.basic_consume(self.on_mq_message, 289 | self.QUEUE) 290 | 291 | def on_bindok(self, unused_frame): 292 | """ 293 | Invoked by pika when the Queue.Bind method has completed. At this 294 | point we will start consuming messages by calling start_consuming 295 | which will invoke the needed RPC commands to start the process. 296 | :param pika.frame.Method unused_frame: The Queue.BindOk response frame 297 | """ 298 | LOGGER.debug('Queue bind: OK') 299 | self.start_consuming() 300 | 301 | def close_channel(self): 302 | """ 303 | Call to close the channel with RabbitMQ cleanly by issuing the 304 | Channel.Close RPC command. 305 | """ 306 | LOGGER.debug('Closing the channel') 307 | self._channel.close() 308 | 309 | def open_channel(self): 310 | """ 311 | Open a new channel with RabbitMQ by issuing the Channel.Open RPC 312 | command. When RabbitMQ responds that the channel is open, the 313 | on_channel_open callback will be invoked by pika. 314 | """ 315 | LOGGER.debug('Creating a new channel') 316 | self._connection.channel(on_open_callback=self.on_channel_open) 317 | 318 | def run(self): 319 | """ 320 | Run the example consumer by connecting to RabbitMQ and then 321 | starting the IOLoop to block and allow the SelectConnection to operate. 322 | """ 323 | self._connection = self.connect() 324 | 325 | def stop(self): 326 | """ 327 | Cleanly shutdown the connection to RabbitMQ by stopping the consumer 328 | with RabbitMQ. When RabbitMQ confirms the cancellation, on_cancelok 329 | will be invoked by pika, which will then closing the channel and 330 | connection. The IOLoop is started again because this method is invoked 331 | when CTRL-C is pressed raising a KeyboardInterrupt exception. This 332 | exception stops the IOLoop which needs to be running for pika to 333 | communicate with RabbitMQ. 334 | """ 335 | LOGGER.debug('Stopping') 336 | self._closing = True 337 | self.stop_consuming() 338 | 339 | 340 | class TaskEventConsumer(BaseEventConsumer): 341 | 342 | EXCHANGE = TASK_EVENTS 343 | EXCHANGE_TYPE = 'fanout' 344 | QUEUE = str(uuid4()) 345 | ROUTING_KEY = TASK_ROUTING_KEY 346 | 347 | def __init__(self): 348 | self._event_listeners = {} 349 | super(TaskEventConsumer, self).__init__() 350 | 351 | def on_mq_message(self, unused_channel, basic_deliver, properties, body): 352 | """ 353 | Consume messages based on team_id 354 | """ 355 | LOGGER.info('Received message # %s from %s: %s', 356 | basic_deliver.delivery_tag, properties.app_id, body) 357 | self.acknowledge_message(basic_deliver.delivery_tag) 358 | msg = json.loads(body) 359 | listener = self._event_listeners.get(msg['task_id'], None) 360 | if listener is not None: 361 | self.io_loop.add_callback(listener.on_task_completed) 362 | else: 363 | LOGGER.warning('No listener for task with id: %s', msg['task_id']) 364 | 365 | def add_event_listener(self, listener, notification_id): 366 | """ Add an event listener based on team uuid """ 367 | self._event_listeners[notification_id] = listener 368 | 369 | def remove_event_listener(self, notification_id): 370 | """ Remove an event listener """ 371 | if notification_id in self._event_listeners: 372 | del self._event_listeners[notification_id] 373 | logging.debug("Removed listener for task with id: %s", notification_id) 374 | 375 | -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | --------------------------------------------------------------------------------