chg: Complete rework of the login system, add UI for MISP Push

pull/167/head
Raphaël Vinot 2021-02-04 19:51:43 +01:00
parent 13e1614f5b
commit 39dd2021dd
8 changed files with 171 additions and 49 deletions

View File

@ -953,7 +953,8 @@ class Lookyloo():
obj.comment = f'Redirect {nb}'
self.__misp_add_ips_to_URLObject(obj, ct.root_hartree.hostname_tree)
redirects.append(obj)
obj.comment = f'Last redirect ({nb})'
if redirects:
redirects[-1].comment = f'Last redirect ({nb})'
if redirects:
prec_object = initial_url
@ -977,7 +978,7 @@ class Lookyloo():
screenshot: MISPAttribute = event.add_attribute('attachment', 'screenshot_landing_page.png', data=self.get_screenshot(capture_uuid), disable_correlation=True) # type: ignore
try:
fo = FileObject(pseudofile=ct.root_hartree.rendered_node.body, filename='body_response.html')
fo = FileObject(pseudofile=ct.root_hartree.rendered_node.body, filename=ct.root_hartree.rendered_node.filename)
fo.comment = 'Content received for the final redirect (before rendering)'
fo.add_reference(event.objects[-1], 'loaded-by', 'URL loading that content')
fo.add_reference(screenshot, 'rendered-as', 'Screenshot of the page')

View File

@ -44,11 +44,14 @@ class MISP():
self.enable_lookup = True
if config.get('enable_push'):
self.enable_push = True
self.default_tags: List[str] = config.get('default_tags') # type: ignore
self.default_tags: List[str] = config.get('default_tags') # type: ignore
self.auto_publish = config.get('auto_publish')
self.storage_dir_misp = get_homedir() / 'misp'
self.storage_dir_misp.mkdir(parents=True, exist_ok=True)
def get_fav_tags(self):
return self.client.tags(pythonify=True, favouritesOnly=1)
def push(self, event: MISPEvent) -> Union[MISPEvent, Dict]:
if self.available and self.enable_push:
for tag in self.default_tags:

20
poetry.lock generated
View File

