mirror of https://github.com/MISP/misp-dashboard
new: [authentication] Flask-login authentication via MISP instance.
parent
60ce6ce5cd
commit
2be101fdfc
|
@ -3,6 +3,10 @@ host = localhost
|
|||
port = 8001
|
||||
debug = False
|
||||
|
||||
[Auth]
|
||||
misp_fqdn = "https://misp.local"
|
||||
|
||||
|
||||
[Dashboard]
|
||||
#hours
|
||||
graph_log_refresh_rate = 1
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
argparse
|
||||
flask
|
||||
flask-login
|
||||
wtforms
|
||||
geoip2
|
||||
redis
|
||||
phonenumbers
|
||||
|
|
156
server.py
156
server.py
|
@ -6,6 +6,7 @@ import json
|
|||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import random
|
||||
from time import gmtime as now
|
||||
from time import sleep, strftime
|
||||
|
@ -14,10 +15,14 @@ import redis
|
|||
|
||||
import util
|
||||
from flask import (Flask, Response, jsonify, render_template, request,
|
||||
send_from_directory, stream_with_context)
|
||||
send_from_directory, stream_with_context, url_for, redirect)
|
||||
from flask_login import (UserMixin, LoginManager, current_user, login_user, logout_user, login_required)
|
||||
from helpers import (contributor_helper, geo_helper, live_helper,
|
||||
trendings_helper, users_helper)
|
||||
|
||||
import requests
|
||||
from wtforms import Form, SubmitField, StringField, PasswordField, validators
|
||||
|
||||
configfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config/config.cfg')
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(configfile)
|
||||
|
@ -28,6 +33,7 @@ logger.setLevel(logging.ERROR)
|
|||
server_host = cfg.get("Server", "host")
|
||||
server_port = cfg.getint("Server", "port")
|
||||
server_debug = cfg.get("Server", "debug")
|
||||
auth_host = cfg.get("Auth", "misp_fqdn")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
@ -56,6 +62,113 @@ contributor_helper = contributor_helper.Contributor_helper(serv_redis_db, cfg)
|
|||
users_helper = users_helper.Users_helper(serv_redis_db, cfg)
|
||||
trendings_helper = trendings_helper.Trendings_helper(serv_redis_db, cfg)
|
||||
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.init_app(app)
|
||||
|
||||
##########
|
||||
## Auth ##
|
||||
##########
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, id, password):
|
||||
self.id = id
|
||||
self.password = password
|
||||
|
||||
def misp_login(self):
|
||||
"""
|
||||
Use login form data to authenticate a user to MISP.
|
||||
|
||||
This function uses requests to log a user into the MISP web UI. When authentication is successful MISP redirects the client to the '/users/routeafterlogin' endpoint. The requests session history is parsed for a redirect to this endpoint.
|
||||
:param misp_url: The FQDN of a MISP instance to authenticate against.
|
||||
:param user: The user account to authenticate.
|
||||
:param password: The user account password.
|
||||
:return:
|
||||
"""
|
||||
post_data = {
|
||||
"data[_Token][key]": "",
|
||||
"data[_Token][fields]": "",
|
||||
"data[_Token][unlocked]": "",
|
||||
"data[User][email]": self.id,
|
||||
"data[User][password]": self.password,
|
||||
}
|
||||
|
||||
misp_login_page = auth_host + "/users/login"
|
||||
session = requests.Session()
|
||||
|
||||
# The login page contains hidden form values required for authenticaiton.
|
||||
login_page = session.get(misp_login_page, ssl=True)
|
||||
|
||||
# This regex matches the "data[_Token][fields]" value needed to make a POST request on the MISP login page.
|
||||
token_fields_exp = re.compile(r'name="data\[_Token]\[fields]" value="([^\s]+)"')
|
||||
token_fields = token_fields_exp.search(login_page.text)
|
||||
|
||||
# This regex matches the "data[_Token][fields]" value needed to make a POST request on the MISP login page.
|
||||
token_key_exp = re.compile(r'name="data\[_Token]\[key]" value="([^\s]+)"')
|
||||
token_key = token_key_exp.search(login_page.text)
|
||||
|
||||
post_data["data[_Token][fields]"] = token_fields.group(1)
|
||||
post_data["data[_Token][key]"] = token_key.group(1)
|
||||
|
||||
# POST request with user credentials + hidden form values.
|
||||
post_to_login_page = session.post(misp_login_page, data=post_data)
|
||||
|
||||
# Authentication is successful if MISP returns a redirect to '/users/routeafterlogin'.
|
||||
for resp in post_to_login_page.history:
|
||||
if resp.url == auth_host + '/users/routeafterlogin':
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
"""
|
||||
Return a User object required by flask-login to keep state of a user session.
|
||||
|
||||
Typically load_user is used to perform a user lookup on a db; it should return a User object or None if the user is not found. Authentication is defered to MISP via User.misp_login() and so this function always returns a User object .
|
||||
:param user_id: A MISP username.
|
||||
:return:
|
||||
"""
|
||||
return User(user_id, "")
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
"""
|
||||
Logout the user and redirect to the login form.
|
||||
:return:
|
||||
"""
|
||||
logout_user()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET','POST'])
|
||||
def login():
|
||||
"""
|
||||
Login form.
|
||||
:return:
|
||||
"""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
form = LoginForm(request.form)
|
||||
if request.method == 'POST' and form.validate():
|
||||
user = User(form.username.data, form.password.data)
|
||||
|
||||
if user.misp_login():
|
||||
login_user(user)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return redirect(url_for('login'))
|
||||
return render_template('login.html', title='Login', form=form)
|
||||
|
||||
|
||||
|
||||
class LoginForm(Form):
|
||||
username = StringField('Username', [validators.Length(min=4, max=50)])
|
||||
password = PasswordField('Password', [validators.Length(min=4, max=50)])
|
||||
submit = SubmitField('Sign In')
|
||||
|
||||
|
||||
##########
|
||||
## UTIL ##
|
||||
|
@ -159,6 +272,7 @@ class EventMessage():
|
|||
''' MAIN ROUTE '''
|
||||
|
||||
@app.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
ratioCorrection = 88
|
||||
pannelSize = [
|
||||
|
@ -180,11 +294,13 @@ def index():
|
|||
)
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
@login_required
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app.root_path, 'static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
@app.route("/geo")
|
||||
@login_required
|
||||
def geo():
|
||||
return render_template('geo.html',
|
||||
zoomlevel=cfg.getint('GEO' ,'zoomlevel'),
|
||||
|
@ -192,6 +308,7 @@ def geo():
|
|||
)
|
||||
|
||||
@app.route("/contrib")
|
||||
@login_required
|
||||
def contrib():
|
||||
categ_list = contributor_helper.categories_in_datatable
|
||||
categ_list_str = [ s[0].upper() + s[1:].replace('_', ' ') for s in categ_list]
|
||||
|
@ -243,12 +360,14 @@ def contrib():
|
|||
)
|
||||
|
||||
@app.route("/users")
|
||||
@login_required
|
||||
def users():
|
||||
return render_template('users.html',
|
||||
)
|
||||
|
||||
|
||||
@app.route("/trendings")
|
||||
@login_required
|
||||
def trendings():
|
||||
maxNum = request.args.get('maxNum')
|
||||
try:
|
||||
|
@ -265,6 +384,7 @@ def trendings():
|
|||
''' INDEX '''
|
||||
|
||||
@app.route("/_logs")
|
||||
@login_required
|
||||
def logs():
|
||||
if request.accept_mimetypes.accept_json or request.method == 'POST':
|
||||
key = 'Attribute'
|
||||
|
@ -283,6 +403,7 @@ def logs():
|
|||
return Response(stream_with_context(event_stream_log()), mimetype="text/event-stream")
|
||||
|
||||
@app.route("/_maps")
|
||||
@login_required
|
||||
def maps():
|
||||
if request.accept_mimetypes.accept_json or request.method == 'POST':
|
||||
key = 'Map'
|
||||
|
@ -292,6 +413,7 @@ def maps():
|
|||
return Response(event_stream_maps(), mimetype="text/event-stream")
|
||||
|
||||
@app.route("/_get_log_head")
|
||||
@login_required
|
||||
def getLogHead():
|
||||
return json.dumps(LogItem('').get_head_row())
|
||||
|
||||
|
@ -325,6 +447,7 @@ def event_stream_maps():
|
|||
''' GEO '''
|
||||
|
||||
@app.route("/_getTopCoord")
|
||||
@login_required
|
||||
def getTopCoord():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -334,6 +457,7 @@ def getTopCoord():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getHitMap")
|
||||
@login_required
|
||||
def getHitMap():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -343,6 +467,7 @@ def getHitMap():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getCoordsByRadius")
|
||||
@login_required
|
||||
def getCoordsByRadius():
|
||||
try:
|
||||
dateStart = datetime.datetime.fromtimestamp(float(request.args.get('dateStart')))
|
||||
|
@ -359,14 +484,17 @@ def getCoordsByRadius():
|
|||
''' CONTRIB '''
|
||||
|
||||
@app.route("/_getLastContributors")
|
||||
@login_required
|
||||
def getLastContributors():
|
||||
return jsonify(contributor_helper.getLastContributorsFromRedis())
|
||||
|
||||
@app.route("/_eventStreamLastContributor")
|
||||
@login_required
|
||||
def getLastContributor():
|
||||
return Response(eventStreamLastContributor(), mimetype="text/event-stream")
|
||||
|
||||
@app.route("/_eventStreamAwards")
|
||||
@login_required
|
||||
def getLastStreamAwards():
|
||||
return Response(eventStreamAwards(), mimetype="text/event-stream")
|
||||
|
||||
|
@ -404,6 +532,7 @@ def eventStreamAwards():
|
|||
subscriber_lastAwards.unsubscribe()
|
||||
|
||||
@app.route("/_getTopContributor")
|
||||
@login_required
|
||||
def getTopContributor(suppliedDate=None, maxNum=100):
|
||||
if suppliedDate is None:
|
||||
try:
|
||||
|
@ -417,6 +546,7 @@ def getTopContributor(suppliedDate=None, maxNum=100):
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getFameContributor")
|
||||
@login_required
|
||||
def getFameContributor():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -427,6 +557,7 @@ def getFameContributor():
|
|||
return getTopContributor(suppliedDate=date, maxNum=10)
|
||||
|
||||
@app.route("/_getFameQualContributor")
|
||||
@login_required
|
||||
def getFameQualContributor():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -437,10 +568,12 @@ def getFameQualContributor():
|
|||
return getTopContributor(suppliedDate=date, maxNum=10)
|
||||
|
||||
@app.route("/_getTop5Overtime")
|
||||
@login_required
|
||||
def getTop5Overtime():
|
||||
return jsonify(contributor_helper.getTop5OvertimeFromRedis())
|
||||
|
||||
@app.route("/_getOrgOvertime")
|
||||
@login_required
|
||||
def getOrgOvertime():
|
||||
try:
|
||||
org = request.args.get('org')
|
||||
|
@ -449,6 +582,7 @@ def getOrgOvertime():
|
|||
return jsonify(contributor_helper.getOrgOvertime(org))
|
||||
|
||||
@app.route("/_getCategPerContrib")
|
||||
@login_required
|
||||
def getCategPerContrib():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -458,6 +592,7 @@ def getCategPerContrib():
|
|||
return jsonify(contributor_helper.getCategPerContribFromRedis(date))
|
||||
|
||||
@app.route("/_getLatestAwards")
|
||||
@login_required
|
||||
def getLatestAwards():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -467,10 +602,12 @@ def getLatestAwards():
|
|||
return jsonify(contributor_helper.getLastAwardsFromRedis())
|
||||
|
||||
@app.route("/_getAllOrg")
|
||||
@login_required
|
||||
def getAllOrg():
|
||||
return jsonify(contributor_helper.getAllOrgFromRedis())
|
||||
|
||||
@app.route("/_getOrgRank")
|
||||
@login_required
|
||||
def getOrgRank():
|
||||
try:
|
||||
org = request.args.get('org')
|
||||
|
@ -479,6 +616,7 @@ def getOrgRank():
|
|||
return jsonify(contributor_helper.getCurrentOrgRankFromRedis(org))
|
||||
|
||||
@app.route("/_getContributionOrgStatus")
|
||||
@login_required
|
||||
def getContributionOrgStatus():
|
||||
try:
|
||||
org = request.args.get('org')
|
||||
|
@ -487,6 +625,7 @@ def getContributionOrgStatus():
|
|||
return jsonify(contributor_helper.getCurrentContributionStatus(org))
|
||||
|
||||
@app.route("/_getHonorBadges")
|
||||
@login_required
|
||||
def getHonorBadges():
|
||||
try:
|
||||
org = request.args.get('org')
|
||||
|
@ -495,6 +634,7 @@ def getHonorBadges():
|
|||
return jsonify(contributor_helper.getOrgHonorBadges(org))
|
||||
|
||||
@app.route("/_getTrophies")
|
||||
@login_required
|
||||
def getTrophies():
|
||||
try:
|
||||
org = request.args.get('org')
|
||||
|
@ -503,7 +643,9 @@ def getTrophies():
|
|||
return jsonify(contributor_helper.getOrgTrophies(org))
|
||||
|
||||
@app.route("/_getAllOrgsTrophyRanking")
|
||||
@login_required
|
||||
@app.route("/_getAllOrgsTrophyRanking/<string:categ>")
|
||||
@login_required
|
||||
def getAllOrgsTrophyRanking(categ=None):
|
||||
return jsonify(contributor_helper.getAllOrgsTrophyRanking(categ))
|
||||
|
||||
|
@ -511,6 +653,7 @@ def getAllOrgsTrophyRanking(categ=None):
|
|||
''' USERS '''
|
||||
|
||||
@app.route("/_getUserLogins")
|
||||
@login_required
|
||||
def getUserLogins():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -522,10 +665,12 @@ def getUserLogins():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getAllLoggedOrg")
|
||||
@login_required
|
||||
def getAllLoggedOrg():
|
||||
return jsonify(users_helper.getAllOrg())
|
||||
|
||||
@app.route("/_getTopOrglogin")
|
||||
@login_required
|
||||
def getTopOrglogin():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -536,6 +681,7 @@ def getTopOrglogin():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getLoginVSCOntribution")
|
||||
@login_required
|
||||
def getLoginVSCOntribution():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -546,6 +692,7 @@ def getLoginVSCOntribution():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getUserLoginsAndContribOvertime")
|
||||
@login_required
|
||||
def getUserLoginsAndContribOvertime():
|
||||
try:
|
||||
date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
|
||||
|
@ -558,6 +705,7 @@ def getUserLoginsAndContribOvertime():
|
|||
|
||||
''' TRENDINGS '''
|
||||
@app.route("/_getTrendingEvents")
|
||||
@login_required
|
||||
def getTrendingEvents():
|
||||
try:
|
||||
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
|
||||
|
@ -571,6 +719,7 @@ def getTrendingEvents():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getTrendingCategs")
|
||||
@login_required
|
||||
def getTrendingCategs():
|
||||
try:
|
||||
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
|
||||
|
@ -584,6 +733,7 @@ def getTrendingCategs():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getTrendingTags")
|
||||
@login_required
|
||||
def getTrendingTags():
|
||||
try:
|
||||
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
|
||||
|
@ -597,6 +747,7 @@ def getTrendingTags():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getTrendingSightings")
|
||||
@login_required
|
||||
def getTrendingSightings():
|
||||
try:
|
||||
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
|
||||
|
@ -609,6 +760,7 @@ def getTrendingSightings():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getTrendingDisc")
|
||||
@login_required
|
||||
def getTrendingDisc():
|
||||
try:
|
||||
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
|
||||
|
@ -622,6 +774,7 @@ def getTrendingDisc():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getTypeaheadData")
|
||||
@login_required
|
||||
def getTypeaheadData():
|
||||
try:
|
||||
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
|
||||
|
@ -634,6 +787,7 @@ def getTypeaheadData():
|
|||
return jsonify(data)
|
||||
|
||||
@app.route("/_getGenericTrendingOvertime")
|
||||
@login_required
|
||||
def getGenericTrendingOvertime():
|
||||
try:
|
||||
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
|
||||
|
|
Loading…
Reference in New Issue