new: [authentication] Flask-login authentication via MISP instance.

pull/129/head
VVX7 2019-10-01 21:06:29 -04:00
parent 60ce6ce5cd
commit 2be101fdfc
3 changed files with 161 additions and 1 deletions

View File

@ -3,6 +3,10 @@ host = localhost
port = 8001 port = 8001
debug = False debug = False
[Auth]
misp_fqdn = "https://misp.local"
[Dashboard] [Dashboard]
#hours #hours
graph_log_refresh_rate = 1 graph_log_refresh_rate = 1

View File

@ -1,5 +1,7 @@
argparse argparse
flask flask
flask-login
wtforms
geoip2 geoip2
redis redis
phonenumbers phonenumbers

156
server.py
View File

@ -6,6 +6,7 @@ import json
import logging import logging
import math import math
import os import os
import re
import random import random
from time import gmtime as now from time import gmtime as now
from time import sleep, strftime from time import sleep, strftime
@ -14,10 +15,14 @@ import redis
import util import util
from flask import (Flask, Response, jsonify, render_template, request, 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, from helpers import (contributor_helper, geo_helper, live_helper,
trendings_helper, users_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') configfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config/config.cfg')
cfg = configparser.ConfigParser() cfg = configparser.ConfigParser()
cfg.read(configfile) cfg.read(configfile)
@ -28,6 +33,7 @@ logger.setLevel(logging.ERROR)
server_host = cfg.get("Server", "host") server_host = cfg.get("Server", "host")
server_port = cfg.getint("Server", "port") server_port = cfg.getint("Server", "port")
server_debug = cfg.get("Server", "debug") server_debug = cfg.get("Server", "debug")
auth_host = cfg.get("Auth", "misp_fqdn")
app = Flask(__name__) 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) users_helper = users_helper.Users_helper(serv_redis_db, cfg)
trendings_helper = trendings_helper.Trendings_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 ## ## UTIL ##
@ -159,6 +272,7 @@ class EventMessage():
''' MAIN ROUTE ''' ''' MAIN ROUTE '''
@app.route("/") @app.route("/")
@login_required
def index(): def index():
ratioCorrection = 88 ratioCorrection = 88
pannelSize = [ pannelSize = [
@ -180,11 +294,13 @@ def index():
) )
@app.route('/favicon.ico') @app.route('/favicon.ico')
@login_required
def favicon(): def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'), return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon') 'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route("/geo") @app.route("/geo")
@login_required
def geo(): def geo():
return render_template('geo.html', return render_template('geo.html',
zoomlevel=cfg.getint('GEO' ,'zoomlevel'), zoomlevel=cfg.getint('GEO' ,'zoomlevel'),
@ -192,6 +308,7 @@ def geo():
) )
@app.route("/contrib") @app.route("/contrib")
@login_required
def contrib(): def contrib():
categ_list = contributor_helper.categories_in_datatable categ_list = contributor_helper.categories_in_datatable
categ_list_str = [ s[0].upper() + s[1:].replace('_', ' ') for s in categ_list] categ_list_str = [ s[0].upper() + s[1:].replace('_', ' ') for s in categ_list]
@ -243,12 +360,14 @@ def contrib():
) )
@app.route("/users") @app.route("/users")
@login_required
def users(): def users():
return render_template('users.html', return render_template('users.html',
) )
@app.route("/trendings") @app.route("/trendings")
@login_required
def trendings(): def trendings():
maxNum = request.args.get('maxNum') maxNum = request.args.get('maxNum')
try: try:
@ -265,6 +384,7 @@ def trendings():
''' INDEX ''' ''' INDEX '''
@app.route("/_logs") @app.route("/_logs")
@login_required
def logs(): def logs():
if request.accept_mimetypes.accept_json or request.method == 'POST': if request.accept_mimetypes.accept_json or request.method == 'POST':
key = 'Attribute' key = 'Attribute'
@ -283,6 +403,7 @@ def logs():
return Response(stream_with_context(event_stream_log()), mimetype="text/event-stream") return Response(stream_with_context(event_stream_log()), mimetype="text/event-stream")
@app.route("/_maps") @app.route("/_maps")
@login_required
def maps(): def maps():
if request.accept_mimetypes.accept_json or request.method == 'POST': if request.accept_mimetypes.accept_json or request.method == 'POST':
key = 'Map' key = 'Map'
@ -292,6 +413,7 @@ def maps():
return Response(event_stream_maps(), mimetype="text/event-stream") return Response(event_stream_maps(), mimetype="text/event-stream")
@app.route("/_get_log_head") @app.route("/_get_log_head")
@login_required
def getLogHead(): def getLogHead():
return json.dumps(LogItem('').get_head_row()) return json.dumps(LogItem('').get_head_row())
@ -325,6 +447,7 @@ def event_stream_maps():
''' GEO ''' ''' GEO '''
@app.route("/_getTopCoord") @app.route("/_getTopCoord")
@login_required
def getTopCoord(): def getTopCoord():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -334,6 +457,7 @@ def getTopCoord():
return jsonify(data) return jsonify(data)
@app.route("/_getHitMap") @app.route("/_getHitMap")
@login_required
def getHitMap(): def getHitMap():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -343,6 +467,7 @@ def getHitMap():
return jsonify(data) return jsonify(data)
@app.route("/_getCoordsByRadius") @app.route("/_getCoordsByRadius")
@login_required
def getCoordsByRadius(): def getCoordsByRadius():
try: try:
dateStart = datetime.datetime.fromtimestamp(float(request.args.get('dateStart'))) dateStart = datetime.datetime.fromtimestamp(float(request.args.get('dateStart')))
@ -359,14 +484,17 @@ def getCoordsByRadius():
''' CONTRIB ''' ''' CONTRIB '''
@app.route("/_getLastContributors") @app.route("/_getLastContributors")
@login_required
def getLastContributors(): def getLastContributors():
return jsonify(contributor_helper.getLastContributorsFromRedis()) return jsonify(contributor_helper.getLastContributorsFromRedis())
@app.route("/_eventStreamLastContributor") @app.route("/_eventStreamLastContributor")
@login_required
def getLastContributor(): def getLastContributor():
return Response(eventStreamLastContributor(), mimetype="text/event-stream") return Response(eventStreamLastContributor(), mimetype="text/event-stream")
@app.route("/_eventStreamAwards") @app.route("/_eventStreamAwards")
@login_required
def getLastStreamAwards(): def getLastStreamAwards():
return Response(eventStreamAwards(), mimetype="text/event-stream") return Response(eventStreamAwards(), mimetype="text/event-stream")
@ -404,6 +532,7 @@ def eventStreamAwards():
subscriber_lastAwards.unsubscribe() subscriber_lastAwards.unsubscribe()
@app.route("/_getTopContributor") @app.route("/_getTopContributor")
@login_required
def getTopContributor(suppliedDate=None, maxNum=100): def getTopContributor(suppliedDate=None, maxNum=100):
if suppliedDate is None: if suppliedDate is None:
try: try:
@ -417,6 +546,7 @@ def getTopContributor(suppliedDate=None, maxNum=100):
return jsonify(data) return jsonify(data)
@app.route("/_getFameContributor") @app.route("/_getFameContributor")
@login_required
def getFameContributor(): def getFameContributor():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -427,6 +557,7 @@ def getFameContributor():
return getTopContributor(suppliedDate=date, maxNum=10) return getTopContributor(suppliedDate=date, maxNum=10)
@app.route("/_getFameQualContributor") @app.route("/_getFameQualContributor")
@login_required
def getFameQualContributor(): def getFameQualContributor():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -437,10 +568,12 @@ def getFameQualContributor():
return getTopContributor(suppliedDate=date, maxNum=10) return getTopContributor(suppliedDate=date, maxNum=10)
@app.route("/_getTop5Overtime") @app.route("/_getTop5Overtime")
@login_required
def getTop5Overtime(): def getTop5Overtime():
return jsonify(contributor_helper.getTop5OvertimeFromRedis()) return jsonify(contributor_helper.getTop5OvertimeFromRedis())
@app.route("/_getOrgOvertime") @app.route("/_getOrgOvertime")
@login_required
def getOrgOvertime(): def getOrgOvertime():
try: try:
org = request.args.get('org') org = request.args.get('org')
@ -449,6 +582,7 @@ def getOrgOvertime():
return jsonify(contributor_helper.getOrgOvertime(org)) return jsonify(contributor_helper.getOrgOvertime(org))
@app.route("/_getCategPerContrib") @app.route("/_getCategPerContrib")
@login_required
def getCategPerContrib(): def getCategPerContrib():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -458,6 +592,7 @@ def getCategPerContrib():
return jsonify(contributor_helper.getCategPerContribFromRedis(date)) return jsonify(contributor_helper.getCategPerContribFromRedis(date))
@app.route("/_getLatestAwards") @app.route("/_getLatestAwards")
@login_required
def getLatestAwards(): def getLatestAwards():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -467,10 +602,12 @@ def getLatestAwards():
return jsonify(contributor_helper.getLastAwardsFromRedis()) return jsonify(contributor_helper.getLastAwardsFromRedis())
@app.route("/_getAllOrg") @app.route("/_getAllOrg")
@login_required
def getAllOrg(): def getAllOrg():
return jsonify(contributor_helper.getAllOrgFromRedis()) return jsonify(contributor_helper.getAllOrgFromRedis())
@app.route("/_getOrgRank") @app.route("/_getOrgRank")
@login_required
def getOrgRank(): def getOrgRank():
try: try:
org = request.args.get('org') org = request.args.get('org')
@ -479,6 +616,7 @@ def getOrgRank():
return jsonify(contributor_helper.getCurrentOrgRankFromRedis(org)) return jsonify(contributor_helper.getCurrentOrgRankFromRedis(org))
@app.route("/_getContributionOrgStatus") @app.route("/_getContributionOrgStatus")
@login_required
def getContributionOrgStatus(): def getContributionOrgStatus():
try: try:
org = request.args.get('org') org = request.args.get('org')
@ -487,6 +625,7 @@ def getContributionOrgStatus():
return jsonify(contributor_helper.getCurrentContributionStatus(org)) return jsonify(contributor_helper.getCurrentContributionStatus(org))
@app.route("/_getHonorBadges") @app.route("/_getHonorBadges")
@login_required
def getHonorBadges(): def getHonorBadges():
try: try:
org = request.args.get('org') org = request.args.get('org')
@ -495,6 +634,7 @@ def getHonorBadges():
return jsonify(contributor_helper.getOrgHonorBadges(org)) return jsonify(contributor_helper.getOrgHonorBadges(org))
@app.route("/_getTrophies") @app.route("/_getTrophies")
@login_required
def getTrophies(): def getTrophies():
try: try:
org = request.args.get('org') org = request.args.get('org')
@ -503,7 +643,9 @@ def getTrophies():
return jsonify(contributor_helper.getOrgTrophies(org)) return jsonify(contributor_helper.getOrgTrophies(org))
@app.route("/_getAllOrgsTrophyRanking") @app.route("/_getAllOrgsTrophyRanking")
@login_required
@app.route("/_getAllOrgsTrophyRanking/<string:categ>") @app.route("/_getAllOrgsTrophyRanking/<string:categ>")
@login_required
def getAllOrgsTrophyRanking(categ=None): def getAllOrgsTrophyRanking(categ=None):
return jsonify(contributor_helper.getAllOrgsTrophyRanking(categ)) return jsonify(contributor_helper.getAllOrgsTrophyRanking(categ))
@ -511,6 +653,7 @@ def getAllOrgsTrophyRanking(categ=None):
''' USERS ''' ''' USERS '''
@app.route("/_getUserLogins") @app.route("/_getUserLogins")
@login_required
def getUserLogins(): def getUserLogins():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -522,10 +665,12 @@ def getUserLogins():
return jsonify(data) return jsonify(data)
@app.route("/_getAllLoggedOrg") @app.route("/_getAllLoggedOrg")
@login_required
def getAllLoggedOrg(): def getAllLoggedOrg():
return jsonify(users_helper.getAllOrg()) return jsonify(users_helper.getAllOrg())
@app.route("/_getTopOrglogin") @app.route("/_getTopOrglogin")
@login_required
def getTopOrglogin(): def getTopOrglogin():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -536,6 +681,7 @@ def getTopOrglogin():
return jsonify(data) return jsonify(data)
@app.route("/_getLoginVSCOntribution") @app.route("/_getLoginVSCOntribution")
@login_required
def getLoginVSCOntribution(): def getLoginVSCOntribution():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -546,6 +692,7 @@ def getLoginVSCOntribution():
return jsonify(data) return jsonify(data)
@app.route("/_getUserLoginsAndContribOvertime") @app.route("/_getUserLoginsAndContribOvertime")
@login_required
def getUserLoginsAndContribOvertime(): def getUserLoginsAndContribOvertime():
try: try:
date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) date = datetime.datetime.fromtimestamp(float(request.args.get('date')))
@ -558,6 +705,7 @@ def getUserLoginsAndContribOvertime():
''' TRENDINGS ''' ''' TRENDINGS '''
@app.route("/_getTrendingEvents") @app.route("/_getTrendingEvents")
@login_required
def getTrendingEvents(): def getTrendingEvents():
try: try:
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
@ -571,6 +719,7 @@ def getTrendingEvents():
return jsonify(data) return jsonify(data)
@app.route("/_getTrendingCategs") @app.route("/_getTrendingCategs")
@login_required
def getTrendingCategs(): def getTrendingCategs():
try: try:
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
@ -584,6 +733,7 @@ def getTrendingCategs():
return jsonify(data) return jsonify(data)
@app.route("/_getTrendingTags") @app.route("/_getTrendingTags")
@login_required
def getTrendingTags(): def getTrendingTags():
try: try:
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
@ -597,6 +747,7 @@ def getTrendingTags():
return jsonify(data) return jsonify(data)
@app.route("/_getTrendingSightings") @app.route("/_getTrendingSightings")
@login_required
def getTrendingSightings(): def getTrendingSightings():
try: try:
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
@ -609,6 +760,7 @@ def getTrendingSightings():
return jsonify(data) return jsonify(data)
@app.route("/_getTrendingDisc") @app.route("/_getTrendingDisc")
@login_required
def getTrendingDisc(): def getTrendingDisc():
try: try:
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
@ -622,6 +774,7 @@ def getTrendingDisc():
return jsonify(data) return jsonify(data)
@app.route("/_getTypeaheadData") @app.route("/_getTypeaheadData")
@login_required
def getTypeaheadData(): def getTypeaheadData():
try: try:
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))
@ -634,6 +787,7 @@ def getTypeaheadData():
return jsonify(data) return jsonify(data)
@app.route("/_getGenericTrendingOvertime") @app.route("/_getGenericTrendingOvertime")
@login_required
def getGenericTrendingOvertime(): def getGenericTrendingOvertime():
try: try:
dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS')))