@ -280,9 +280,9 @@ docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-
dotenv = ["python-dotenv"]
[[package]]
name = "flask-httpauth"
version = "4.2.0"
description = "Basic and Digest HTTP authentication for Flask routes"
name = "flask-login"
version = "0.5.0"
description = "User session management for Flask"
category = "main"
optional = false
python-versions = "*"
@ -744,7 +744,7 @@ requests = ">=2.22.0,<3.0.0"
[[package]]
name = "pymisp"
version = "2.4.137.3"
version = "2.4.137.4"
description = "Python API for MISP."
category = "main"
optional = false
@ -1147,7 +1147,7 @@ misp = ["python-magic", "pydeep"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "b3e7ae942355125f62268270e7888093f64b33035d04d574d49ad39189c48f40"
content-hash = "51d0bb7529658d3b5323d7c87d7676c1126d88c2b927af77a91c426f842a9249"
[metadata.files]
aiohttp = [
@ -1354,9 +1354,9 @@ flask = [
{file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
{file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
]
flask-httpauth = [
{file = "Flask-HTTPAuth-4.2.0.tar.gz", hash = "sha256:8c7e49e53ce7dc14e66fe39b9334e4b7ceb8d0b99a6ba1c3562bb528ef9da84a"},
{file = "Flask_HTTPAuth-4.2.0-py2.py3-none-any.whl", hash = "sha256:3fcedb99a03985915335a38c35bfee6765cbd66d7f46440fa3b42ae94a90fac7"},
flask-login = [
{file = "Flask-Login-0.5.0.tar.gz", hash = "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b"},
{file = "Flask_Login-0.5.0-py2.py3-none-any.whl", hash = "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"},
]
gunicorn = [
{file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
@ -1724,8 +1724,8 @@ pylookyloo = [
{file = "pylookyloo-1.3.tar.gz", hash = "sha256:3996798b68203c1ebfdb4744d029a2c741e3a7d4e5d98541afd0c9b4a20eee86"},
]
pymisp = [
{file = "pymisp-2.4.137.3-py3-none-any.whl", hash = "sha256:433a58cd5d6ec8bee9394c43bb679dc0422bec68450f7642f7fd51b870e97118"},
{file = "pymisp-2.4.137.3.tar.gz", hash = "sha256:d3a8074e3276a698d25bcd56f4ce3913d375b05a04fb36f4c8cfe732f4aaefd9"},
{file = "pymisp-2.4.137.4-py3-none-any.whl", hash = "sha256:814c3e5cd3218ba885edab7b8808f45dbe16bbfccb3cd9d19bf062b1ced70fc0"},
{file = "pymisp-2.4.137.4.tar.gz", hash = "sha256:ea029360d7e76646403571b479c8208a678c8b51b9e683d51b0ce95d6ebe3274"},
]
pyopenssl = [
{file = "pyOpenSSL-20.0.1-py2.py3-none-any.whl", hash = "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"},

View File

@ -42,7 +42,6 @@ bootstrap-flask = "^1.5.1"
cloudscraper = "^1.2.56"
defang = "^0.5.3"
vt-py = "^0.6.1"
Flask-HTTPAuth = "^4.2.0"
pyeupi = "^1.0"
scrapysplashwrapper = "^1.3"
pysanejs = "^1.3"
@ -56,6 +55,7 @@ python-magic = {version = "^0.4.18", optional = true}
pydeep = {version = "^0.4", optional = true}
Pillow = "^8.1.0"
lief = "^0.11.0"
Flask-Login = "^0.5.0"
[tool.poetry.extras]
misp = ['python-magic', 'pydeep']

View File

@ -16,7 +16,7 @@ import hashlib
from flask import Flask, render_template, request, send_file, redirect, url_for, Response, flash, jsonify
from flask_bootstrap import Bootstrap # type: ignore
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth, MultiAuth # type: ignore
import flask_login # type: ignore
from werkzeug.security import generate_password_hash, check_password_hash
from pymisp import MISPEvent
@ -44,9 +44,8 @@ app.config['SESSION_COOKIE_NAME'] = 'lookyloo'
app.debug = False
# Auth stuff
basic_auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth('LookylooToken')
auth = MultiAuth(basic_auth, token_auth)
login_manager = flask_login.LoginManager()
login_manager.init_app(app)
try:
# Use legacy user mgmt
users = get_config('generic', 'cache_clean_user')
@ -79,17 +78,61 @@ for username, authstuff in users_table.items():
keys_table[authstuff['authkey']] = username
@basic_auth.verify_password
def verify_password(username, password):
if users_table.get(username):
if check_password_hash(users_table['username']['password'], password):
return username
class User(flask_login.UserMixin):
pass
@token_auth.verify_token
def verify_token(token):
if token in keys_table:
return keys_table[token]
@login_manager.user_loader
def user_loader(username):
if username not in users_table:
return None
user = User()
user.id = username
return user
@login_manager.request_loader
def load_user_from_request(request):
api_key = request.headers.get('Authorization')
if not api_key:
return None
user = User()
api_key = api_key.strip()
if api_key in keys_table:
user.id = keys_table[api_key]
return user
return None
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return '''
<form action='login' method='POST'>
<input type='text' name='username' id='username' placeholder='username'/>
<input type='password' name='password' id='password' placeholder='password'/>
<input type='submit' name='submit'/>
</form>
'''
username = request.form['username']
if username in users_table and check_password_hash(users_table[username]['password'], request.form['password']):
user = User()
user.id = username
flask_login.login_user(user)
flash(f'Logged in as: {flask_login.current_user.id}', 'success')
else:
flash(f'Unable to login as: {username}', 'error')
return redirect(url_for('index'))
@app.route('/logout')
@flask_login.login_required
def logout():
flask_login.logout_user()
flash('Successfully logged out.', 'success')
return redirect(url_for('index'))
# Config
@ -212,7 +255,7 @@ def hostnode_popup(tree_uuid: str, node_uuid: str):
# ##### Tree level Methods #####
@app.route('/tree/<string:tree_uuid>/rebuild')
@auth.login_required
@flask_login.login_required
def rebuild_tree(tree_uuid: str):
try:
lookyloo.remove_pickle(tree_uuid)
@ -361,7 +404,7 @@ def export(tree_uuid: str):
@app.route('/tree/<string:tree_uuid>/hide', methods=['GET'])
@auth.login_required
@flask_login.login_required
def hide_capture(tree_uuid: str):
lookyloo.hide_capture(tree_uuid)
return redirect(url_for('tree', tree_uuid=tree_uuid))
@ -383,6 +426,7 @@ def send_mail(tree_uuid: str):
email = ''
comment: str = request.form.get('comment') if request.form.get('comment') else '' # type: ignore
lookyloo.send_mail(tree_uuid, email, comment)
flash("Email notification sent", 'success')
return redirect(url_for('tree', tree_uuid=tree_uuid))
@ -426,7 +470,6 @@ def tree(tree_uuid: str, node_uuid: Optional[str]=None):
except IndexError as e:
print(e)
pass
return render_template('tree.html', tree_json=ct.to_json(),
start_time=ct.start_time.isoformat(),
user_agent=ct.user_agent, root_url=ct.root_url,
@ -437,6 +480,7 @@ def tree(tree_uuid: str, node_uuid: Optional[str]=None):
enable_context_by_users=enable_context_by_users,
enable_categorization=enable_categorization,
enable_bookmark=enable_bookmark,
misp_push=lookyloo.misp.enable_push,
blur_screenshot=blur_screenshot, urlnode_uuid=hostnode_to_highlight,
auto_trigger_modules=auto_trigger_modules,
has_redirects=True if cache.redirects else False)
@ -446,7 +490,7 @@ def tree(tree_uuid: str, node_uuid: Optional[str]=None):
@app.route('/tree/<string:tree_uuid>/mark_as_legitimate', methods=['POST'])
@auth.login_required
@flask_login.login_required
def mark_as_legitimate(tree_uuid: str):
if request.data:
legitimate_entries = request.get_json(force=True)
@ -502,7 +546,7 @@ def index():
@app.route('/hidden', methods=['GET'])
@auth.login_required
@flask_login.login_required
def index_hidden():
return index_generic(show_hidden=True)
@ -538,14 +582,14 @@ def categories():
@app.route('/rebuild_all')
@auth.login_required
@flask_login.login_required
def rebuild_all():
lookyloo.rebuild_all()
return redirect(url_for('index'))
@app.route('/rebuild_cache')
@auth.login_required
@flask_login.login_required
def rebuild_cache():
lookyloo.rebuild_cache()
return redirect(url_for('index'))
@ -733,7 +777,7 @@ def hashes_urlnode(tree_uuid: str, node_uuid: str):
@app.route('/tree/<string:tree_uuid>/url/<string:node_uuid>/add_context', methods=['POST'])
@auth.login_required
@flask_login.login_required
def add_context(tree_uuid: str, node_uuid: str):
if not enable_context_by_users:
return redirect(url_for('ressources'))
@ -766,25 +810,43 @@ def add_context(tree_uuid: str, node_uuid: str):
return redirect(url_for('ressources'))
@app.route('/tree/<string:tree_uuid>/misp_push', methods=['GET'])
@auth.login_required
def web_misp_push(tree_uuid: str):
@app.route('/tree/<string:tree_uuid>/misp_push', methods=['GET', 'POST'])
@flask_login.login_required
def web_misp_push_view(tree_uuid: str):
error = False
if not lookyloo.misp.available:
flash('MISP module not available.', 'error')
error = True
elif not lookyloo.misp.enable_push:
flash('Push not enabled in MISP module.', 'error')
error = True
else:
event = lookyloo.misp_export(tree_uuid)
if isinstance(event, dict):
flash(f'Unable to generate the MISP export: {event}', 'error')
else:
event = lookyloo.misp.push(event)
if isinstance(event, MISPEvent):
flash(f'MISP event {event.id} created on {lookyloo.misp.client.root_url}', 'success')
else:
flash(f'Unable to create event: {event}', 'error')
error = True
if error:
return redirect(url_for('tree', tree_uuid=tree_uuid))
return redirect(url_for('tree', tree_uuid=tree_uuid))
if request.method == 'POST':
# event is a MISPEvent at this point
# Submit the event
tags = request.form.getlist('tags')
for tag in tags:
event.add_tag(tag) # type: ignore
event = lookyloo.misp.push(event) # type: ignore
if isinstance(event, MISPEvent):
flash(f'MISP event {event.id} created on {lookyloo.misp.client.root_url}', 'success')
else:
flash(f'Unable to create event: {event}', 'error')
return redirect(url_for('tree', tree_uuid=tree_uuid))
fav_tags = lookyloo.misp.get_fav_tags()
return render_template('misp_push_view.html', tree_uuid=tree_uuid,
event=event, fav_tags=fav_tags,
auto_publish=lookyloo.misp.auto_publish,
default_tags=lookyloo.misp.default_tags)
# Query API
@ -793,9 +855,9 @@ def web_misp_push(tree_uuid: str):
def json_get_token():
auth = request.get_json(force=True)
if 'username' in auth and 'password' in auth: # Expected keys in json
username = verify_password(auth['username'], auth['password'])
if username == auth['username']:
return jsonify({'authkey': users_table[username]['authkey']})
if (auth['username'] in users_table
and check_password_hash(users_table[auth['username']]['password'], auth['password'])):
return jsonify({'authkey': users_table[auth['username']]['authkey']})
return jsonify({'error': 'User/Password invalid.'})
@ -830,7 +892,7 @@ def misp_export(tree_uuid: str):
@app.route('/json/<string:tree_uuid>/misp_push', methods=['GET'])
@auth.login_required
@flask_login.login_required
def misp_push(tree_uuid: str):
to_return: Dict = {}
if not lookyloo.misp.available:

View File

@ -14,6 +14,12 @@
stroke-width: 2px;
}
.flashed-messages {
position: fixed;
top: 5px;
text-align: center;
}
#menu_container {
position: fixed;
top: 5px;

View File

@ -0,0 +1,18 @@
<div>
<p>Event to push: {{event.info}}</p>
<p>Auto Publish: {{auto_publish}}</p>
<p>Default tags: {{', '.join(default_tags)}}</p>
<form role="form" action="{{ url_for('web_misp_push_view', tree_uuid=tree_uuid) }}" method=post enctype=multipart/form-data>
<div class="form-group row">
<label for="tags" class="col-sm-2 col-form-label">Available tags:</label>
<div class="col-sm-10">
<select class="form-control" name="tags" id="tags" multiple>
{% for tag in fav_tags %}
<option value="{{ tag.name }}">{{ tag.name }}</option>
{% endfor %}
</select>
</div>
</div>
<button type="submit" class="btn btn-info" id="btn-misp-push">Push to MISP</button>
</form>
</div>

View File

@ -65,6 +65,13 @@
modal.find('.modal-body').load(button.data("remote"));
});
</script>
<script>
$('#mispPushModal').on('show.bs.modal', function(e) {
var button = $(e.relatedTarget);
var modal = $(this);
modal.find('.modal-body').load(button.data("remote"));
});
</script>
<script>
{% if urlnode_uuid %}
@ -146,6 +153,12 @@
data-toggle="modal" data-target="#categoriesModal" role="button">Manage categories</a>
</li>
{% endif %}
{% if current_user.is_authenticated and misp_push%}
<li>
<a href="#mispPushModal" data-remote="{{ url_for('web_misp_push_view', tree_uuid=tree_uuid) }}"
data-toggle="modal" data-target="#mispPushModal" role="button">Prepare push to MISP</a>
</li>
{% endif %}
<li>
<a href="#statsModal" data-remote="{{ url_for('stats', tree_uuid=tree_uuid) }}"
data-toggle="modal" data-target="#statsModal" role="button">Show Statistics</a>
@ -312,6 +325,25 @@
</div>
</div>
<div class="modal fade" id="mispPushModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mispPushModalLabel">MISP Push</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
... loading MISP Push view ...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="screenshotModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">