diff --git a/addon_example/hello_addon.tar.gz b/addon_example/hello_addon.tar.gz
new file mode 100644
index 00000000..bf9c1901
Binary files /dev/null and b/addon_example/hello_addon.tar.gz differ
diff --git a/addon_example/hello_addon/__init__.py b/addon_example/hello_addon/__init__.py
new file mode 100644
index 00000000..1e5303ba
--- /dev/null
+++ b/addon_example/hello_addon/__init__.py
@@ -0,0 +1 @@
+__author__ = 'mercolino'
diff --git a/addon_example/hello_addon/templates/hello_world.html b/addon_example/hello_addon/templates/hello_world.html
new file mode 100644
index 00000000..20e44f99
--- /dev/null
+++ b/addon_example/hello_addon/templates/hello_world.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+{% block title %} Hello World Add-on{% endblock %}
+{% block content %}
+
+
+
Hello World Add-On
+
+
+
+
Here it is the Add-On Content.
+
You could add everything you want here!!!
+
+
+
+{% endblock %}
+
+{% block footer %}
+
+
+{% endblock %}
diff --git a/addon_example/hello_addon/views.py b/addon_example/hello_addon/views.py
new file mode 100644
index 00000000..c95bae41
--- /dev/null
+++ b/addon_example/hello_addon/views.py
@@ -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'
\ No newline at end of file
diff --git a/scripts/deploy_kippo.sh b/scripts/deploy_kippo.sh
index d940db3e..a02b92ba 100755
--- a/scripts/deploy_kippo.sh
+++ b/scripts/deploy_kippo.sh
@@ -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 <{}'.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)
\ No newline at end of file
diff --git a/server/mhn/auth/views.py b/server/mhn/auth/views.py
index 73546d9e..59628add 100644
--- a/server/mhn/auth/views.py
+++ b/server/mhn/auth/views.py
@@ -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()
@@ -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({})
diff --git a/server/mhn/common/contextprocessors.py b/server/mhn/common/contextprocessors.py
index 8fe808d9..ffe6cef5 100644
--- a/server/mhn/common/contextprocessors.py
+++ b/server/mhn/common/contextprocessors.py
@@ -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)
diff --git a/server/mhn/common/utils.py b/server/mhn/common/utils.py
index c32432fa..6b758d24 100644
--- a/server/mhn/common/utils.py
+++ b/server/mhn/common/utils.py
@@ -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):
diff --git a/server/mhn/constants.py b/server/mhn/constants.py
index 6e07ccf5..5c4eea2d 100644
--- a/server/mhn/constants.py
+++ b/server/mhn/constants.py
@@ -1 +1,2 @@
PAGE_SIZE = 15
+ALLOWED_ADDON_EXTENSIONS = ['tar.gz']
diff --git a/server/mhn/static/js/main.js b/server/mhn/static/js/main.js
index 327acd7e..71979dfa 100644
--- a/server/mhn/static/js/main.js
+++ b/server/mhn/static/js/main.js
@@ -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();
diff --git a/server/mhn/templates/base.html b/server/mhn/templates/base.html
index 5aa2b9da..b1632abb 100644
--- a/server/mhn/templates/base.html
+++ b/server/mhn/templates/base.html
@@ -42,10 +42,19 @@
Add sensor
-
-
-
-
+
+ {% if settings.add_ons_enabled %}
+
+ Add-Ons
+
+ {% for item in settings.add_ons%}
+ - {{ item[1] }}
+ {% endfor %}
+
+
+ {% endif %}
+
+
Charts
- Kippo Top Passwords
@@ -61,13 +70,17 @@
+ {% if current_user.has_role('admin') %}
+ -
+ Settings
+
+
+ {% endif %}
- LOGOUT
- {% if current_user.has_role('admin') %}
-
- {% endif %}
{% endif %}
diff --git a/server/mhn/templates/ui/addons_settings.html b/server/mhn/templates/ui/addons_settings.html
new file mode 100644
index 00000000..bd87f9aa
--- /dev/null
+++ b/server/mhn/templates/ui/addons_settings.html
@@ -0,0 +1,139 @@
+{% extends "base.html" %}
+{% block title %}Settings{% endblock %}
+{% block content %}
+
+ {% for a in active_addons %}
+
+
+
+
+
+ {{ a.menu_name }}
+
+
+
+
+
+
+ {% else %}
+
+
+
+
+
+ There are no Active Add-Ons
+
+
+
+
+
+ {% endfor %}
+
+
+ {% for i in inactive_addons %}
+
+
+
+
+
+ {{ i.menu_name }}
+
+
+
+
+
+
+ {% else %}
+
+
+
+
+
+ There are no Inactive Add-Ons
+
+
+
+
+
+ {% endfor %}
+ {% if need_reboot %}
+
+
+
+
+ There are recently loaded add-ons, this add-ons will be loaded by mhn after a reboot of the server:
+ Issue: sudo supervisorctl restart mhn-uwsgi
+
+
+
+ {% endif %}
+
+
+{% endblock %}
diff --git a/server/mhn/templates/ui/settings.html b/server/mhn/templates/ui/user_settings.html
similarity index 100%
rename from server/mhn/templates/ui/settings.html
rename to server/mhn/templates/ui/user_settings.html
diff --git a/server/mhn/ui/constants.py b/server/mhn/ui/constants.py
index ac27f1a2..7ade0858 100644
--- a/server/mhn/ui/constants.py
+++ b/server/mhn/ui/constants.py
@@ -1 +1 @@
-DEFAULT_FLAG_URL = '/static/img/unknown.png'
+DEFAULT_FLAG_URL = '/static/img/unknown.png'
\ No newline at end of file
diff --git a/server/mhn/ui/utils.py b/server/mhn/ui/utils.py
index f9dedcd7..716df958 100644
--- a/server/mhn/ui/utils.py
+++ b/server/mhn/ui/utils.py
@@ -73,4 +73,4 @@ def _get_flag_ip(ipaddr):
if os.path.exists(MHN_SERVER_HOME +"/mhn"+flag):
return flag
else:
- return constants.DEFAULT_FLAG_URL
+ return constants.DEFAULT_FLAG_URL
\ No newline at end of file
diff --git a/server/mhn/ui/views.py b/server/mhn/ui/views.py
index e481c5e7..2b56c0fa 100644
--- a/server/mhn/ui/views.py
+++ b/server/mhn/ui/views.py
@@ -3,20 +3,26 @@
import pygal
from flask import (
Blueprint, render_template, request, url_for,
- redirect, g)
+ redirect, g, jsonify, flash)
from flask_security import logout_user as logout
-from sqlalchemy import desc, func
+from sqlalchemy import desc, func, and_
from mhn.ui.utils import get_flag_ip, get_sensor_name
from mhn.api.models import (
Sensor, Rule, DeployScript as Script,
- RuleSource)
-from mhn.auth import login_required, current_user
+ RuleSource, AddOns)
+from mhn.auth import login_required, current_user, roles_accepted
from mhn.auth.models import User, PasswdReset, ApiKey
from mhn import db, mhn
from mhn.common.utils import (
- paginate_options, alchemy_pages, mongo_pages)
+ paginate_options, alchemy_pages, mongo_pages, allowed_addon_filename, error_response, change_own)
from mhn.common.clio import Clio
+from mhn.api import errors as apierrors
+from sqlalchemy.exc import IntegrityError
+from werkzeug import secure_filename
+import os
+import shutil
+import tarfile
ui = Blueprint('ui', __name__, url_prefix='/ui')
from mhn import mhn as app
@@ -157,21 +163,145 @@ def deploy_mgmt():
'ui/script.html', scripts=Script.query.order_by(Script.date.desc()),
script=script)
+
@ui.route('/honeymap/', methods=['GET'])
@login_required
def honeymap():
return render_template('ui/honeymap.html')
+
@ui.route('/add-user/', methods=['GET'])
@login_required
-def settings():
+def user_settings():
return render_template(
- 'ui/settings.html',
+ 'ui/user_settings.html',
users=User.query.filter_by(active=True),
apikey=ApiKey.query.filter_by(user_id=current_user.id).first()
)
+@ui.route('/add-addons/', methods=['GET'])
+@login_required
+def addons_settings():
+ if AddOns.query.filter(and_(AddOns.reboot == True, AddOns.active == True)).count() != 0:
+ return render_template('ui/addons_settings.html',
+ active_addons=AddOns.query.filter_by(active=True),
+ inactive_addons=AddOns.query.filter_by(active=False), need_reboot=True)
+ else:
+ return render_template('ui/addons_settings.html',
+ active_addons=AddOns.query.filter_by(active=True),
+ inactive_addons=AddOns.query.filter_by(active=False))
+
+
+@ui.route('/load-addons/', methods=['POST'])
+@login_required
+def load_addons():
+ data = request.form.to_dict()
+ addon_file = request.files['dir_name']
+
+ data['reboot'] = True
+
+ if 'active' not in data:
+ data['active'] = False
+ else:
+ data['active'] = True
+
+ if 'dir_name' not in data:
+ data['dir_name'] = secure_filename(addon_file.filename)
+
+ missing = AddOns.check_required(data)
+ if missing:
+ flash(apierrors.API_FIELDS_MISSING.format(missing), 'alert-box alert round')
+ return redirect(url_for('ui.addons_settings'))
+ else:
+ try:
+ addon = AddOns()
+ addon.menu_name = data['menu_name']
+ filename = data['dir_name']
+ resp = allowed_addon_filename(filename)
+ if not resp[0]:
+ flash(resp[1], 'alert-box alert round')
+ return redirect(url_for('ui.addons_settings'))
+
+ try:
+ addon_file.save(os.path.join(os.getcwd(), "mhn/addons/", filename))
+ except:
+ flash(apierrors.API_ADDON_UPLOAD_PROBLEM.format(addon.dir_name), 'alert-box alert round')
+ os.remove(os.path.join(os.getcwd(), "mhn/addons/", filename))
+ return redirect(url_for('ui.addons_settings'))
+
+
+ addon.dir_name = filename.split(".")[0]
+ addon.active = data['active']
+ addon.reboot = data['reboot']
+
+ try:
+ tar = tarfile.open(os.path.join(os.getcwd(), "mhn/addons/", filename))
+ tar.extractall(os.path.join(os.getcwd(), "mhn/addons/"))
+ tar.close()
+ os.remove(os.path.join(os.getcwd(), "mhn/addons/", filename))
+ change_own(os.path.join(os.getcwd(), "mhn/addons/", addon.dir_name), "www-data", "www-data")
+ except:
+ flash(apierrors.API_ADDON_NOT_TARFILE.format(addon.dir_name), 'alert-box alert round')
+ if os.path.exists(os.path.join(os.getcwd(), "mhn/addons/", filename)):
+ os.remove(os.path.join(os.getcwd(), "mhn/addons/", filename))
+ elif os.path.exists(os.path.join(os.getcwd(), "mhn/addons/", addon.dir_name)):
+ shutil.rmtree(os.path.join(os.getcwd(), "mhn/addons/", addon.dir_name))
+ return redirect(url_for('ui.addons_settings'))
+
+
+ db.session.add(addon)
+ db.session.commit()
+
+ except IntegrityError:
+ flash(apierrors.API_ADDON_NAME_EXISTS.format(addon.dir_name), 'alert-box alert round')
+ if os.path.exists(os.path.join(os.getcwd(), "mhn/addons/", filename)):
+ os.remove(os.path.join(os.getcwd(), "mhn/addons/", filename))
+ elif os.path.exists(os.path.join(os.getcwd(), "mhn/addons/", addon.dir_name)):
+ shutil.rmtree(os.path.join(os.getcwd(), "mhn/addons/", addon.dir_name))
+ return redirect(url_for('ui.addons_settings'))
+ else:
+ flash('Add-On {} Successfully Loaded'.format(addon.dir_name), 'alert-box success round')
+ return redirect(url_for('ui.addons_settings'))
+
+
+@ui.route('/toggle_activate_addon//', methods=['GET'])
+@login_required
+@roles_accepted('admin')
+def toggle_active_addon(addon_id, toggle_action):
+ addon = AddOns.query.get(addon_id)
+ if not addon:
+ return redirect(url_for('ui.addons_settings'))
+ if toggle_action == 'deactivate':
+ addon.active = False
+ elif toggle_action == 'activate':
+ addon.active = True
+ else:
+ return redirect(url_for('ui.addons_settings'))
+
+ db.session.add(addon)
+ db.session.commit()
+
+ return redirect(url_for('ui.addons_settings'))
+
+
+@ui.route('/delete_addon//', methods=['DELETE'])
+@roles_accepted('admin')
+def delete_addon(addon_id):
+ addon = AddOns.query.get(addon_id)
+ if not addon:
+ return error_response(apierrors.API_ADDON_NOT_FOUND, 404)
+
+ try:
+ shutil.rmtree(os.path.join(os.getcwd(), "mhn/addons/", addon.dir_name))
+ except:
+ return error_response(apierrors.API_ADDON_NOT_DELETED, 400)
+
+ db.session.delete(addon)
+ db.session.commit()
+ return jsonify({})
+
+
@ui.route('/forgot-password//', methods=['GET'])
def forgot_passwd(hashstr):
logout()