Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add-On Engine for MHN #138

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added addon_example/hello_addon.tar.gz
Binary file not shown.
1 change: 1 addition & 0 deletions addon_example/hello_addon/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__author__ = 'mercolino'
31 changes: 31 additions & 0 deletions addon_example/hello_addon/templates/hello_world.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %} Hello World Add-on{% endblock %}
{% block content %}
<div class="row">
<div class="small-6 large-centered columns" style="text-align:center;">
<h2>Hello World Add-On</h2>
</div>
<div class="small-11 large-centered columns">
<div class="panel callout radius">
<h5><b>Here it is the Add-On Content.</b></h5>
<p>You could add everything you want here!!!</p>
</div>
</div>
</div>
{% endblock %}

{% block footer %}
<style>
#footer {
margin-top: 40px;
margin-bottom: 40px;
bottom: 0;
width: 100%;
}
</style>
<div class="row" id="footer">
<div class="small-4 large-centered columns" style="text-align:center;">
<span><small>Hello World Add-on developed by John Doe</small><span>
</div>
</div>
{% endblock %}
11 changes: 11 additions & 0 deletions addon_example/hello_addon/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from flask import (Blueprint, render_template)
from mhn.auth import login_required

hello_addon = Blueprint('hello_addon', __name__, template_folder='templates/', url_prefix='/addons/hello_addon')

@hello_addon.route('/home/', methods=['GET'])
@login_required
def home():
return render_template('hello_world.html')

__author__ = 'mercolino'
4 changes: 2 additions & 2 deletions scripts/deploy_kippo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ sed -i 's/twistd -y kippo.tac -l log\/kippo.log --pidfile kippo.pid/su kippo -c
# Config for supervisor.
cat > /etc/supervisor/conf.d/kippo.conf <<EOF
[program:kippo]
command=/opt/kippo/start.sh
command=su kippo -c "authbind --deep twistd -n -y /opt/kippo/kippo.tac -l /opt/kippo/log/kippo.log --pidfile /opt/kippo/kippo.pid"
directory=/opt/kippo
stdout_logfile=/opt/kippo/log/kippo.out
stderr_logfile=/opt/kippo/log/kippo.err
autostart=true
autorestart=true
redirect_stderr=true
stopsignal=QUIT
stopasgroup = true
EOF

supervisorctl update
4 changes: 4 additions & 0 deletions server/config.py.template
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ SERVER_BASE_URL = '{{SERVER_BASE_URL}}'
HONEYMAP_URL = '{{HONEYMAP_URL}}'
DEPLOY_KEY = '{{DEPLOY_KEY}}'
LOG_FILE_PATH = '{{LOG_FILE_PATH}}'
ADD_ONS = '{{ADD_ONS}}'

MAIL_SERVER = '{{MAIL_SERVER}}'
MAIL_PORT = {{MAIL_PORT}}
Expand Down Expand Up @@ -69,3 +70,6 @@ HONEYPOT_CHANNELS = {
'p0f': ['p0f.events'],
'suricata': ['suricata.events'],
}

# Disable problematic Add-ons
DISABLED_ADDONS = []
9 changes: 9 additions & 0 deletions server/generateconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def generate_config():
help='Log file path')
parser_unatt.add_argument('-d', '--debug', action='store_true',
help='Run in debug mode')
parser_unatt.add_argument('-a', '--add_ons', action='store_true',
help='Activate the add-ons engine')

if (len(sys.argv) < 2):
args = parser.parse_args(['generate'])
Expand All @@ -96,6 +98,7 @@ def generate_config():
mail_password = args.mail_pass
default_mail_sender = args.mail_sender
log_file_path = args.log_file_path
add_ons = args.add_ons
else:
# Collect values from user
debug = raw_input('Do you wish to run in Debug mode?: y/n ')
Expand Down Expand Up @@ -147,6 +150,11 @@ def generate_config():

log_file_path = raw_input('Path for log file ["{}"]: '.format(default_log_path))

add_ons = raw_input('Do you wish to enable the Add-Ons engine?: y/n ')
while add_ons not in ['y', 'n']:
add_ons = raw_input('Please y or n ')
add_ons = True if add_ons == 'y' else False

server_base_url = server_base_url if server_base_url.strip() else default_base_url
honeymap_url = honeymap_url if honeymap_url.strip() else default_honeymap_url
log_file_path = log_file_path if log_file_path else default_log_path
Expand All @@ -164,6 +172,7 @@ def generate_config():
localconfig['MAIL_PASSWORD'] = mail_password if mail_password else ''
localconfig['DEFAULT_MAIL_SENDER'] = default_mail_sender if default_mail_sender else ""
localconfig['LOG_FILE_PATH'] = log_file_path
localconfig['ADD_ONS'] = add_ons

