mirror of https://github.com/CIRCL/lookyloo
chg: Complete rework of the login system, add UI for MISP Push
parent
13e1614f5b
commit
39dd2021dd
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.flashed-messages {
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#menu_container {
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
|
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue