Merge pull request #129 from VVX7/master

new: [authentication] Flask-login authentication via MISP.
pull/135/head
Sami Mokaddem 2019-10-11 09:51:52 +02:00 committed by GitHub
commit 47c4c2e529
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 261 additions and 1 deletions

View File

@ -3,6 +3,18 @@ host = localhost
port = 8001 port = 8001
debug = False debug = False
[Auth]
auth_enabled = False
misp_fqdn = https://misp.local
ssl_verify = True
session_secret = **Change_Me**
# Only send cookies with requests over HTTPS if the cookie is marked secure.
session_cookie_secure = True
# Prevent sending cookies in all external requests including regular links.
session_cookie_samesite = Strict
# Expire session cookie after n days.
permanent_session_lifetime = 1
[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

190
server.py
View File

@ -1,11 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import configparser import configparser
import datetime import datetime
import uuid
import errno import errno
import json import json
import logging import logging
import math import math
import os import os
import re
from datetime import timedelta
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 +17,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,8 +35,22 @@ 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")
auth_enabled = cfg.getboolean("Auth", "auth_enabled")
auth_ssl_verify = cfg.getboolean("Auth", "ssl_verify")
auth_session_secret = cfg.get("Auth", "session_secret")
auth_session_cookie_secure = cfg.getboolean("Auth", "session_cookie_secure")
auth_session_cookie_samesite = cfg.get("Auth", "session_cookie_samesite")
auth_permanent_session_lifetime = cfg.getint("Auth", "permanent_session_lifetime")
app = Flask(__name__) app = Flask(__name__)
#app.secret_key = auth_session_secret
app.config.update(
SECRET_KEY=auth_session_secret,
SESSION_COOKIE_SECURE=auth_session_cookie_secure,
SESSION_COOKIE_SAMESITE=auth_session_cookie_samesite,
PERMANENT_SESSION_LIFETIME=timedelta(days=auth_permanent_session_lifetime)
)
redis_server_log = redis.StrictRedis( redis_server_log = redis.StrictRedis(
host=cfg.get('RedisGlobal', 'host'), host=cfg.get('RedisGlobal', 'host'),
@ -56,6 +77,133 @@ 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.session_protection = "strong"
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()
session.verify = auth_ssl_verify
# The login page contains hidden form values required for authenticaiton.
login_page = session.get(misp_login_page)
# 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, "")
@login_manager.unauthorized_handler
def unauthorized():
"""
Redirect unauthorized user to login page.
:return:
"""
return redirect(url_for('login'))
@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 route.
:return:
"""
if not auth_enabled:
# Generate a random user name and redirect the automatically authenticated user to index.
user = User(str(uuid.uuid4()).replace('-',''), '')
login_user(user)
return redirect(url_for('index'))
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):
"""
WTForm form object. This object defines form fields in the login endpoint.
"""
username = StringField('Username', [validators.Length(max=255)])
password = PasswordField('Password', [validators.Length(max=255)])
submit = SubmitField('Sign In')
########## ##########
## UTIL ## ## UTIL ##
@ -159,6 +307,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 +329,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 +343,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 +395,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 +419,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 +438,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 +448,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 +482,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 +492,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 +502,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 +519,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 +567,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 +581,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 +592,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 +603,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 +617,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 +627,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 +637,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 +651,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 +660,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 +669,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')
@ -504,6 +679,7 @@ def getTrophies():
@app.route("/_getAllOrgsTrophyRanking") @app.route("/_getAllOrgsTrophyRanking")
@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 +687,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 +699,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 +715,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 +726,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 +739,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 +753,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 +767,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 +781,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 +794,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 +808,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 +821,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')))

BIN
static/pics/misp-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -221,6 +221,7 @@ div.leaflet-bottom {
<li><a href="{{ url_for('contrib') }}">MISP Contributors</a></li> <li><a href="{{ url_for('contrib') }}">MISP Contributors</a></li>
<li><a href="{{ url_for('users') }}">MISP Users</a></li> <li><a href="{{ url_for('users') }}">MISP Users</a></li>
<li><a href="{{ url_for('trendings') }}">MISP Trendings</a></li> <li><a href="{{ url_for('trendings') }}">MISP Trendings</a></li>
<li><a href="{{ url_for('logout') }}">Logout</a></li>
</ul> </ul>
<div id="ledsHolder" style="float: right; height: 50px;"></div> <div id="ledsHolder" style="float: right; height: 50px;"></div>

57
templates/login.html Normal file
View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width" />
<title>
Users - MISP
</title>
<!-- Bootstrap Core JavaScript -->
<script src="{{ url_for('static', filename='js/bootstrap.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap3-typeahead.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="text/css">
</head>
<body>
<div id="flashContainer" style="padding-top:50px; !important;">
<div id="main-view-container" class="container-fluid ">
</div>
</div>
<div>
<div style="width:100%;">
<table style="margin-left:auto;margin-right:auto;">
<tr>
<td style="text-align:right;width:250px;padding-right:50px"></td>
<td style="width:460px">
<div>
<img src="{{ url_for('static', filename='/pics/misp-logo.png') }}" style="display:block; margin-left: auto; margin-right: auto;"/>
</div>
<form action="" id="UserLoginForm" method="post" accept-charset="utf-8">
<br><legend>Welcome to MISP-Dashboard</legend><br>
<div class="input email required">
{{ form.username.label }}<br>
{{ form.username(size=32, maxlength=255, autocomplete="off", autofocus="autofocus") }}
</div>
<div class="input password required">
{{ form.password.label }}<br>
{{ form.password(size=32, maxlength=255, autocomplete="off") }}
</div>
<div class="clear"></div>
<p>{{ form.submit() }}</p>
</form>
</td>
<td style="width:250px;padding-left:50px"></td>
</tr>
</table>
</div>
</div>
</body>
</html>