with open('config.py.template', 'r') as templfile,\
open('config.py', 'w') as confile:
Expand Down
7 changes: 7 additions & 0 deletions server/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@

from mhn import mhn, db
from mhn.tasks.rules import fetch_sources
from mhn.api.models import (
User, Sensor, Rule, DeployScript as Script,
RuleSource, AddOns)


if __name__ == '__main__':
Expand All @@ -54,4 +57,8 @@ def runlocal():
def fetch_rules():
fetch_sources()

@manager.shell
def make_shell_context():
return dict(mhn=mhn, db=db, User=User, Sensor=Sensor, Rule=Rule, RuleSource=RuleSource, Script=Script, AddOns=AddOns)

manager.run()
28 changes: 24 additions & 4 deletions server/mhn/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from urlparse import urljoin

from flask import Flask, request, jsonify, abort, url_for, session
from flask import Flask, request, jsonify, abort, url_for
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, SQLAlchemyUserDatastore
from flask.ext.security.utils import encrypt_password as encrypt
from flask.ext.mail import Mail
from werkzeug.contrib.atom import AtomFeed
import xmltodict
import uuid
import random
import string
import os
import imp

from flask_wtf.csrf import CsrfProtect
csrf = CsrfProtect()

Expand All @@ -34,7 +35,7 @@
# Setup flask-security for auth.
Security(mhn, user_datastore)

# Registering blueprints.
# Registering core blueprints.
from mhn.api.views import api
mhn.register_blueprint(api)

Expand All @@ -44,6 +45,25 @@
from mhn.auth.views import auth
mhn.register_blueprint(auth)

# Registering Add-Ons (Blueprints)
with mhn.test_request_context():
db.create_all()
from mhn.api.models import AddOns

if mhn.config['ADD_ONS']:
addons_basedir = os.path.join(os.getcwd(), "mhn/addons/")
addons = AddOns.query.all()

for addon in addons:
if addon.dir_name not in mhn.config['DISABLED_ADDONS']:
fp, pathname, description = imp.find_module('views', [os.path.join(addons_basedir, addon.dir_name)])
loaded_module = imp.load_module(addon.dir_name, fp, pathname, description)
mhn.register_blueprint(getattr(loaded_module, addon.dir_name))

addon.reboot = False
db.session.add(addon)
db.session.commit()

# Trigger templatetag register.
from mhn.common.templatetags import format_date
mhn.jinja_env.filters['fdate'] = format_date
Expand Down
1 change: 1 addition & 0 deletions server/mhn/addons/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__author__ = 'mercolino'
7 changes: 7 additions & 0 deletions server/mhn/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@
API_SOURCE_EXISTS = 'Source with uri "{}" already exists.'
API_NOT_AUTHORIZED = 'Not authorized to perform this request.'
API_RESOURCE_NOT_FOUND = 'Requested resource was not found.'
API_ADDON_NAME_INVALID = '"{}", Name not valid. ex: addon_name.tar.gz'
API_ADDON_EXTENSION_INVALID = 'The addon should be a tar.gz file'
API_ADDON_NAME_EXISTS = 'The name {} already exists!'
API_ADDON_UPLOAD_PROBLEM = 'There was a problem uploading {}'
API_ADDON_NOT_FOUND = "The Add-On doesn't exists"
API_ADDON_NOT_DELETED = "The Add-On could not be deleted"
API_ADDON_NOT_TARFILE = "It Appears that the file {} is not a tar.gz file"
29 changes: 29 additions & 0 deletions server/mhn/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,32 @@ def __repr__(self):
def to_dict(self):
return dict(script=self.script, date=self.date, notes=self.notes,
user=self.user.email, id=self.id)


class AddOns(db.Model, APIModel):
all_fields = {
'menu_name': {'required': True, 'editable': True},
'dir_name': {'required': True, 'editable': False},
'active': {'required': False, 'editable': True},
'reboot': {'required': False, 'editable': False}
}

__tablename__ = 'addons'

id = db.Column(db.Integer, primary_key=True)
menu_name = db.Column(db.String(50), nullable=False)
dir_name = db.Column(db.String(100), nullable=False, unique=True)
active = db.Column(db.Boolean, nullable=False)
reboot = db.Column(db.Boolean, nullable=False)

def __init__(self, menu_name=None, dir_name=None, active=None, reboot=None):
self.menu_name = menu_name
self.dir_name = dir_name
self.active = active
self.reboot = reboot

def __repr__(self):
return '<Add-Ons>{}'.format(self.to_dict())

def to_dict(self):
return dict(menu_name=self.menu_name, dir_name=self.dir_name, active=self.active, reboot=self.reboot)
14 changes: 8 additions & 6 deletions server/mhn/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ def create_user():
return error_response(
apierrors.API_FIELDS_MISSING.format(missing), 400)
else:
user = get_datastore().create_user(
try:
user = get_datastore().create_user(
email=request.json.get('email'),
password=encrypt_password(request.json.get('password')))
userrole = user_datastore.find_role('admin')
user_datastore.add_role_to_user(user, userrole)
userrole = user_datastore.find_role('admin')
user_datastore.add_role_to_user(user, userrole)

try:
db.session.add(user)
db.session.flush()

Expand All @@ -81,8 +81,10 @@ def delete_user(user_id):
user = User.query.get(user_id)
if not user:
return error_response(errors.AUTH_NOT_FOUND.format(user_id), 404)
user.active= False
db.session.add(user)
#user.active= False
#db.session.add(user)
#Lets delete the user instead of deactivate them
db.session.delete(user)
db.session.commit()
return jsonify({})

Expand Down
7 changes: 6 additions & 1 deletion server/mhn/common/contextprocessors.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
from flask import current_app

import mhn.common.utils as utils

def config_ctx():
"""
Inserts some settings to be used in templates.
"""

settings = {
'server_url': current_app.config['SERVER_BASE_URL'],
'honeymap_url': current_app.config['HONEYMAP_URL'],
'deploy_key': current_app.config['DEPLOY_KEY'],
'supported_honeypots': current_app.config['HONEYPOT_CHANNELS'].keys()
'supported_honeypots': current_app.config['HONEYPOT_CHANNELS'].keys(),
'add_ons_enabled': current_app.config['ADD_ONS']
}
if current_app.config['ADD_ONS']:
settings['add_ons'] = utils.get_addons()
return dict(settings=settings)
57 changes: 55 additions & 2 deletions server/mhn/common/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,61 @@
from math import ceil

from flask import jsonify, g
from flask import jsonify, g, current_app

from mhn.constants import PAGE_SIZE
from mhn.constants import PAGE_SIZE, ALLOWED_ADDON_EXTENSIONS

from mhn.api.models import AddOns

import os
import pwd
import grp

import mhn.api.errors as apierrors

def change_own(path, user, group):
uid = pwd.getpwnam(user).pw_uid
gid = grp.getgrnam(group).gr_gid
os.chown(path, uid, gid)
for item in os.listdir(path):
itempath = os.path.join(path, item)
if os.path.isfile(itempath):
os.chown(itempath, uid, gid)
elif os.path.isdir(itempath):
os.chown(itempath, uid, gid)
change_own(itempath, user, group)


def get_addons():
add_ons = []
for addon in AddOns.query.all():
if addon.active == True and addon.reboot == False:
if addon.dir_name not in current_app.config['DISABLED_ADDONS']:
add_ons.append((addon.dir_name, addon.menu_name))
return add_ons


def allowed_addon_filename(filename):
"""
Function to check if the nam of the file to upload is valid
:return:
return a tuple (Boolean, Error_Text)
True: If the filename follows a pattern [name_of_the_file_without_spaces].allowed_extension, Error is empty
False: Pattern is no correct, Error send
"""
extensions = filename.split(".")
if len(extensions) > 2:
extension = extensions[1]
for subext in extensions[2:]:
extension = extension + "." + subext
else:
extension = extensions[1]

if (len(extensions) != 3) or (len(extensions[0].split(" ")) !=1):
return (False, apierrors.API_ADDON_NAME_INVALID.format(filename))
elif extension not in ALLOWED_ADDON_EXTENSIONS:
return (False, apierrors.API_ADDON_EXTENSION_INVALID)

return (True, '', '')


def error_response(message, status_code=400):
Expand Down
1 change: 1 addition & 0 deletions server/mhn/constants.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
PAGE_SIZE = 15
ALLOWED_ADDON_EXTENSIONS = ['tar.gz']
19 changes: 19 additions & 0 deletions server/mhn/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,25 @@ $(document).ready(function() {
});
});

$('.delete-addon').click(function(e) {
e.preventDefault();
var addonId = $(this).attr('addon-id');

$.ajax({
type: 'DELETE',
headers: {'X-CSRFToken': $('#_csrf_token').val()},
url: '/ui/delete_addon/' + addonId + '/',
contentType: 'application/json',
success: function(resp) {
window.location.reload();

},
error: function(resp) {
alert('Could not delete add-on.');
}
});
});

if ($('#pass-form').length >= 1) {
$('#submit-pass').click(function(e) {
e.preventDefault();
Expand Down
Loading